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 自签等。Hookfetch/xhr拿到调用栈,倒推到生成函数。 - API 形态摸底:把站内全部 fetch/xhr 流量按域名 / 路径分组,看请求和响应结构。
- 加密算法识别:拦截
crypto.subtle.encrypt|decrypt|importKey|deriveKey与 CryptoJSAES.encrypt/HmacSHA256等,拿到 key、iv、算法配置和明文。 - 反反调试:屏蔽
setInterval(debugger)、console.clear刷屏、performance.now时间差检测、outerWidth - innerWidthDevTools 探测等。 - 存储读写监控: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 三个不变量
- 幂等:每个 preset 头部用
if (window.__hookPresets && window.__hookPresets['<name>']) return;守住,重复注入不会双重包装。 - 保引用:
const _orig = target.method.bind(target)在最前面拿到,包装函数总是落到_orig,不会因后续 hook 覆盖把链断了。 - 不抛错:所有
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
步骤
- 注入上述 4 个 preset。
- 人工触发一次接口请求。
JSON.stringify(window.__aiHooks['preset-fetch'])→ 找到目标接口 entry。- 看
entry.reqHeaders['x-sign']已经有了,再看entry.stack。 - 栈里第一个非 fetch wrapper 帧就是上层调用者,往上爬到一个名字像
sign/getSign/_$x的函数,那就是入口。 - 入口函数体内若调用
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
步骤
- 注入 4 个 preset。
- 全量加载 + 操作页面。
window.__aiHooks['preset-eval']与preset-fn里的src/body字段直接是动态代码字符串。- 如果代码先 base64 → atob → eval,
preset-atob会先记下解码后的明文。
输出:动态拼出的全部脚本字符串,可拿去单独反混淆 / 静态阅读。### 8.3 范式 3:解密响应(看不懂的密文响应)
目标:服务端返回的 body 是密文(base64 / AES / 自研),UI 显示却是明文,即客户端解密。
preset:fetch + xhr + crypto-subtle + aes-crypto-js + atob
步骤
- 注入。
- 触发返回密文的接口。
- 看
preset-fetch.respBody是否为 base64 / 16 进制。 - 看接下来的
preset-atob与preset-aes-crypto-js/preset-crypto-subtle条目,时间戳紧跟 fetch 之后的就是同一密文的解密链。 - 拿到 algo + key + iv,离线复算解密。
输出:解密算法配置 + 一段可独立运行的解密代码。
8.4 范式 4:反反调试 / 防卡住
目标:站点用 setInterval(()=>{debugger}, 100) 之类卡死 Sources 面板,无法下断点。
preset:anti-debug-bypass
步骤
- 第一时间(document_start)注入 §6.12,必须用通道 B / C / D / E,不能用 A 粘贴 Console(已经晚了)。
- F12 即可正常使用,不会被 debugger 截停。
- 注意:仅屏蔽常见技法,遇到检测函数 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+ 和
webcracknpm 包。
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 流程:
- §6 先抓到关键文件 URL(
stack字段里的源),比如app.4f7a.min.js。 - 浏览器或 curl 把它拉下来。
- 上 webcrack。
- 进 §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 · 八个常见坑
-
document_start 没赶上:很多签名/反调试在第一帧就跑。一定用
Page.addScriptToEvaluateOnNewDocument/add_init_script/evaluateOnNewDocument/@run-at document-start,不能在 Page.loaded 之后注入。 -
iframe / Worker 没覆盖:跨域 iframe 必须各自 attach;Worker 主线程 hook 不会传染,要么 hook
new Worker(url)改 url,要么在 worker 入口里注入。 -
Promise 链丢错:
pre/post回调若抛异常会破坏 Promise 链,必须try{...}catch{}包裹。§4 通用引擎已默认包裹。 -
直接 eval vs 间接 eval:包装后
window.eval(s)是间接 eval,作用域变成全局。如果业务依赖直接 eval(取局部变量),hook 后会失败。逆向调试时通常可接受;离线攻防需注意。 -
toString 检测:站点检测
window.fetch.toString()是否含[native code]。本 SOP 已在 wrapped 上挂toString = ()=> orig.toString()。仍可能被Function.prototype.toString.call(fn)绕过,需用 Proxy +Reflect.get进一步伪装。 -
大 body 卡死:
resp.clone().text()对几十 MB 的响应会爆内存。建议先看content-length决定要不要消费 body,或在 entry 里只存body.length与前 2 KB。 -
多 hook 之间互相吃:先 hook
Function,再 hookeval,eval 的包装函数本身会经过 Function 构造的统计,造成噪声。建议把 §4 的__aiHook在所有 wrap 内加if (this === window && fn === wrapped) return orig.apply(...)短路。 -
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 的全部威力来自 三件简单事:
- 拿到原引用(不要被链式 hook 互相覆盖)。
- 包装函数(pre 拿入参与栈,post 拿返回值)。
- 写 bucket(让数据离开沙箱进入分析阶段)。
剩下 90% 是工程细节:注入时机、跨上下文、反检测、性能。把这三件事吃透,配合 §6 的 13 个 preset 与 §8 的 5 个范式,绝大多数 Web 逆向场景都能在 2 小时内拿到明文链路。
License:本 SOP 仅用于合法授权的安全研究、自有系统调试、合规 API 联调。请遵守目标站点 ToS 与所在司法辖区相关法律。