JS逆向中Hook技术怎么用?重写原型链绕过混淆获取关键参数
在面对高度混淆的JavaScript代码时,静态分析和盲目下断点往往效率极低。本文详细讲解如何利用JS原型链机制实施Hook技术,通过重写原生原型方法实现精准拦截,绕过控制流平坦化与字符串混淆,快速回溯并获取请求签名、Token等关键参数。
一、核心原理:为什么重写原型链能降维穿透混淆代码?
现代前端安全防护通常采用商业级混淆工具(如 Obfuscator、Terser 等)对核心逻辑进行保护。经过混淆的代码具备三个典型特征:所有语义化变量名都被替换为类似 _0x4f12 的无意义符号、真实的控制流程被切碎并放入庞大的 switch-case 循环(即控制流平坦化)、字符串常数被整体抽离并引入复杂的自解密解密函数。
面对这类代码,很多逆向新手习惯直接在网络面板发起请求处下 XHR/fetch 断点,或者顺着接口搜索参数名。然而,混淆代码中的网络发送入口往往被二次封装,搜索参数名可能匹配到数百处无用变量;而在控制流平坦化结构中进行单步调试(Step Over),极易陷入几十次无实际业务含义的死循环计算,甚至触发反调试逻辑导致页面卡死。
原型链 Hook 技术之所以能够实现“降维打击”,其核心在于:混淆代码无论如何对语法和局部逻辑进行变形,最终在执行网络交互、数据持久化或正则校验时,都必须依赖 JavaScript 运行环境的原生对象与原型方法。
在 JavaScript 中,几乎所有对象都基于原型链构造。当混淆脚本执行 obj.toString()、reg.test(str)、JSON.stringify(payload) 或 xhr.send(data) 时,引擎在实例本身找不到对应属性,便会沿着原型链向上查找。如果我们直接在全局环境中重写原型对象的方法(例如修改 RegExp.prototype.test 或重写 JSON.stringify),那么整套运行环境中由混淆代码创建出的成千上万个实例对象,只要调用该方法,就一定会经过我们植入的 Hook 探针。
这意味着逆向人员不需要知道混淆变量的名字,不需要还原复杂的平坦化流程,只要在数据被送往原生方法的那一瞬间将其拦截,就可以拿到未加密的参数原文,并借由执行环境的调用栈直接反推出加密算法的计算源头。
二、标准实现模式:规范的原型链 Hook 代码范式
编写原型链 Hook 并不是简单地给某个方法赋上一个新函数。一个工业级可用的 Hook 脚本必须严格满足三个准则:保留原始逻辑执行与返回值、维持原始调用的 this 绑定、以及配置精确的条件过滤。否则,无差别拦截会导致网页自身正常脚本报错,甚至导致浏览器陷入死循环崩溃。
1. JSON.stringify 标准拦截模板
在前后端交互中,现代单页面应用(SPA)在发送 POST 请求之前,往往需要将包含签名字段(如 sign、token、nonce)的 JavaScript 对象序列化为 JSON 字符串。这是最理想的拦截入口之一:
(function() {
'use strict';
// 1. 备份原生引用,闭包隔离防止被外部重新覆盖
const _originalStringify = JSON.stringify;
// 2. 规范重写
JSON.stringify = function(...args) {
const payload = args[0];
// 3. 设定精确过滤条件,避免网页其他组件调用产生大量干扰
if (payload && typeof payload === 'object') {
if ('sign' in payload || 'token' in payload || 'signature' in payload) {
console.group('%c[Hook 命中] JSON.stringify 捕获核心参数', 'color: #B96555; font-weight: bold;');
console.log('捕获入参对象:', payload);
console.trace('调用栈追踪 (Call Stack):');
console.groupEnd();
// 4. 触发断点,挂起浏览器线程以供 DevTools 检查
debugger;
}
}
// 5. 必须保持原有上下文与参数原样返回
return _originalStringify.apply(this, args);
};
// 6. 抹平函数特征(反 Hook 伪装)
JSON.stringify.toString = function() {
return 'function stringify() { [native code] }';
};
})();
在重写包含上下文依赖的方法(例如 RegExp.prototype.test 或 XMLHttpRequest.prototype.send)时,被调用的方法内部通常依赖 this 指向具体的正则实例或请求实例。如果不使用 apply(this, args) 或 call(this, ...args),直接调用会导致 TypeError: Illegal invocation 报错,从而直接暴露 Hook 并破坏网页正常执行流程。
2. RegExp.prototype 原型链方法拦截模板
很多混淆框架在执行解密时,会使用正则表达式去匹配校验参数格式或提取关键凭据。通过对 RegExp.prototype.test 进行挂钩,可以捕获传递进来的敏感字符串:
(function() {
const _originTest = RegExp.prototype.test;
RegExp.prototype.test = function(...args) {
const candidate = args[0];
// 过滤:检测入参是否为可疑加密串(如特定长度十六进制或包含特定头)
if (typeof candidate === 'string' && (candidate.includes('bearer') || candidate.length === 32)) {
console.log('[Hook Hit] RegExp.test 拦截到可疑字符串:', candidate);
console.trace();
debugger;
}
return _originTest.apply(this, args);
};
RegExp.prototype.test.toString = function() {
return 'function test() { [native code] }';
};
})();
三、实施流程:原型链 Hook 实操四步法
要让原型链 Hook 发挥威力,必须建立一套标准化的实操流程。整个逆向过程分为四个关键阶段:
步骤一:分析目标参数形态,确定挂载靶标
在动手写 Hook 之前,先抓取目标接口的请求包,观察待逆向参数的呈现方式:
- 请求 Body 为 JSON 格式:首选
JSON.stringify,能在对象转化为字符串的瞬间截获未加密字段。 - 参数存储在本地:观察是否存储在 Cookie 或 WebStorage 中,选择
Storage.prototype.setItem或document.__defineSetter__('cookie')。 - 参数包含特征长度的 Hash:如果发现参数类似 32 位 MD5 或 40 位 SHA1,且代码中有大量正则替换行为,选择
RegExp.prototype.test或String.prototype.match。 - 数据通过 WebSocket 传输:选择
WebSocket.prototype.send。
步骤二:选择正确的注入时机(抢占先机)
原型链 Hook 必须在目标网站的混淆业务脚本执行之前完成挂载。如果混淆脚本先执行并缓存了原生方法的局部副本(例如 const _localStringify = JSON.stringify;),后注入的 Hook 将无法拦截该闭包内的后续调用。
在实际操作中,主要采用以下三种环境注入方式:
- Tampermonkey(油猴脚本):在脚本元数据中使用
// @run-at document-start,确保在 DOM 树刚开始解析、首个页面内联或外部脚本执行之前运行。 - Chrome DevTools Local Overrides(本地替换):直接在 Sources 面板中将目标 HTML 文件保存到本地,将 Hook 脚本直接写在
<head>标签的第一行。这是最稳定且不会被网站检测到的方式。 - 抓包代理工具注入(mitmproxy / Fiddler):配置代理规则,在返回 HTML 文档时,通过脚本自动在头部注入
<script>/* Hook Code */</script>。
步骤三:编写无感劫持逻辑并处理上下文
注入脚本后,务必在内部完成环境隔离。将变量放在立即执行函数(IIFE)内,避免在 window 全局对象下挂载 _originalXxx 导致被网站的安全脚本扫描到暴露身份。
步骤四:设置断点条件并开启调用栈回溯
刷新页面,在浏览器控制台中复现触发操作(例如点击登录、滑动滑块或翻页)。一旦满足过滤条件,DevTools 会自动在 debugger; 语句处暂停。
四、实战推演:从命中断点到回溯定位真实加密入口
当 Hook 触发并在 DevTools 中暂停时,屏幕将停留在我们编写的 Hook 脚本内部。此时最关键的操作不是单步往下走,而是利用 Chrome DevTools 右侧的 Call Stack(调用栈)面板向上排查。
以下是排查调用栈的标准排查技巧:
- 展开右侧 Call Stack 列表:列表的最顶层是我们自定义的 Hook 函数,下一层往往是调用
JSON.stringify的混淆底层包装函数。 - 自顶向下点击第 2 层与第 3 层堆栈帧:点击堆栈帧时,Sources 面板会直接跳转到该层函数被调用的代码行,并在右侧 Scope 面板展示该层函数执行时的局部变量值。
- 跳过分发器(Dispatcher),寻找算法边界:在控制流平坦化代码中,第 2 层可能是一个
while(true) switch分发器。不要在分发器里纠缠,继续向上查看第 3 层或第 4 层。通常会看到类似generateToken(options)或encryptPayload(data)的函数调用。 - 在算法入口处设置长期断点:在定位到的真实加密函数头部打上普通条件断点。此时,你已经彻底穿透了混淆层,既拿到了原始明文入参,又拿到了核心加密函数的执行环境。
五、防护对抗:绕过网站的反 Hook 检测机制
成熟的前端防护系统不会任由逆向人员重写原型。它们会部署反 Hook 检测脚本。了解这些检测手法及其对策,是确保 Hook 脚本稳定运行的必备技能。
1. 函数 toString 反检测
许多安全脚本会定期运行 JSON.stringify.toString()。如果是原生函数,浏览器会输出 "function stringify() { [native code] }";但如果被我们替换成了自定义函数,默认会输出我们的源码字符串,检测脚本便会立即识别出环境异常并上报风控或强制跳转。
标准应对方案:统一劫持 Function.prototype.toString,维护一个受保护函数映射列表:
(function() {
const _originToString = Function.prototype.toString;
const hookedPool = new Set();
// 对外暴露的安全注册方法
window.__protectHook = function(fn, nativeName) {
hookedPool.add(fn);
fn.__nativeName = nativeName;
};
Function.prototype.toString = function() {
if (hookedPool.has(this)) {
return `function ${this.__nativeName || this.name}() { [native code] }`;
}
return _originToString.apply(this, arguments);
};
// 保护 toString 自身
window.__protectHook(Function.prototype.toString, 'toString');
})();
2. 原型对象冻结防御(Object.freeze)
部分高安全性网站会在脚本最开头执行 Object.freeze(Object.prototype) 或 Object.freeze(JSON),将原生对象的描述符设置为不可配置(configurable: false)且只读(writable: false),导致后续的属性重写直接抛出异常。
应对方案:必须保证在页面加载的绝对初始阶段(通过 Tampermonkey @run-at document-start 或 DevTools 本地覆盖)抢先介入,在页面执行冻结之前重写目标原型,或者直接将 Object.freeze 和 Object.seal 重写为空操作:
(function() {
// 抢先置空冻结与密封函数
Object.freeze = function(obj) { return obj; };
Object.seal = function(obj) { return obj; };
})();
3. 使用 ES6 Proxy 作为原型 Hook 的备选方案
除了直接修改原型的属性,在部分场景下,直接使用 Proxy 代理原生对象可以实现更细粒度的控制,如监听属性的读取(getter)与写入(setter):
// 监听特定对象的属性赋值(如 window.sign)
let _internalSign = '';
Object.defineProperty(window, 'sign', {
get: function() {
return _internalSign;
},
set: function(val) {
console.log('[Hook Hit] window.sign 被写入:', val);
console.trace();
debugger;
_internalSign = val;
},
configurable: true
});
六、关键操作与避坑总结
| 操作维度 | 常见错误做法 | 正确工程实践 |
|---|---|---|
| 调用上下文 | 直接调用 _rawFn(...args) 导致 this 丢失 |
始终使用 _rawFn.apply(this, args) 维持原宿主调用链 |
| 过滤粒度 | 无条件打印所有调用,导致控制台被常规日志刷爆并卡死 | 严格判断 args[0] 是否包含业务敏感 Key(如 sign, token) |
| 注入时序 | 在页面完全加载后再打开 Console 粘贴脚本 | 利用 DevTools Local Overrides 或油猴 document-start 抢跑 |
| 特征隐藏 | 暴露自定义变量到 window,未处理 toString |
使用 IIFE 闭包,统一伪装 Function.prototype.toString |
重写原型链 Hook 并不是万能的解药,但它是 JavaScript 逆向工程中最具性价比的突破口。只要掌握了“精准识别底层原型、抢先注入安全探针、结合调用栈回溯算法”这套方法,绝大多数基于前端混淆的参数加密都会在第一时间暴露其真实源头。
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09








