JS逆向调试技巧:编写Hook脚本监控加密函数输入输出

作者:缓存笔记时间:2026-11-04 10:42:46浏览:147

在前端JavaScript逆向工程中,通过编写Hook脚本可以精准拦截并监控加密函数的输入参数、密钥信息、返回值及调用堆栈。本文详细讲解Hook的核心拦截原理、通用脚本模板、不同注入时机选择、反检测处理技巧以及实战定位排查流程。

分类:前端逆向 / JS调试技术 适用场景:参数加解密分析、接口签名逆向、调用栈溯源 实用教程

在面对经过代码混淆、Webpack打包以及变量名压缩的前端项目时,通过静态阅读源码还原加密逻辑通常耗时费力。Hook(钩子)技术提供了一种在运行时动态劫持目标函数的机制:通过重写或代理目标加密函数的引用,在原函数执行前后拦截入参明文、捕获生成密文并输出调用堆栈,从而帮助分析人员快速验证算法猜想并准确定位核心加密代码的触发源头。

JS逆向调试技巧:编写Hook脚本监控加密函数输入输出
图 1:前端运行时 Hook 监控技术概览与核心拦截能力示意

一、Hook 监控的核心原理与捕获价值

JavaScript 是一门高度动态的弱类型脚本语言,对象的方法本质上是指向函数对象的内存指针。如果一个对象的方法可读写且未被特殊封装锁定,开发者可以在运行时直接修改该对象的属性指向,将其替换为自定义的包装函数。

当业务代码尝试调用该方法时,实际触发的是包装函数。包装函数首先记录传入的参数(例如用户的明文密码、未签名的请求体、Key 和 IV),随后使用 apply 或 call 调用原本保存的原始函数,获得真实的计算结果(例如 AES 密文或签名串),最后将计算结果无缝返回给业务调用方。整个过程中,原业务调用链路完全不受破坏。

JS逆向调试技巧:编写Hook脚本监控加密函数输入输出
图 2:JavaScript 函数 Hook 拦截执行与参数监控数据流向

在逆向调试过程中,对加密函数实施 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 脚本编写完成后,能否生效取决于执行时机。如果加密函数是挂载在全局变量上的普通方法,控制台直接运行即可;但若目标函数在页面初始加载或自执行函数执行时就已经被赋值并引用,控制台手动注入就会因为“注入过晚”而无法捕获任何调用。

JS逆向调试技巧:编写Hook脚本监控加密函数输入输出
图 3:编写与注入 Hook 脚本的标准实施步骤

常见四种注入载体与场景对比

注入方式 注入时机 操作成本 适用场景
DevTools 控制台即时执行 用户手动交互时 最低(复制粘贴即用) 目标对象为全局静态实例,且在用户点击按钮后才触发加密
DevTools Snippets (代码片段) 用户快捷键运行 低(可持久保存脚本) 高频使用的调试 Hook 库,便于在各个测试网页间快速复用
Tampermonkey (油猴脚本) document-start 中等(需配置匹配规则) 页面加载初期立即执行的自解密或 Token 初始化请求
DevTools 本地覆写 (Overrides) HTML / JS 文件解析时 中等(需开启本地存储) 将 Hook 脚本直接硬编码进网页原本引用的首个 JS 文件头部

以油猴脚本注入为例,如果目标函数属于某个页面脚本启动即使用的加解密函数,可以在脚本头声明 // @run-at document-start,确保油猴脚本在页面任何 DOM 构建和业务 JS 加载前完成注入,抢先占领目标原型或全局对象。

四、反 Hook 检测机制与应对策略

随着前端安全防护的增强,部分包含反调试机制的网页会在关键函数执行前校验函数的原型或字符串表示。如果发现函数已被外部篡改,则主动中断执行或清空内存。

JS逆向调试技巧:编写Hook脚本监控加密函数输入输出
图 4:基础 Hook 与防检测增强型 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 脚本可能因为环境隔离、闭包未导出或递归调用等原因发生异常。遇到监控未如期触发时,可以依据以下自检体系进行定位:

JS逆向调试技巧:编写Hook脚本监控加密函数输入输出
图 5:JS 逆向 Hook 调试监控核心自检指南

关键避坑要点

  1. 死循环防范(Recursion Guard):如果被 Hook 的是底层基础方法(例如 JSON.stringify 或 window.btoa),在包装函数体内打印日志时如果再次隐式调用了该方法,会导致无限递归并抛出 RangeError: Maximum call stack size exceeded。必须在包装函数内设置标志位(如 let isHooking = false;),检测到处于 Hook 过程中时直接放行原函数。
  2. 闭包与私有作用域处理:Webpack 模块常将加密函数封闭在闭包内,外部无法直接访问 targetObj.encrypt。此时应逆向寻找该函数调用的公共接口,例如 Hook XMLHttpRequest.prototype.send 或 fetch,在请求发送前截取密文,再从堆栈向上寻找闭包内的加密入口。
  3. 异步与 Promise 处理:当遇到使用浏览器原生 Web Crypto API(如 crypto.subtle.encrypt)的场景时,该方法返回的是一个 Promise 对象。包装函数在拦截返回值时必须使用 .then() 或 async/await 处理解析后的 ArrayBuffer,切忌直接尝试同步打印返回值。
合规与安全提示:逆向分析与动态调试技术应仅用于技术研究、代码审计及经过授权的渗透测试评估,切勿将其用于未授权的数据窃取、接口越权调用等危害网络安全的行为。
作者最新文章
相关文章
更多