JS 运行时 Hook 全栈实战:13 个内置探针 + 5 个逆向范式 + 反混淆工作流

下载 .md

JavaScript Runtime Hooking & Reverse-Engineering Playbook

浏览器侧 window.fetch / XMLHttpRequest / WebSocket / crypto.subtle / eval / Function / setTimeout / Storage 等运行时 API 现场 Hook 与反混淆方法论。配 33 个开箱即用 preset、5 种注入通道、5 个完整逆向范式与一份反混淆 Worker。

适用人群:做 Web 逆向、签名算法定位、API 联调、反爬研究、反反调试、混淆代码还原的工程师。 通道无关:CDP / Playwright / Puppeteer / DevTools 粘贴 / 用户脚本(Tampermonkey)任选其一。


0 · TL;DR 路由表

你想做的事 看哪一节
抓登录请求的签名怎么算出来 §1 范式 1 + §6.1 fetch + §6.2 xhr + §4 调用栈字段
跟踪 setInterval(debugger) 反调试 §6.11 anti-debug-bypass
拦截 CryptoJS / WebCrypto 的 key/iv/明文 §6.8 crypto-subtle + §6.9 crypto-key-capture + §6.10 aes-crypto-js
在浏览器一加载就注入(页面 JS 还没跑前) §3 通道 B (CDP) / 通道 C (Playwright) / 通道 D (Puppeteer)
把 webpack 打包后的 bundle 拆模块还原 §9 反混淆 Worker
33 个 hook 点都有什么、写哪里 §5 preset 速查
多 frame / iframe / Worker 怎么覆盖 §3.6 跨上下文 + §11 坑 4
数据怎么读出来分析 §4.2 bucket schema + §7 五步法

1 · 适用场景与边界

1.1 强适用

  • 签名 / 加密参数定位X-SignmsTokena_bogus__RequestVerificationToken、JWT 自签等。Hook fetch / xhr 拿到调用栈,倒推到生成函数。
  • API 形态摸底:把站内全部 fetch/xhr 流量按域名 / 路径分组,看请求和响应结构。
  • 加密算法识别:拦截 crypto.subtle.encrypt|decrypt|importKey|deriveKey 与 CryptoJS AES.encrypt / HmacSHA256 等,拿到 key、iv、算法配置和明文。
  • 反反调试:屏蔽 setInterval(debugger)console.clear 刷屏、performance.now 时间差检测、outerWidth - innerWidth DevTools 探测等。
  • 存储读写监控:cookie / localStorage / sessionStorage / IndexedDB 全链路审计,定位某个 token 是谁写进来的。
  • WebSocket 流量审计:抓握手 URL、上下行帧、关闭码。
  • 混淆代码还原:webpack / jsjiami / Sojson / AAEncode / Obfuscator.io 风格,配合 webcrack 一键拆模块。

1.2 不适用 / 性价比低

  • WASM 内部逻辑还原:JS hook 只能拦 import/export 边界,内部得用 wabt + 反编译。
  • V8 字节码 / JSVMP:本 SOP 只能识别"它在调用什么",不能还原"它在算什么",需专门 VMP 反汇编器。
  • 跨域 iframe:单次注入不会自动覆盖到子 origin 上下文,必须在每个 origin 各注入一次(见 §3.6)。
  • Service Worker / Worker 上下文:主线程 hook 不会传染到 worker,需要单独 hook new Worker() / Worker.prototype.postMessage 或在 worker 入口里注入。
  • Native code 内部crypto.subtle.encrypt 等被标记为 [native code] 的方法可以包装,但站点可以通过 Function.prototype.toString 检测包装函数体差异(见 §11 坑 5)。

2 · 设计原理(三层模型)

┌─────────────────────────────────────┐
│ ① 注入通道:把 hook JS 投到目标页    │  ← 见 §3
│   CDP / Playwright / Puppeteer /     │
│   DevTools 粘贴 / 用户脚本           │
└──────────────┬──────────────────────┘
               │ runs at  document_start ▼
┌─────────────────────────────────────┐
│ ② 通用注入引擎 + Preset 函数包装     │  ← 见 §4 §6
│   保留原引用 → 包装新函数 →           │
│   写入捕获缓冲 window.__aiHooks      │
└──────────────┬──────────────────────┘
               │ async  ▼
┌─────────────────────────────────────┐
│ ③ 离线读取 / 静态分析                 │  ← 见 §7 §9
│   evaluate window.__aiHooks → JSON   │
│   栈定位 → 取函数源码 → 反混淆        │
└─────────────────────────────────────┘

2.1 数据契约(统一 Bucket Schema)

所有 preset 命中后写入 window.__aiHooks['preset-<name>'],元素结构:

{
  ts: 1714660000000,
  // 网络类
  url, method, reqHeaders, reqBody, status, respHeaders, respBody,
  // 函数类
  fn, args, ret,
  // 加密类
  algo, key, iv, mode, padding, msg,
  // 通用
  stack: 'caller (file:line:col) | caller2 | caller3'
}

读取(任何执行 JS 的通道都行):

JSON.stringify(window.__aiHooks['preset-fetch'])

或在 Node 端用 CDP Runtime.evaluate({expression:'JSON.stringify(window.__aiHooks)', returnByValue:true})

2.2 三个不变量

  1. 幂等:每个 preset 头部用 if (window.__hookPresets && window.__hookPresets['<name>']) return; 守住,重复注入不会双重包装。
  2. 保引用const _orig = target.method.bind(target) 在最前面拿到,包装函数总是落到 _orig,不会因后续 hook 覆盖把链断了。
  3. 不抛错:所有 try{...}catch(e){} 包裹,hook 自身出错绝不影响业务页面。

2.3 stack 字段是逆向的灵魂

new Error().stack 在所有现代浏览器(Chromium / Firefox / Safari)都返回从当前调用点向上的调用链。Hook 中提取前 3-5 帧 → bucket → 离线分析时直接看:

at sign (https://target.com/static/js/app.4f7a.min.js:1:8842) | at request | at fetchOrders

app.4f7a.min.js:1:8842 就是签名函数所在文件 + 行列号,下一步可在 DevTools Sources 用同一行列号下断点。---

3 · 五种注入通道

任选一种即可。A 最快验证,B / C / D 适合自动化和"首屏前注入",E 适合给浏览器开发者协作分发。

3.1 通道 A · DevTools Console 粘贴(最快)

打开目标站 → F12 → Console → 粘贴 §6 任意 preset → 回车 → 触发业务动作 → 再敲:

JSON.stringify(window.__aiHooks['preset-fetch'], null, 2)

:F5 刷新后失效;如果业务在页面初始化阶段就调签名(如登录页打开就预签 csrf),来不及 hook。这种场景必须用 B / C / D。

3.2 通道 B · Chrome DevTools Protocol(首屏前注入)

启动 Chrome:

chrome --remote-debugging-port=9222 --user-data-dir=./profile

任何能讲 CDP 的客户端均可注入:

# Python + websockets
import json, asyncio, websockets, urllib.request

async def inject(target_ws, hook_js):
    async with websockets.connect(target_ws, max_size=None) as ws:
        _id = [0]
        async def call(method, params=None):
            _id[0] += 1
            mid = _id[0]
            await ws.send(json.dumps({'id': mid, 'method': method, 'params': params or {}}))
            while True:
                msg = json.loads(await ws.recv())
                if msg.get('id') == mid:
                    return msg.get('result')
        await call('Page.enable')
        # 关键 API:每次导航/iframe 创建都会重跑此脚本
        await call('Page.addScriptToEvaluateOnNewDocument', {'source': hook_js})
        await call('Page.reload')

tabs = json.loads(urllib.request.urlopen('http://127.0.0.1:9222/json').read())
target = next(t for t in tabs if 'target.example.com' in t['url'])
asyncio.run(inject(target['webSocketDebuggerUrl'], open('hooks.js').read()))

Page.addScriptToEvaluateOnNewDocument 是首屏前注入的关键。比 Runtime.evaluate 早一帧,能赶在页面任何脚本之前。

3.3 通道 C · Playwright

from playwright.sync_api import sync_playwright

hook_js = open('hooks.js').read()
with sync_playwright() as p:
    ctx = p.chromium.launch_persistent_context('./profile', headless=False)
    ctx.add_init_script(hook_js)            # 首屏前注入;同源 iframe 一并覆盖
    page = ctx.new_page()
    page.goto('https://target.example.com/login')
    page.click('button[type=submit]')
    data = page.evaluate("JSON.stringify(window.__aiHooks)")
    print(data)

3.4 通道 D · Puppeteer

const puppeteer = require('puppeteer');
const fs = require('fs');
(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.evaluateOnNewDocument(fs.readFileSync('hooks.js', 'utf8'));
  await page.goto('https://target.example.com/login');
  console.log(await page.evaluate(() => JSON.stringify(window.__aiHooks)));
})();

3.5 通道 E · 用户脚本(Tampermonkey / Violentmonkey)

// ==UserScript==
// @name         hook-fetch
// @match        https://target.example.com/*
// @run-at       document-start
// @grant        none
// ==/UserScript==
/* 这里粘贴 §6 任意 preset 主体 */

@run-at document-start + 沙箱关闭(@grant none 在 Tampermonkey;Violentmonkey 勾 "use page context")才能在页面 JS 之前抢位。

3.6 跨上下文:iframe / Worker

上下文 CDP Playwright Puppeteer
同源 iframe addScriptToEvaluateOnNewDocument 自动覆盖 add_init_script 自动覆盖 evaluateOnNewDocument 自动覆盖
跨域 iframe Target.attachToTarget 拿子 target → 重跑 frame.target() 单独处理 同 Playwright
Worker 主线程注入不传染。Hook new Worker(url) 改 url,或拦 Worker.prototype.postMessage 同左 同左
Service Worker Service Worker target 单独 attach;或在源码层修改 同 CDP 同 CDP

4 · 通用注入引擎 __core.js

把这一段记作 __core.js,放在你 init script 的最顶部。所有 preset 都可基于它重写得更短;如果你只想用单个 preset 也可以省略此段——每个 preset 已自带最小实现。

// __core.js  极简通用 hook 引擎;幂等、保引用、不抛错
(function () {
  if (window.__aiHook) return;
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const TRUNC = (window.__aiHookTrunc = 2000);

  function _stack() {
    return ((new Error()).stack || '')
      .split('\n').slice(2, 6).map(s => s.trim()).join(' | ');
  }
  function _short(v) {
    if (v == null) return v;
    if (typeof v === 'string') return v.length > TRUNC ? v.slice(0, TRUNC) + '...[trunc]' : v;
    if (v instanceof FormData) return '[FormData]';
    if (v instanceof URLSearchParams) return v.toString().slice(0, TRUNC);
    if (v instanceof ArrayBuffer) return '[ArrayBuffer ' + v.byteLength + 'B]';
    if (v instanceof Uint8Array)  return '[Uint8Array ' + v.byteLength + 'B]';
    try { return JSON.stringify(v).slice(0, TRUNC); } catch (e) { return '[Unserializable]'; }
  }
  function _push(key, entry) {
    (buckets[key] = buckets[key] || []).push(entry);
    try { if (window.__aiHookLog !== false) console.log('[Hook:' + key + ']', entry); } catch (e) {}
  }

  // hook(obj, methodName, {pre, post, bucket})
  window.__aiHook = function (obj, name, opts) {
    opts = opts || {};
    const key = opts.bucket || ('hook-' + name);
    const orig = obj[name];
    if (!orig || orig.__aiHooked) return orig;
    function wrapped() {
      const args = Array.prototype.slice.call(arguments);
      const ctx = { fn: name, args: args.map(_short), bucket: key, ts: Date.now(), stack: _stack() };
      try { if (opts.pre) opts.pre(args, ctx); } catch (e) { ctx.preErr = String(e); }
      let ret;
      try { ret = orig.apply(this, args); }
      catch (err) { ctx.err = String(err); _push(key, ctx); throw err; }
      try {
        if (ret && typeof ret.then === 'function') {
          ret.then(r => { ctx.ret = _short(r); if (opts.post) opts.post(args, r, ctx); _push(key, ctx); },
                   e => { ctx.err = String(e); _push(key, ctx); });
        } else {
          ctx.ret = _short(ret);
          if (opts.post) opts.post(args, ret, ctx);
          _push(key, ctx);
        }
      } catch (e) { _push(key, ctx); }
      return ret;
    }
    wrapped.__aiHooked = true;
    // 反检测:让 toString 看起来像 native
    wrapped.toString = function () { return orig.toString(); };
    obj[name] = wrapped;
    return orig;
  };
})();

用法举例(极简版 fetch hook):

window.__aiHook(window, 'fetch', {
  bucket: 'preset-fetch',
  pre: (args, ctx) => {
    const [input, init] = args;
    ctx.url = typeof input === 'string' ? input : input && input.url;
    ctx.method = (init && init.method) || 'GET';
  },
  post: (args, resp, ctx) => {
    ctx.status = resp && resp.status;
    if (resp && resp.clone) {
      resp.clone().text().then(t => { ctx.respBody = t.length > 2000 ? t.slice(0, 2000) + '...[trunc]' : t; });
    }
  }
});

5 · 33 个 Preset 速查表

类别 preset bucket key 一句话用途
网络 fetch preset-fetch 抓 fetch 全量请求/响应+栈
网络 xhr preset-xhr 抓 XMLHttpRequest 全量+栈
网络 websocket preset-websocket WS 握手与上下行帧
网络 response-tap preset-response-tap 仅抓匹配 window.__hookFilter 的响应体
网络 fetch-clone preset-fetch-clone fetch 强制 clone 出 body 二次消费
网络 xhr-overrideMimeType preset-xhr-mime 强制覆盖响应 MIME 给加密响应解明文
运行时 eval preset-eval 拦 eval(string) 抓动态代码
运行时 function-constructor preset-fn 拦 new Function('...')
运行时 settimeout preset-st setTimeout 回调源码
运行时 setinterval preset-si setInterval 回调源码
运行时 addeventlistener preset-aev 抓所有事件订阅
运行时 postmessage preset-pm iframe / opener 通讯流量
运行时 mutationobserver preset-mo DOM 变更监听器注册
运行时 proxy preset-proxy new Proxy() 创建拦截
运行时 reflect preset-reflect Reflect.* 调用拦截
运行时 object-defineproperty preset-odp 元编程拦截
存储 cookie preset-cookie document.cookie set/get
存储 localstorage preset-localstorage localStorage 读写
存储 sessionstorage preset-sessionstorage sessionStorage 读写
存储 indexeddb preset-idb IDB open / put / get
加密 atob preset-atob base64 解码进出
加密 btoa preset-btoa base64 编码进出
加密 crypto-subtle preset-crypto-subtle WebCrypto encrypt/decrypt
加密 crypto-key-capture preset-crypto-key-capture importKey + deriveKey 抓 raw key
加密 aes-crypto-js preset-aes-crypto-js CryptoJS.AES + Hmac/Hash 全家桶
反检测 anti-debug-bypass preset-anti-debug-bypass 干掉 debugger/devtools 检测
反检测 navigator-useragent preset-nav-ua navigator.userAgent 读取审计
反检测 navigator-stack preset-nav-stack navigator 全属性读取栈
反检测 canvas-fingerprint preset-canvas-fp 拦 toDataURL/getImageData
反检测 webassembly preset-wasm wasm.instantiate 入口
反检测 webassembly-full preset-wasm-full wasm 全量 (含 compile/Module)
跳转 location-href preset-loc location.href 写入审计
跳转 window-open preset-win-open window.open 调用审计
DOM document-write preset-doc-write document.write 拦截

下文 §6 内联了 13 个最常用 preset 完整可粘贴代码。其余 20 个按相同模式照抄即可:拿到原引用 → 包装 → 写 bucket。---

6 · 13 个核心 Preset 全文

每段都自带最小依赖,可独立粘贴到 Console 或注入脚本中运行。所有 preset 写到统一 bucket:window.__aiHooks['preset-<name>']

6.1 fetch

(function(){
  if (window.__hookFetchInstalled) return; window.__hookFetchInstalled = true;
  const _fetch = window.fetch.bind(window);
  const KEY = 'preset-fetch';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);

  window.fetch = function(input, init) {
    const stack = ((new Error()).stack || '').split('\n').slice(2, 6).join(' | ');
    const url = typeof input === 'string' ? input : (input && input.url) || '';
    const method = (init && init.method) || (input && input.method) || 'GET';
    let reqHeaders = {};
    try {
      const h = (init && init.headers) || (input && input.headers);
      if (h) {
        if (h.forEach) h.forEach((v,k) => reqHeaders[k] = v);
        else Object.assign(reqHeaders, h);
      }
    } catch(e){}
    let reqBody = (init && init.body) || (input && input.body);
    if (reqBody && reqBody instanceof FormData) {
      const tmp = {}; reqBody.forEach((v,k) => tmp[k] = String(v).slice(0,500)); reqBody = tmp;
    } else if (reqBody && reqBody instanceof URLSearchParams) {
      reqBody = reqBody.toString();
    } else if (reqBody && typeof reqBody !== 'string') {
      try { reqBody = JSON.stringify(reqBody); } catch(e) { reqBody = '[unserializable]'; }
    }
    const entry = { ts: Date.now(), url, method, reqHeaders, reqBody, stack };

    return _fetch(input, init).then(resp => {
      entry.status = resp.status;
      try {
        const rh = {}; resp.headers.forEach((v,k) => rh[k] = v); entry.respHeaders = rh;
      } catch(e){}
      try {
        resp.clone().text().then(t => {
          entry.respBody = t.length > 2000 ? t.slice(0,2000)+'...[trunc]' : t;
        });
      } catch(e){}
      bucket.push(entry);
      return resp;
    }, err => {
      entry.err = String(err); bucket.push(entry); throw err;
    });
  };
  window.fetch.toString = () => _fetch.toString();
})();

6.2 xhr (XMLHttpRequest)

(function(){
  if (window.__hookXhrInstalled) return; window.__hookXhrInstalled = true;
  const _open = XMLHttpRequest.prototype.open;
  const _send = XMLHttpRequest.prototype.send;
  const _setRH = XMLHttpRequest.prototype.setRequestHeader;
  const KEY = 'preset-xhr';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);

  XMLHttpRequest.prototype.open = function(method, url) {
    this.__hookEntry = {
      ts: Date.now(), method, url,
      reqHeaders: {}, stack: ((new Error()).stack||'').split('\n').slice(2,6).join(' | ')
    };
    return _open.apply(this, arguments);
  };
  XMLHttpRequest.prototype.setRequestHeader = function(k, v) {
    if (this.__hookEntry) this.__hookEntry.reqHeaders[k] = v;
    return _setRH.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function(body) {
    const e = this.__hookEntry;
    if (e) {
      let b = body;
      if (b instanceof FormData) { const t={}; b.forEach((v,k)=>t[k]=String(v).slice(0,500)); b=t; }
      else if (b instanceof URLSearchParams) b = b.toString();
      else if (b && typeof b !== 'string') { try{b=JSON.stringify(b);}catch(_){b='[unserializable]';} }
      e.reqBody = b;
      const xhr = this;
      this.addEventListener('loadend', function(){
        e.status = xhr.status;
        try {
          const rh = {};
          (xhr.getAllResponseHeaders()||'').trim().split(/\r?\n/).forEach(line => {
            const i = line.indexOf(':'); if (i>0) rh[line.slice(0,i).trim()] = line.slice(i+1).trim();
          });
          e.respHeaders = rh;
        } catch(_){}
        try {
          const t = xhr.responseType==='' || xhr.responseType==='text' ? xhr.responseText : '[non-text]';
          e.respBody = t.length > 2000 ? t.slice(0,2000)+'...[trunc]' : t;
        } catch(_){}
        bucket.push(e);
      });
    }
    return _send.apply(this, arguments);
  };
})();

6.3 response-tap(按 URL 关键字筛选响应体)

(function(){
  if (window.__hookRespTap) return; window.__hookRespTap = true;
  // 用法:window.__hookFilter = (url) => /\/sign|\/login|\/api\/secure/.test(url);
  const _fetch = window.fetch.bind(window);
  const KEY = 'preset-response-tap';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);
  const matchFn = () => (typeof window.__hookFilter === 'function' ? window.__hookFilter : (u)=>true);

  window.fetch = function(input, init) {
    const url = typeof input === 'string' ? input : (input && input.url) || '';
    const p = _fetch(input, init);
    if (matchFn()(url)) {
      p.then(resp => {
        try {
          resp.clone().text().then(t => {
            bucket.push({ ts: Date.now(), url, status: resp.status,
              body: t.length > 4000 ? t.slice(0,4000)+'...[trunc]' : t,
              stack: ((new Error()).stack||'').split('\n').slice(2,5).join(' | ')
            });
          });
        } catch(_){}
      }, ()=>{});
    }
    return p;
  };
})();
```### 6.4 websocket

```js
(function(){
  if (window.__hookWS) return; window.__hookWS = true;
  const _WS = window.WebSocket;
  const KEY = 'preset-websocket';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);

  function Hooked(url, protocols) {
    const ws = protocols ? new _WS(url, protocols) : new _WS(url);
    const conn = { ts: Date.now(), url, protocols, frames: [],
      stack: ((new Error()).stack||'').split('\n').slice(2,5).join(' | ') };
    bucket.push(conn);
    const _send = ws.send.bind(ws);
    ws.send = function(d) {
      let p = d;
      if (p instanceof ArrayBuffer) p = '[ArrayBuffer '+p.byteLength+'B]';
      else if (p instanceof Blob) p = '[Blob '+p.size+'B]';
      conn.frames.push({ dir:'out', ts: Date.now(), data: typeof p==='string' ? p.slice(0,2000) : p });
      return _send(d);
    };
    ws.addEventListener('message', ev => {
      let d = ev.data;
      if (d instanceof ArrayBuffer) d = '[ArrayBuffer '+d.byteLength+'B]';
      else if (d instanceof Blob) d = '[Blob '+d.size+'B]';
      conn.frames.push({ dir:'in', ts: Date.now(), data: typeof d==='string' ? d.slice(0,2000) : d });
    });
    ws.addEventListener('close', ev => {
      conn.closeCode = ev.code; conn.closeReason = ev.reason;
    });
    return ws;
  }
  Hooked.prototype = _WS.prototype;
  Object.assign(Hooked, _WS);
  window.WebSocket = Hooked;
})();

6.5 eval

(function(){
  if (window.__hookEval) return; window.__hookEval = true;
  const _eval = window.eval;
  const KEY = 'preset-eval';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);
  // 注意:eval 必须保持 `直接 eval` 的语义,包装会变间接 eval 影响作用域
  // 这里牺牲直接 eval 语义换取审计;逆向场景通常可接受
  window.eval = function(src) {
    bucket.push({
      ts: Date.now(),
      src: typeof src === 'string' ? (src.length > 4000 ? src.slice(0,4000)+'...[trunc]' : src) : '[non-string]',
      stack: ((new Error()).stack||'').split('\n').slice(2,6).join(' | ')
    });
    return _eval(src);
  };
  window.eval.toString = () => _eval.toString();
})();

6.6 function-constructor (new Function('...'))

(function(){
  if (window.__hookFn) return; window.__hookFn = true;
  const _Fn = window.Function;
  const KEY = 'preset-fn';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);
  function Hooked() {
    const args = Array.prototype.slice.call(arguments);
    const body = args.length ? args[args.length-1] : '';
    bucket.push({
      ts: Date.now(),
      params: args.slice(0,-1),
      body: typeof body === 'string' ? (body.length > 4000 ? body.slice(0,4000)+'...[trunc]' : body) : '[non-string]',
      stack: ((new Error()).stack||'').split('\n').slice(2,6).join(' | ')
    });
    return _Fn.apply(this, args);
  }
  Hooked.prototype = _Fn.prototype;
  Hooked.toString = () => _Fn.toString();
  window.Function = Hooked;
})();

6.7 settimeout / setinterval(合一,按需注释一边)

(function(){
  if (window.__hookTimers) return; window.__hookTimers = true;
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bST = (buckets['preset-st'] = buckets['preset-st'] || []);
  const bSI = (buckets['preset-si'] = buckets['preset-si'] || []);
  const _st = window.setTimeout, _si = window.setInterval;

  window.setTimeout = function(cb, delay) {
    bST.push({
      ts: Date.now(), delay,
      src: typeof cb === 'function' ? cb.toString().slice(0,2000) : (typeof cb === 'string' ? cb.slice(0,2000) : '[other]'),
      stack: ((new Error()).stack||'').split('\n').slice(2,5).join(' | ')
    });
    return _st.apply(this, arguments);
  };
  window.setInterval = function(cb, delay) {
    bSI.push({
      ts: Date.now(), delay,
      src: typeof cb === 'function' ? cb.toString().slice(0,2000) : (typeof cb === 'string' ? cb.slice(0,2000) : '[other]'),
      stack: ((new Error()).stack||'').split('\n').slice(2,5).join(' | ')
    });
    return _si.apply(this, arguments);
  };
})();
```### 6.8 cookie / localStorage / sessionStorage(合一)

```js
(function(){
  if (window.__hookStorage) return; window.__hookStorage = true;
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bC = (buckets['preset-cookie'] = buckets['preset-cookie'] || []);
  const bL = (buckets['preset-localstorage'] = buckets['preset-localstorage'] || []);
  const bS = (buckets['preset-sessionstorage'] = buckets['preset-sessionstorage'] || []);

  // cookie
  try {
    const desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') ||
                 Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
    if (desc && desc.configurable) {
      Object.defineProperty(document, 'cookie', {
        configurable: true,
        get() { const v = desc.get.call(document); bC.push({op:'get', val:v, ts:Date.now()}); return v; },
        set(v) { bC.push({op:'set', val:v, ts:Date.now(),
                          stack:((new Error()).stack||'').split('\n').slice(2,5).join(' | ')});
                 return desc.set.call(document, v); }
      });
    }
  } catch(e){}

  // localStorage / sessionStorage 公共包装
  function wrap(storage, bucket) {
    const _set = storage.setItem.bind(storage);
    const _get = storage.getItem.bind(storage);
    const _rm  = storage.removeItem.bind(storage);
    storage.setItem = function(k,v){
      bucket.push({op:'set', k, v: String(v).slice(0,1000), ts:Date.now(),
        stack:((new Error()).stack||'').split('\n').slice(2,5).join(' | ')});
      return _set(k,v);
    };
    storage.getItem = function(k){
      const v = _get(k);
      bucket.push({op:'get', k, v: v==null?null:String(v).slice(0,500), ts:Date.now()});
      return v;
    };
    storage.removeItem = function(k){
      bucket.push({op:'rm', k, ts:Date.now()});
      return _rm(k);
    };
  }
  try { wrap(window.localStorage,   bL); } catch(e){}
  try { wrap(window.sessionStorage, bS); } catch(e){}
})();

6.9 atob / btoa

(function(){
  if (window.__hookB64) return; window.__hookB64 = true;
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bA = (buckets['preset-atob'] = buckets['preset-atob'] || []);
  const bB = (buckets['preset-btoa'] = buckets['preset-btoa'] || []);
  const _atob = window.atob, _btoa = window.btoa;
  window.atob = function(s){
    const r = _atob(s);
    bA.push({ts:Date.now(), in:String(s).slice(0,500), out:String(r).slice(0,500),
             stack:((new Error()).stack||'').split('\n').slice(2,5).join(' | ')});
    return r;
  };
  window.btoa = function(s){
    const r = _btoa(s);
    bB.push({ts:Date.now(), in:String(s).slice(0,500), out:String(r).slice(0,500),
             stack:((new Error()).stack||'').split('\n').slice(2,5).join(' | ')});
    return r;
  };
})();

6.10 crypto.subtle(WebCrypto)

(function(){
  if (!window.crypto || !window.crypto.subtle) return;
  if (window.__hookSubtle) return; window.__hookSubtle = true;
  const subtle = window.crypto.subtle;
  const KEY = 'preset-crypto-subtle';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);

  function buf2hex(b){
    if (!b) return null;
    const u = b instanceof ArrayBuffer ? new Uint8Array(b) :
              (b.buffer ? new Uint8Array(b.buffer) : null);
    if (!u) return '[non-buffer]';
    let s=''; for (let i=0;i<Math.min(u.length,256);i++) s += u[i].toString(16).padStart(2,'0');
    return s + (u.length>256 ? '...['+u.length+'B]' : '');
  }

  ['encrypt','decrypt','sign','verify','digest','deriveBits','deriveKey'].forEach(method => {
    const _orig = subtle[method].bind(subtle);
    subtle[method] = function(algo, keyOrData, data){
      const entry = {
        ts: Date.now(), method,
        algo: typeof algo === 'string' ? algo : (algo && algo.name) + (algo && algo.iv ? '+iv:'+buf2hex(algo.iv) : ''),
        msg: method==='digest' ? buf2hex(keyOrData) : buf2hex(data),
        stack: ((new Error()).stack||'').split('\n').slice(2,6).join(' | ')
      };
      const p = _orig.apply(subtle, arguments);
      p.then(r => { entry.ret = buf2hex(r instanceof ArrayBuffer ? r : null) || '[Key/CryptoKey]'; bucket.push(entry); },
             e => { entry.err = String(e); bucket.push(entry); });
      return p;
    };
  });

  // importKey 单独处理:拿原始 key 字节
  const _imp = subtle.importKey.bind(subtle);
  subtle.importKey = function(format, keyData, algo, extractable, usages){
    bucket.push({
      ts: Date.now(), method: 'importKey', format,
      algo: typeof algo === 'string' ? algo : algo && algo.name,
      keyHex: format === 'raw' ? buf2hex(keyData) : '['+format+']',
      extractable, usages,
      stack: ((new Error()).stack||'').split('\n').slice(2,5).join(' | ')
    });
    return _imp(format, keyData, algo, extractable, usages);
  };
})();
```### 6.11 aes-crypto-js(CryptoJS 全家桶)

```js
(function(){
  if (window.__hookCryptoJS) return; window.__hookCryptoJS = true;
  const KEY = 'preset-aes-crypto-js';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);

  // CryptoJS 通常挂在 window.CryptoJS;有些站点放 require 模块里
  function tryHook() {
    const C = window.CryptoJS;
    if (!C || C.__hooked) return false;
    C.__hooked = true;

    // AES / DES / TripleDES / Rabbit / RC4
    ['AES','DES','TripleDES','Rabbit','RC4'].forEach(name => {
      const cipher = C[name];
      if (!cipher) return;
      ['encrypt','decrypt'].forEach(op => {
        const _orig = cipher[op].bind(cipher);
        cipher[op] = function(msg, key, cfg) {
          bucket.push({
            ts: Date.now(), algo: name, op,
            msg: msg && msg.toString ? msg.toString().slice(0,500) : String(msg).slice(0,500),
            key: key && key.toString ? key.toString() : String(key),
            mode: cfg && cfg.mode && cfg.mode.toString && cfg.mode.toString().match(/function (\w+)/),
            iv:   cfg && cfg.iv && cfg.iv.toString ? cfg.iv.toString() : null,
            padding: cfg && cfg.padding && cfg.padding.toString && cfg.padding.toString().match(/function (\w+)/),
            stack: ((new Error()).stack||'').split('\n').slice(2,6).join(' | ')
          });
          return _orig(msg, key, cfg);
        };
      });
    });

    // HMAC + 哈希
    ['HmacSHA256','HmacSHA1','HmacSHA512','HmacMD5','SHA256','SHA1','SHA512','MD5'].forEach(name => {
      const fn = C[name]; if (!fn) return;
      C[name] = function(msg, key) {
        const r = fn.apply(this, arguments);
        bucket.push({
          ts: Date.now(), algo: name,
          msg: msg && msg.toString ? msg.toString().slice(0,500) : String(msg).slice(0,500),
          key: key && key.toString ? key.toString() : (key ? String(key) : null),
          ret: r && r.toString ? r.toString() : null,
          stack: ((new Error()).stack||'').split('\n').slice(2,5).join(' | ')
        });
        return r;
      };
    });
    return true;
  }

  if (!tryHook()) {
    // 轮询等 CryptoJS 加载
    const t = setInterval(() => { if (tryHook()) clearInterval(t); }, 200);
    setTimeout(() => clearInterval(t), 30000);
  }
})();

6.12 anti-debug-bypass(屏蔽常见反调试)

(function(){
  if (window.__hookAntiDbg) return; window.__hookAntiDbg = true;
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets['preset-anti-debug-bypass'] = buckets['preset-anti-debug-bypass'] || []);

  // 1) 阻断 setInterval / setTimeout 中的 debugger 字面量
  const _st = window.setTimeout, _si = window.setInterval;
  function clean(cb) {
    if (typeof cb === 'function') {
      const src = cb.toString();
      if (/\bdebugger\b/.test(src)) {
        bucket.push({type:'debugger-fn-blocked', src:src.slice(0,500), ts:Date.now()});
        return function(){};
      }
    } else if (typeof cb === 'string' && /\bdebugger\b/.test(cb)) {
      bucket.push({type:'debugger-str-blocked', src:cb.slice(0,500), ts:Date.now()});
      return '';
    }
    return cb;
  }
  window.setTimeout  = function(cb, d){ return _st(clean(cb), d); };
  window.setInterval = function(cb, d){ return _si(clean(cb), d); };

  // 2) Function 构造里塞 debugger
  const _Fn = window.Function;
  function HookedFn(){
    const args = [].slice.call(arguments);
    const body = args[args.length-1];
    if (typeof body === 'string' && /\bdebugger\b/.test(body)) {
      bucket.push({type:'fn-debugger-blocked', body:body.slice(0,500), ts:Date.now()});
      args[args.length-1] = body.replace(/\bdebugger\b/g, ';');
    }
    return _Fn.apply(this, args);
  }
  HookedFn.prototype = _Fn.prototype;
  window.Function = HookedFn;

  // 3) 屏蔽 console.clear
  const _cc = console.clear; console.clear = function(){ bucket.push({type:'console.clear-blocked', ts:Date.now()}); };

  // 4) performance.now 时间差检测:返回稳定步进
  try {
    const _pn = performance.now.bind(performance);
    let last = _pn(), step = 16;
    performance.now = function(){ last += step; return last; };
  } catch(e){}

  // 5) outerWidth-innerWidth DevTools 探测:固定为 0
  try {
    Object.defineProperty(window, 'outerWidth',  { get: () => window.innerWidth });
    Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight });
  } catch(e){}
})();

6.13 postmessage

(function(){
  if (window.__hookPM) return; window.__hookPM = true;
  const KEY = 'preset-pm';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);
  const _pm = window.postMessage.bind(window);
  window.postMessage = function(msg, target, transfer) {
    let m = msg;
    try { m = typeof msg === 'string' ? msg.slice(0,1000) : JSON.stringify(msg).slice(0,1000); } catch(e){ m = '[unserializable]'; }
    bucket.push({dir:'out', target, msg:m, ts:Date.now(),
                 stack:((new Error()).stack||'').split('\n').slice(2,5).join(' | ')});
    return _pm(msg, target, transfer);
  };
  window.addEventListener('message', ev => {
    let m = ev.data;
    try { m = typeof m === 'string' ? m.slice(0,1000) : JSON.stringify(m).slice(0,1000); } catch(e){ m = '[unserializable]'; }
    bucket.push({dir:'in', origin:ev.origin, msg:m, ts:Date.now()});
  }, true);
})();

其余 20 个 preset(mutationobserver / proxy / reflect / object-defineproperty / addeventlistener / canvas-fingerprint / webassembly / location-href / window-open / document-write / xhr-overrideMimeType / fetch-clone / crypto-key-capture / nav-ua / nav-stack / indexeddb 等)实现完全同构:拿原引用 → 包装 → 写 bucket。可直接套 §4 通用引擎一行写完。---

7 · 数据读取与分析五步法

1. 注入 → 2. 触发业务 → 3. 读 bucket → 4. 栈定位 → 5. 取源码 / 反混淆

Step 1 · 注入

按 §3 选通道;建议把 §6 用到的 preset 拼到一个 hooks.js,按需多个 preset 同时打开。

Step 2 · 触发业务

人工或脚本走一遍要逆向的链路(点登录、提交搜索、滑滚刷新)。只有走过的路径才会被采到

Step 3 · 读 bucket

JSON.stringify(window.__aiHooks)         // 全量
JSON.stringify(window.__aiHooks['preset-fetch'].slice(-5))  // 最近 5 条

或写盘(DevTools 控制台):

copy(JSON.stringify(window.__aiHooks))   // 复制到剪贴板

CDP / Playwright / Puppeteer 直接在 Node / Python 端 evaluate 拿回。

Step 4 · 栈定位

把感兴趣条目的 stack 字段拿出来,定位关键帧,例如:

at sign (https://target.com/static/js/app.4f7a.min.js:1:8842)
   | at Object.request (...:1:9120)
   | at fetchOrders (...:1:9555)

→ 在 DevTools Sources 打开 app.4f7a.min.js,跳到 1:8842,就是签名函数现场;右键 "Pretty print" 后下断点。

Step 5 · 取源码 / 反混淆

对着 sign 函数:

  • DevTools 右键 → "Copy function" 拿源码片段;
  • 或在 Console:window.__getFnSrc = (fn) => fn.toString() 获取整体;
  • 函数体被混淆 → 进 §9 反混淆 Worker 还原。

8 · 五个完整逆向范式

每个范式给出 目标 → 用什么 preset → 操作步骤 → 输出物,覆盖 90% 实战场景。

8.1 范式 1:定位"签名"是哪个函数算出来的

目标:知道 X-Sign / __token / _signature 这种请求头/参数从哪个函数生成。

preset:fetch + xhr + function-constructor + eval

步骤

  1. 注入上述 4 个 preset。
  2. 人工触发一次接口请求。
  3. JSON.stringify(window.__aiHooks['preset-fetch']) → 找到目标接口 entry。
  4. entry.reqHeaders['x-sign'] 已经有了,再看 entry.stack
  5. 栈里第一个非 fetch wrapper 帧就是上层调用者,往上爬到一个名字像 sign / getSign / _$x 的函数,那就是入口。
  6. 入口函数体内若调用 crypto.subtle / CryptoJS / atob → 配合 §6.10 / §6.11 / §6.9 拿到 key 与算法。

输出{ entry_function: 'app.js:1:8842', algo: 'HmacSHA256', key: 'xxx', input_template: 'METHOD\nURL\nTIMESTAMP\nBODY' }

8.2 范式 2:抓动态加载/动态求值的代码

目标:业务用 eval / new Function('...') / document.write('<script>...') / WebAssembly 拼出运行时逻辑,源代码看不到。

preset:eval + function-constructor + document-write + atob

步骤

  1. 注入 4 个 preset。
  2. 全量加载 + 操作页面。
  3. window.__aiHooks['preset-eval']preset-fn 里的 src / body 字段直接是动态代码字符串。
  4. 如果代码先 base64 → atob → eval,preset-atob 会先记下解码后的明文。

输出:动态拼出的全部脚本字符串,可拿去单独反混淆 / 静态阅读。### 8.3 范式 3:解密响应(看不懂的密文响应)

目标:服务端返回的 body 是密文(base64 / AES / 自研),UI 显示却是明文,即客户端解密。

preset:fetch + xhr + crypto-subtle + aes-crypto-js + atob

步骤

  1. 注入。
  2. 触发返回密文的接口。
  3. preset-fetch.respBody 是否为 base64 / 16 进制。
  4. 看接下来的 preset-atobpreset-aes-crypto-js / preset-crypto-subtle 条目,时间戳紧跟 fetch 之后的就是同一密文的解密链。
  5. 拿到 algo + key + iv,离线复算解密。

输出:解密算法配置 + 一段可独立运行的解密代码。

8.4 范式 4:反反调试 / 防卡住

目标:站点用 setInterval(()=>{debugger}, 100) 之类卡死 Sources 面板,无法下断点。

preset:anti-debug-bypass

步骤

  1. 第一时间(document_start)注入 §6.12,必须用通道 B / C / D / E,不能用 A 粘贴 Console(已经晚了)。
  2. F12 即可正常使用,不会被 debugger 截停。
  3. 注意:仅屏蔽常见技法,遇到检测函数 toString 的高级反调试需要逐项专攻(见 §11 坑 5)。

输出:DevTools 可正常调试。

8.5 范式 5:跟踪某个全局变量是谁写进去的

目标window.__INITIAL_STATE__ / window.csrf / window.__sign_seed 这种全局态,想知道哪段代码塞的。

preset:object-defineproperty(自写 trap)

(function(){
  const KEY = 'preset-trace-global';
  const buckets = (window.__aiHooks = window.__aiHooks || {});
  const bucket = (buckets[KEY] = buckets[KEY] || []);
  const TARGET = '__INITIAL_STATE__';   // ← 改这里
  let _v;
  Object.defineProperty(window, TARGET, {
    configurable: true,
    get(){ return _v; },
    set(v){
      bucket.push({op:'set', val: typeof v==='string'?v.slice(0,500):'[obj]',
        stack: ((new Error()).stack||'').split('\n').slice(2,8).join(' | '), ts: Date.now()});
      _v = v;
    }
  });
})();

输出:写入栈直指业务赋值代码所在的 file:line。


9 · 反混淆 Worker(webcrack 集成)

把 webpack / Sojson / Obfuscator.io 风格的 bundle 还原成可读源码。前置依赖:Node 18+ 和 webcrack npm 包。

mkdir deobf && cd deobf
npm init -y
npm i webcrack

deobf.js

#!/usr/bin/env node
// 用法: node deobf.js input.js out_dir
import fs from 'fs';
import path from 'path';
import { webcrack } from 'webcrack';

const [,, input, outDir = './out'] = process.argv;
if (!input) { console.error('usage: node deobf.js <input.js> [outDir]'); process.exit(1); }

const code = fs.readFileSync(input, 'utf8');
const result = await webcrack(code, {
  jsx: true,           // 还原 React.createElement → JSX
  unpack: true,        // webpack/browserify 拆模块
  deobfuscate: true,   // Obfuscator.io 字符串数组、控制流平坦化
  unminify: true,      // 还原变量名 / 控制结构
  mangle: false
});

fs.mkdirSync(outDir, { recursive: true });
await result.save(outDir);
console.log('[ok]', result.bundle ? `bundle ${result.bundle.type} restored` : 'flat file restored',
            '→', path.resolve(outDir));

典型入参

# 1) 单文件混淆
node deobf.js obfuscated.js out

# 2) 整个 webpack runtime
curl -s https://target.com/static/js/main.4f7a.min.js > main.js
node deobf.js main.js out
ls out/   # → 拆出 src/<modId>.js 一组可读文件

配合 §6 流程

  1. §6 先抓到关键文件 URL(stack 字段里的源),比如 app.4f7a.min.js
  2. 浏览器或 curl 把它拉下来。
  3. 上 webcrack。
  4. 进 §8 范式 1 / 3 用拆好的可读源码定位算法。

  • webcrack 处理 5MB+ 文件会很慢且吃内存(>4GB),对超大 bundle 先用 await webcrack(code, { unpack: true, deobfuscate: false }) 分两步走。
  • VMP(虚拟机保护)不在 webcrack 适用范围。---

10 · 端到端示例:抓某站登录请求签名

假设目标站 https://demo.example.com/login 提交时带 X-Sign 请求头,想知道算法。

10.1 准备

hooks.js

// 把 §6.1 fetch + §6.2 xhr + §6.10 crypto-subtle + §6.11 aes-crypto-js 全文拼接到这里

启动 Chrome:

chrome --remote-debugging-port=9222 --user-data-dir=./profile https://demo.example.com/login

注入(Python):

import asyncio, json, urllib.request, websockets

HOOKS = open('hooks.js').read()
TARGET_URL_KEYWORD = 'demo.example.com'

async def main():
    tabs = json.loads(urllib.request.urlopen('http://127.0.0.1:9222/json').read())
    tab = next(t for t in tabs if TARGET_URL_KEYWORD in t['url'])
    async with websockets.connect(tab['webSocketDebuggerUrl'], max_size=None) as ws:
        _id = [0]
        async def call(m, p=None):
            _id[0]+=1; mid=_id[0]
            await ws.send(json.dumps({'id':mid,'method':m,'params':p or {}}))
            while True:
                msg = json.loads(await ws.recv())
                if msg.get('id')==mid: return msg.get('result')
        await call('Page.enable')
        await call('Page.addScriptToEvaluateOnNewDocument', {'source': HOOKS})
        await call('Page.reload')
        await asyncio.sleep(3)
        # 模拟点击登录后人工或脚本完成动作;这里假设你已用 Input.dispatchMouseEvent 完成
        input('>>> 在浏览器里完成一次登录后回车继续\n')
        r = await call('Runtime.evaluate', {
            'expression':"JSON.stringify(window.__aiHooks)", 'returnByValue': True})
        data = json.loads(r['result']['value'])
        with open('out.json','w',encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)
        print('saved out.json, fetch buckets:', len(data.get('preset-fetch', [])))

asyncio.run(main())

10.2 分析

jq '.["preset-fetch"][] | select(.url | contains("/login")) | {url, reqHeaders, reqBody, stack}' out.json

输出(示意):

{
  "url": "https://demo.example.com/api/login",
  "reqHeaders": { "x-sign": "1714660000:7f3a2c..." },
  "reqBody": "{\"u\":\"alice\",\"p\":\"...\"}",
  "stack": "at sign (https://demo.example.com/static/app.4f7a.js:1:8842) | at request | at submit"
}

再看 preset-aes-crypto-js

[{ "algo": "HmacSHA256", "msg": "1714660000\nPOST\n/api/login\n{\"u\":\"alice\"...", "key": "demo-shared-key-2024", "ret": "7f3a2c..." }]

→ 算法:HmacSHA256(timestamp + '\n' + METHOD + '\n' + PATH + '\n' + body, 'demo-shared-key-2024'),ret 即 x-sign 的冒号右侧。 → 用任何 HMAC 库即可离线复算。

10.3 整段流程总结

注入 → 触发登录 → 读 bucket
      ├─ preset-fetch    : 拿到 URL + headers + stack
      ├─ preset-aes-crypto-js: 拿到 algo + key + 输入模板 + 输出
      └─ stack 文件位置 → DevTools Sources 二次确认

11 · 八个常见坑

  1. document_start 没赶上:很多签名/反调试在第一帧就跑。一定用 Page.addScriptToEvaluateOnNewDocument / add_init_script / evaluateOnNewDocument / @run-at document-start不能在 Page.loaded 之后注入

  2. iframe / Worker 没覆盖:跨域 iframe 必须各自 attach;Worker 主线程 hook 不会传染,要么 hook new Worker(url) 改 url,要么在 worker 入口里注入。

  3. Promise 链丢错pre/post 回调若抛异常会破坏 Promise 链,必须 try{...}catch{} 包裹。§4 通用引擎已默认包裹。

  4. 直接 eval vs 间接 eval:包装后 window.eval(s) 是间接 eval,作用域变成全局。如果业务依赖直接 eval(取局部变量),hook 后会失败。逆向调试时通常可接受;离线攻防需注意。

  5. toString 检测:站点检测 window.fetch.toString() 是否含 [native code]。本 SOP 已在 wrapped 上挂 toString = ()=> orig.toString()。仍可能被 Function.prototype.toString.call(fn) 绕过,需用 Proxy + Reflect.get 进一步伪装。

  6. 大 body 卡死resp.clone().text() 对几十 MB 的响应会爆内存。建议先看 content-length 决定要不要消费 body,或在 entry 里只存 body.length 与前 2 KB。

  7. 多 hook 之间互相吃:先 hook Function,再 hook eval,eval 的包装函数本身会经过 Function 构造的统计,造成噪声。建议把 §4 的 __aiHook 在所有 wrap 内加 if (this === window && fn === wrapped) return orig.apply(...) 短路。

  8. frozen Object:站点把 Object.freeze(window) / Object.freeze(navigator) 后再读属性,所有 defineProperty 都失败。需在 Object.freeze 自身先 hook(让其 no-op),再继续后续 hook。


12 · 极简命令行入口(参考实现)

把所有 preset 拼接 + 注入 + 读 bucket 串成一行:

# hook_runner.py  ——  500 行级别的开箱命令
import argparse, asyncio, json, urllib.request, websockets, sys, pathlib

PRESETS = pathlib.Path('presets')   # 每个 preset 一个 .js 文件,本 SOP §6 全文落盘到这里

async def cdp(ws_url):
    return await websockets.connect(ws_url, max_size=None)

def build_hooks(names):
    return '\n;\n'.join((PRESETS / f'{n}.js').read_text(encoding='utf-8') for n in names)

async def inject(host, port, url_keyword, hooks):
    tabs = json.loads(urllib.request.urlopen(f'http://{host}:{port}/json').read())
    tab = next(t for t in tabs if url_keyword in t['url'])
    ws = await cdp(tab['webSocketDebuggerUrl'])
    async def call(m,p=None):
        nonlocal ws
        await ws.send(json.dumps({'id':1,'method':m,'params':p or {}}))
        while True:
            msg = json.loads(await ws.recv())
            if msg.get('id')==1: return msg.get('result')
    await call('Page.enable')
    await call('Page.addScriptToEvaluateOnNewDocument', {'source': hooks})

async def dump(host, port, url_keyword, out):
    tabs = json.loads(urllib.request.urlopen(f'http://{host}:{port}/json').read())
    tab = next(t for t in tabs if url_keyword in t['url'])
    async with await cdp(tab['webSocketDebuggerUrl']) as ws:
        await ws.send(json.dumps({'id':1,'method':'Runtime.evaluate',
            'params':{'expression':'JSON.stringify(window.__aiHooks)','returnByValue':True}}))
        while True:
            msg = json.loads(await ws.recv())
            if msg.get('id')==1:
                pathlib.Path(out).write_text(msg['result']['result']['value'], encoding='utf-8')
                print('saved', out); return

if __name__ == '__main__':
    ap = argparse.ArgumentParser()
    sub = ap.add_subparsers(dest='cmd', required=True)
    p1 = sub.add_parser('inject');  p1.add_argument('--port', type=int, default=9222); p1.add_argument('--url'); p1.add_argument('--presets', nargs='+')
    p2 = sub.add_parser('dump');    p2.add_argument('--port', type=int, default=9222); p2.add_argument('--url'); p2.add_argument('--out', default='out.json')
    args = ap.parse_args()
    if args.cmd == 'inject':
        asyncio.run(inject('127.0.0.1', args.port, args.url, build_hooks(args.presets)))
    else:
        asyncio.run(dump('127.0.0.1', args.port, args.url, args.out))

用法:

python hook_runner.py inject --url demo.example.com --presets fetch xhr crypto-subtle aes-crypto-js
# 在浏览器里手动登录
python hook_runner.py dump   --url demo.example.com --out out.json

13 · 写在最后

JS 运行时 hook 的全部威力来自 三件简单事

  1. 拿到原引用(不要被链式 hook 互相覆盖)。
  2. 包装函数(pre 拿入参与栈,post 拿返回值)。
  3. 写 bucket(让数据离开沙箱进入分析阶段)。

剩下 90% 是工程细节:注入时机、跨上下文、反检测、性能。把这三件事吃透,配合 §6 的 13 个 preset 与 §8 的 5 个范式,绝大多数 Web 逆向场景都能在 2 小时内拿到明文链路。

License:本 SOP 仅用于合法授权的安全研究、自有系统调试、合规 API 联调。请遵守目标站点 ToS 与所在司法辖区相关法律。

评论(0)

登录 后可发表评论。

暂无评论。