在前端逆向、调试、性能监控等场景中,Hook(钩子)技术是不可或缺的核心手段。它能让我们在不破坏原有代码结构的前提下,“截胡”函数执行、监控数据流转,甚至修改程序行为——比如捕获加密参数、定位解密逻辑、拦截请求等。但要真正用好Hook,首先得搞懂它的底层支撑:JavaScript对象属性机制。今天就从原理到实战,手把手教你掌握JS Hook技术,轻松实现函数截胡。
一、先搞懂:什么是JS Hook技术?
Hook,中文译为“钩子”,本质是一种动态拦截技术。简单来说,就是在程序运行过程中,拦截目标函数(或对象属性)的执行/访问,在其执行前、执行后插入我们自己的逻辑,从而实现对原始行为的监控、扩展或修改。
核心亮点:不侵入原有代码。无需修改目标函数的源代码,就能实现对其行为的控制,这也是Hook在前端逆向、调试中如此实用的关键——毕竟我们无法直接修改别人网站的源码,但可以通过Hook“借力打力”。
举个通俗的例子:就像在快递运输的中途装了一个监控,快递(函数执行、数据流转)正常送达,但我们能实时看到快递的状态(监控逻辑),甚至可以在中途拦截、修改快递内容(修改逻辑)。
二、Hook的底层支撑:JS对象属性机制
很多人用Hook只知其然,不知其所以然,其实Hook的核心原理,完全依赖于JavaScript的对象属性机制。想要灵活运用Hook,必须先搞懂对象属性的“底层规则”。
1. 对象属性的3个核心特性
JavaScript中,每个对象的属性(比如obj.name),除了“名字(name)”和“值(value)”,还有3个隐藏的核心特性,它们决定了我们能对这个属性做什么操作:
writable(可写性):控制属性的值是否能被修改。比如设置为
false,就无法给这个属性重新赋值。enumerable(可枚举性):控制属性是否能在
for...in循环中被遍历到。比如设置为false,遍历对象时会“隐藏”这个属性。configurable(可配置性):控制属性是否能被删除,或重新定义其特性(比如把
writable从true改成false)。
默认情况下,我们手动创建的普通对象(比如const obj = { name: '张三' }),这3个特性的值都是true——也就是说,属性可修改、可枚举、可配置。
2. 关键概念:属性描述符(Property Descriptor)
属性描述符,就是用来“描述”对象属性这3个核心特性的结构化对象。它分为两种类型,也是实现Hook的核心工具,务必分清:
(1)数据描述符:描述“有具体值”的属性
适用于直接存储值的属性,包含4个配置项(3个核心特性+1个值):
value:属性的实际值(比如name: '张三'中,value就是'张三')。writable:是否可写(true/false)。
(2)存取描述符:用函数拦截属性的“读/写”
不直接存储值,而是通过两个函数来拦截属性的“读取”和“赋值”操作,这也是Hook最常用的方式。包含4个配置项(3个核心特性+2个函数):
get:读取属性时自动调用的函数,返回值就是属性的“实际值”。set:给属性赋值时自动调用的函数,接收赋值的新值,可在函数内处理赋值逻辑。
⚠️⚠️⚠️ 重要提醒:数据描述符和存取描述符是互斥的。如果定义了get和set,就不能再设置value和writable,否则会报错。
3. 操作属性描述符的2个核心方法
想要查看、修改属性的特性,实现Hook,就必须掌握这两个方法:
(1)Object.getOwnPropertyDescriptor():查看属性特性
用于获取对象某个属性的完整描述符,语法:Object.getOwnPropertyDescriptor(对象, 属性名)。
示例:查看普通对象的属性描述符
const person = { name: '张三', age: 25 };
// 查看name属性的描述符
const nameDescriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(nameDescriptor);输出结果(默认特性全为true):
{
"value": "张三",
"writable": true,
"enumerable": true,
"configurable": true
}(2)Object.defineProperty():定义/修改属性特性
用于给对象添加新属性,或修改已有属性的特性,语法:Object.defineProperty(对象, 属性名, 描述符对象)。
示例:定义一个“只读、不可枚举、不可删除”的属性
const config = {};
// 给config添加appkey属性,设置为只读、不可枚举、不可删除
Object.defineProperty(config, 'appkey', {
value: '123456',
writable: false, // 不可写
enumerable: false, // 不可枚举
configurable: false // 不可配置(不能删除、不能修改特性)
});
// 尝试修改appkey的值
config.appkey = 'abcdefg';
console.log(config.appkey); // 输出:123456(修改失败)注意:现代浏览器默认是非严格模式,修改只读属性不会报错,但修改无效;如果加上'use strict'(严格模式),会直接抛出“不能对只读属性重新赋值”的错误。
4. 存取描述符实战:拦截属性的读/写
利用get和set,我们可以轻松实现对属性的拦截——这就是Hook的最基础形态。
const data = {};
// 给data添加message属性,用get/set拦截读/写
Object.defineProperty(data, 'message', {
get: function() {
console.log('🔍有人正在读取message属性');
return '默认值'; // 读取时返回的值
},
set: function(newValue) {
console.log('✏️有人正在修改message属性:', newValue);
// 可以在这里添加自定义逻辑,比如校验新值
if (typeof newValue === 'string') {
console.log('✅赋值有效');
} else {
console.log('❌赋值无效(必须是字符串)');
}
},
enumerable: true,
configurable: true
});
// 测试拦截效果
console.log(data.message); // 触发get,输出提示+默认值
data.message = '12345'; // 触发set,输出提示+赋值有效
data.message = 123; // 触发set,输出提示+赋值无效
运行后会发现,只要读取或修改data.message,我们的拦截逻辑就会触发——这就是Hook的核心思想:拦截操作,插入自定义逻辑。
三、实战进阶:JS Hook如何“强行截胡”函数?
了解了对象属性机制,我们就可以正式进入Hook实战了。在JavaScript中,函数也是一种对象,所以我们可以通过修改函数对象的属性(比如重写函数),实现对函数的截胡。
1. Hook的基本结构
无论是什么场景的Hook,核心结构都离不开这3步,记牢就能应对80%的需求:
function hook() {
// 1. 保存原始函数(关键!避免覆盖后无法调用原始逻辑)
let originalFunc = 被Hook的函数;
// 2. 重写函数,插入自定义逻辑(截胡核心)
被Hook的函数 = function(参数) {
// 执行前插入逻辑(比如监控参数、打印日志)
console.log('🔍函数被调用,参数:', 参数);
// 调用原始函数,获取返回值(保证原有逻辑正常执行)
let result = originalFunc.apply(this, arguments);
// 执行后插入逻辑(比如监控返回值、修改返回值)
console.log('✅函数执行完成,返回值:', result);
// 返回原始结果(或修改后的结果)
return result;
};
}核心原则:先保存原始函数,再重写函数。如果直接重写,会导致原始函数无法调用,破坏原有程序逻辑。
2. 3个高频实战案例(直接复制可用)
下面结合实际逆向场景,分享3个最常用的Hook案例,覆盖“请求头、请求参数、响应解析”,都是前端逆向中经常遇到的场景。
案例1:请求头Hook——捕获加密参数(观鸟网实战)
场景:观鸟网的请求头中,有一个加密参数sign,我们需要捕获这个参数的值,定位其加密逻辑。
思路:重写XMLHttpRequest.prototype.setRequestHeader(设置请求头的方法),拦截sign参数的设置过程。
// 封装成自执行函数,避免污染全局变量
(function () {
// 1. 保存原始方法
var originalSetHeader = XMLHttpRequest.prototype.setRequestHeader;
// 2. 重写setRequestHeader方法
XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
// 自定义逻辑:拦截sign参数,触发断点
if (key === 'sign') {
debugger; // 断点触发,此时value就是加密后的sign值
console.log('📌捕获到sign参数:', value);
}
// 3. 调用原始方法,保证请求头正常设置
return originalSetHeader.apply(this, arguments);
}
})();
使用方法:
打开观鸟网,按F12打开开发者工具,切换到「Sources > 片段(Snippets)」;
新建片段,粘贴上述代码,保存并执行;
刷新页面,当请求头中设置
sign时,会自动触发断点,此时就能看到加密后的sign值,再通过调用栈定位加密函数。
案例2:请求参数Hook——捕获URL中的加密参数(闲鱼实战)
场景:闲鱼的请求参数中,加密参数sign会拼接到URL中,我们需要捕获这个参数,分析其生成逻辑。
思路:重写XMLHttpRequest.prototype.open(创建请求的方法),判断URL中是否包含sign,触发断点。
(function () {
// 1. 保存原始方法
var originalOpen = window.XMLHttpRequest.prototype.open;
// 2. 重写open方法
window.XMLHttpRequest.prototype.open = function (method, url, async) {
// 自定义逻辑:判断URL中是否包含sign参数
if (url.indexOf("sign") !== -1) {
debugger; // 断点触发,此时url就是包含sign的请求地址
console.log('📌捕获到含sign的URL:', url);
}
// 3. 调用原始方法,保证请求正常创建
return originalOpen.apply(this, arguments);
};
})();
使用方法:
打开闲鱼首页,打开开发者工具,创建片段并执行上述代码;
点击页面翻页、搜索等操作,触发请求;
当URL中包含
sign时,会触发断点,通过调用栈就能找到sign的生成位置。
案例3:响应数据解析Hook——定位解密逻辑(数位观察网实战)
场景:数位观察网的响应数据是加密密文,解密后会调用JSON.parse解析成JSON,我们需要定位解密函数的位置。
思路:重写JSON.parse方法,在解析数据时触发断点,通过调用栈回溯到解密函数。
(function () {
// 1. 保存原始方法
var originalParse = JSON.parse;
// 2. 重写JSON.parse方法
JSON.parse = function (value) {
// 自定义逻辑:触发断点,此时value就是解密后的字符串
debugger;
console.log('📌捕获到JSON解析数据:', value);
// 3. 调用原始方法,保证数据正常解析
return originalParse.apply(this, arguments);
}
})();使用方法:
打开目标网站,在开发者工具的控制台中粘贴上述代码并执行;
刷新页面,当网站调用
JSON.parse解析解密后的数据时,会触发断点;
查看调用栈(Call Stack),往上回溯,就能找到解密函数的位置(解密函数的返回值,就是
JSON.parse的参数value)。
四、Hook实战注意事项
执行时机要早:Hook代码必须在目标函数被调用前注入(比如页面刷新前执行),否则目标函数已经执行完毕,Hook无法生效(这也是很多人Hook失败的核心原因)。
保存原始函数:务必先保存原始函数,再重写,否则会破坏原有程序逻辑,导致页面异常。
注意作用域:如果网站用了Webpack打包、闭包封装,全局Hook可能无效,需要在对应作用域内注入Hook代码。
避免过度Hook:不要Hook所有函数,只针对目标函数进行拦截,否则会影响页面性能,甚至导致调试混乱。
五、总结
JS Hook技术的核心,是利用JavaScript对象属性的存取描述符,通过“保存原始函数→重写函数→插入自定义逻辑”的流程,实现对函数的截胡。它的优势在于“不侵入原有代码”,这让它在前端逆向、调试、性能监控等场景中发挥着不可替代的作用。
从底层原理来看,理解对象属性的3个核心特性、两种描述符,是用好Hook的基础;从实战来看,掌握“请求头、请求参数、响应解析”这3个高频场景的Hook方法,就能应对大部分前端逆向需求。
当然,Hook技术也在不断升级,比如网站会通过重写原生函数、代码混淆等方式反Hook,但万变不离其宗——只要掌握了对象属性机制和Hook的核心逻辑,就能找到对应的破解方法。