简单来说,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
属性为访问器属性
![](../../../../../Library/Application Support/typora-user-images/image-20231128184842648.png)
可以通过 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
脚本了