Skip to content

Hook代码注入

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

前面我们见识了可读性很低的混淆 JS 代码,得益于反混淆工具将其还原可读性很高的代码,才使得我们逆向之路顺畅了不少。但是反混淆工具也不是万能的,假如混淆的 JS 代码特别多,逻辑特别复杂,它还能 100% 还原代码吗?这恐怕需要打一个问号。通过前面的学习,其实我们可以大体能看到,JS 逆向不需要去了解每一行代码,只要找到关键的加密位置,逆向就能迎刃而解。这里我们就要学习一项代码逻辑定位技术,Hook代码注入。

Hook技术

**Hook 技术,当中的 Hook 是“钩、钩子”的意思,又叫做 Hook 钩子函数,其实就是一小段代码。**在系统没有调用该函数之前,钩子函数就先捕获该消息得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。**简单来说,就是在程序执行的过程中插入我们自己的代码片段,便于我们分析 JS 逻辑,找到函数入口以及一些参数变化,但是 Hook 的能力远不及此,我们只借助其寻找函数入口。**Hook 步骤的大体流程如下:

  1. 寻找 Hook 点,首先我们要分析确定需要 Hook 的内容;
  2. 编写 Hook 逻辑,针对需要 Hook 的内容,编写 Hook 代码;
  3. 注入 Hook 代码,判断对象的性质,在合适的位置将 Hook 代码注入进去,继续运行代码等待 Hook 被触发。
  4. 触发跟栈调试,当钩子函数被触发时,就执行我们定义的钩子函数的逻辑,这时我们就可以往上跟栈调试。

提醒

客户端拥有 JS 的最高解释权,可以决定在任何时候注入 JS,而服务器只能通过检测和混淆手段令 Hook 难度加大,但无法阻止。

警告

注入的 Hook 代码仅当前页面有效使用,如果刷新页面或者打开的新的标签页,需要重新注入 Hook 代码。

Hook属性

首先,我们来看一个很简单的例子:新建一个对象 Person 添加 name 属性并赋值,然后输出该属性的值。

javascript
// 定义两个变量
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() 函数,它的作用就是添加、修改、获取、赋值对象上的属性。

javascript
Object.defineProperty(obj, prop, desc)
  • obj 需要定义属性的当前对象,这里可以传入例子中的 Person 对象;
  • prop 需要监控或控制的对象属性名,这里可以传入 Person 对象的 name 属性名,注意必须传入字符串,即传入 'name' 值;
  • desc 针对监控或控制属性的描述,具体可以参照下表;
描述默认值含义
valueundefinedobj 对象的 prop 属性赋值
writablefalse默认 obj 对象的 prop 属性值不可写
getundefined获取 obj 对象的 prop 属性时触发
setundefined赋值 obj 对象的 prop 属性时触发
configrablefalse默认 obj 对象的 prop 属性不可配置
enumerablefalse默认 obj 对象的 prop 属性不可枚举

[!ATTENTION]

需要注意的访问器(getset)和值(value)或可写属性(writable)是互斥的,也就说两者只能留其一,否则就是无效描述。

这里我们举第一个例子进行说明:例子中同样定义了 Person 对象,和上面不同的是,Person 对象的 name 属性是通过 Object.defineProperty 函数进行添加的,也就意味着该函数拥有对 name 属性的支配权,在后面 {} 当中的描述中,value 描述给 name 属性赋值了 'John' 字符串,在后面也输出了该字符串,但是当我们修改 name 属性时,发现失败了,其原因就是 writable 描述默认为 false,虽然它没有写出来,但效果是一样的,也就是 name 属性不可写。

javascript
// 定义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 属性可写。

javascript
// 定义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 属性时,就会触发该函数。

javascript
// 定义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 属性赋值时,就会触发该函数。

javascript
// 定义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 逻辑了,具体代码如下:

javascript
// 给Person对象的name属性赋值时,会执行set函数里的debugger触发断点调试。
Object.defineProperty(Person, 'name', {
    set: function(val) {
		debugger;
    }
})

Hook函数

**上面我们 Hook 了对象的属性,现在我们来 Hook 函数,实际上就是重写函数。注意一定要先定义,后重写,且是函数定义之后,函数调用之前重写才有意义。**代码如下:

javascript
// 新建变量old_func保留原有函数func
old_func = func
// 重写函数func
func = function(val){
	debugger;
	return old_func(val)
}

例如,我们来 Hook 一个函数,当函数的传入值等于 5 时,对其进行断点调试:

javascript
// 针对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 代码:

javascript
// 新建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 代码,回车就将其注入进去了:

image-20240106225919071

  • 通过 Snippets 脚本片段注入,我们打开开发者工具,点击 Sources 资源面板,在左侧选择 Snippets 脚本片段,点击 New snippets 新建脚本片段,粘贴 Hook 代码,点击右下角的 Ctrl+Enter 按钮将 Hook 代码注入进去,注入后会在下方弹出 Console 控制台提示注入情况:

image-20240106230759310

当代码注入成功后,点击右上角的运行按钮,让浏览器继续执行网站的 JS 代码,等待 Hook 被触发。当 Hook 被触发后就直接进入了 get 函数里面的 debugger 了,在右侧 Call Stack 就能看到调用的堆栈,这样就实现了对 document.cookie 的监控:

20220226013138

自动注入

如果不想手动注入 Hook 代码,可以试试 Tampermonkey 油猴脚本。简单讲,就是一个给浏览器注入 JS 代码的脚本,支持Chrome(谷歌)、Firefox(火狐)等浏览器。安装油猴脚本,首先需要科学上网,进入Google搜索,搜索“Chrome应用商店”关键词:

20220226134649

进入商店后,输入 Tampermonkey 进行查找,点击第一个进行安装:

20220226135524

如果安装不成功,本地会有一个油猴脚本的 crx 文件:

20220226140227

我们打开chrome浏览器的“更多工具”中的“扩展程序”选项:

20220226140212

打开开发者模式,将 crx 文件拖入其中,油猴脚本就自动安装成功了:

20220226140606

接下来,我们点击扩展程序按钮,再点击固定按钮将油猴程序固定再工具栏:

20220226140720

现在我们点击图标,点击“添加新脚本”:

20220226140955

就会出现编辑脚本的页面:

20220226141044

这里给一个 Hook 的脚本案例:

javascript
// ==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 了。

20220226141756

缺陷弊端

  1. Hook 函数一般情况下不会出现 Hook 失败的情况,只有可能是 __proto__ 模拟的不好导致被检测到了。
  2. 需要注意的是,同样的一段 Hook 代码只能注入一次,第二次注入的时候就会报错,如果想重新注入,只有刷新页面才行。

20220226015318

  1. **使用 Object.defineProperty 进行属性定义时,get 方法、set 方法它们用于定义属性的读取和写入行为,这两者通常是成对出现的。如果在使用时只定义了 set 方法,而没有定义 get 方法,可能会导致一些奇怪的行为。在某些情况下,可能期望能够读取属性的值,但由于没有定义 get 方法,可能无法正确获取属性的值,从而导致一些问题。**例如,下面的例子中 Hook 函数中必须存在 get 方法才能 Hook:

20221028181921

20221028182149

网站实践

控制台检测

题目难度:非常简单

首先,看题目是控制台呼出检测,说明是对控制台检测:

20220227003148

我们先不打开控制台,进入题目,发现需要 window.threshold 的值,带 window 说明该值是一个全局变量:

20220227003255

既然这道题针对的是 window.threshold 的值,我们就可以使用前面所学的 Hook 技术对该值进行监控:

javascript
// 针对window中的threshold属性值进行检测
Object.defineProperty(window, 'threshold', {
    // 当threshold属性值被重新赋值时,执行set里面的函数
    set: function(val) {
		debugger;
        return val;
    }
})

接着,我们打开控制台时,页面跳转到了空白页,说明检测到了我们打开控制台的行为。这里我们要知道在网页上的行为事件都是通过 JS 实现的,例如鼠标单击事件、内容选中事件、加载事件等,因此控制台检测一定是通过 JS 实现。

20220227003527

既然是通过 JS 来检测事件行为的,那我们就在 Sources 资源面板右侧的 Event Listener Breakpoints 事件监听断点中勾选 Script 选项,对执行 JS 的位置打断点:

20220227003959

现在我们重新进入题目页面,勾选的 Script 事件断点就触发了,断在了执行第一个 JS 文件的第一行 JS 代码的位置:

20220301143441

**因为 window.threshold 是 window 全局对象的属性值,而 window 全局对象现在已定义,因此在这里我们就可以进行 Hook。**点击右上角的配置选项,点击里面的 Search 选项,调出下方的搜索栏:

20220301143755

点击下方的 Console 选项,手动注入 Hook 代码,取消前面我们勾选的 Script 事件断点,再点击右上方运行按钮,继续执行 JS 代码:

20220227004917

运行后,马上就被 Hook 住了,说明当前 JS 代码对全局变量 window.threshold 进行了赋值,看右侧的 Call Stack 回调栈位置在第一行,就是我们现在所在的函数 set 位置:

20220301144530

点击右侧的 Call Stack 回调栈第二行,回到上一层函数,发现 Hook 在了这里,我们在输出栏中打印出该信息:

javascript
_$ob('0x45') = 'threshold'
0xa0 = 160

20220227020545

动态sign值

题目难度:简单

访问题目获取题目信息,发现题目答案是一个固定时间戳对应的 sign 值,说明 Cookie 中的 sign 值是会随着时间戳的变化而变化的,因此接下来我们就必须要找到 sign 值的生成方法:

20220226233129

首先我们点击 Application 选项卡,点击 Cookies 中的网址,选中 sign 值右键点击 Delete 进行删除:

20220308004004

**现在 Cookies 中的 sign 值已经被删除,重新访问肯定会触发 sign 值的生成逻辑,那我们就重新打开一个浏览器选项卡,再打上 Script 事件来对重新访问进行监听。**可以看到该页面首先加载了 md5.js 资源,根据经验判断这和 sign 值得生成关系不大,我们点击右上角运行按钮继续:

20220308010527

出现了一个名称为 2 的文件,一看内容第三行是一个大数组说明也是经过了 ob 混淆的,里面的内容就需要进行分析了,现在就可以取消Script事件监听了:

20220308011011

方案一

将混淆的代码放入猿人学的 ob 反混淆工具中,注意将去掉首位 <script> 的标签,再进行反混淆:http://tool.yuanrenxue.com/decode_obfuscator

20220227000747

我们将解混淆后的代码拷贝下来,可以看到 Cookie 的 sign 值生成方法就在其中,而且可以看出变量 c 就是那个影响 sign 值的时间戳:

20220227001036

方案二

既然这个文件可能涉及 sign 值逻辑,那我们就可以在 Console 控制台中注入 Hook 代码来监控 Cookie 值:

javascript
Object.defineProperty(document, 'cookie', {
    // 设置document.cookie属性时就会执行set里面的函数内容
    set: function(cookie) {
        debugger;
        return cookie
	}
})

20220226235648

点击运行按钮后,这时触发了混淆代码中设置的无限 debugger,选择 debugger 一行左侧,然后右键选中 Never pause here 选项,即不在这里暂停:

20220308011546

点击继续运行,这时钩子函数被触发,说明 cookie 过期重新设置时,被钩住了:

20220308012428

右侧的 Call Stack 回调栈第一行是我们的钩子函数的名称 set,第二行就是开始设置 cookie 的地方:

20220308012538

猿人学第5题

难度:中等

访问网址获取任务,并在任务当中已提示“cookie有效期仅有50秒钟”,说明有很大的可能性使用了 Cookie 加密,在 Network 里面的 Fetch/XHR 选项中定位到了该网页数据的来源请求,继续分析:

20211110142117

多次访问前面 3 页的页面,分析比较请求头参数并结合以往爬虫经验,可以得出初步接结论:变动的加密参数有两大部分,cookie 加密参数和 get 加密参数。

# cookie加密参数
m:未知加密参数
RM4hZBv0dDon443M:未知加密参数

# get加密参数
page: 页码
m: 和时间戳有关
f: 和时间戳有关

20211110142616

**从上面的初步结论也可以看出来,get 加密参数比较简单,我们从简单的开始弄。**现在我们需要定位到,哪一行的代码发送了当前的请求,点击左侧的 Initiator 选项,它主要是标记请求是由哪个对象或进程发起的(请求源),重点关注里面的request请求,点击后面的地址:

20211110143458

跳转到对应的文件的指定位置,在上面可以看到三个熟悉的 get 请求参数:

20211110143801

可以看到 m 参数的对应值 window._$isf 参数的对应值 window.$_zw[23],全局搜索 window.$_zw 有四处,经过断点调试发现 window.$_zw 原本是一个空列表,当中所有的元素来源于下面 $_aiding.$_zw.push(元素) 填充:

20211114173618

$_t1 这个变量恰好就在上面,那么就有 f = window.$_zw[23] = $_t1 这个关系,这样我们就破解了第一个 get 加密的 f 参数:

20211114174044

**然而在当前文件当中搜索并未找到该参数 mwindow._$is 的赋值过程,就暂时放下,去定位 cookie 加密参数的位置。**任务中有提示“cookie 有效期仅有50秒钟”,也就是距离上次请求时间过去 50 秒后,这时我们去请求其他页面必定会有一个对 cookie 重新赋值的过程。这时回到我们的抓包工具,对流程进行分析:

20211110154246

**通过对比请求,发现 cookie 参数改变发生在 loginInfor 请求和 /api/match/5 请求之间,然而中并没有能导致 cookie 改变的请求或其他的js文件,那么就有可能是虚拟机产生了临时的 JS 文件改变了 cookie,然而这种临时文件是抓包工具抓不到的,说明在肯定在 JS 代码中执行了 eval 方法。**现在我们回到 JS 文件当中,在所有执行 eval 方法的地方打上断点,断点过来后,我们点击执行下一步的操作按钮:

20211110182653

果然就跳进了临时 JS 文件 VM 当中:

20211110182839

**我们继续在浏览器中进行逆向代码的调试,因为浏览器中已经加载好环境、函数、变量,是最好的调试场所。**点击 {} 将 VM 文件格式化后,得到如下代码:

20211114010459

可以看到这也是经过 ob 混淆后的代码,如果我们硬着头皮去找 cookie 的两个加密参数肯定不好找,因此我们这里就要使用 Hook 技术来获取我们想要的值。现在我们在 VM 文件第一行处打上断点,当断点过来过后,由于 cookie 加密参数 RM4hZBv0dDon443M 相比于加密参数 m 的名字更好定位,因此我们在 Console 控制台中注入以下 Hook 代码,通过方法 indexOf 监控 Cookie 中的 RM4hZBv0dDon443M 的值,若没有值出现,则该方法返回 -1 值:

javascript
// hook cookie字段RM4hZBv0dDon443M
(function () {
   Object.defineProperty(document, 'cookie', {
       set: function (cookie) {
           // 方法indexOf检索字符串的值若没有出现,则该方法返回-1。
           if(cookie.indexOf('RM4hZBv0dDon443M') != -1){
                debugger;
           }
           return cookie;
       }
   })
})();

20211114015531

继续运行 JS 代码,马上 Hook 就被触发了,只不过当前 cookie 中 RM4hZBv0dDon443M 字段还未被定义,其方法 indexOf 返回值等于 0,所以触发了 Hook,没关系点击右侧按钮继续运行

20211114020221

经过了几次 Hook 触发,终于 RM4hZBv0dDon443M 字段被赋值了,这时关注右侧边栏中 Call Stack 调用堆栈,这里可以看到函数的调用顺序,我们点击第二个栏里面的函数,即回到上一层函数中:

20211114022114

回到上一层函数发现 RM4hZBv0dDon443M 字段是通过字符拼接的方式生成的,所以这也给了我们一个经验,关键的字符串在代码中不一定都是现成的,也可能是通过拼接生成:

20211114023219

同时也说明了 _0x3d0f3f[_$Fe] = document['cookie'] 这个关系:

20211114161419

现在可以看出 RM4hZBv0dDon443M 字段值等于 _0x4e96b4['_$ss'] 的值,通过调试发现 _0x4e96b4 就是 window 窗口,但搜索没有发现给 _$ss 全局变量属性赋值:

20211114023732

那我们再对 window._$ss 全局变量属性进行监控,再次注入以下 Hook 代码:

javascript
(function () {
   Object.defineProperty(window, '_$ss', {
       set: function (window) {
           debugger;
           return window;
       }
   })
})();

20211114024758

继续运行 JS 文件,Hook 被触发,继续关注 Call Stack 调用堆栈中的第二栏:

20211114024935

点击跳转后,发现 _$ss 这个变量名称竟然也是通过拼接生成的,其值等于 _0x29dd83[toString]() 的值:

20211114025301

现在关注点变到 _0x29dd83 函数上了,刚好函数就在上方,我们将所有未知参数进行打印:

20211114030623

最终解析出来的代码如下,是一个AES 加密:

javascript
_$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 个字符串数组的列表:

20211114153628

全局搜索发现初始的 _0x4e96b4['_$pr'] 是个空数组:

20211114154448

将有关 _0x4e96b4['_$pr'] 的赋值操作全部打上断点,发现在 1717 行执行了 4 次 push 操作,也就是往里面推了 4 个字符串:

20211114154209

在第 868 行执行了 1 次 push 操作,往 _0x4e96b4['_$pr'] 里面推了 1 个字符串,加上上面的 4 个字符串刚好就是 5 个字符串串,而且可以发现 866 行 m 的值和第 868 行推的字符串的值相等,顺带就把 cookie 当中加密参数 m 给解决了

20211122150421

现在就是扣取和 _0x4e96b4['_$pr'] 该变量有关的代码了,**这步需要不断的调试、替换,保持耐心。**接下来就是 _0x4e96b4['_$qF'] 这个变量了,通过全局搜索只有两个地方,断点显示走的上部分代码,将其他未知变量打印出来。在最后面有一个 ['slice'] 含义就是将前面的字符串进行切片,(0x0, 0x10) 含义就是范围 0 到 16,其中 0x 代表十六进制:

20211114155646

接下来就是找 _0x4e96b4['_$is'] 这个变量了,全局搜索发现 3 个位置,断点显示在 867 行进行了赋值:

20211114160154

在扣取上面 _0x4e96b4['_$pr'] 相关代码,就会发现 _$yw 这个变量已经扣过了:

20211114160437

这下就可以宣告 RM4hZBv0dDon443M 加密参数和 m 加密参数已经全部知晓了。现在我们搞定了 cookie 的加密参数,接下来就是回看 get 加密的 m 参数:

"m": window._$is

对照 VM 虚拟文件内容发现 window._$is = _0x4e96b4['_$is'] 关系,m 参数的值就是一个时间戳:

20211114165454

20211114160154

20211114160437

建议

本题结果的值受到三个变量 _0x4e96b4['_$Jy']_0x4e96b4['_$tT']_0x4e96b4['_$6_']的影响,在调试的时候,和这三个变量赋值的相关地方都要打上断点,观察其变化。

警告

本题有一个坑,就是加密参数和时间戳相关,因此最后的结果要一次性全部生成,不可写成两个函数分别调用返回。