JS逆向中Hook技术怎么用?重写原型链绕过混淆获取关键参数

作者:函数茶馆

时间:2026-11-03

浏览:467

在面对高度混淆的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 探针。

JS逆向中Hook技术怎么用?重写原型链绕过混淆获取关键参数
图 1:混淆执行链路与原型链拦截的力学关系图

这意味着逆向人员不需要知道混淆变量的名字,不需要还原复杂的平坦化流程,只要在数据被送往原生方法的那一瞬间将其拦截,就可以拿到未加密的参数原文,并借由执行环境的调用栈直接反推出加密算法的计算源头。

二、标准实现模式:规范的原型链 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] }';
  };
})();
关键细节:为什么必须使用 apply(this, args)?

在重写包含上下文依赖的方法(例如 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 发挥威力,必须建立一套标准化的实操流程。整个逆向过程分为四个关键阶段:

JS逆向中Hook技术怎么用?重写原型链绕过混淆获取关键参数
图 2:原型链 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 将无法拦截该闭包内的后续调用。

在实际操作中,主要采用以下三种环境注入方式:

  1. Tampermonkey(油猴脚本):在脚本元数据中使用 // @run-at document-start,确保在 DOM 树刚开始解析、首个页面内联或外部脚本执行之前运行。
  2. Chrome DevTools Local Overrides(本地替换):直接在 Sources 面板中将目标 HTML 文件保存到本地,将 Hook 脚本直接写在 <head> 标签的第一行。这是最稳定且不会被网站检测到的方式。
  3. 抓包代理工具注入(mitmproxy / Fiddler):配置代理规则,在返回 HTML 文档时,通过脚本自动在头部注入 <script>/* Hook Code */</script>。

步骤三:编写无感劫持逻辑并处理上下文

注入脚本后,务必在内部完成环境隔离。将变量放在立即执行函数(IIFE)内,避免在 window 全局对象下挂载 _originalXxx 导致被网站的安全脚本扫描到暴露身份。

步骤四:设置断点条件并开启调用栈回溯

刷新页面,在浏览器控制台中复现触发操作(例如点击登录、滑动滑块或翻页)。一旦满足过滤条件,DevTools 会自动在 debugger; 语句处暂停。

四、实战推演:从命中断点到回溯定位真实加密入口

当 Hook 触发并在 DevTools 中暂停时,屏幕将停留在我们编写的 Hook 脚本内部。此时最关键的操作不是单步往下走,而是利用 Chrome DevTools 右侧的 Call Stack(调用栈)面板向上排查。

JS逆向中Hook技术怎么用?重写原型链绕过混淆获取关键参数
图 3:DevTools 调试器调用栈(Call Stack)倒查回溯定位模型

以下是排查调用栈的标准排查技巧:

  1. 展开右侧 Call Stack 列表:列表的最顶层是我们自定义的 Hook 函数,下一层往往是调用 JSON.stringify 的混淆底层包装函数。
  2. 自顶向下点击第 2 层与第 3 层堆栈帧:点击堆栈帧时,Sources 面板会直接跳转到该层函数被调用的代码行,并在右侧 Scope 面板展示该层函数执行时的局部变量值。
  3. 跳过分发器(Dispatcher),寻找算法边界:在控制流平坦化代码中,第 2 层可能是一个 while(true) switch 分发器。不要在分发器里纠缠,继续向上查看第 3 层或第 4 层。通常会看到类似 generateToken(options) 或 encryptPayload(data) 的函数调用。
  4. 在算法入口处设置长期断点:在定位到的真实加密函数头部打上普通条件断点。此时,你已经彻底穿透了混淆层,既拿到了原始明文入参,又拿到了核心加密函数的执行环境。

五、防护对抗:绕过网站的反 Hook 检测机制

成熟的前端防护系统不会任由逆向人员重写原型。它们会部署反 Hook 检测脚本。了解这些检测手法及其对策,是确保 Hook 脚本稳定运行的必备技能。

JS逆向中Hook技术怎么用?重写原型链绕过混淆获取关键参数
图 4:原型链 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 逆向工程中最具性价比的突破口。只要掌握了“精准识别底层原型、抢先注入安全探针、结合调用栈回溯算法”这套方法,绝大多数基于前端混淆的参数加密都会在第一时间暴露其真实源头。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章