
Hook代码注入
更新: 2025/2/24 字数: 0 字 时长: 0 分钟
前面我们见识了可读性很低的混淆 JS 代码,得益于反混淆工具将其还原可读性很高的代码,才使得我们逆向之路顺畅了不少。但是反混淆工具也不是万能的,假如混淆的 JS 代码特别多,逻辑特别复杂,它还能 100% 还原代码吗?这恐怕需要打一个问号。通过前面的学习,其实我们可以大体能看到,JS 逆向不需要去了解每一行代码,只要找到关键的加密位置,逆向就能迎刃而解。这里我们就要学习一项代码逻辑定位技术,Hook代码注入。
Hook技术
**Hook 技术,当中的 Hook 是“钩、钩子”的意思,又叫做 Hook 钩子函数,其实就是一小段代码。**在系统没有调用该函数之前,钩子函数就先捕获该消息得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。**简单来说,就是在程序执行的过程中插入我们自己的代码片段,便于我们分析 JS 逻辑,找到函数入口以及一些参数变化,但是 Hook 的能力远不及此,我们只借助其寻找函数入口。**Hook 步骤的大体流程如下:
- 寻找 Hook 点,首先我们要分析确定需要 Hook 的内容;
- 编写 Hook 逻辑,针对需要 Hook 的内容,编写 Hook 代码;
- 注入 Hook 代码,判断对象的性质,在合适的位置将 Hook 代码注入进去,继续运行代码等待 Hook 被触发。
- 触发跟栈调试,当钩子函数被触发时,就执行我们定义的钩子函数的逻辑,这时我们就可以往上跟栈调试。
提醒
客户端拥有 JS 的最高解释权,可以决定在任何时候注入 JS,而服务器只能通过检测和混淆手段令 Hook 难度加大,但无法阻止。
警告
注入的 Hook 代码仅当前页面有效使用,如果刷新页面或者打开的新的标签页,需要重新注入 Hook 代码。
Hook属性
首先,我们来看一个很简单的例子:新建一个对象 Person
添加 name
属性并赋值,然后输出该属性的值。
// 定义两个变量
var first = "John";
var last = "Smith";
// 定义Person对象
let Person = {}
console.log(Person.name) // 输出:undefined。注释:此时Person对象的name属性还未定义。
// 给Person对象新增name属性,并赋值为'John Smith'
Person.name = first + ' ' + last
console.log(Person.name) // 输出:'John Smith'。注释:此时Person对象的name属性值为'John Smith'。
假如上面是一大段混淆后的 JS 代码,而请求必须要用到 Person.name
属性参数,但又不知道该参数在哪一行进行赋值的,无法定位,也就无法逆向该参数的组成。因此我们希望在定义 Person
对象之后,在 Person.name
属性参数赋值之前,插入一段代码用来监控 Person
对象的 name
属性的赋值情况,刚好在 JS 中就有这么一个 Object.defineProperty()
函数,它的作用就是添加、修改、获取、赋值对象上的属性。
Object.defineProperty(obj, prop, desc)
obj
需要定义属性的当前对象,这里可以传入例子中的Person
对象;prop
需要监控或控制的对象属性名,这里可以传入Person
对象的name
属性名,注意必须传入字符串,即传入'name'
值;desc
针对监控或控制属性的描述,具体可以参照下表;
描述 | 默认值 | 含义 |
---|---|---|
value | undefined | 给 obj 对象的 prop 属性赋值 |
writable | false | 默认 obj 对象的 prop 属性值不可写 |
get | undefined | 获取 obj 对象的 prop 属性时触发 |
set | undefined | 赋值 obj 对象的 prop 属性时触发 |
configrable | false | 默认 obj 对象的 prop 属性不可配置 |
enumerable | false | 默认 obj 对象的 prop 属性不可枚举 |
[!ATTENTION]
需要注意的访问器(
get
或set
)和值(value
)或可写属性(writable
)是互斥的,也就说两者只能留其一,否则就是无效描述。
这里我们举第一个例子进行说明:例子中同样定义了 Person
对象,和上面不同的是,Person
对象的 name
属性是通过 Object.defineProperty
函数进行添加的,也就意味着该函数拥有对 name
属性的支配权,在后面 {}
当中的描述中,value
描述给 name
属性赋值了 'John'
字符串,在后面也输出了该字符串,但是当我们修改 name
属性时,发现失败了,其原因就是 writable
描述默认为 false
,虽然它没有写出来,但效果是一样的,也就是 name
属性不可写。
// 定义Person对象
var Person = {};
// 添加Person对象的name属性
Object.defineProperty(Person, 'name', {
value: 'John', // 给Person对象的name属性赋值为'John'
});
console.log(Person.name); // 输出:'John'
person.name = 'Doe'; // 注释:尝试修改属性值。
console.log(Person.name); // 输出:'John'。注释:属性修改不成功。
这里我们举第二个例子进行说明:例子中同样定义了 Person
对象,和上面不同的是,writable
描述默认为 true
,也就意味着 Person
对象的 name
属性可写。
// 定义Person对象
var Person = {};
// 添加Person对象的name属性
Object.defineProperty(Person, 'name', {
value: 'John', // 给Person对象的name属性赋值为'John'
writable: true, // Person对象的name属性可以被改写
});
console.log(Person.name); // 输出:'John'
person.name = 'Doe'; // 注释:尝试修改属性值。
console.log(Person.name); // 输出:'Doe'。注释:属性修改成功。
这里我们举第三个例子进行说明:例子中使用了 get
描述,描述内容是一个函数,后续再获取 Person
对象的 name
属性时,就会触发该函数。
// 定义Person对象
let Person = {}
// 添加Person对象的name属性
Object.defineProperty(Person, 'name', {
// 获取Person.name属性时就会执行get里面的函数内容
get: function(){
return '你获取了Person对象的name属性'
}
});
console.log(Person.name); // 输出:'你获取了Person对象的name属性'
这里我们举第四个例子进行说明:和第三个例子相比,这里多了 set
描述,描述内容是一个函数,后续给 Person
对象的 name
属性赋值时,就会触发该函数。
// 定义Person对象
let Person = {}
let temp = null
// 添加Person对象的name属性
Object.defineProperty(Person, 'name', {
// 获取Person.name属性时就会执行get里面的函数内容
get: function(){
return temp
},
// 赋值Person.name属性时就会执行set里面的函数内容
set: function(val){
temp = val
}
});
console.log(Person.name); // 输出:null。注释:触发get描述函数,得到变量temp的值。
Person.name = 'Doe' // 注释:触发set描述函数,变量temp被重新赋值。
console.log(Person.name); // 输出:'Doe'。注释:触发get描述函数,得到变量temp被赋值后的值。
现在我们就可以正对上面的例子编写 Hook 逻辑了,具体代码如下:
// 给Person对象的name属性赋值时,会执行set函数里的debugger触发断点调试。
Object.defineProperty(Person, 'name', {
set: function(val) {
debugger;
}
})
Hook函数
**上面我们 Hook 了对象的属性,现在我们来 Hook 函数,实际上就是重写函数。注意一定要先定义,后重写,且是函数定义之后,函数调用之前重写才有意义。**代码如下:
// 新建变量old_func保留原有函数func
old_func = func
// 重写函数func
func = function(val){
debugger;
return old_func(val)
}
例如,我们来 Hook 一个函数,当函数的传入值等于 5 时,对其进行断点调试:
// 针对b的值等于5的时候进行Hook
old_b = b;
b = function(val){
if(val === 5){
debugger;
}
return old_b(val)
}
代码注入
当 Hook 代码写好以后,我们需要把它注入到浏览器执行 JS 的过程中,来监控或断点调式一些信息。
手动注入
**手动注入,就是在浏览器执行 JS 的时,在一个合适位置我们将其断住,通过浏览器的 Console 控制台或者在 Sources 资源面板中新建 Snippets
脚本片段将我们的 Hook 代码注入进去。**例如有下面一段 Hook 代码:
// 新建old_cookie属性,保存cookie属性值
document.old_cookie = document.cookie
// hook钩子document.cookie属性
Object.defineProperty(document, 'cookie', {
// 获取document.cookie属性时就会执行get里面的函数内容
get: function() {
debugger;
return document.old_cookie
}
})
- 通过
Console
控制台注入,我们打开开发者工具,点击Console
控制台,粘贴 Hook 代码,回车就将其注入进去了:
- 通过
Snippets
脚本片段注入,我们打开开发者工具,点击Sources
资源面板,在左侧选择Snippets
脚本片段,点击New snippets
新建脚本片段,粘贴 Hook 代码,点击右下角的Ctrl+Enter
按钮将 Hook 代码注入进去,注入后会在下方弹出Console
控制台提示注入情况:
当代码注入成功后,点击右上角的运行按钮,让浏览器继续执行网站的 JS 代码,等待 Hook 被触发。当 Hook 被触发后就直接进入了 get
函数里面的 debugger
了,在右侧 Call Stack
就能看到调用的堆栈,这样就实现了对 document.cookie
的监控:
自动注入
如果不想手动注入 Hook 代码,可以试试 Tampermonkey 油猴脚本。简单讲,就是一个给浏览器注入 JS 代码的脚本,支持Chrome(谷歌)、Firefox(火狐)等浏览器。安装油猴脚本,首先需要科学上网,进入Google搜索,搜索“Chrome应用商店”关键词:
进入商店后,输入 Tampermonkey 进行查找,点击第一个进行安装:
如果安装不成功,本地会有一个油猴脚本的 crx
文件:
我们打开chrome浏览器的“更多工具”中的“扩展程序”选项:
打开开发者模式,将 crx
文件拖入其中,油猴脚本就自动安装成功了:
接下来,我们点击扩展程序按钮,再点击固定按钮将油猴程序固定再工具栏:
现在我们点击图标,点击“添加新脚本”:
就会出现编辑脚本的页面:
这里给一个 Hook 的脚本案例:
// ==UserScript==
// @name 万能hook eval函数
// @namespace http://tampermonkey.net/
// @version 0.1
// @description eval-everything
// @author An-lan
// @include *
// @grant none
// @run-at document-start
// ==/UserScript==
alert('hook success');
var eval_bk = eval;
eval = function(val){
console.log(val)
return eval_bk(val);
};
eval.toString = function(){
return "function eval() { [native code] }"
};
eval.length = 1;
将脚本拷贝到油猴当中,Ctrl+S 进行保存,点击左边的按钮启用这样,油猴脚本就能自动对网站 JS 当中的 eval
函数进行 Hook 了。
缺陷弊端
- Hook 函数一般情况下不会出现 Hook 失败的情况,只有可能是
__proto__
模拟的不好导致被检测到了。 - 需要注意的是,同样的一段 Hook 代码只能注入一次,第二次注入的时候就会报错,如果想重新注入,只有刷新页面才行。
- **使用
Object.defineProperty
进行属性定义时,get
方法、set
方法它们用于定义属性的读取和写入行为,这两者通常是成对出现的。如果在使用时只定义了set
方法,而没有定义get
方法,可能会导致一些奇怪的行为。在某些情况下,可能期望能够读取属性的值,但由于没有定义get
方法,可能无法正确获取属性的值,从而导致一些问题。**例如,下面的例子中 Hook 函数中必须存在get
方法才能 Hook:
网站实践
控制台检测
题目难度:非常简单
首先,看题目是控制台呼出检测,说明是对控制台检测:
我们先不打开控制台,进入题目,发现需要 window.threshold
的值,带 window 说明该值是一个全局变量:
既然这道题针对的是 window.threshold
的值,我们就可以使用前面所学的 Hook 技术对该值进行监控:
// 针对window中的threshold属性值进行检测
Object.defineProperty(window, 'threshold', {
// 当threshold属性值被重新赋值时,执行set里面的函数
set: function(val) {
debugger;
return val;
}
})
接着,我们打开控制台时,页面跳转到了空白页,说明检测到了我们打开控制台的行为。这里我们要知道在网页上的行为事件都是通过 JS 实现的,例如鼠标单击事件、内容选中事件、加载事件等,因此控制台检测一定是通过 JS 实现。
既然是通过 JS 来检测事件行为的,那我们就在 Sources 资源面板右侧的 Event Listener Breakpoints
事件监听断点中勾选 Script
选项,对执行 JS 的位置打断点:
现在我们重新进入题目页面,勾选的 Script
事件断点就触发了,断在了执行第一个 JS 文件的第一行 JS 代码的位置:
**因为 window.threshold
是 window 全局对象的属性值,而 window 全局对象现在已定义,因此在这里我们就可以进行 Hook。**点击右上角的配置选项,点击里面的 Search 选项,调出下方的搜索栏:
点击下方的 Console 选项,手动注入 Hook 代码,取消前面我们勾选的 Script
事件断点,再点击右上方运行按钮,继续执行 JS 代码:
运行后,马上就被 Hook 住了,说明当前 JS 代码对全局变量 window.threshold
进行了赋值,看右侧的 Call Stack
回调栈位置在第一行,就是我们现在所在的函数 set
位置:
点击右侧的 Call Stack
回调栈第二行,回到上一层函数,发现 Hook 在了这里,我们在输出栏中打印出该信息:
_$ob('0x45') = 'threshold'
0xa0 = 160
动态sign值
题目难度:简单
访问题目获取题目信息,发现题目答案是一个固定时间戳对应的 sign
值,说明 Cookie 中的 sign
值是会随着时间戳的变化而变化的,因此接下来我们就必须要找到 sign
值的生成方法:
首先我们点击 Application 选项卡,点击 Cookies 中的网址,选中 sign
值右键点击 Delete 进行删除:
**现在 Cookies 中的 sign
值已经被删除,重新访问肯定会触发 sign
值的生成逻辑,那我们就重新打开一个浏览器选项卡,再打上 Script 事件来对重新访问进行监听。**可以看到该页面首先加载了 md5.js 资源,根据经验判断这和 sign
值得生成关系不大,我们点击右上角运行按钮继续:
出现了一个名称为 2 的文件,一看内容第三行是一个大数组说明也是经过了 ob
混淆的,里面的内容就需要进行分析了,现在就可以取消Script事件监听了:
方案一
将混淆的代码放入猿人学的 ob 反混淆工具中,注意将去掉首位 <script>
的标签,再进行反混淆:http://tool.yuanrenxue.com/decode_obfuscator
我们将解混淆后的代码拷贝下来,可以看到 Cookie 的 sign
值生成方法就在其中,而且可以看出变量 c
就是那个影响 sign
值的时间戳:
方案二
既然这个文件可能涉及 sign
值逻辑,那我们就可以在 Console 控制台中注入 Hook 代码来监控 Cookie 值:
Object.defineProperty(document, 'cookie', {
// 设置document.cookie属性时就会执行set里面的函数内容
set: function(cookie) {
debugger;
return cookie
}
})
点击运行按钮后,这时触发了混淆代码中设置的无限 debugger
,选择 debugger
一行左侧,然后右键选中 Never pause here
选项,即不在这里暂停:
点击继续运行,这时钩子函数被触发,说明 cookie
过期重新设置时,被钩住了:
右侧的 Call Stack
回调栈第一行是我们的钩子函数的名称 set
,第二行就是开始设置 cookie
的地方:
猿人学第5题
难度:中等
访问网址获取任务,并在任务当中已提示“cookie有效期仅有50秒钟”,说明有很大的可能性使用了 Cookie 加密,在 Network 里面的 Fetch/XHR 选项中定位到了该网页数据的来源请求,继续分析:
多次访问前面 3 页的页面,分析比较请求头参数并结合以往爬虫经验,可以得出初步接结论:变动的加密参数有两大部分,cookie 加密参数和 get 加密参数。
# cookie加密参数
m:未知加密参数
RM4hZBv0dDon443M:未知加密参数
# get加密参数
page: 页码
m: 和时间戳有关
f: 和时间戳有关
**从上面的初步结论也可以看出来,get 加密参数比较简单,我们从简单的开始弄。**现在我们需要定位到,哪一行的代码发送了当前的请求,点击左侧的 Initiator 选项,它主要是标记请求是由哪个对象或进程发起的(请求源),重点关注里面的request请求,点击后面的地址:
跳转到对应的文件的指定位置,在上面可以看到三个熟悉的 get 请求参数:
可以看到 m
参数的对应值 window._$is
和 f
参数的对应值 window.$_zw[23]
,全局搜索 window.$_zw
有四处,经过断点调试发现 window.$_zw
原本是一个空列表,当中所有的元素来源于下面 $_aiding.$_zw.push(元素)
填充:
$_t1
这个变量恰好就在上面,那么就有 f = window.$_zw[23] = $_t1
这个关系,这样我们就破解了第一个 get 加密的 f
参数:
**然而在当前文件当中搜索并未找到该参数 m
即 window._$is
的赋值过程,就暂时放下,去定位 cookie 加密参数的位置。**任务中有提示“cookie 有效期仅有50秒钟”,也就是距离上次请求时间过去 50 秒后,这时我们去请求其他页面必定会有一个对 cookie 重新赋值的过程。这时回到我们的抓包工具,对流程进行分析:
**通过对比请求,发现 cookie 参数改变发生在 loginInfor
请求和 /api/match/5
请求之间,然而中并没有能导致 cookie 改变的请求或其他的js文件,那么就有可能是虚拟机产生了临时的 JS 文件改变了 cookie,然而这种临时文件是抓包工具抓不到的,说明在肯定在 JS 代码中执行了 eval
方法。**现在我们回到 JS 文件当中,在所有执行 eval
方法的地方打上断点,断点过来后,我们点击执行下一步的操作按钮:
果然就跳进了临时 JS 文件 VM 当中:
**我们继续在浏览器中进行逆向代码的调试,因为浏览器中已经加载好环境、函数、变量,是最好的调试场所。**点击 {}
将 VM 文件格式化后,得到如下代码:
可以看到这也是经过 ob 混淆后的代码,如果我们硬着头皮去找 cookie 的两个加密参数肯定不好找,因此我们这里就要使用 Hook 技术来获取我们想要的值。现在我们在 VM 文件第一行处打上断点,当断点过来过后,由于 cookie 加密参数 RM4hZBv0dDon443M
相比于加密参数 m
的名字更好定位,因此我们在 Console 控制台中注入以下 Hook 代码,通过方法 indexOf
监控 Cookie 中的 RM4hZBv0dDon443M
的值,若没有值出现,则该方法返回 -1 值:
// hook cookie字段RM4hZBv0dDon443M
(function () {
Object.defineProperty(document, 'cookie', {
set: function (cookie) {
// 方法indexOf检索字符串的值若没有出现,则该方法返回-1。
if(cookie.indexOf('RM4hZBv0dDon443M') != -1){
debugger;
}
return cookie;
}
})
})();
继续运行 JS 代码,马上 Hook 就被触发了,只不过当前 cookie 中 RM4hZBv0dDon443M
字段还未被定义,其方法 indexOf
返回值等于 0,所以触发了 Hook,没关系点击右侧按钮继续运行:
经过了几次 Hook 触发,终于 RM4hZBv0dDon443M
字段被赋值了,这时关注右侧边栏中 Call Stack
调用堆栈,这里可以看到函数的调用顺序,我们点击第二个栏里面的函数,即回到上一层函数中:
回到上一层函数发现 RM4hZBv0dDon443M
字段是通过字符拼接的方式生成的,所以这也给了我们一个经验,关键的字符串在代码中不一定都是现成的,也可能是通过拼接生成:
同时也说明了 _0x3d0f3f[_$Fe] = document['cookie']
这个关系:
现在可以看出 RM4hZBv0dDon443M
字段值等于 _0x4e96b4['_$ss']
的值,通过调试发现 _0x4e96b4
就是 window
窗口,但搜索没有发现给 _$ss
全局变量属性赋值:
那我们再对 window._$ss
全局变量属性进行监控,再次注入以下 Hook 代码:
(function () {
Object.defineProperty(window, '_$ss', {
set: function (window) {
debugger;
return window;
}
})
})();
继续运行 JS 文件,Hook 被触发,继续关注 Call Stack
调用堆栈中的第二栏:
点击跳转后,发现 _$ss
这个变量名称竟然也是通过拼接生成的,其值等于 _0x29dd83[toString]()
的值:
现在关注点变到 _0x29dd83
函数上了,刚好函数就在上方,我们将所有未知参数进行打印:
最终解析出来的代码如下,是一个AES 加密:
_$Ww = CryptoJS['enc']['Utf8']['parse'](_0x4e96b4['_$pr']['toString']());
_0x29dd83 = CryptoJS['AES']['encrypt'](_$Ww, _0x4e96b4['_$qF'], {
'mode': CryptoJS['mode']['ECB'],
'padding': CryptoJS['pad']['Pkcs7']
});
_0x4e96b4['_$ss'] = _0x29dd83['toString']();
通过打印 _0x4e96b4['_$pr']
是一个含有 5 个字符串数组的列表:
全局搜索发现初始的 _0x4e96b4['_$pr']
是个空数组:
将有关 _0x4e96b4['_$pr']
的赋值操作全部打上断点,发现在 1717 行执行了 4 次 push
操作,也就是往里面推了 4 个字符串:
在第 868 行执行了 1 次 push
操作,往 _0x4e96b4['_$pr']
里面推了 1 个字符串,加上上面的 4 个字符串刚好就是 5 个字符串串,而且可以发现 866 行 m
的值和第 868 行推的字符串的值相等,顺带就把 cookie 当中加密参数 m
给解决了:
现在就是扣取和 _0x4e96b4['_$pr']
该变量有关的代码了,**这步需要不断的调试、替换,保持耐心。**接下来就是 _0x4e96b4['_$qF']
这个变量了,通过全局搜索只有两个地方,断点显示走的上部分代码,将其他未知变量打印出来。在最后面有一个 ['slice']
含义就是将前面的字符串进行切片,(0x0, 0x10)
含义就是范围 0 到 16,其中 0x
代表十六进制:
接下来就是找 _0x4e96b4['_$is']
这个变量了,全局搜索发现 3 个位置,断点显示在 867 行进行了赋值:
在扣取上面 _0x4e96b4['_$pr']
相关代码,就会发现 _$yw
这个变量已经扣过了:
这下就可以宣告 RM4hZBv0dDon443M
加密参数和 m
加密参数已经全部知晓了。现在我们搞定了 cookie 的加密参数,接下来就是回看 get 加密的 m
参数:
"m": window._$is
对照 VM 虚拟文件内容发现 window._$is = _0x4e96b4['_$is']
关系,m
参数的值就是一个时间戳:
建议
本题结果的值受到三个变量 _0x4e96b4['_$Jy']
、_0x4e96b4['_$tT']
、_0x4e96b4['_$6_']
的影响,在调试的时候,和这三个变量赋值的相关地方都要打上断点,观察其变化。
警告
本题有一个坑,就是加密参数和时间戳相关,因此最后的结果要一次性全部生成,不可写成两个函数分别调用返回。