JS逆向调试技巧:编写Hook脚本监控加密函数输入输出
在前端JavaScript逆向工程中,通过编写Hook脚本可以精准拦截并监控加密函数的输入参数、密钥信息、返回值及调用堆栈。本文详细讲解Hook的核心拦截原理、通用脚本模板、不同注入时机选择、反检测处理技巧以及实战定位排查流程。
在面对经过代码混淆、Webpack打包以及变量名压缩的前端项目时,通过静态阅读源码还原加密逻辑通常耗时费力。Hook(钩子)技术提供了一种在运行时动态劫持目标函数的机制:通过重写或代理目标加密函数的引用,在原函数执行前后拦截入参明文、捕获生成密文并输出调用堆栈,从而帮助分析人员快速验证算法猜想并准确定位核心加密代码的触发源头。
一、Hook 监控的核心原理与捕获价值
JavaScript 是一门高度动态的弱类型脚本语言,对象的方法本质上是指向函数对象的内存指针。如果一个对象的方法可读写且未被特殊封装锁定,开发者可以在运行时直接修改该对象的属性指向,将其替换为自定义的包装函数。
当业务代码尝试调用该方法时,实际触发的是包装函数。包装函数首先记录传入的参数(例如用户的明文密码、未签名的请求体、Key 和 IV),随后使用 apply 或 call 调用原本保存的原始函数,获得真实的计算结果(例如 AES 密文或签名串),最后将计算结果无缝返回给业务调用方。整个过程中,原业务调用链路完全不受破坏。
在逆向调试过程中,对加密函数实施 Hook 监控主要带来三项直接价值:
- 明文与密文实时对照:直接在控制台输出加密前的数据结构与最终密文字段,快速核实当前函数是否正是生成抓包中目标字段的关键节点。
- 动态提取密钥与配置参数:很多 AES、DES 或 RSA 加密会将密钥(Key)、偏移向量(IV)或公钥字符串作为独立参数传入加密函数,Hook 可直接读取传参对象,无需在数万行混淆代码中痛苦寻找密钥生成逻辑。
- 调用栈精准溯源(Trace):利用
console.trace()可以自动打印出自业务事件触发到当前加密函数执行的完整调用堆栈,顺着堆栈向上单步跳转,即可直达加密前的参数组装逻辑。
二、通用 Hook 脚本编写与标准模板
编写高质量的 Hook 脚本需要满足三项基本要求:不改变原函数的 this 绑定、不遗漏或污染原函数的入参与返回值、以及具备必要的异常隔离。以下是适用于常规对象方法的标准化 Hook 模板:
(function hookCryptoFunction() {
// 1. 确定并暂存目标对象与原始函数
const targetObj = window.CryptoJS ? window.CryptoJS.AES : null;
const targetMethodName = 'encrypt';
if (!targetObj || typeof targetObj[targetMethodName] !== 'function') {
console.warn('[Hook Warning] 目标对象或方法不存在,请检查注入时机');
return;
}
const _originalMethod = targetObj[targetMethodName];
// 2. 覆盖并重写方法
targetObj[targetMethodName] = function (...args) {
console.group('%c[Hook Monitor] ' + targetMethodName + ' 被调用', 'color: #438A80; font-weight: bold;');
// 入参快照与打印(避免控制台引用后续被异步修改)
try {
console.log('%c[Input Arguments]', 'color: #456581; font-weight: bold;', args);
if (args[0]) {
console.log('├─ 明文内容:', args[0]);
}
if (args[1]) {
console.log('├─ 密钥/Key:', args[1].toString ? args[1].toString() : args[1]);
}
if (args[2]) {
console.log('└─ 配置选项(IV/Mode):', args[2]);
}
} catch (err) {
console.error('[Hook Print Error]', err);
}
// 打印当前调用堆栈
console.trace('%c[Call Stack 溯源]', 'color: #D97706;');
// 3. 执行原始逻辑(保持 this 上下文与原样参数)
let result;
try {
result = _originalMethod.apply(this, args);
} catch (execError) {
console.error('[Target Execution Error]', execError);
console.groupEnd();
throw execError;
}
// 4. 打印并回传输出结果
console.log('%c[Output Result]', 'color: #10B981; font-weight: bold;', result ? result.toString() : result);
console.groupEnd();
return result;
};
console.log('[Hook Success] 已成功注入监控:' + targetMethodName);
})();
JSON.parse(JSON.stringify(args)) 或将其属性格式化后打印,以固化入参快照。
三、Hook 脚本的注入时机与四步实施法
Hook 脚本编写完成后,能否生效取决于执行时机。如果加密函数是挂载在全局变量上的普通方法,控制台直接运行即可;但若目标函数在页面初始加载或自执行函数执行时就已经被赋值并引用,控制台手动注入就会因为“注入过晚”而无法捕获任何调用。
常见四种注入载体与场景对比
| 注入方式 | 注入时机 | 操作成本 | 适用场景 |
|---|---|---|---|
| DevTools 控制台即时执行 | 用户手动交互时 | 最低(复制粘贴即用) | 目标对象为全局静态实例,且在用户点击按钮后才触发加密 |
| DevTools Snippets (代码片段) | 用户快捷键运行 | 低(可持久保存脚本) | 高频使用的调试 Hook 库,便于在各个测试网页间快速复用 |
| Tampermonkey (油猴脚本) | document-start |
中等(需配置匹配规则) | 页面加载初期立即执行的自解密或 Token 初始化请求 |
| DevTools 本地覆写 (Overrides) | HTML / JS 文件解析时 | 中等(需开启本地存储) | 将 Hook 脚本直接硬编码进网页原本引用的首个 JS 文件头部 |
以油猴脚本注入为例,如果目标函数属于某个页面脚本启动即使用的加解密函数,可以在脚本头声明 // @run-at document-start,确保油猴脚本在页面任何 DOM 构建和业务 JS 加载前完成注入,抢先占领目标原型或全局对象。
四、反 Hook 检测机制与应对策略
随着前端安全防护的增强,部分包含反调试机制的网页会在关键函数执行前校验函数的原型或字符串表示。如果发现函数已被外部篡改,则主动中断执行或清空内存。
1. 针对 Function.prototype.toString 检测的伪装
许多前端风控脚本会执行 func.toString()。如果是原生函数(例如 window.btoa),原生实现会返回 "function btoa() { [native code] }";而常规 Hook 包装后,toString() 会直接输出包装函数的源码字符串,从而暴露被 Hook 的事实。
应对方法是同步对 Function.prototype.toString 进行保护,或者覆写包装函数自身的 toString 方法:
function makeNativeString(fn, originalName) {
const nativeStr = 'function ' + (originalName || fn.name || '') + '() { [native code] }';
Object.defineProperty(fn, 'toString', {
value: function () {
return nativeStr;
},
writable: false,
configurable: true,
enumerable: false
});
}
// 使用示例:将包装函数伪装为原生格式
makeNativeString(targetObj[targetMethodName], targetMethodName);
2. 原型链与对象描述符同步
如果目标函数本身挂载了静态属性(如 CryptoJS.enc.Utf8 或算法原型),替换引用后必须保持原型链继承与属性拷贝:
// 继承原函数的 prototype 与静态属性
Object.setPrototypeOf(wrapperFunction, _originalMethod);
Object.assign(wrapperFunction, _originalMethod);
五、实战演练:定位并监控 AES 加密全过程
下面通过一个典型的登录流程说明实战操作。假设在登录抓包中发现密码字段为一串类似 U2FsdGVkX1... 的密文,且网络面板的 Initiator 堆栈经过了深层混淆。
第 1 步:检索疑似挂载点并编写 Hook 脚本
在控制台搜索 CryptoJS、encrypt、AES 等关键词,确认全局或局部对象中存在加密实例。在 Chrome DevTools 的 Console 中输入以下脚本:
(function hookAES() {
if (typeof window.CryptoJS === 'undefined' || !window.CryptoJS.AES) {
console.error('当前环境尚未加载 CryptoJS.AES');
return;
}
const _rawEncrypt = window.CryptoJS.AES.encrypt;
window.CryptoJS.AES.encrypt = function (message, key, cfg) {
console.group('%c[AES Encrypt 拦截成功]', 'background:#456581;color:#fff;padding:2px 6px;border-radius:3px;');
// 1. 打印明文
console.log('明文 Message:', message);
// 2. 打印 Key
let keyStr = key;
if (key && key.words && window.CryptoJS.enc.Utf8) {
try {
keyStr = window.CryptoJS.enc.Utf8.stringify(key);
} catch (e) {}
}
console.log('密钥 Key:', keyStr);
// 3. 打印配置项 (IV, Mode, Padding)
if (cfg) {
console.log('配置 cfg:', cfg);
if (cfg.iv && window.CryptoJS.enc.Utf8) {
try {
console.log('偏移量 IV:', window.CryptoJS.enc.Utf8.stringify(cfg.iv));
} catch (e) {}
}
}
// 4. 输出调用栈
console.trace('调用栈追踪');
// 5. 执行原函数
const ciphertext = _rawEncrypt.apply(this, arguments);
// 6. 打印输出
console.log('生成密文 Output:', ciphertext.toString());
console.groupEnd();
return ciphertext;
};
console.log('%c[Hook Status] CryptoJS.AES.encrypt 监控已生效', 'color:#438A80;font-weight:bold;');
})();
第 2 步:触发页面事件并分析输出
在登录表单中输入测试密码 Test@2026!,点击“登录”按钮。此时 DevTools 控制台立即弹出结构化日志:
- 明文 Message:显示为
"Test@2026!"; - 密钥 Key:解析出 16 位字符串
"k9#mP2$xL1@vN4*z"; - 偏移量 IV:解析出
"0102030405060708"; - 调用栈追踪:显示
at doSubmit (login.min.js:142:5321)。
此时直接点击堆栈中的 login.min.js:142:5321 链接,DevTools 源码面板会自动跳转并高亮显示加密调用的上游代码行,逆向人员即可在此处打上普通断点进行单步分析。
六、核心排查与自检体系
在实际逆向操作中,Hook 脚本可能因为环境隔离、闭包未导出或递归调用等原因发生异常。遇到监控未如期触发时,可以依据以下自检体系进行定位:
关键避坑要点
- 死循环防范(Recursion Guard):如果被 Hook 的是底层基础方法(例如
JSON.stringify或window.btoa),在包装函数体内打印日志时如果再次隐式调用了该方法,会导致无限递归并抛出RangeError: Maximum call stack size exceeded。必须在包装函数内设置标志位(如let isHooking = false;),检测到处于 Hook 过程中时直接放行原函数。 - 闭包与私有作用域处理:Webpack 模块常将加密函数封闭在闭包内,外部无法直接访问
targetObj.encrypt。此时应逆向寻找该函数调用的公共接口,例如 HookXMLHttpRequest.prototype.send或fetch,在请求发送前截取密文,再从堆栈向上寻找闭包内的加密入口。 - 异步与 Promise 处理:当遇到使用浏览器原生 Web Crypto API(如
crypto.subtle.encrypt)的场景时,该方法返回的是一个Promise对象。包装函数在拦截返回值时必须使用.then()或async/await处理解析后的ArrayBuffer,切忌直接尝试同步打印返回值。
为什么JS调用reload后页面没有立即更新
2026-11-05 13:38:29
crypto-js 加密结果与 Java/Python 后端不一致怎么解决
2026-11-05 12:09:48
js文件缓存导致代码不更新怎么强制刷新
2026-11-04 22:30:24
Babylon.js 性能优化技巧:实例化网格与遮挡剔除
2026-11-03 23:43:22
dva 动态加载 Model 实现路由级代码分割优化
2026-11-03 22:09:43







