# 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-Sign`、`msToken`、`a_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>']`，元素结构：

```js
{
  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 的通道都行）：

```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 → 回车 → 触发业务动作 → 再敲：

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

**坑**：F5 刷新后失效；如果业务在页面初始化阶段就调签名（如登录页打开就预签 csrf），来不及 hook。这种场景必须用 B / C / D。

### 3.2 通道 B · Chrome DevTools Protocol（首屏前注入）

启动 Chrome：

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

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

```python
# 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

```python
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

```js
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）

```js
// ==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 已自带最小实现。

```js
// __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）：

```js
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

```js
(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)

```js
(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 关键字筛选响应体）

```js
(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

```js
(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('...')`)

```js
(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（合一，按需注释一边）

```js
(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

```js
(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）

```js
(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（屏蔽常见反调试）

```js
(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

```js
(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

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

或写盘（DevTools 控制台）：

```js
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-atob` 与 `preset-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）

```js
(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 包。

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

`deobf.js`：

```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));
```

**典型入参**：

```bash
# 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`：

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

启动 Chrome：

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

注入（Python）：

```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 分析

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

输出（示意）：

```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`：

```json
[{ "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 串成一行：

```python
# 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))
```

用法：

```bash
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 与所在司法辖区相关法律。