简单来说,js hook 就是通过修改 javascript 代码,改变原有的代码执行流程,得到我们希望的结果
javascript 代码是在我们本地浏览器执行的,因此从理论上来说,无论执行流程多么长,多么复杂,我们想让其在某个位置停下,代码就得停下,之后我们进行一顿操作:修改变量的值、修改函数执行逻辑、修改类的原型等,之后代码根据我们的修改继续执行下去
这里引用K哥爬虫文章中的一个比喻,我觉得很恰当
通俗来讲,Hook 其实就是拦路打劫,马邦德带着老婆,出了城,吃着火锅,还唱着歌,突然就被麻匪劫了,张麻子劫下县长马邦德的火车,摇身一变化身县长,带着手下赶赴鹅城上任。Hook 的过程,就是张麻子顶替马邦德的过程。
https://segmentfault.com/a/1190000040756228
这是一段大家经常在 XSS 中用来绕过 WAF 的语句
x"; var a = alert; a(1); var b = "
其实本质上就是一种 hook,只不过我们没有对 alert 做额外的操作来改变执行流程而已
这里主要谈 hook 对我们在进行 js 逆向过程中的意义
很多时候我们需要确定某个特定的值是如何生成的,例如请求数据的签名值 sign
即使请求参数没有加密,但是在请求数据包中的 Cookie、Header、URL 等部分加入了基于请求数据的加密签名值,这会导致我们在请求数据中加入 payload 后,签名验证不通过
通过对特定函数的 hook 可以有效监控特定值的添加位置,此时通过断点加上跟栈分析就可以有效追踪该签名值的生成过程
其实之前在《Linux 后门系列》文章中就涉及到赋值替换的 hook 技术
上面举的关于XSS绕过waf的例子也是赋值替换的方法,但不够完整,现在我们尝试完善它
hook alert函数
首先,我们需要先找到 alert 函数的定义
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/alert
alert(message)
参数
message 可选
要显示在警告对话框中的字符串,如果传入其他类型的值,会转换成字符串。
返回值
无(undefined)
// 备份需要 hook 的方法
const originalAlert = window.alert;// 定义钩子函数
window.alert = function () {
// 方法执行前执行的内容
console.log('alert 初始化');
// 执行原函数
originalAlert.apply(this, arguments);
// 方法执行后执行的内容
console.log('alert 执行结束');
};
// 防止钩子被检测
window.alert.toString = function () {
return "function alert() { [native code] }";
};
我们让 chatgpt 帮我们生成 hook 代码
hook 前的 alert("hello")
hook 后的 alert("hello")
这样我们就可以针对性的进行相关的检测和修改了
我希望在 alert("hello") 的时候进入 debugger
const originalAlert = window.alert;window.alert = function (...args) {
const message = args[0];
if (message === "hello") {
console.log("调试信息:参数值为 'hello'");
debugger;
}
originalAlert.apply(this, args);
};
相信你看到这里已经知道我们为什么要 hook 了,当然不知道也没关系,后面会举例说明
直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Object.defineProperty(obj, prop, descriptor)
obj
要定义属性的对象。
prop
一个字符串或 Symbol,指定了要定义或修改的属性键。
descriptor
要定义或修改的属性的描述符。
相比于直接给对象的属性赋值,Object.defineProperty 方法提供了更细粒度的属性控制和定制能力
对象中存在的属性描述符有两种主要类型:数据描述符和访问器描述符。数据描述符是一个具有可写或不可写值的属性。访问器描述符是由 getter/setter 函数对描述的属性。描述符只能是这两种类型之一,不能同时为两者(常规情况)。
举个例子
const obj = {}; // 创建一个空对象Object.defineProperty(obj, 'name', {
value: 'hello', // 属性的值
writable: true, // 属性是否可写,默认为 false
enumerable: true, // 属性是否可枚举,默认为 false
configurable: true // 属性是否可配置,默认为 false
});
相比于 obj.name='hello',还可以通过 descriptor 变量设置该属性是否可写、可枚举、可配置的描述符
writable 如果设置为 false,则属性的值不可修改enumerable 如果设置为 true,则属性可以被 for...in 循环或 Object.keys() 获取到configurable 如果设置为 false,则属性的描述符不可更改,也不能被删除如果设置该属性为不可写
如果设置该属性的描述符为不可更改
这里可以发现一个例外,就是设置为不可配置时,仍然可以将可写属性由 true 配置为 false,反过来则不行
举个例子
var obj = {
_value: 0,
// 定义访问器属性
get value() {
console.log("Getting value");
return this._value;
},
set value(newValue) {
console.log("Setting value to", newValue);
this._value = newValue;
}
};console.log(obj.value); // 输出: Getting value, 0
obj.value = 5; // 输出: Setting value to 5
console.log(obj.value); // 输出: Getting value, 5
obj 对象的 _value 属性为数据属性;value 属性为访问器属性

可以通过 Object.defineProperty 来定义一个访问器描述符的属性 value
var obj = {
_value: 0
};Object.defineProperty(obj, "value", {
get: function() {
console.log("Getting value");
return this._value;
},
set: function(newValue) {
console.log("Setting value to", newValue);
this._value = newValue;
}
});
console.log(obj.value); // 输出: Getting value, 0
obj.value = 5; // 输出: Setting value to 5
console.log(obj.value); // 输出: Getting value, 5
document.cookie 是一个特殊的属性,它既不是典型的数据属性也不是典型的访问器属性。它是一个混合类型的属性,可以被视为既包含数据属性的特征,又包含访问器属性的特征。
也就是说我们可以设置 set 描述符,进而监控 cookie 中值的添加与修改
(function() {
"use strict"; var cookieTemp = "";
Object.defineProperty(document, "cookie", {
writable: false, // 表示能否修改属性的值,即值是可写的还是只读
configurable: false, // 表示能否通过 delete 删除属性、能否修改属性的特性,或者将属性修改为访问器属性
set: function(val) {
if (val.indexOf("cookie的参数名称") !== -1) {
debugger;
}
console.log("Hook捕获Cookie设置 ->", val);
cookieTemp = val;
return val;
},
get: function() {
return cookieTemp;
}
});
})();
// 代码来自 https://blog.csdn.net/Python_DJ/article/details/125360704
假设网站通过 javascript 添加了一个 cookie 名为 sign ,那么程序就会进入 debugger
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
代理是 javaScript 中的一种高级特性,它允许你拦截并自定义对目标对象的操作。通过使用代理,你可以在目标对象的操作前后注入自己的逻辑,或者修改默认的行为。这为你提供了更大的灵活性和控制力。
proxy 这个特性看官方介绍很容易看迷糊,其实非常简单,我们定义了一个对象,其中包含一些属性,之后根据需要对这些属性进行增删改查,如果我们不满足于当前的这些功能,对其进行修改,就用 proxy 来进行处理
所以其实 proxy 就是一种 hook
const p = new Proxy(target, handler)
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
在 handler 中我们可以对各种方法进行修改,例如我们之前遇到的 get、set等,这些方法称为捕获器(trap)
举个例子大家就能明白了
var target = {
name: "John",
age: 30
};var handler = {
get: function(target, property) {
console.log("正在读取属性: " + property);
return target[property];
},
set: function(target, property, value) {
console.log("正在设置属性: " + property + ",值为: " + value);
target[property] = value;
}
};
var proxy = new Proxy(target, handler);
console.log(proxy.name); // 输出: 正在读取属性: name \n John
proxy.age = 35; // 输出: 正在设置属性: age,值为: 35
这里基于对象 target 创建了代理对象 proxy , proxy 中的 handler 针对 get 和 set 方法进行了设置,除了基本的功能以外,还添加了打印输出功能
这里有一点需要注意,创建的 proxy 对象并不是将原本的 target 对象复制了一遍。对于proxy 属性的修改也会同步作用在 target 上
接下来我们尝试通过 proxy 的方法监控 Cookie 的添加,这是一个简单的 demo
// 创建一个代理对象,用于拦截 document.cookie 的 get 和 set 方法
var cookieProxy = new Proxy(document, {
get: function(target, property, receiver) {
if (property === 'cookie') {
console.log('正在读取 cookie');
// 返回原始的 document.cookie 值
return target.cookie;
}
// 对于其他属性,直接返回原始对象的属性值
return Reflect.get(target, property, receiver);
},
set: function(target, property, value, receiver) {
if (property === 'cookie') {
console.log('正在设置 cookie,值为:', value);
// 设置原始的 document.cookie 值
target.cookie = value;
// 返回设置后的值
return true;
}
// 对于其他属性,直接设置原始对象的属性值
return Reflect.set(target, property, value, receiver);
}
});// 使用代理对象访问 document.cookie
console.log(cookieProxy.cookie); // 输出: 正在读取 cookie \n <当前 cookie 值>
cookieProxy.cookie = 'username=John'; // 输出: 正在设置 cookie,值为: username=John
我们在通过 cookieProxy.cookie = 'username=John'添加 cookie 时被成功被捕获。但是通过 document.cookie = 'pass=123' 添加 cookie 时并未被捕获到,毕竟我们不是通过代理访问的嘛
在js逆向的使用场景中,显然后续代码不可能是使用的我们的代理对象,所以我们尝试以下代码
// 创建一个代理对象,用于拦截 document.cookie 的 get 和 set 方法
document = new Proxy(document, {
get: function(target, property, receiver) {
if (property === 'cookie') {
console.log('正在读取 cookie');
// 返回原始的 document.cookie 值
return target.cookie;
}
// 对于其他属性,直接返回原始对象的属性值
return Reflect.get(target, property, receiver);
},
set: function(target, property, value, receiver) {
if (property === 'cookie') {
console.log('正在设置 cookie,值为:', value);
// 设置原始的 document.cookie 值
target.cookie = value;
// 返回设置后的值
return true;
}
// 对于其他属性,直接设置原始对象的属性值
return Reflect.set(target, property, value, receiver);
}
});// 使用代理对象访问 document.cookie
console.log(document.cookie);
document.cookie = 'username=John';
既然我们是对 document 对象创建的代理,我们直接将代理的名字设置为 document,这样就直接覆盖了之前的 document
显然,我们这个想法有点天真了,document 对象不允许被直接覆盖,因此 proxy 这种方法得选择合适的时机使用
搞爬虫的这些博主早就已经写好了常用的 hook 脚本,这里将其直接拿过来进行分析
https://www.dnslin.com/archives/104.html
var code = function(){
var org = document.cookie.__lookupSetter__('cookie');
document.__defineSetter__("cookie",function(cookie){
if(cookie.indexOf('TSdc75a61a')>-1){
debugger;
}
org = cookie;
});
document.__defineGetter__("cookie",function(){return org;});
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);// 当cookie中匹配到了 TSdc75a61a, 则插入断点。
这是一段赋值替换型的 hook 代码
具体来说,这段代码做了以下几件事情:
code 的函数,函数内部包含了对 document.cookie 的拦截和监控逻辑。__lookupSetter__ 方法获取原始的 document.cookie 的 setter 函数,并将它保存在变量 org 中。__defineSetter__ 方法重新定义了 document.cookie 的 setter 函数。在新定义的 setter 函数中,如果设置的 cookie 值中包含特定的字符串("TSdc75a61a"),则会触发一个断点调试器(debugger)。__defineGetter__ 方法重新定义了 document.cookie 的getter 函数,使其返回变量 org 的值,即原始的 document.cookie 的值。<script> 元素,并将其内容设置为调用 code 函数的代码字符串,通过在代码字符串外添加括号和调用操作符 () 来立即执行该函数。<script> 元素添加到当前文档的 <head> 元素或根元素中。<script> 元素。通过以上步骤,这段代码成功地将对 document.cookie 的读取和设置操作进行了拦截和监控。如果设置的 cookie 值中包含字符串"TSdc75a61a",则会触发一个断点调试器,允许开发者在浏览器的调试工具中进行进一步的调试和处理。
https://zhuanlan.zhihu.com/p/231651573
// ==UserScript==
// @name Hook Cookie
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @include *
// @grant none
// @run-at document-start
// ==/UserScript==(function () {
'use strict';
var cookie_cache = document.cookie;
Object.defineProperty(document, 'cookie', {
get: function () {
return cookie_cache;
},
set: function (val) {
console.log('Setting cookie', val);
// 填写cookie名
if (val.indexOf('cookie名') != -1) {
debugger;
}
var cookie = val.split(";")[0];
var ncookie = cookie.split("=");
var flag = false;
var cache = cookie_cache.split("; ");
cache = cache.map(function (a) {
if (a.split("=")[0] === ncookie[0]) {
flag = true;
return cookie;
}
return a;
})
cookie_cache = cache.join("; ");
if (!flag) {
cookie_cache += cookie + "; ";
}
return cookie_cache;
}
});
})();
这是一段 Object.defineProperty 类型的 hook 代码
具体来说,这段代码做了以下几件事情:
cookie_cache,用于缓存原始的 document.cookie 的值。Object.defineProperty 方法重新定义了 document.cookie属性的 getter 和 setter 方法。cookie_cache 值,即原始的 document.cookie 的值。debugger)。cookie_cache 值。通过以上步骤,这段代码成功地将对 document.cookie 的读取和设置操作进行了拦截和监控。在设置 cookie 值时,会进行特定条件的判断和处理,并更新缓存中的 cookie 值。你可以根据实际需求修改代码中的条件和处理逻辑。
https://www.dnslin.com/archives/104.html
// 定义拦截函数
var code = function() {
// 保存原始的 setRequestHeader 方法
var org = window.XMLHttpRequest.prototype.setRequestHeader; // 重定义 setRequestHeader 方法
window.XMLHttpRequest.prototype.setRequestHeader = function(key, value) {
// 如果请求头中包含 'Authorization' 字段,则触发断点调试器
if (key === 'Authorization') {
debugger;
}
// 调用原始的 setRequestHeader 方法
return org.apply(this, arguments);
};
};
// 创建一个 <script> 元素
var script = document.createElement('script');
// 将拦截函数代码作为文本内容赋给 <script> 元素
script.textContent = '(' + code + ')()';
// 将 <script> 元素添加到文档的头部或根元素中
(document.head || document.documentElement).appendChild(script);
// 从文档中移除 <script> 元素
script.parentNode.removeChild(script);
以上代码的作用是拦截并监控浏览器中的 XMLHttpRequest 对象的 setRequestHeader 方法,因为在浏览器环境下,使用 XMLHttpRequest 对象的 setRequestHeader 方法是一种常见的设置 HTTP 请求头的方式,我们通过 hook 该函数找到特定的 header 添加到请求对象的位置,之后向前跟栈分析就好
具体解释如下:
code 的函数,用于定义拦截逻辑。setRequestHeader 方法到变量 org 中。XMLHttpRequest 对象的 setRequestHeader 方法。在新定义的方法中,如果请求头中包含 'Authorization' 字段,则会触发一个断点调试器。<script> 元素。<script> 元素。<script> 元素添加到文档的头部或根元素中。<script> 元素。通过以上步骤,这段代码成功地拦截并监控了 XMLHttpRequest 对象的 setRequestHeader 方法。如果请求头中包含 'Authorization'字段,则会触发一个断点调试器,允许开发者在浏览器的调试工具中进行进一步的调试和处理。
https://www.dnslin.com/archives/104.html
// 定义拦截函数
var code = function() {
// 保存原始的 open 方法
var open = window.XMLHttpRequest.prototype.open; // 重定义 open 方法
window.XMLHttpRequest.prototype.open = function(method, url, async) {
// 如果 URL 中包含特定字符串("MmEwMD"),则触发断点调试器
if (url.indexOf("MmEwMD") > -1) {
debugger;
}
// 调用原始的 open 方法
return open.apply(this, arguments);
};
};
// 创建一个 <script> 元素
var script = document.createElement('script');
// 将拦截函数代码作为文本内容赋给 <script> 元素
script.textContent = '(' + code + ')()';
// 将 <script> 元素添加到文档的头部或根元素中
(document.head || document.documentElement).appendChild(script);
// 从文档中移除 <script> 元素
script.parentNode.removeChild(script);
以上代码的作用是拦截并监控浏览器中的 XMLHttpRequest 对象的 open 方法,可以在发送 AJAX 请求之前检查和干预请求的 URL 地址。
具体解释如下:
code 的函数,用于定义拦截逻辑。open 方法到变量 open 中。XMLHttpRequest 对象的 open 方法。在新定义的方法中,如果 URL 中包含特定字符串("MmEwMD"),则会触发一个断点调试器。<script> 元素。<script> 元素。<script> 元素添加到文档的头部或根元素中。<script> 元素。通过以上步骤,这段代码成功地拦截并监控了 XMLHttpRequest 对象的 open 方法。如果请求的 URL 中包含特定字符串("MmEwMD"),则会触发一个断点调试器,允许开发者在浏览器的调试工具中进行进一步的调试和处理。
以上三种主要是为了在http请求包中找到加密的值添加位置,之后再跟栈分析定位到加密位置,进而确定原始数据以及加密方法,但 hook 在 js 逆向过程中可不只是这点作用,前端攻防现在打得很激烈,各种检测,加密都可以通过 hook 来进行辅助,甚至可以用 hook 来绕过反 hook
可以看看上面代码的来源文章,里面介绍了近 30 种 hook 脚本
https://www.dnslin.com/archives/104.html
一般可以通过浏览器、代理软件、浏览器插件三种方法实施 hook
源代码 -> 片段 -> 新片段
以 Hook Cookie 为例
点击即可运行
这里我们监控了 Cookie H_PS_645EC的添加,发现添加后就进入 debugger,搜索 123 就生成了该 Cookie
如果页面刷新,脚本功能就会失效
我们最常用的 burpsuite ,直接抓页面返回包,之后将 hook 代码添加进去
搜索 123
成功在添加该 Cookie 时进入断点
浏览器插件可能最知名的就是油猴了
https://www.tampermonkey.net/
可以直接在官网上搜索并安装脚本,官网也可以找到详细的脚本编写规范,当然文章之前部分提到的脚本可以直接使用
这里推荐一个监控 Cookie 变化的脚本
https://github.com/CC11001100/js-cookie-monitor-debugger-hook
这个脚本有 500 多行,就不在这里展示了,我们还是以百度为例
刷新页面,打开开发者工具,搜索 123
成功在添加该 Cookie 时进入断点
除非你想在特定时机进行 hook ,不然一般就是在网页最开始进行
油猴脚本默认情况下存在 @run-at 配置项,可以指定 hook 的时机
代理软件可以考虑在网页代码中可以执行 js 代码的最前端插入 hook 脚本
浏览器的话,可以根据网页 html 以及 js 加载顺序,在其中搭上断点,刷新网页,在断点处添加 hook 脚本
在网络的响应处是无法直接打断点的,我们需要右键在源面板中打开
这里就可以打断点了,刷新网页
此时就可以注入我们的 hook 脚本了