投稿声明
欢迎各位师傅积极投稿我们会相对应的给出部分稿费20到2000不等通过下方微信进行投稿,谢谢各位师傅
web页面大家都不陌生,在web开发过程中后端负责程序架构和数据管理,前端负责页面展示和用户交互,有种不严谨的说法是:前端代码给浏览器看,后端代码给服务器看。
有开发经验的同学对前后端交互的理解会更深一点,在这种前后端分离的开发方式中,以接口为标准来进行联调整合。为了保证接口在调用时数据的安全性,也是为了防止请求的参数被篡改,大多数接口都进行了请求签名、身份认证、动态Cookie等机制。另外部分网站会对返回的数据进行加密,通常利用AES、RSA等加密方式,也有在传输时对数据进行序列化,比如Protobuf等,这些会在后面进行详细讲解。
请求签名十分常见,比如URL中的加密参数sign,身份认证也有很多例子,比如动态Cookie。这些参数的生成方法都是Js来控制的,如果想要直接从接口上获取数据,就要去调试分析JavaScript的调用逻辑、堆栈调用关系来弄清楚网站加密的实现方法,根据网站的参数生成规则还原加密参数,可以称这个过程为Js逆向。
我们总结了一下目前加密参数的常用逆向方式,一种是根据源码的生成逻辑还原加密代码,另一种是补环境Copy源码模拟加密参数生成,还有一种是通过RPC的方式远程调用。
在一些逆向案例中,其中的关键就是将浏览器环境移植到Node环境中,Node Js采用的内核也为V8引擎,该引擎调用对方Js的可行性并不是100%,同时由于Node没有界面渲染,因此在浏览器中可使用的例如window、navigator、dom等操作在node中是不存在的,所以对于Node Js的环境搭建和浏览器环境补齐也是Js逆向需要掌握的。
还有就是Chrome作为Js逆向的核心工具,熟练掌握Chrome的控制台、插件编写就足够应对绝大多数的抓包、调试、Hook等,这些内容都会在后续进行讲解。
第一章 逆向基础
1.1 语法基础
Js调试相对方便,通常只需要chrome或者一些抓包工具、扩展插件,就能顺利的完成逆向分析。但是Js的弱类型和语法多样,各种闭包,逗号表达式等语法让代码可读性变得不如其他语言顺畅。所以需要学习一下基础语法。
-
基本数据类型
字符串 | String |
---|---|
数字 | Number |
布尔 | Boolean |
空值 | Null |
未定义 | Undefined |
独一无二的值 | Symbol |
-
引用数据类型
对象 | Object |
---|---|
数组 | Array |
函数 | Function |
-
语句标识符
在条件为true时重复执行 | do……while |
---|---|
在条件为true时执行 | while |
循环遍历 | for |
条件判断 | if……else |
根据情况执行代码块 | switch |
退出循环 | break |
异常捕获 | try……catch……finally |
抛出异常 | Throw |
声明固定值的变量 | const |
声明类 | class |
停止函数并返回 | return |
声明块作用域的变量 | let |
声明变量 | var |
断点调试 | debugger |
当前所属对象 | this |
-
算数运算符
加 | + |
---|---|
减 | - |
乘 | * |
除 | / |
取余 | % |
累加 | ++ |
递减 | -- |
-
比较运算符
等于 | == |
---|---|
相等值或者相等类型 | === |
不等于 | != |
不相等值或者不相等类型 | !== |
大于 | > |
小于 | < |
大于等于 | >= |
小于等于 | <= |
在JavaScript中将数字存储为64位浮点数,但所有按位运算都以32位二进制数执行。在执行位运算之前,JavaScript将数字转换位32位有符号整数。执行按位操作后,结果将转换回64位JavaScript数。
Javascript 函数
JavaScript 中的函数是头等公民,不仅可以像变量一样使用它,同时它还具有十分强大的抽象能力
定义函数的 2 种方式
在JavaScript 中,定义函数的方式如下:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
上述 abs() 函数的定义如下:
-
function 指出这是一个函数定义;
-
abs 是函数的名称;
-
(x) 括号内列出函数的参数,多个参数以,分隔;
-
{……} 之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
请注意,函数体内部的语句在执行时,一旦执行到 return 时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有 return 语句,函数执行完毕后也会返回结果,只是结果为 undefined。
由于JavaScript的函数也是一个对象,上述定义的 abs()
函数实际上是一个函数对象,而函数名 abs
可以视为指向该函数的变量。
因此,第二种定义函数的方式如下:
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
在这种方式下,function(x){……} 是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量 abs,所以,通过变量 abs 就可以调用该函数。
注意:上述两种定义 完全等价 ,第二种方式按照完整语法需要在函数体末尾加一个 ;,表示赋值语句结束。( 加不加都一样,如果按照语法完整性要求,需要加上)
调用函数时,按顺序传入参数即可:
abs(10); // 返回10
abs(-9); // 返回9
由于JavaScript 允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
传入的参数比定义的少也没有问题:
abs(); // 返回NaN
此时 abs(s) 函数的参数 x 将收到 undefined,计算结果为NaN。要避免收到undefined,可以对参数进行检查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number'; // 停止并抛出错误信息
}
if (x >= 0) {
return x;
} else {
return -x;
}
}
1.2 作用域
Js中有一个被称为作用域的特性。作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
Js的作用域分为三种:全局作用域、函数作用域、块级作用域。全局作用域可以让用户在任何位置进行调用,需要注意的是最外层函数和在最外层函数外面定义的变量拥有全局作用域,所有未定义直接赋值的变量会自动声明为拥有全局作用域,所有window对象的属性也拥有全局作用域。函数作用域也就是说只有在函数内部可以被访问,当然函数内部是可以访问全局作用域的。块级作用域则是在if和switch的条件语句或for和while的循环语句中,块级作用域可通过新增命令let和const声明,所声明的变量在指定的作用域外无法被访问。
1.3 窗口对象属性
我们总结了浏览器window的常见属性和方法。因为很多环境监测都是基于这些属性和方法的,在补环境前,需要了解window对象的常用属性和方法。
-
Window
-
Window对象表示浏览器当前打开的窗口。
Document对象 | document |
---|---|
History对象 | history |
Location对象 | location |
Navigator对象 | navigator |
Screen对象 | screen |
按照指定的像素值来滚动内容 | scrollBy() |
把内容滚动到指定的坐标 | scrollTo() |
定时器 | setInterval() |
延时器 | setTimeout() |
弹出警告框 | alert() |
弹出对话框 | prompt() |
打开新页面 | open() |
关闭页面 | close() |
-
Document
-
载入浏览器的HTML文档。
<body>元素 | body |
---|---|
当前cookie | cookie |
文档域名 | domain |
文档最后修改日期和时间 | lastModified |
访问来源 | referrer |
文档标题 | title |
当前URL | URL |
返回指定id的引用对象 | getElementById() |
返回指定名称的对象集合 | getElementByName() |
返回指定标签名的对象集合 | getElementByTagName() |
打开流接收输入输出 | open() |
向文档输入 | write() |
-
Navigator
-
Navigator对象包含的属性描述了当前使用的浏览器,可以使用这些属性进行平台专用的配置。
用户代理 | userAgent |
---|---|
浏览器代码名 | AppCodeName |
浏览器名称 | AppName |
浏览器版本 | AppVersion |
浏览器语言 | browserLanguage |
指明是否启用cookie的布尔值 | cookieEnabled |
浏览器系统的cpu等级 | cpuClass |
是否处于脱机模式 | onLine |
浏览器的操作系统平台 | platform |
插件,所有嵌入式对象的引用 | plugins |
是否启用驱动 | webdriver |
引擎名 | product |
硬件支持并发数 | hardwareConcurrency |
网络信息 | connection |
是否启用java | javaEnabled() |
是否启用数据污点 | taintEnabled() |
-
Location
-
Location对象包含有关当前URL的信息
URL锚 | hash |
---|---|
当前主机名和端口号 | host |
当前主机名 | hostname |
当前URL | href |
当前URL的路径 | pathname |
当前URL的端口号 | port |
当前URL的协议 | protocol |
设置URL查询部分 | search |
加载新文件 | assign() |
重新加载文件 | reload() |
替换当前文档 | replace() |
-
Screen
-
每个window对象的screen属性都引用一个Screen对象。Screen对象中存放着有关显示浏览器屏幕的信息。
屏幕高度 | avaiHeight |
---|---|
屏幕宽度 | availWidth |
调色版比特深度 | bufferDepth |
显示屏每英寸水平点数 | deviceXDPI |
显示屏每英寸垂直点数 | deviceYDPI |
是否启用字体平滑 | fontSmoothingEnabled |
显示屏高度 | height |
显示屏分辨率 | pixeIDepth |
屏幕刷新率 | updateInterval |
显示屏宽度 | width |
-
History
-
History对象包含用户在浏览器窗口中访问过的URL。
浏览器历史列表中的URL数量 | length |
---|---|
加载前一个URL | back() |
加载下一个URL | forward() |
加载某个具体页面 | go() |
window中还有很多属性和方法,这里就不再过多的描述,大家可以自行查看。
1.4 事件
HTML 事件是发生在 HTML 元素上的事情。
当在 HTML 页面中使用 JavaScript 时, JavaScript 可以触发这些事件。
HTML 事件可以是浏览器行为,也可以是用户行为。
以下是 HTML 事件的实例:
-
HTML 页面完成加载
-
HTML input 字段改变时
-
HTML 按钮被点击
通常,当事件发生时,你可以做些事情。
在事件触发时 JavaScript 可以执行一些代码。
HTML 元素中可以添加事件属性,使用 JavaScript 代码来添加 HTML 元素。
事件可以用于处理表单验证,用户输入,用户行为及浏览器动作:
-
页面加载时触发事件
-
页面关闭时触发事件
-
用户点击按钮执行动作
-
验证用户输入内容的合法性
-
等等 ...
可以使用多种方法来执行 JavaScript 事件代码:
-
HTML 事件属性可以直接执行 JavaScript 代码
-
HTML 事件属性可以调用 JavaScript 函数
-
你可以为 HTML 元素指定自己的事件处理程序
-
你可以阻止事件的发生。
-
等等 ..
HTML事件
事件 | 描述 |
---|---|
onclick | 当用户单击HTML元素时触发 |
ondblclick | 当用户双击对象时触发 |
onmove | 当对象移动时触发 |
onmoveend | 当对象停止移动时触发 |
onmovestart | 当对象开始移动时触发 |
onkeydown | 当用户按下键盘按键时触发 |
onkeyup | 当用户释放键盘按键时触发 |
onload | 当某个页面或图像被完成加载 |
onselect | 当文本被选定 |
onblur | 当元素失去焦点 |
onchange | 当HTML元素改变时触发 |
onfocusin | 当元素将要被设置为焦点之前触发 |
onhelp | 当用户在按F1键时触发 |
onkeypress | 当用户按下字面键时触发 |
onmousedown | 当用户用任何鼠标按钮单击对象时触发 |
onmousemove | 当用户将鼠标划过对象时触发 |
onmouseover | 当用户从一个HTML元素上移动鼠标时触发 |
onmouseout | 当用户从一个HTML元素上移开鼠标时触发 |
onmouseup | 当用户在对象之上释放鼠标按钮时触发 |
onmousewheel | 当鼠标滚轮按钮旋转时触发 |
onstop | 当用户单击停止按钮或者离开页面时触发 |
onactivate | 当对象设置为活动元素时触发 |
onreadystatechange | 当在对象上发生对象属性更改时触发 |
ondragend | 当用户拖拽操作结束后释放鼠标时触发 |
接下来说一下HTMl中绑定事件的几种方法,分别是行内绑定、动态绑定、事件监听、bind和on绑定。
-
行内绑定是指把触发事件直接写到元素的标签中
<li>
<div onclick="xxx()">点击</div>
</li>
-
动态绑定是指先获取到dom元素,然后在元素上绑定事件
<script>
var xx = document.getElementById("lx");
xx.onclick = function(){}
</script>
-
事件监听主要通过addEventListener()方法来实现
<script>
var xx = document.getElementById("lx");
xx.addEventListener("click", function(){})
</script>
-
bind()和on()绑定都是属于JQuery的事件绑定方法,bind()的事件函数只能针对已经存在的元素进行事件的设置
$("button").bind("click",function(){
$("p").slideToggle();
});
-
on()支持对将要添加的新元素设置事件
$(document).ready(function(){
$("p").on("click", function(){});
});
还有live()和delegate()等事件绑定方法,目前并不常用。
第二章 浏览器控制台
首先介绍一下浏览器控制台的使用,以开发者使用最多的chrome为例。Windows操作系统下的F12键可以打开控制台,mac操作系统下用Fn+F12键打开。我们选择平时使用较多的模块进行介绍
2.1 Network
Network是Js调试的重点,面板上由控制器、过滤器、数据流概览、请求列表、数据统计这五部分组成。
-
控制器:Presserve Log是保留请求日志的作用,在跳转页面的时候勾选上可以看到跳转前的请求。Disable cache是禁止缓存的作用,Offline是离线模拟。
-
过滤器:根据规则过滤器请求列表的内容,可以选择XHR,JS,CSS,WS等。
-
数据流概览:显示HTTP请求、响应的时间轴。
-
请求列表:默认是按时间排序,可以看到浏览器所有的请求,主要用于网络请求的查看和分析,可以查看请求头、响应状态和内容、Form表单等。
-
数据统计:请求总数、总数据量、总花费时间等。
浏览器控制台Network下各项属性的含义
作用:
-
可以查看调取接口是否正确,后台返回的数据;
-
查看请求状态、请求类型、请求地址
2.1.1 Network-Headers
首先打开控制台,找到Network. 刷新页面可以看到Name
Name对应的是资源的名称及路径, Status Code 是请求服务器返回的状态码,一般情况下当状态码为200时,则表示接口匹配成功。点击任一文件名,右侧会出现Header选项。
Network-Header-General
-
Request URL: 资源请求的url
-
Request Method: 请求方法(HTTP方法)
-
Status Code: 状态码
-
200(状态码) OK
-
301 - 资源(网页等)被永久转移到其它URL
-
404 - 请求的资源(网页等)不存在
-
500 - 内部服务器错误(后台问题)
-
Remote Address: 远程地址;
-
Referrer Policy: 控制请求头中 refrrer 的内容包含值的情况:
-
当一个用户点击页面中的一个链接,然后跳转到目标页面时,本变页面会收到一个信息,即用户是从哪个源链接跳转过来的。
-
也就是说当你发起一个HTTP请求,请求头中的 referrer 字段就说明了你是从哪个页面发起该请求的;
-
"", 空串默认按照浏览器的机制设置referrer的内容,默认情况下是和no-referrer-when-downgrade设置得一样
-
"no-referrer", 不显示 referrer的任何信息在请求头中
-
"no-referrer-when-downgrade", 默认值。当从https网站跳转到http网站或者请求其资源时(安全降级HTTPS→HTTP),不显示 referrer的信息,其他情况(安全同级HTTPS→HTTPS,或者HTTP→HTTP)则在 referrer中显示完整的源网站的URL信息
-
"same-origin", 表示浏览器只会显示 referrer信息给同源网站,并且是完整的URL信息。所谓同源网站,是协议、域名、端口都相同的网站
-
"origin", 表示浏览器在 referrer字段中只显示源网站的源地址(即协议、域名、端口),而不包括完整的路径
-
"strict-origin", 该策略更为安全些,和 origin策略相似,只是不允许 referrer信息显示在从https网站到http网站的请求中(安全降级)
-
"origin-when-cross-origin", 当发请求给同源网站时,浏览器会在 referrer中显示完整的URL信息,发个非同源网站时,则只显示源地址(协议、域名、端口)
-
"strict-origin-when-cross-origin", 和 origin-when-cross-origin相似,只是不允许 referrer信息显示在从https网站到http网站的请求中(安全降级)
-
"unsafe-url" 浏览器总是会将完整的URL信息显示在 referrer字段中,无论请求发给任何网站
-
补充: 什么是referrer?
Network-Header-Response Headers
-
Access-Control-Allow-Origin: 请求头中允许设置的请求方法
-
Connection: 连接方式
-
content-length: 响应数据的数据长度,单位是byte
-
content-type: 客户端发送的类型及采用的编码方式
-
Date: 客户端请求服务端的时间
-
Vary: 用来指示缓存代理(例如squid)根据什么条件去缓存一个请求
-
Last-Modified: 服务端对该资源最后修改的时间
-
Server: 服务端的web服务端名
-
Content-Encoding: gzip 压缩编码类型
-
Transfer-Encoding:chunked: 分块传递数据到客户端
Network-Header-Request Headers
-
Accept: 客户端能接收的资源类型
-
Accept-Encoding: 客户端能接收的压缩数据的类型
-
Accept-Language: 客户端接收的语言类型
-
Cache-Control: no-cache 服务端禁止客户端缓存页面数据
-
Connection: keep-alive 维护客户端和服务端的连接关系
-
Cookie:客户端暂存服务端的信息
-
Host: 连接的目标主机和端口号
-
Pragma: no-cache 服务端禁止客户端缓存页面数据
-
Referer: 来于哪里(即从哪个页面跳转过来的)
-
User-Agent: 客户端版本号的名字
2.2 Sources
Sources按列分为三列,从左至右分别是文件列表区、当前文件区、断点调试区。
文件列表区中有Page、Snippets、FileSytem等。Page可以看到当前所在的文件位置,在Snippets中单击New Snippets可以添加自定义的Js代码,FileSytem可以把本地的文件系统导入到chrome中。
当前文件区是需要重点操作的区域,单击下方的{}来格式化代码,就能看到美观的Js代码,然后可以根据指定行数进行断点调试。
断点调试区也非常重要,每个操作点都需要了解是什么作用。最上方的功能区分别是暂停、跳过、进入、跳出、步骤进入、禁用断点、异常断点。
Watch:变量监听,对加入监听列表的变量进行监听。
Call Stack:断点的调用堆栈列表,完整地显示了导致代码被暂停的执行路径。
Scope:当前断点所在函数执行的作用域内容。
Breakpoints:断点列表,将每个断点所在文件/行数/改成简略内容进行展示。
DOM Breakpoints:DOM断点列表。
XHR/fetch Breakpoints:对达到满足过滤条件的请求进行断点拦截。
Event Listener Breakpoints:打开可监听的事件监听列表,可以在监听事件并且触发该事件时进入断点,调试器会停留在触发事件代码行。
2.3 Application
Application是应用管理部分,主要记录网站加载的所有资源信息。包括存储数据(Local Storage、Session Storage、InDexedDB、Web SQL、Cookies)、缓存数据、字体、图片、脚本、样式表等。Local Storage(本地存储)和 Session Storage中可以查看和管理其存储的键值对。这里使用最多的是对Cookies的管理了,有时候调试需要清除Cookies,可以在Application的Cookies位置单击鼠标右键,选择Clear进行清除,或者根据Cookies中指定的Name和Value来进行清除,便于进一步调试。
注意:我们辨别Cookie来源时,可以看httpOnly这一栏,有√的是来自于服务端,没有√的则是本地生成的。
2.4 Console
谷歌控制台中的Console区域用于审查DOM元素、调试JavaScript代码、查看HTML解析,一般是通过Console.log()来输出调试信息。在Console中也可以输出window、document、location等关键字查看浏览器环境,如果对某函数使用了断点,也可以在Console中调用该函数。
如果你平时只是用console.log()来输出一些变量的值,那你肯定还没有用过console的强大的功能。下面带你用console玩玩花式调试。
来看下主要的调试函数及用法:
console.log(), console.error(), console.warn(), console.info()
最基本也是最常用的用法了,分别表示输出普通信息、错误信息、警示信息和提示性信息,且error和warn方法有特定的图标和颜色标识。
-
console.assert(expression, message)
-
参数:
-
expression: 条件语句,语句会被解析成 Boolean,且为 false 的时候会触发message语句输出
-
message: 输出语句,可以是任意类型
-
该函数会在 expression 为 false 的时候,在控制台输出一段语句,输出的内容就是传入的第二个参数 message 的内容。当我们在只需要在特定的情况下才输出语句的时候,可以使用 console.assert
-
示例如下:
-
function greaterThan(a,b) {
console.assert(a > b, {"message":"a is not greater than b","a":a,"b":b});
}
greaterThan(5,6); -
console.count(label)
-
参数:
-
label: 计算数量的标识符
-
该函数用于计算并输出特定标识符为参数的console.count函数被调用的次数。下面的例子更能直观的了解:
-
function login(name) {
console.count(name + ' logged in');
} -
console.dir(object)
-
参数:
-
object:被输出扎实的对象
-
该函数用于打印出对象的详细的属性、函数及表达式等信息。如果该对象已经被记录为一个HTML元素,则该HTML元素的DOM表达式的属性会被像下面这样打印出来:
-
console.dir(document.body);
-
console.dirxml(object)
-
该函数将打印输出XML元素及其子孙后代元素,且对HTML和XML元素调用 console.dirxml() 和 调用 console.log() 是等价的
-
console.group([label]), console.groupEnd([label])
-
参数:
-
label: group分组的标识符
-
在控制台创建一个新的分组,随后输出到控制台上的内容都会自动添加一个缩进,表示该内容属于当前分组,知道调用 console.groupEnd() 之后,当前分组结束。
-
举个例子:
-
console.log("This is the outer level");
console.group();
console.log("Level 2");
console.group();
console.log("Level 3");
console.warn("More of level 3");
console.groupEnd();
console.log("Back to level 2");
console.groupEnd();
console.log("Back to the outer level"); -
console.groupCollapsed(label)
-
该函数同console.group(),唯一的区别是该函数的输出默认不展开分组,而console.group()是默认展开分组。
-
console.time([label]), console.timeEnd([label])
-
label: 用于标记计时器的名称,不填的话,默认为 default
-
console.time() 会开始一个计时器,并当执行到 console.timeEnd() 函数时(需要两个函数的lable参数相同),结束计时器,并将计时器的总时间输出到控制台上。
-
再举几个例子:
-
console.time();
var arr = new Array(10000);
for (var i = 0; i < arr.length; i++) {
arr[i] = new Object();
}
console.timeEnd();
// default: 3.696044921875ms -
对 console.time(label) 设置一个自定义的 label 字段,并使用console.timeEnd(label) 设置相同的 label 字段来结束计时器。
-
console.time('total');
var arr = new Array(10000);
for (var i = 0; i < arr.length; i++) {
arr[i] = new Object();
}
console.timeEnd('total');
// total: 3.696044921875ms -
设置多个 label 属性,开启多个计时器同步计时。
-
console.time('total');
console.time('init arr');
var arr = new Array(10000);
console.timeEnd('init arr');
for (var i = 0; i < arr.length; i++) {
arr[i] = new Object();
}
console.timeEnd('total');
// init arr: 0.0546875ms
// total: 2.5419921875ms -
console.trace(object)
-
该函数将在控制台打印出从 console.trace() 被调用的位置开始的堆栈信息。
第三章 加密参数的定位方法
想要找到Js加密参数的生成过程,就必须要找到参数的位置,然后通过debug来进行观察调试。我们总结了目前通用的调试方式。每种方法都有其独特的运用之道,大家只有灵活运用这些参数定位方法,才能更好地提高逆向效率。
3.1 巧用搜索
搜索操作比较简单,打开控制台,通过快捷键Ctrl + F打开搜索框。在Network中的不同位置使用Ctrl + F会打开不同的搜索区域,有全局搜索、页面搜索。
另外关于搜索也有一定的技巧,如果加密参数的关键词是signature,可以直接全局搜索signature,搜索不到可以尝试搜索sign或者搜索接口名。如果还没有找到位置,则可以使用下面几种方法。
3.2 堆栈调试
控制台的 Initiator 堆栈调试是我们比较喜欢的调试方式之一,不过新版本的谷歌浏览器才有,如果没有 Initiator 需要更新Chrome版本。Initiator主要是为了监听请求是怎样发起的,通过它可以快速定位到调用栈中。
具体使用方法是先确定请求的接口,然后进入Initiator,单击第一个Request call stack参数,进入Js文件后,在跳转行上打上断点,然后刷新页面等待调试。
3.3 控制台调试
控制台的Console中可以由console.log()方法来执行某些函数,该方法对于开发调试很有帮助,有时通过输出会比找起来更便捷。在断点到某一处时,可以通过console.log()输出此时的参数来查看状态和属性,console.log()方法在后面的参数还原中也很重要。
3.4 监听XHR
XHR是XMLHttpRequest的简称,通过监听XHR的断点,可以匹配URl中params参数的触发点和调用堆栈,另外post请求中From Data的参数也可以用XHR来拦截。
使用方法:打开控制台,单击Sources,右侧有一个XHR/fetch Breakpoints,单击+号即可添加监听事件。像一些URL中的_signature参数就很适合使用XHR断点。
3.5 事件监听
这里其实和监听XHR有些相似,为了方便记忆,我们将其单独放在一个小节中。
有的时候找不到参数位置,但是知道它的触发条件,此时可以使用事件监听器进行断点,在Sources中有
DOM Breakpoints、Global Listeners、Event Listener Breakpoints都可以进行DOM事件监听。
比如需要对Canvas进行断点,就在Event Listener Breakpoints中选择Canvas,勾选Create canvas context时就是对创建canvas时的事件进行了断点。
3.6 添加代码片
在控制台中添加代码片来完成Js代码注入,也是一种不错的方式。
使用方法:打开控制台,单击Sources,然后单击左侧的snippets,新建一个Script Snippet,就可以在空白区域编辑Js代码了。
3.7 Hook
在Js中也需要用到Hook技术,例如当想分析某个cookie是如何生成时,如果想通过直接从代码里搜索该cookie的名称来找到生成逻辑,可能会需要审核非常多的代码。这个时候,如果能够用hook document.cookie的set方法,那么就可以通过打印当时的调用方法堆栈或者直接下断点来定位到该cookie的生成代码位置。
什么是hook?
在 JS 逆向中,我们通常把替换原函数的过程都称为 Hook。一般使用 Object.defineProperty() 来进行hook。
以下先用一段简单的代码理解Hook的过程:
function a() {
console.log("I'm a.");
}
a = function b() {
console.log("I'm b.");
};
a() // I'm b.
直接覆盖原函数是以最简单的做法,以上代码将a函数进行了重写,再次调用a函数将会输出I'm b.
如果还想执行原来a函数的内容,可以使用中间变量进行存储:
function a() {
console.log("I'm a.");
}
var c = a;
a = function b() {
console.log("I'm b.");
};
a() // I'm b.
c() // I'm a.
此时,调用 a 函数会输出 I’m b.,调用 c 函数会输出 I’m a.
这种原函数直接覆盖的方法通常只用来进行临时调试,实用性不大,但是它能够帮助我们理解 Hook 的过程,在实际 JS 逆向过程中,我们会用到更加高级一点的方法,比如 Object.defineProperty()。
Object.defineProperty()
Object.defineProperty(obj, prop, descriptor)
-
obj:需要定义属性的当前对象;
-
prop:当前需要定义的属性名;
-
descriptor:属性描述符,可以取以下值;
属性描述符的取值通常为以下:
属性名 | 默认值 | 含义 |
---|---|---|
get | undefined | 存取描述符,目标属性获取值的方法 |
set | undefined | 存取描述符,目标属性设置值的方法 |
value | undefined | 数据描述符,设置属性的值 |
writable | false | 数据描述符,目标属性的值是否可以被重写 |
enumerable | false | 目标属性是否可以被枚举 |
configurable | false | 目标属性是否可以被删除或是否可以再次修改特性 |
通常情况下,对象的定义与赋值是这样的:
var people = {}
people.name = "Bob"
people["age"] = "18"
console.log(people)
// { name: 'Bob', age: '18' }
使用 defineProperty() 方法:
var people = {}
Object.defineProperty(people, 'name', {
value: 'Bob',
writable: true // 是否可以被重写
})
console.log(people.name) // 'Bob'
people.name = "Tom"
console.log(people.name) // 'Tom'
我们一般hook使用的是get和set方法:
var people = {
name: 'Bob',
};
var count = 18;
// 定义一个 age 获取值时返回定义好的变量 count
Object.defineProperty(people, 'age', {
get: function () {
console.log('获取值!');
return count;
},
set: function (val) {
console.log('设置值!');
count = val + 1;
},
});
console.log(people.age);
people.age = 20;
console.log(people.age);
输出:
获取值!
18
设置值!
获取值!
21
通过这样的方法,我们就可以在设置某个值的时候,添加一些代码,比如 debugger;
让其断下,然后利用调用栈进行调试,找到参数加密、或者参数生成的地方,需要注意的是,网站加载时首先要运行我们的Hook代码,再运行网站自己的代码,才能够成功断下,这个过程我们可以称之为Hook代码的注入。
TamperMonkey 注入
TamperMonkey 俗称油猴插件,是一款免费的浏览器扩展和最为流行的用户脚本管理器,支持很多主流的浏览器, 包括 Chrome、Microsoft Edge、Safari、Opera、Firefox、UC 浏览器、360 浏览器、QQ 浏览器等等,基本上实现了脚本的一次编写,所有平台都能运行,可以说是基于浏览器的应用算是真正的跨平台了。用户可以在 GreasyFork、OpenUserJS 等平台直接获取别人发布的脚本,功能众多且强大,比如视频解析、去广告等。
来到谋奇异首页,可以看到cookie里面有个__dfp值:
我们想通过Hook的方式,让在生成__dfp的地方断下,就可以编写如下函数:
我们以某奇艺的 cookie 为例来演示如何编写 TamperMonkey 脚本,首先去应用商店安装 TamperMonkey,安装过程不再赘述,然后点击图标,添加新脚本,或者点击管理面板,再点击加号新建脚本,写入以下代码:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('__dfp') != -1) {
debugger;
}
console.log('Hook捕获到cookie设置->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();
if (val.indexOf('__dfp') != -1) {debugger;}
的意思是检索 __dfp
在字符串中首次出现的位置,等于 -1 表示这个字符串值没有出现,反之则出现。如果出现了,那么就 debugger 断下,这里要注意的是不能写成 if (val == '__dfp') {debugger}
,因为 val 传过来的值类似于 __dfp=xxxxxxxxxx
,这样写是无法断下的。
写入后进行保存
主体的JavaScript自执行函数和前面的都是一样的,这里需要注意的是最前面的注释,每个选项都是有意义的,所有的选项参考 TamperMonkey 官方文档,以下列出了比较常用、比较重要的部分选项(其中需要特别注意 @match、@include、@run-at)
选项 | 含义 |
---|---|
@name | 脚本的名称 |
@namespace | 命名空间,用来区分相同名称的脚本,一般写作者名字或者网址就可以 |
@version | 脚本版本,油猴脚本的更新会读取这个版本号 |
@description | 描述这个脚本是干什么用的 |
@author | 编写这个脚本的作者的名字 |
@match | 从字符串的起始位置匹配正则表达式,只有匹配的网址才会执行对应的脚本,例如 * 匹配所有,https://www.baidu.com/* 匹配百度等,可以参考 Python re 模块里面的 re.match() 方法,允许多个实例 |
@include | 和 @match 类似,只有匹配的网址才会执行对应的脚本,但是 @include 不会从字符串起始位置匹配,例如 *://*baidu.com/* 匹配百度,具体区别可以参考 TamperMonkey 官方文档 |
@icon | 脚本的 icon 图标 |
@grant | 指定脚本运行所需权限,如果脚本拥有相应的权限,就可以调用油猴扩展提供的 API 与浏览器进行交互。如果设置为 none 的话,则不使用沙箱环境,脚本会直接运行在网页的环境中,这时候无法使用大部分油猴扩展的 API。如果不指定的话,油猴会默认添加几个最常用的 API |
@require | 如果脚本依赖其他 JS 库的话,可以使用 require 指令导入,在运行脚本之前先加载其它 |
@run-at | 脚本注入时机,该选项是能不能 hook 到的关键,有五个值可选:document-start :网页开始时;document-body :body出现时;document-end :载入时或者之后执行;document-idle :载入完成后执行,默认选项;context-menu :在浏览器上下文菜单中单击该脚本时,一般将其设置为 document-start |
清除 cookie,开启 TamperMonkey 插件,再次来到某奇艺首页,可以成功被断下,也可以跟进调用栈来进一步分析 __dfp 值的来源。
长按二维码识别关注
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论