feat(v2.5.0): upgrade to four-axe JSVMP methodology + MCP v0.4.0 alignment
- Add fourth axe: source-level instrumentation via instrument_jsvmp_source - Add new reference: references/jsvmp-source-instrumentation.md - Align MCP tool count 52 -> 65 across SKILL.md / README.md / mcp-cookbook.md - New scenario 11: universal JSVMP source instrumentation (Rui Shu / Akamai / webmssdk) - Enhance scenario 2 with analyze_cookie_sources for cookie attribution - Extend experience rules from 22 to 27 - New case skeleton: cases/universal-vmp-source-instrumentation.md - New case: cases/jsvmp-ruishu6-cookie-412-sdenv.md - Update jsvmp-analysis.md from three-axe to four-axe methodology - Document new presets 'cookie' and 'runtime_probe' - Document pre_inject_hooks / via_blank / reload_with_hooks for first-screen challenges - Add troubleshooting section for 412/403 first-screen challenges
This commit is contained in:
@@ -16,8 +16,8 @@ Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函
|
||||
[camoufox-reverse] evaluate_js(expression=HookScript)
|
||||
→ 在当前页面上下文执行,适合页面已加载后的动态注入
|
||||
|
||||
[camoufox-reverse] inject_hook_preset(preset="xhr|fetch|crypto|websocket|debugger_bypass")
|
||||
→ 一键注入预设 Hook,覆盖常见逆向场景
|
||||
[camoufox-reverse] inject_hook_preset(preset="xhr|fetch|crypto|websocket|debugger_bypass|cookie|runtime_probe")
|
||||
→ 一键注入预设 Hook,覆盖常见逆向场景(v2.5.0 新增 cookie / runtime_probe 两个预设)
|
||||
|
||||
[camoufox-reverse] hook_function(function_path="目标函数", hook_code="...", position="before|after|replace")
|
||||
→ 对指定函数注入自定义 Hook
|
||||
@@ -390,9 +390,130 @@ Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函
|
||||
## MCP 注入最佳实践
|
||||
|
||||
1. **使用 `add_init_script`**:确保 Hook 在目标代码之前生效
|
||||
2. **优先使用 `inject_hook_preset`**:一键注入 xhr/fetch/crypto/websocket/debugger_bypass 预设 Hook
|
||||
2. **优先使用 `inject_hook_preset`**:一键注入 xhr/fetch/crypto/websocket/debugger_bypass/cookie/runtime_probe 预设 Hook(v2.5.0 更新)
|
||||
3. **使用 `hook_function`**:对特定函数注入 before/after/replace Hook
|
||||
4. **使用 `console.log` 输出**:通过 `get_console_logs` 收集结果
|
||||
5. **使用 `console.trace`**:在关键点输出调用栈
|
||||
6. **Camoufox 优势**:Juggler 协议沙箱隔离,Hook 不会被页面 JS 检测到
|
||||
7. **使用 Proxy 代替直接覆写**:更隐蔽,不改变 `typeof` 结果
|
||||
8. **首屏挑战页用 `navigate(pre_inject_hooks=[...])`** [v2.5.0]:瑞数/Akamai 首包挑战在 hook 装好前就跑完了,用这个参数让 hook 先装再 goto
|
||||
9. **装完 hook 想让它先于页面 JS 跑**:用 `reload_with_hooks()` [v2.5.0] 替代裸 `reload()`,同时会清掉各类 `__mcp_*_log`
|
||||
|
||||
---
|
||||
|
||||
## cookie 预设:原型链级 document.cookie Hook(v2.5.0 新增)
|
||||
|
||||
### 为什么不能直接 `Object.defineProperty(document, 'cookie', ...)`
|
||||
|
||||
`document.cookie` 的 getter/setter 定义在**原型链**上(具体是 `Document.prototype` 或 `HTMLDocument.prototype`),不在 `document` 实例上。直接在实例上 defineProperty 会被浏览器忽略或抛错——这是很多自写 Hook 脚本"看起来装上了但完全抓不到 cookie 写入"的根本原因。
|
||||
|
||||
### 正确做法(cookie_hook.js 的实现)
|
||||
|
||||
```javascript
|
||||
// 沿原型链找到定义 cookie 描述符的 owner
|
||||
function findCookieDescriptor() {
|
||||
var proto = Object.getPrototypeOf(document);
|
||||
while (proto) {
|
||||
var d = Object.getOwnPropertyDescriptor(proto, 'cookie');
|
||||
if (d) return { descriptor: d, owner: proto };
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var found = findCookieDescriptor();
|
||||
if (!found) return;
|
||||
|
||||
var origSet = found.descriptor.set;
|
||||
var origGet = found.descriptor.get;
|
||||
|
||||
// 在正确的 owner 上替换(关键:owner 是 Document.prototype 或 HTMLDocument.prototype)
|
||||
Object.defineProperty(found.owner, 'cookie', {
|
||||
set: function (value) {
|
||||
window.__mcp_cookie_log.push({
|
||||
op: 'set', value: String(value),
|
||||
stack: new Error().stack, ts: Date.now()
|
||||
});
|
||||
return origSet.call(this, value);
|
||||
},
|
||||
get: function () {
|
||||
var v = origGet.call(this);
|
||||
window.__mcp_cookie_log.push({
|
||||
op: 'get', value: String(v),
|
||||
stack: new Error().stack, ts: Date.now()
|
||||
});
|
||||
return v;
|
||||
},
|
||||
configurable: true, enumerable: true
|
||||
});
|
||||
```
|
||||
|
||||
### 使用方式(三步)
|
||||
|
||||
```
|
||||
# 1. 注入(默认持久化)
|
||||
[camoufox-reverse] inject_hook_preset(preset="cookie", persistent=True)
|
||||
|
||||
# 2. 触发场景
|
||||
|
||||
# 3. 拉日志(两种方式)
|
||||
# a. 读原始日志
|
||||
[camoufox-reverse] evaluate_js(expression="JSON.stringify(window.__mcp_cookie_log.slice(-20))")
|
||||
|
||||
# b. 或直接走 Cookie 归因(推荐)
|
||||
[camoufox-reverse] analyze_cookie_sources(name_filter="目标cookie名")
|
||||
```
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 所有涉及 JS 写入 cookie 的场景(eval 首包、指纹 cookie、JS 计算 token 后 `document.cookie = ...`)
|
||||
- **不适用**:HTTP Set-Cookie 写入的场景(这种用 `analyze_cookie_sources` 的 http_responses 分支)
|
||||
|
||||
---
|
||||
|
||||
## runtime_probe 预设:低开销广谱运行时探针(v2.5.0 新增)
|
||||
|
||||
### 与 jsvmp_hook 的区别
|
||||
|
||||
| 维度 | `jsvmp_hook`(`hook_jsvmp_interpreter`) | `runtime_probe` |
|
||||
|------|---------------------------------------|----------------|
|
||||
| 实现方式 | 在 navigator/screen 等全局对象上装 **Proxy** | **不装 Proxy**,只 override 具体热点 API |
|
||||
| 开销 | 高(每次属性读取都进 Proxy trap) | 低(只在调用热点 API 时记录) |
|
||||
| 覆盖面 | 全局对象所有属性 + apply/call/bind/Reflect.* | 固定一组 API |
|
||||
| 安全性 | 个别页面可能被 Proxy 破坏 | 非常安全 |
|
||||
| 典型用途 | JSVMP 深度分析 | "这个页面都在做什么"快速摸底 |
|
||||
|
||||
### runtime_probe 覆盖的 API 清单
|
||||
|
||||
| 类别 | 覆盖项 | 日志 type |
|
||||
|------|-------|----------|
|
||||
| XHR | `XMLHttpRequest.prototype.open/send` | `xhr_open` / `xhr_send` |
|
||||
| fetch | `window.fetch` | `fetch` |
|
||||
| Canvas 指纹 | `HTMLCanvasElement.prototype.toDataURL` | `canvas_toDataURL` |
|
||||
| Canvas 上下文 | `HTMLCanvasElement.prototype.getContext` | `canvas_getContext` |
|
||||
| WebGL | `WebGLRenderingContext.prototype.getParameter` | `webgl_getParameter` |
|
||||
| navigator | userAgent / platform / language / languages / webdriver / hardwareConcurrency / deviceMemory / vendor / appVersion / plugins / mimeTypes 的 getter | `nav_read` |
|
||||
| 事件 | `EventTarget.prototype.addEventListener`(只记 mouse/key/devicemotion 等 bot 检测类) | `addEventListener` |
|
||||
|
||||
### 使用方式
|
||||
|
||||
```
|
||||
# 1. 注入
|
||||
[camoufox-reverse] inject_hook_preset(preset="runtime_probe", persistent=True)
|
||||
|
||||
# 2. 触发场景
|
||||
|
||||
# 3. 拉日志(自动按 type 聚合)
|
||||
[camoufox-reverse] get_runtime_probe_log(limit=300)
|
||||
|
||||
# 按类型过滤
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="canvas_toDataURL", limit=50)
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="xhr_send", limit=100)
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="nav_read", limit=100)
|
||||
```
|
||||
|
||||
### 典型诊断模式
|
||||
|
||||
- **反爬是否做 canvas 指纹检测?** → 看 `by_type.canvas_toDataURL` 是否 > 0
|
||||
- **反爬是否读 navigator.webdriver?** → `get_runtime_probe_log(type_filter="nav_read")` 里找 `prop: "webdriver"`
|
||||
- **反爬是否检测鼠标移动?** → 看 `by_type.addEventListener` 里 mousemove/mousedown 的计数
|
||||
|
||||
+152
-43
@@ -8,7 +8,9 @@ JSVMP(JavaScript Virtual Machine Protection)是一种高级 JS 代码保护
|
||||
- 代码以字节码数组 + 解释器循环的形式运行
|
||||
- 常见于瑞数、极验、某数等商业级反爬方案
|
||||
|
||||
**核心原则:不反编译字节码,用行为追踪法(Hook / 插桩 / 日志分析)从 I/O 两端夹逼定位签名逻辑。**
|
||||
**核心原则:不反编译字节码,用行为追踪法(Hook / 插桩 / 日志分析 / 源码级插桩四板斧)从 I/O 两端夹逼 + 中间层观察定位签名逻辑。**
|
||||
|
||||
> **v2.5.0 更新**:第四板斧「源码级插桩」由 camoufox-reverse MCP v0.4.0 的 `instrument_jsvmp_source` 提供支持,在 HTTP 层改写 VMP 源码,对每个 `obj[key]` 和 `fn(args)` 插入 tap,捕获 VM 内部 switch/case 调度——是瑞数 5/6、Akamai sensor_data、webmssdk、obfuscator.io 这类"VM 自包含"场景的通用武器。详见 [`jsvmp-source-instrumentation.md`](./jsvmp-source-instrumentation.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -49,22 +51,89 @@ function _0xefgh() {
|
||||
### 如何确认是 JSVMP
|
||||
|
||||
```
|
||||
MCP 操作:
|
||||
- list_scripts → 找到异常大的 JS 文件
|
||||
- get_script_source(script_id=N, start_line=1, end_line=50) → 查看文件头部结构
|
||||
- search_code(keyword="while") → 搜索解释器循环
|
||||
- search_code_in_script(script_id=N, keyword="switch") → 在指定大文件中精确搜索(前后 3 行上下文)
|
||||
- search_code(keyword="push|pop|shift") → 搜索栈操作
|
||||
- dump_jsvmp_strings → 提取字符串数组,识别 API 名称和混淆模式(快速判断)
|
||||
MCP 操作(推荐顺序):
|
||||
- list_scripts → 找到异常大的 JS 文件(100KB+)
|
||||
- find_dispatch_loops(script_url=<大 JS 的 url>, min_case_count=20) ← v0.4.0 推荐首选
|
||||
→ 返回 candidates: [{fn_name, case_count, char_range, preview}]
|
||||
→ case_count > 50 的基本确认是 VMP 解释器
|
||||
- dump_jsvmp_strings(script_url=<大 JS 的 url>) → 提取字符串数组
|
||||
→ 看 suspicious_patterns 是否包含 "JSVMP interpreter loop (while+switch, large source)"
|
||||
→ 看 api_names 是否包含 navigator/screen/encrypt/md5/hmac 等
|
||||
|
||||
兜底手动方式(大文件 find_dispatch_loops 超时时):
|
||||
- search_code_in_script(script_id=N, keyword="switch") → 前后 3 行上下文
|
||||
- search_code_in_script(script_id=N, keyword="case 0:|case 1:|case 2:")
|
||||
- get_script_source(script_id=N, start_line=..., end_line=...) → 按 char_range 读取分发体
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三板斧方法论
|
||||
## 四板斧方法论(v2.5.0 更新)
|
||||
|
||||
### 快速路径:一键 JSVMP 分析(推荐先试)
|
||||
### 四板斧关系图
|
||||
|
||||
对于典型 JSVMP,可以跳过手动 Hook 直接使用专项工具:
|
||||
```
|
||||
┌── 第一板斧 Hook 出入口 ──────────┐
|
||||
│ inject_hook_preset(xhr|fetch │
|
||||
│ |crypto|cookie|websocket │
|
||||
│ |debugger_bypass|runtime_probe)│
|
||||
│ freeze_prototype │
|
||||
│ analyze_cookie_sources │
|
||||
└────────┬─────────────────────────┘
|
||||
│
|
||||
夹逼 I/O ──→ ┌─┴─┐ ──→ 推断签名公式
|
||||
│VMP│
|
||||
中间层 ──→ ┌─┘ └─┐ ──→ 追踪执行链路
|
||||
│
|
||||
┌── 第二板斧 插桩解释器 ────────────┐ ┌── 第四板斧 源码级插桩 ──────────┐
|
||||
│ find_dispatch_loops │ │ instrument_jsvmp_source (新) │
|
||||
│ trace_function(分发函数/子函数) │ │ get_instrumentation_log │
|
||||
│ trace_property_access │ │ → summary.hot_keys │
|
||||
│ hook_jsvmp_interpreter (多路径) │ │ → summary.hot_methods │
|
||||
└────────┬─────────────────────────┘ │ → summary.hot_functions │
|
||||
└────────┬────────────────────────┘
|
||||
│
|
||||
↓ ↓
|
||||
┌── 第三板斧 日志分析 ──────────────────┐
|
||||
│ get_trace_data + get_jsvmp_log │
|
||||
│ + get_runtime_probe_log │
|
||||
│ + get_instrumentation_log │
|
||||
│ 反向追踪法(从签名值→明文) │
|
||||
│ 多次请求对比法(找变化因子) │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**板斧选择矩阵**:
|
||||
|
||||
| VMP 特征 | 推荐板斧组合 |
|
||||
|---------|-------------|
|
||||
| 签名通过 CryptoJS / atob / MD5 等可 hook API 走 | 第一 + 第三(hot API hook + 日志反推) |
|
||||
| VMP 通过 `Function.apply/call` 调子函数 | 第二 + 第三(`hook_jsvmp_interpreter` 多路径 + 日志) |
|
||||
| VMP 自包含,算法全在 switch/case 内(瑞数 5/6、Akamai、webmssdk、obfuscator.io) | **第四板斧首选**,配合第一板斧做 I/O 边界 |
|
||||
| VMP 深度绑定浏览器环境(compare_env 差异大 + 签名随环境变) | 路径 B 环境伪装(见 SKILL.md 场景 10),配第四板斧 `hot_keys` 定位指纹集 |
|
||||
|
||||
---
|
||||
|
||||
### 快速路径:v0.4.0 黄金 8 步流程(推荐先试,70%+ 瑞数/Akamai/webmssdk 场景直接搞定)
|
||||
|
||||
```
|
||||
Step 1 — find_dispatch_loops 确认 VMP + 记下 fn_name / char_range
|
||||
Step 2 — hook_jsvmp_interpreter(script_url=<VMP basename>) 装多路径探针
|
||||
Step 3 — inject_hook_preset("cookie", persistent=True) + inject_hook_preset("xhr", persistent=True)
|
||||
Step 4 — instrument_jsvmp_source(url_pattern="**/<VMP 文件>", mode="ast", tag="vmp1") ← 核心新步
|
||||
Step 5 — reload_with_hooks() 让所有探针先于 VMP 生效 + 清日志
|
||||
Step 6 — 触发业务操作
|
||||
Step 7 — get_instrumentation_log(tag_filter="vmp1", limit=300) 看 hot_keys / hot_methods / hot_functions
|
||||
Step 8 — analyze_cookie_sources() 归因最终 cookie 来源
|
||||
```
|
||||
|
||||
详细步骤见 [`jsvmp-source-instrumentation.md`](./jsvmp-source-instrumentation.md)「黄金 8 步流程」节。
|
||||
|
||||
---
|
||||
|
||||
### 三板斧旧流程(保留以应对第四板斧无法使用的场景)
|
||||
|
||||
对于典型 JSVMP,也可以继续用以下一键工具:
|
||||
|
||||
```
|
||||
MCP 操作:
|
||||
@@ -438,49 +507,89 @@ MCP 操作:
|
||||
|
||||
---
|
||||
|
||||
## 还原策略决策
|
||||
## 还原策略决策(v2.5.0 扩充源码级插桩分支)
|
||||
|
||||
```
|
||||
JSVMP 签名还原决策树:
|
||||
├─ Hook 确认使用标准算法(MD5/HMAC/AES)?
|
||||
|
||||
第 0 步:先看 hot_methods(第四板斧 summary)
|
||||
├─ hot_methods 包含 CryptoJS.MD5 / SubtleCrypto.digest / HMAC / btoa
|
||||
│ → 算法是标准加密,继续看第 1 步分支
|
||||
│
|
||||
├─ hot_methods 全是自定义 fn 名,几乎没有 CryptoJS/Subtle
|
||||
│ → VMP 自实现加密,直接跳到第 3 步(沙箱运行完整 VM)
|
||||
|
||||
第 1 步:标准算法分支
|
||||
├─ 能从 hot_methods + 调用栈反推出明文拼接顺序?
|
||||
│ ├─ YES → 纯算法还原(Node.js crypto / Python hashlib+pycryptodome)
|
||||
│ │ · 还原拼接顺序和密钥即可
|
||||
│ │ · 这是最理想的结果
|
||||
│ └─ NO → 继续分析
|
||||
│
|
||||
├─ 能提取出独立的签名函数?
|
||||
│ ├─ YES → 沙箱执行(Node.js vm / Python execjs)
|
||||
│ │ · 提取函数及其依赖
|
||||
│ │ · 构造最小执行环境
|
||||
│ └─ NO → 继续分析
|
||||
│
|
||||
├─ VM 劫持了整个请求链路?
|
||||
│ ├─ YES → 加载完整 VM(仅调用签名入口)
|
||||
│ │ · 下载完整 JS 文件
|
||||
│ │ · 在 Node.js vm 中加载
|
||||
│ │ · 补最小浏览器环境
|
||||
│ │ · 调用签名函数获取结果
|
||||
│ └─ NO → 浏览器自动化兜底
|
||||
│
|
||||
└─ 无法脱离浏览器
|
||||
→ 使用 Camoufox 浏览器自动化(最后手段)
|
||||
│ └─ NO(明文来自 VM 内部)→ 进第 2 步
|
||||
|
||||
第 2 步:能提取独立的签名函数?
|
||||
├─ YES → 沙箱执行(Node.js vm / Python execjs)
|
||||
│ · 提取函数及其依赖
|
||||
│ · 构造最小执行环境(根据 hot_keys 对齐环境变量)
|
||||
└─ NO → 第 3 步
|
||||
|
||||
第 3 步:VMP 劫持了整个请求链路 + hot_keys 显示大量环境读取?
|
||||
├─ YES → 走路径 B(jsdom 环境伪装)
|
||||
│ · hot_keys 直接告诉你要对齐哪些环境属性(省去 58 项人工 diff)
|
||||
│ · analyze_cookie_sources 确认 cookie 是否来自 HTTP Set-Cookie
|
||||
│ · 参考 references/jsdom-env-patches.md
|
||||
└─ NO(jsdom 加载失败 / VMP 有 CDN 反射检测)→ 第 4 步
|
||||
|
||||
第 4 步:加载完整 VM(仅调用签名入口)
|
||||
· 下载完整 JS 文件
|
||||
· 在 Node.js vm 中加载
|
||||
· 补最小浏览器环境(仍然根据 hot_keys 指导)
|
||||
· 调用签名函数获取结果
|
||||
|
||||
第 5 步:实在无法脱离浏览器
|
||||
→ 使用 Camoufox 浏览器自动化(最后手段,不是协议优先方案)
|
||||
```
|
||||
|
||||
**关键:第四板斧的 `hot_keys` / `hot_methods` 是所有决策的共同输入**——不要在没看 hot_keys 之前就选策略。即使最后选了路径 B,`hot_keys` 也能把"要对齐什么环境"从 58 项人工 diff 降为 20 项精确对齐。
|
||||
|
||||
---
|
||||
|
||||
## 实战检查清单
|
||||
## 实战检查清单(v2.5.0 扩充四板斧项)
|
||||
|
||||
在 JSVMP 分析过程中,确保以下每项都已完成:
|
||||
|
||||
- [ ] 确认文件是 JSVMP(解释器循环 + 字节码数组 + 栈操作)
|
||||
- [ ] Hook XHR/Fetch 出口,捕获签名值的最终形态
|
||||
- [ ] Hook Cookie setter,确认是否有动态 Cookie
|
||||
- [ ] Hook 加密原语入口(btoa/atob/CryptoJS/MD5/SHA)
|
||||
**第一板斧 — Hook 出入口**:
|
||||
- [ ] `find_dispatch_loops` 确认是 VMP(case_count > 50)
|
||||
- [ ] `inject_hook_preset("xhr", persistent=True)` + `("fetch", persistent=True)` 捕获请求出口
|
||||
- [ ] `inject_hook_preset("cookie", persistent=True)` **原型链级** cookie hook(替代手写 Document.prototype.cookie 的做法)
|
||||
- [ ] `analyze_cookie_sources` 归因最终 cookie(HTTP vs JS)
|
||||
- [ ] `inject_hook_preset("crypto", persistent=True)` 捕获加密原语入口(btoa/atob/CryptoJS/MD5/SHA)
|
||||
- [ ] Hook String.fromCharCode(JSVMP 高频信号)
|
||||
- [ ] 定位解释器核心分发函数
|
||||
- [ ] 分层插桩(粗 → 中 → 细)
|
||||
- [ ] 日志反向追踪,找到签名值首次出现的位置
|
||||
- [ ] `freeze_prototype` 防止 VMP 覆盖 hook
|
||||
|
||||
**第二板斧 — 插桩解释器**:
|
||||
- [ ] `hook_jsvmp_interpreter(script_url=<VMP basename>)` 多路径探针(apply/call/bind + Reflect.*/Proxy)
|
||||
- [ ] 分层 `trace_function`(粗 → 中 → 细,用 `max_captures` 限制日志量)
|
||||
- [ ] `trace_property_access(["navigator.*", "screen.*", ...])` 监控签名容器
|
||||
|
||||
**第三板斧 — 日志分析**:
|
||||
- [ ] `get_jsvmp_log` + `get_trace_data` + `get_runtime_probe_log`(如果装了 runtime_probe)+ `get_console_logs`
|
||||
- [ ] 反向追踪法,找到签名值首次出现的位置
|
||||
- [ ] 多次请求对比,确认变化因子和固定因子
|
||||
- [ ] 根据输出特征判断算法类型
|
||||
- [ ] 选择还原策略(纯算法 / 沙箱 / 完整 VM / 浏览器)
|
||||
|
||||
**第四板斧 — 源码级插桩 [v2.5.0 新增]**:
|
||||
- [ ] `instrument_jsvmp_source(url_pattern=..., mode="ast"|"regex", tag=...)` 装好
|
||||
- [ ] `reload_with_hooks()` 让插桩先于 VMP 生效
|
||||
- [ ] 读 `get_instrumentation_log` 三个 summary:`hot_keys` / `hot_methods` / `hot_functions`
|
||||
- [ ] `hot_keys` 给出 VMP 读取的环境指纹集(top 30)
|
||||
- [ ] `hot_methods` 给出 VMP 调用的方法(ObjectType.methodName 格式)
|
||||
- [ ] `hot_functions` 给出 VMP 调用的函数名
|
||||
- [ ] `stop_instrumentation` 完工清理
|
||||
|
||||
**环境/时序**:
|
||||
- [ ] 若目标是首屏挑战页,用 `navigate(pre_inject_hooks=[...])` 或 `via_blank=True`
|
||||
- [ ] `compare_env` 采集环境基准
|
||||
- [ ] `bypass_debugger_trap` 绕过反调试
|
||||
|
||||
**还原与验证**:
|
||||
- [ ] 根据 hot_methods 判断算法类型
|
||||
- [ ] 选择还原策略(纯算法 / 沙箱 / 完整 VM / jsdom 环境伪装 / 浏览器)
|
||||
- [ ] 实现并验证签名计算结果
|
||||
- [ ] 端到端验证(连续 5+ 次请求稳定)
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
# JSVMP 源码级插桩专项指南(第四板斧)
|
||||
|
||||
> **v2.5.0 新增文档**。本指南讲解 camoufox-reverse MCP v0.4.0+ 提供的源码级插桩能力(`instrument_jsvmp_source` / `get_instrumentation_log` / `find_dispatch_loops`)的使用方法论,是 JSVMP 四板斧中"第四板斧"的完整说明。
|
||||
>
|
||||
> 与 `jsvmp-analysis.md` 的第一/二/三板斧互补——前三板斧诊断"VM 看外界 / 外界看 VM",本文档讲解"VM 看自己"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么需要源码级插桩
|
||||
|
||||
### 1.1 传统 hook 的盲区
|
||||
|
||||
**`hook_jsvmp_interpreter`** 即使升级到 v0.4.0 的多路径版本(apply/call/bind + Reflect.*/Proxy 全局对象 + timing/random),仍然只能看到 VMP 路由到**可 hook JS API** 的部分。但瑞数 5/6、Akamai sensor_data v2/v3、webmssdk(TikTok)、obfuscator.io 这类 VMP 的典型结构是:
|
||||
|
||||
```js
|
||||
// 典型的自包含 VMP 字节码分发循环
|
||||
function _vm(bytecode) {
|
||||
var stack = [], pc = 0, env = window;
|
||||
while (pc < bytecode.length) {
|
||||
var op = bytecode[pc++];
|
||||
switch (op) {
|
||||
case 1: stack.push(env[bytecode[pc++]]); break; // ← GET 操作:env[key] 直接访问,不经过 apply/Reflect
|
||||
case 2: var k = stack.pop(), o = stack.pop(); stack.push(o[k]); break; // ← 对象属性读取
|
||||
case 3: var args = stack.splice(-bytecode[pc++]);
|
||||
var fn = stack.pop(); stack.push(fn.apply(null, args)); break; // ← 这种才被 apply hook 看到
|
||||
case 4: stack.push(bytecode[pc++] + stack.pop()); break; // ← 纯字符串拼接,hook 完全抓不到
|
||||
// ... 数十到数百个 case
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `case 1` 直接访问 `env[key]`,hook `navigator`/`screen` 的 Proxy 能看到(如果 env 是被 Proxy 的全局),但嵌套多层后 Proxy 链会断。
|
||||
- `case 2` 读任意对象的任意属性——hook 不可能覆盖所有对象。
|
||||
- `case 4` 纯内存里的字符串拼接,任何 hook 都看不到。
|
||||
|
||||
**结果**:VMP 生成签名需要的关键中间值在 dispatch 循环内部全部丢失,传统 hook 返回的日志里只有零散的 API 调用,无法拼出算法。
|
||||
|
||||
### 1.2 源码级插桩的原理
|
||||
|
||||
**`instrument_jsvmp_source`** 在 HTTP 层拦截目标脚本,在脚本抵达浏览器前把源码改写成等价的"带 tap 的版本":
|
||||
|
||||
```js
|
||||
// 改写前
|
||||
stack.push(env[key]);
|
||||
|
||||
// 改写后(regex 模式)
|
||||
stack.push(__mcp_tap_get(env, key, 'vmp1'));
|
||||
|
||||
// 改写前
|
||||
fn.apply(null, args);
|
||||
|
||||
// 改写后(AST 模式)
|
||||
__mcp_tap_call(fn, null, [args[0], args[1], ...], 'vmp1');
|
||||
```
|
||||
|
||||
`__mcp_tap_get` / `__mcp_tap_call` / `__mcp_tap_method` 三个运行时函数会把每次访问记录到 `window.__mcp_vmp_log`,包括:
|
||||
|
||||
- **tap_get**: 读取属性——`{type, tag, key, objType, value (preview)}`
|
||||
- **tap_method**: `obj.method(args)` 调用——`{type, tag, objType, method, argc, arg0, ret}`
|
||||
- **tap_call**: `fn(args)` 直接调用——`{type, tag, name, argc, arg0, ret}`
|
||||
- **tap_call_err**: 调用抛错——`{type, tag, name, err}`
|
||||
|
||||
改写后的源码**继续正常执行**——VMP 照跑不误,但每一次对宿主环境或对象的交互都被记录。
|
||||
|
||||
---
|
||||
|
||||
## 2. 两种改写模式:regex vs AST
|
||||
|
||||
`instrument_jsvmp_source(mode=...)` 支持两种模式:
|
||||
|
||||
### 2.1 `mode="regex"`(默认,零依赖)
|
||||
|
||||
- **原理**:纯正则匹配 `<identifier>[<expr>]`,替换为 `__mcp_tap_get(identifier, expr, tag)`
|
||||
- **优点**:无外部 CDN 依赖,页面离线也能用;速度极快;大文件安全(10MB+ VMP 也能秒级改写)
|
||||
- **覆盖率**:~80% 的 member access;**不覆盖函数调用 tap**(只改写 `obj[key]`,不改写 `fn(args)`)
|
||||
- **局限**:对带模板字符串、正则字面量、嵌套方括号的代码可能误改写;只改 member access,不改 call
|
||||
- **使用场景**:
|
||||
- 目标环境无法访问 cdnjs.cloudflare.com
|
||||
- 极限大文件(AST 模式内存超限)
|
||||
- 快速扫一遍看看 hot_keys 分布
|
||||
|
||||
### 2.2 `mode="ast"`(高保真,需 CDN)
|
||||
|
||||
- **原理**:在页面内加载 Acorn(`cdnjs.cloudflare.com/ajax/libs/acorn/8.11.3/acorn.min.js`)解析源码为 AST,精确识别 `MemberExpression` / `CallExpression`,只对应该改写的节点插入 tap
|
||||
- **优点**:99% 覆盖率,**同时改写 member access 和 call**;不会误改写字符串字面量/正则
|
||||
- **依赖**:页面必须能访问 cdnjs CDN
|
||||
- **使用场景**:
|
||||
- 绝大多数联网场景(默认推荐)
|
||||
- 需要 `hot_methods` / `hot_functions` 数据(只有 AST 模式会生成)
|
||||
- **失败回退**:AST 解析失败时(例如脚本有语法错误或 Acorn 版本不支持的语法),`instrument_jsvmp_source` 会自动 pass-through 源码不变,并在 console 记 warn
|
||||
|
||||
### 2.3 选择建议
|
||||
|
||||
| 情况 | 选 | 原因 |
|
||||
|------|-----|-----|
|
||||
| 首次分析某个 VMP,联网环境 | **AST** | `hot_methods` + `hot_functions` 能 30 秒内画出 VMP 行为画像 |
|
||||
| 离线/受限网络 | regex | 零依赖 |
|
||||
| VMP 文件 > 5 MB | regex | AST 改写内存开销大 |
|
||||
| 关心 VMP 调了哪些方法 | **AST** | regex 不生成 tap_method / tap_call |
|
||||
| 关心 VMP 读了哪些属性 | 任一 | 两种模式都生成 tap_get |
|
||||
|
||||
---
|
||||
|
||||
## 3. 黄金 8 步流程
|
||||
|
||||
> 这是面向瑞数 5/6、Akamai sensor_data、webmssdk、obfuscator.io 的通用流程。照抄 `Actions` 段即可,执行方需要替换的只有 `url_pattern` 和 `script_url`。
|
||||
|
||||
### Step 1 — 启动 + 网络捕获
|
||||
|
||||
```
|
||||
Actions:
|
||||
launch_browser(headless=False) # 有头调试,看浏览器行为
|
||||
start_network_capture(capture_body=True) # 抓响应体,后续 analyze_cookie_sources 需要
|
||||
```
|
||||
|
||||
### Step 2 — 第一次导航定位 VMP 脚本 URL
|
||||
|
||||
```
|
||||
Actions:
|
||||
navigate(url="https://target.com/", wait_until="load")
|
||||
list_network_requests(resource_type="script")
|
||||
→ 找 size 最大的 JS(通常 100KB+,名字像 sdenv-*.js / FuckCookie_*.js / webmssdk.es5.js / akam/xxx.js)
|
||||
→ 记下 url,下面称为 <VMP_URL>
|
||||
```
|
||||
|
||||
### Step 3 — 确认是 VMP(而不是普通混淆)
|
||||
|
||||
```
|
||||
Actions:
|
||||
find_dispatch_loops(script_url="<VMP_URL>", min_case_count=20)
|
||||
→ 返回:
|
||||
{
|
||||
"candidates": [
|
||||
{"fn_name": "_0xabc", "case_count": 87, "char_range": [45321, 182400], "preview": "..."}
|
||||
],
|
||||
"total": 1,
|
||||
"source_length": 382112
|
||||
}
|
||||
→ case_count > 50 基本确认是 VMP
|
||||
→ 记下 fn_name 和 char_range(后续定位方便)
|
||||
```
|
||||
|
||||
### Step 4 — 装源码级插桩(核心)
|
||||
|
||||
```
|
||||
Actions:
|
||||
instrument_jsvmp_source(
|
||||
url_pattern="**/sdenv-*.js", # glob 模式,匹配所有 CDN hash 变种
|
||||
mode="ast", # 联网优先 ast
|
||||
tag="vmp1", # 一次插桩多 VMP 时区分
|
||||
rewrite_member_access=True,
|
||||
rewrite_calls=True,
|
||||
max_rewrites=5000,
|
||||
cache_rewritten=True # 避免重复改写
|
||||
)
|
||||
→ 返回:{"status": "instrumenting", "pattern": "...", "mode": "ast", "tag": "vmp1"}
|
||||
```
|
||||
|
||||
### Step 5 — 装兜底 hook(与源码插桩互补)
|
||||
|
||||
```
|
||||
Actions:
|
||||
inject_hook_preset(preset="cookie", persistent=True) # 原型链级 cookie hook
|
||||
inject_hook_preset(preset="xhr", persistent=True) # 请求出口
|
||||
inject_hook_preset(preset="fetch", persistent=True) # 请求出口
|
||||
inject_hook_preset(preset="crypto", persistent=True) # btoa/atob/JSON.stringify
|
||||
hook_jsvmp_interpreter(script_url="<VMP basename>", # 多路径运行时探针
|
||||
track_calls=True,
|
||||
track_reflect=True,
|
||||
track_props=True)
|
||||
bypass_debugger_trap() # 避免 VMP 的 debugger 陷阱
|
||||
```
|
||||
|
||||
### Step 6 — reload_with_hooks 让探针先于 VMP 生效
|
||||
|
||||
```
|
||||
Actions:
|
||||
reload_with_hooks(clear_log=True, wait_until="networkidle")
|
||||
→ 注意 final_status:
|
||||
· 200 → 正常
|
||||
· 412 → 瑞数挑战页未过,需要等 challenge 完成
|
||||
· 403 → Akamai 拦截,检查指纹或 UA
|
||||
|
||||
特殊场景:若目标就是首屏挑战页(瑞数 412),用:
|
||||
navigate(
|
||||
url="https://target.com/",
|
||||
pre_inject_hooks=["xhr", "fetch", "cookie", "jsvmp_probe"],
|
||||
via_blank=True,
|
||||
wait_until="networkidle"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 7 — 触发业务操作
|
||||
|
||||
```
|
||||
Actions:
|
||||
click(selector=".some-btn")
|
||||
# 或
|
||||
type_text(selector="#search", text="test")
|
||||
click(selector=".submit")
|
||||
# 或
|
||||
evaluate_js(expression="document.querySelector('.nextPage').click()")
|
||||
|
||||
→ VMP 会在这一步生成签名并发起请求
|
||||
```
|
||||
|
||||
### Step 8 — 读日志,分析 hot_keys / hot_methods / hot_functions
|
||||
|
||||
```
|
||||
Actions (hot_keys — VMP 读取了哪些属性):
|
||||
get_instrumentation_log(
|
||||
tag_filter="vmp1",
|
||||
type_filter="tap_get",
|
||||
limit=200
|
||||
)
|
||||
→ summary.hot_keys:
|
||||
{
|
||||
"userAgent": 120,
|
||||
"plugins": 98,
|
||||
"webdriver": 77,
|
||||
"platform": 65,
|
||||
"language": 54,
|
||||
"cookie": 43,
|
||||
...
|
||||
}
|
||||
→ 这就是 VMP 参与签名哈希的完整环境指纹集
|
||||
|
||||
Actions (hot_methods — VMP 调用了哪些方法):
|
||||
get_instrumentation_log(
|
||||
tag_filter="vmp1",
|
||||
type_filter="tap_method",
|
||||
limit=200
|
||||
)
|
||||
→ summary.hot_methods:
|
||||
{
|
||||
"Object.defineProperty": 45,
|
||||
"Array.prototype.join": 38,
|
||||
"String.prototype.charCodeAt": 30,
|
||||
"CryptoJS.MD5": 12,
|
||||
...
|
||||
}
|
||||
→ 这告诉你算法是否用标准加密
|
||||
|
||||
Actions (hot_functions — VMP 调用了哪些函数):
|
||||
get_instrumentation_log(
|
||||
tag_filter="vmp1",
|
||||
type_filter="tap_call",
|
||||
limit=200
|
||||
)
|
||||
→ summary.hot_functions:
|
||||
{
|
||||
"_0xabc": 300,
|
||||
"_0xdef": 150,
|
||||
"encodeURIComponent": 8,
|
||||
"parseInt": 6,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 进阶技巧
|
||||
|
||||
### 4.1 多 VMP 场景(一次分析两个脚本)
|
||||
|
||||
```
|
||||
instrument_jsvmp_source(url_pattern="**/webmssdk.es5.js", mode="ast", tag="webmssdk")
|
||||
instrument_jsvmp_source(url_pattern="**/a_bogus.js", mode="ast", tag="bogus")
|
||||
|
||||
# 分别看
|
||||
get_instrumentation_log(tag_filter="webmssdk", type_filter="tap_get")
|
||||
get_instrumentation_log(tag_filter="bogus", type_filter="tap_get")
|
||||
```
|
||||
|
||||
### 4.2 key_filter 锁定某个具体属性
|
||||
|
||||
```
|
||||
# 只看 VMP 读取 navigator.webdriver 的记录
|
||||
get_instrumentation_log(tag_filter="vmp1", key_filter="webdriver")
|
||||
|
||||
# 只看 VMP 调用 CryptoJS.MD5 的记录(method_name 过滤)
|
||||
get_instrumentation_log(tag_filter="vmp1", key_filter="MD5")
|
||||
```
|
||||
|
||||
### 4.3 管理插桩 route
|
||||
|
||||
```
|
||||
# 查看所有激活的 route
|
||||
get_instrumentation_status()
|
||||
|
||||
# 停止单个
|
||||
stop_instrumentation(url_pattern="**/sdenv-*.js")
|
||||
|
||||
# 全部停止
|
||||
stop_instrumentation()
|
||||
```
|
||||
|
||||
### 4.4 与 runtime_probe 互补
|
||||
|
||||
runtime_probe 是低开销广谱探针,**不**改写源码,只 override 几个热点 API。源码级插桩改写 VMP 源码;runtime_probe 覆盖浏览器原生 API。两者叠加可以看到:
|
||||
|
||||
- VMP 内部 `obj[key]` 调用 → `get_instrumentation_log`
|
||||
- VMP 最终落到 `XMLHttpRequest.open` / `canvas.toDataURL` / `navigator.userAgent` 的 getter 调用 → `get_runtime_probe_log`
|
||||
|
||||
```
|
||||
inject_hook_preset(preset="runtime_probe", persistent=True)
|
||||
instrument_jsvmp_source(url_pattern="**/sdenv-*.js", mode="ast", tag="vmp1")
|
||||
reload_with_hooks()
|
||||
触发操作
|
||||
# 两路都读
|
||||
get_instrumentation_log(tag_filter="vmp1", limit=300)
|
||||
get_runtime_probe_log(type_filter="xhr_send", limit=100)
|
||||
get_runtime_probe_log(type_filter="canvas_toDataURL", limit=50)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题与陷阱
|
||||
|
||||
### Q1:`files_rewritten=0`,没改写到
|
||||
|
||||
**可能原因**:
|
||||
1. `url_pattern` 没命中——确认 VMP 实际 URL(`list_network_requests` 里看),glob 模式要用 `**/` 前缀才能匹配任意路径
|
||||
2. VMP 从缓存加载——强制 `reload_with_hooks(clear_log=True)` 或清浏览器缓存
|
||||
3. pattern 注册太晚——要在 `navigate` / `reload_with_hooks` **之前**调用 `instrument_jsvmp_source`
|
||||
|
||||
### Q2:AST 模式报 `parse_error`
|
||||
|
||||
**可能原因**:脚本有 Acorn 不支持的语法(如极老的 ES3 hack 或极新的 stage-0 提案)。
|
||||
|
||||
**对策**:
|
||||
- 降级为 `mode="regex"`(零依赖,不会 parse)
|
||||
- 或先 `save_script` 把脚本存本地,人工看看是不是有奇怪的语法,再考虑局部 patch
|
||||
|
||||
### Q3:改写后页面崩溃
|
||||
|
||||
**可能原因**:
|
||||
1. VMP 自己对自己的源码做了完整性校验(少见,但存在)——对策:用 regex 模式做轻改写,只改 member access
|
||||
2. Tag 变量名冲突——改个 `tag` 再试
|
||||
3. 改写后代码超过 JS 引擎 token 上限——把 `max_rewrites` 调小
|
||||
|
||||
### Q4:hot_keys 没出现预期的环境指纹
|
||||
|
||||
**可能原因**:
|
||||
1. VMP 还没真正执行——加大 `wait_until="networkidle"`,或 `wait_for` 某个元素后再读日志
|
||||
2. 改写没成功——先 `get_instrumentation_status` 看 `files_rewritten` 是否 > 0
|
||||
3. tag 搞混了——检查 `tag_filter` 是否写对
|
||||
|
||||
### Q5:日志爆炸到上限 20000 条
|
||||
|
||||
**对策**:
|
||||
- `get_instrumentation_log(..., clear=True)` 读完清
|
||||
- 收紧 `url_pattern`,只插桩必要的一个脚本
|
||||
- 第一次跑用 `limit=300`,看 top hot_keys 就够,不需要全部 entries
|
||||
|
||||
---
|
||||
|
||||
## 6. 源码级插桩的还原决策
|
||||
|
||||
拿到 `hot_keys` / `hot_methods` / `hot_functions` 三个摘要后,按下面的表格选策略:
|
||||
|
||||
| hot_methods 特征 | hot_keys 环境属性数 | cookie 来源(analyze_cookie_sources) | 策略 |
|
||||
|-----------------|---------------------|--------------------------------------|------|
|
||||
| 包含 CryptoJS.MD5 / SubtleCrypto.digest / HMAC | 少(< 10) | 全部 js_document_cookie | 纯算法还原(crypto/hashlib) |
|
||||
| 包含 CryptoJS 但环境读取多 | 中(10-30) | 混合或 js_document_cookie | 提取 JS 签名函数 + vm/execjs 沙箱 |
|
||||
| 全是自定义 fn 名,无可识别加密 | 多(30+) | 任意 | 路径 B:jsdom 环境伪装(见 `jsdom-env-patches.md`) |
|
||||
| 全是自定义 fn 名 | 中-多 | 主要 http_set_cookie | 路径 B + 重点关注发 token 的 POST 接口 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 参考
|
||||
|
||||
- `references/jsvmp-analysis.md`:四板斧方法论总纲
|
||||
- `references/jsdom-env-patches.md`:环境伪装补丁库(路径 B)
|
||||
- `references/mcp-cookbook.md` 场景 6:源码级插桩场景化操作
|
||||
- `cases/universal-vmp-source-instrumentation.md`:骨架案例模板
|
||||
- MCP 上游:https://github.com/WhiteNightShadow/camoufox-reverse-mcp(v0.4.0+)
|
||||
+249
-2
@@ -7,7 +7,7 @@
|
||||
|
||||
## 工具速查表
|
||||
|
||||
### camoufox-reverse MCP(52 个工具)
|
||||
### camoufox-reverse MCP(65 个工具,v0.4.0+)
|
||||
|
||||
| 类别 | 工具 | 核心用途 |
|
||||
|------|------|---------|
|
||||
@@ -47,6 +47,15 @@
|
||||
| **指纹** | `get_fingerprint_info` | 检查浏览器指纹 |
|
||||
| **反检测** | `check_detection` | 测试是否被反爬识别 |
|
||||
| **反调试** | `bypass_debugger_trap` | 绕过 debugger 陷阱 |
|
||||
| **分发循环扫描** | `find_dispatch_loops` | **[v0.4.0]** 定位字节码分发函数(while+switch,case 数过滤) |
|
||||
| **源码级插桩** | `instrument_jsvmp_source` | **[v0.4.0]** HTTP 层改写 VMP,每个 obj[key]/fn(args) 插入 tap(通用 VMP 利器) |
|
||||
| **插桩日志** | `get_instrumentation_log` | **[v0.4.0]** 拉取源码插桩日志,带 hot_keys/hot_methods/hot_functions 摘要 |
|
||||
| **插桩状态** | `get_instrumentation_status` / `stop_instrumentation` | **[v0.4.0]** 查看/停止源码插桩 |
|
||||
| **Cookie 归因** | `analyze_cookie_sources` | **[v0.4.0]** 融合 HTTP Set-Cookie + JS document.cookie + cookie jar |
|
||||
| **hook 重载** | `reload_with_hooks` | **[v0.4.0]** 重载使 persistent hook 先于页面 JS 执行(+ 清日志) |
|
||||
| **运行时探针日志** | `get_runtime_probe_log` | **[v0.4.0]** 获取 runtime_probe 预设的广谱事件日志 |
|
||||
| **预设 Hook(扩展)** | `inject_hook_preset("cookie")` / `inject_hook_preset("runtime_probe")` | **[v0.4.0]** 新增两个预设 |
|
||||
| **首屏注入** | `navigate(pre_inject_hooks=[...])` | **[v0.4.0]** 经 about:blank 装 hook 再 goto,解决首屏挑战页 |
|
||||
|
||||
## 场景化操作手册
|
||||
|
||||
@@ -204,6 +213,139 @@
|
||||
[camoufox-reverse] evaluate_js(expression="ws.send(JSON.stringify({...}))")
|
||||
```
|
||||
|
||||
### 场景 6:通用 JSVMP 源码级插桩(瑞数/Akamai/webmssdk)[v2.5.0 新增]
|
||||
|
||||
**目标**:定位 VMP 读取的环境指纹集与调用的加密原语,不依赖 VMP 具体实现
|
||||
|
||||
```
|
||||
步骤 1:启动浏览器 + 开启响应体捕获
|
||||
[camoufox-reverse] launch_browser(headless=False)
|
||||
[camoufox-reverse] start_network_capture(capture_body=True)
|
||||
|
||||
步骤 2:第一次导航定位 VMP 脚本
|
||||
[camoufox-reverse] navigate(url="https://target.com/")
|
||||
[camoufox-reverse] list_network_requests(resource_type="script")
|
||||
→ 找最大的 JS(100KB+,通常是 sdenv-*.js / FuckCookie_*.js / webmssdk.es5.js)
|
||||
|
||||
步骤 3:确认是 VMP
|
||||
[camoufox-reverse] find_dispatch_loops(
|
||||
script_url="https://target.com/xxx/sdenv-xxx.js",
|
||||
min_case_count=20
|
||||
)
|
||||
→ case_count > 50 基本确认是 VMP
|
||||
|
||||
步骤 4:装源码级插桩(核心)
|
||||
[camoufox-reverse] instrument_jsvmp_source(
|
||||
url_pattern="**/sdenv-*.js",
|
||||
mode="ast", # 能联网用 ast(99% 覆盖);离线用 regex(80%)
|
||||
tag="vmp1"
|
||||
)
|
||||
|
||||
步骤 5:兜底 hook
|
||||
[camoufox-reverse] inject_hook_preset(preset="cookie", persistent=True)
|
||||
[camoufox-reverse] inject_hook_preset(preset="xhr", persistent=True)
|
||||
[camoufox-reverse] hook_jsvmp_interpreter(script_url="sdenv-")
|
||||
[camoufox-reverse] bypass_debugger_trap()
|
||||
|
||||
步骤 6:重载让插桩先于 VMP 生效
|
||||
[camoufox-reverse] reload_with_hooks()
|
||||
|
||||
步骤 7:触发业务操作(翻页 / 登录 / 搜索)
|
||||
|
||||
步骤 8:读 hot_keys / hot_methods / hot_functions(30 秒读完 VMP 指纹)
|
||||
[camoufox-reverse] get_instrumentation_log(
|
||||
tag_filter="vmp1",
|
||||
type_filter="tap_get", # 所有 obj[key] 读取
|
||||
limit=200
|
||||
)
|
||||
→ summary.hot_keys: VMP 读了哪些属性(top 30,按频次)
|
||||
|
||||
[camoufox-reverse] get_instrumentation_log(
|
||||
tag_filter="vmp1",
|
||||
type_filter="tap_method", # 所有 obj.method(args) 调用
|
||||
limit=200
|
||||
)
|
||||
→ summary.hot_methods: VMP 调了哪些方法(Array.prototype.join / CryptoJS.MD5 ...)
|
||||
|
||||
[camoufox-reverse] get_instrumentation_log(
|
||||
tag_filter="vmp1",
|
||||
type_filter="tap_call", # 所有 fn(args) 直接调用
|
||||
limit=200
|
||||
)
|
||||
→ summary.hot_functions: VMP 调了哪些函数
|
||||
|
||||
步骤 9:Cookie 归因
|
||||
[camoufox-reverse] analyze_cookie_sources()
|
||||
→ 对每个 cookie 返回 sources(http_set_cookie / js_document_cookie)
|
||||
+ http_responses[].url + js_writes[].stack
|
||||
|
||||
步骤 10:完工清理
|
||||
[camoufox-reverse] stop_instrumentation(url_pattern="**/sdenv-*.js")
|
||||
[camoufox-reverse] remove_hooks()
|
||||
```
|
||||
|
||||
**判定与后续策略**:
|
||||
|
||||
| hot_methods 包含 | hot_keys 环境属性数 | 策略 |
|
||||
|-----------------|---------------------|------|
|
||||
| CryptoJS.MD5 / SubtleCrypto.digest / btoa | 少(< 15) | 纯算法还原(Node.js crypto / Python hashlib) |
|
||||
| 大量自定义 fn 名 | 少(< 15) | 提取 VMP 子片段 + vm 沙箱 |
|
||||
| CryptoJS / SubtleCrypto | 多(40+) + cookie 来自 HTTP Set-Cookie | jsdom 环境伪装(走路径 B) |
|
||||
|
||||
详细方法论见 `references/jsvmp-source-instrumentation.md`。
|
||||
|
||||
### 场景 7:Cookie 归因分析 [v2.5.0 新增]
|
||||
|
||||
**目标**:搞清楚某个 Cookie 到底是谁写的(JS / HTTP Set-Cookie / 混合)
|
||||
|
||||
```
|
||||
步骤 1:启动环境 + 开启响应体捕获 + cookie hook
|
||||
[camoufox-reverse] launch_browser(headless=False)
|
||||
[camoufox-reverse] start_network_capture(capture_body=True)
|
||||
[camoufox-reverse] inject_hook_preset(preset="cookie", persistent=True) # 原型链级
|
||||
|
||||
步骤 2:导航到目标(首屏有挑战用 pre_inject_hooks)
|
||||
[camoufox-reverse] navigate(
|
||||
url="https://target.com/",
|
||||
pre_inject_hooks=["xhr", "fetch", "cookie"]
|
||||
)
|
||||
|
||||
步骤 3:触发可疑 Cookie 的生成场景(刷新 / 登录 / 点业务按钮)
|
||||
|
||||
步骤 4:一把归因
|
||||
[camoufox-reverse] analyze_cookie_sources(name_filter="acw_tc|NfBCSins|x-bogus")
|
||||
→ 返回:
|
||||
{
|
||||
"cookies": {
|
||||
"acw_tc": {
|
||||
"sources": ["http_set_cookie"],
|
||||
"first_set_ts": ...,
|
||||
"http_responses": [{"url": ".../challenge", "ts": ..., "header": "acw_tc=...; path=/"}],
|
||||
"js_writes": [],
|
||||
"current_value": "..."
|
||||
},
|
||||
"x-bogus-js": {
|
||||
"sources": ["js_document_cookie"],
|
||||
"first_set_ts": ...,
|
||||
"http_responses": [],
|
||||
"js_writes": [{"value": "x-bogus-js=...", "stack": "...encrypt@webmssdk.es5.js:..."}],
|
||||
"current_value": "..."
|
||||
}
|
||||
},
|
||||
"total_cookies": 2
|
||||
}
|
||||
|
||||
步骤 5:按归因结果进一步分析
|
||||
a. 纯 http_set_cookie → 看 http_responses[].url 是哪个接口,它的请求体里有什么签名参数
|
||||
→ 用场景 1 分析那个接口的加密参数
|
||||
b. 纯 js_document_cookie → 看 js_writes[].stack 定位写入函数,search_code 打开源码
|
||||
c. 两者都有 → 两步都做(JS 算 token POST 给服务端,服务端 Set-Cookie 回来)
|
||||
```
|
||||
|
||||
**关键洞察**:
|
||||
|
||||
> 对于瑞数、Akamai 这类"JS 算 token、服务端 Set-Cookie"的混合模式,单纯 `hook_function(Document.prototype.cookie)` 是抓不到最终 cookie 写入的——因为它是 HTTP header 写的,不经过 JS。`analyze_cookie_sources` 的价值就是 30 秒内消除这个盲区。
|
||||
|
||||
## 高级技巧
|
||||
|
||||
### 使用 trace_function 自动记录函数调用
|
||||
@@ -314,7 +456,109 @@
|
||||
[camoufox-reverse] intercept_request(url_pattern="**/api/*", action="mock", mock_response={"status": 200, "body": "{\"ok\":1}"})
|
||||
```
|
||||
|
||||
## camoufox-reverse MCP 工具分类总览(52 个)
|
||||
### 使用源码级插桩(v2.5.0 新增,通用 VMP 利器)
|
||||
|
||||
```
|
||||
# 1. 先确认是不是 VMP(case_count > 50 基本是)
|
||||
[camoufox-reverse] find_dispatch_loops(script_url="https://target.com/sdenv-xxx.js", min_case_count=20)
|
||||
|
||||
# 2. 装插桩(AST 模式优先,需 CDN;regex 模式兜底)
|
||||
[camoufox-reverse] instrument_jsvmp_source(
|
||||
url_pattern="**/sdenv-*.js", # glob 模式匹配多 hash 版本
|
||||
mode="ast", # 或 "regex"
|
||||
tag="vmp1", # 区分多 VMP 的标签
|
||||
rewrite_member_access=True, # 改写每个 obj[key]
|
||||
rewrite_calls=True, # 改写每个 fn(args) / obj.method(args)
|
||||
max_rewrites=5000, # 单文件改写上限
|
||||
cache_rewritten=True # 缓存改写后源码
|
||||
)
|
||||
|
||||
# 3. 装完后必须重载让插桩生效
|
||||
[camoufox-reverse] reload_with_hooks()
|
||||
|
||||
# 4. 触发操作后读日志
|
||||
[camoufox-reverse] get_instrumentation_log(tag_filter="vmp1", type_filter="tap_get", limit=200)
|
||||
→ summary.hot_keys 是 VMP 读取的环境属性 top 30(指纹学习金矿)
|
||||
|
||||
[camoufox-reverse] get_instrumentation_log(tag_filter="vmp1", type_filter="tap_method", limit=200)
|
||||
→ summary.hot_methods 是 VMP 调用的方法 top 30(ObjectType.methodName 格式)
|
||||
|
||||
# 5. 查看插桩状态
|
||||
[camoufox-reverse] get_instrumentation_status()
|
||||
|
||||
# 6. 完工移除
|
||||
[camoufox-reverse] stop_instrumentation(url_pattern="**/sdenv-*.js")
|
||||
[camoufox-reverse] stop_instrumentation() # 不传参数 = 全部停止
|
||||
```
|
||||
|
||||
**与 hook_jsvmp_interpreter 的分工**:
|
||||
|
||||
| 工具 | 能看到 | 看不到 |
|
||||
|------|--------|--------|
|
||||
| `hook_jsvmp_interpreter` | VMP 通过 `Function.prototype.apply/call/bind` 或 `Reflect.*` 调子函数,读全局对象属性(Proxy) | VMP 在 switch/case 内部直接 `obj[key]` / `fn(args)` 的调用 |
|
||||
| `instrument_jsvmp_source` | 每一次 member access + 每一次函数调用,无论是否通过可 hook API | 不跨脚本(只改写指定 url_pattern 的那个文件) |
|
||||
| **建议**:两者同时开,互相校验 | | |
|
||||
|
||||
### 使用 Cookie 归因分析(v2.5.0 新增)
|
||||
|
||||
```
|
||||
# 1. 前置条件:网络抓包 + cookie hook 同时开
|
||||
[camoufox-reverse] start_network_capture(capture_body=True)
|
||||
[camoufox-reverse] inject_hook_preset(preset="cookie", persistent=True)
|
||||
|
||||
# 2. 触发场景
|
||||
|
||||
# 3. 一键归因
|
||||
[camoufox-reverse] analyze_cookie_sources(name_filter="ttwid|msToken|acw_tc")
|
||||
|
||||
返回字段:
|
||||
cookies[name].sources: ["http_set_cookie" | "js_document_cookie"] # 可能两者都有
|
||||
cookies[name].first_set_ts: 首次观察到的毫秒时间戳
|
||||
cookies[name].http_responses: [{url, ts, header}] # HTTP 写入来源
|
||||
cookies[name].js_writes: [{value, stack, ts}] # JS 写入来源(含调用栈)
|
||||
cookies[name].current_value: 当前 jar 中的值
|
||||
```
|
||||
|
||||
### 使用 runtime_probe 做低开销广谱观察(v2.5.0 新增)
|
||||
|
||||
```
|
||||
# runtime_probe 是 jsvmp_hook 的轻量版:不装 Proxy,只 override 具体热点 API
|
||||
[camoufox-reverse] inject_hook_preset(preset="runtime_probe", persistent=True)
|
||||
|
||||
# 触发操作
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="xhr_send", limit=100)
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="canvas_toDataURL", limit=50) # canvas 指纹检测
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="nav_read", limit=100) # navigator 读取
|
||||
[camoufox-reverse] get_runtime_probe_log(type_filter="addEventListener", limit=100) # 鼠标/键盘检测
|
||||
|
||||
返回 by_type 字段自动聚合各事件类型的总数,便于快速画出"这个页面都在观察什么"。
|
||||
```
|
||||
|
||||
### 使用 pre_inject_hooks 解决首屏挑战页(v2.5.0 新增)
|
||||
|
||||
```
|
||||
# 瑞数 412 / Akamai 首包检测场景:
|
||||
# 普通 navigate → launch_browser → navigate → 装 hook
|
||||
# 但此时 challenge JS 已经跑完了,hook 完全漏掉第一次 VMP 执行
|
||||
|
||||
# 正确做法:走 about:blank 先装 hook 再 goto
|
||||
[camoufox-reverse] navigate(
|
||||
url="https://target.com/",
|
||||
wait_until="networkidle",
|
||||
pre_inject_hooks=["xhr", "fetch", "cookie", "jsvmp_probe"], # 先装这些
|
||||
via_blank=True, # 强制经 about:blank
|
||||
collect_response_chain=True # 记录重定向链
|
||||
)
|
||||
|
||||
# 返回:
|
||||
# initial_status: 首次响应状态(可能是 412)
|
||||
# final_status: 最终状态(challenge 过后的 200)
|
||||
# redirect_chain: [{url, status, ts}, ...] 整条重定向链
|
||||
# hooks_injected: 实际装上的 hook 名列表
|
||||
# warnings: 装 hook 失败等非致命问题
|
||||
```
|
||||
|
||||
## camoufox-reverse MCP 工具分类总览(65 个,v0.4.0+)
|
||||
|
||||
| 类别 | 工具 | 说明 |
|
||||
|------|------|------|
|
||||
@@ -331,3 +575,6 @@
|
||||
| 网络 | `start_network_capture` `stop_network_capture` `list_network_requests` `get_network_request` `get_request_initiator` `intercept_request` `stop_intercept` | 网络分析(支持响应体捕获) |
|
||||
| 存储 | `get_cookies` `set_cookies` `delete_cookies` `get_storage` `set_storage` `export_state` `import_state` | 状态管理 |
|
||||
| 反检测 | `get_fingerprint_info` `check_detection` `bypass_debugger_trap` | 指纹与反检测 |
|
||||
| 源码级插桩 | `instrument_jsvmp_source` `get_instrumentation_log` `get_instrumentation_status` `stop_instrumentation` `find_dispatch_loops` | **[v0.4.0]** 通用 JSVMP 逆向利器 |
|
||||
| Cookie 归因 | `analyze_cookie_sources` | **[v0.4.0]** HTTP Set-Cookie + JS 写入融合分析 |
|
||||
| 导航增强 | `navigate(pre_inject_hooks)` `reload_with_hooks` `get_runtime_probe_log` | **[v0.4.0]** 首屏挑战/hook 重注入/运行时探针 |
|
||||
|
||||
@@ -226,3 +226,8 @@
|
||||
| 验证环境检测 | `get_fingerprint_info` + `check_detection` | 确认哪些环境项参与校验 |
|
||||
| 追踪调用链 | `get_request_initiator` | 从请求直接定位到签名函数 |
|
||||
| 实时对比 | `evaluate_js` | 在浏览器中执行还原后的签名函数,与脚本输出对比 |
|
||||
| **Cookie 归因** [v2.5.0] | `analyze_cookie_sources` | 辨识 Cookie 是 HTTP Set-Cookie 还是 JS document.cookie 写的 |
|
||||
| **源码级插桩** [v2.5.0] | `instrument_jsvmp_source` + `get_instrumentation_log` | 对 VMP 做 HTTP 层源码改写,hot_keys 暴露环境指纹集 |
|
||||
| **首屏挑战** [v2.5.0] | `navigate(pre_inject_hooks=[...])` | 瑞数/Akamai 412 挑战页 hook 预注入 |
|
||||
| **hook 重载** [v2.5.0] | `reload_with_hooks` | 重载使 persistent hook 先于页面 JS 执行 + 清日志 |
|
||||
| **运行时探针** [v2.5.0] | `get_runtime_probe_log` | 快速摸底页面在读什么 / 调什么(低开销) |
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
│ ├─ 混淆 JS(OB/CFF/eval)→ 反混淆后 → 模式A 或 模式B
|
||||
│ ├─ 服务端返回 JS 动态执行 → 模式B:VM 沙箱执行
|
||||
│ ├─ WASM 二进制 → 模式C:WASM 加载
|
||||
│ ├─ JSVMP 虚拟机保护 → 先尝试 I/O 定位,失败则 → 模式D
|
||||
│ ├─ JSVMP 虚拟机保护 → 先走四板斧(Hook/插桩/日志/源码级插桩) → 失败则 → 模式D/E
|
||||
│ │ · 瑞数 5/6、Akamai、webmssdk、obfuscator.io → 第四板斧源码级插桩最有效
|
||||
│ │ · 深度绑定环境指纹 → 模式E(jsdom 环境伪装)
|
||||
│ └─ 无法脱离浏览器环境 → 模式D:浏览器自动化
|
||||
│
|
||||
├─ 纯请求方案是否可行?
|
||||
@@ -122,7 +124,59 @@
|
||||
↓
|
||||
8. 还原算法并对比中间值
|
||||
↓
|
||||
9. 编写 Node.js 脚本
|
||||
9. 编写 Node.js / Python 脚本
|
||||
↓
|
||||
10. 运行验证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSVMP 分支专属流程(v2.5.0 新增)
|
||||
|
||||
识别到 JSVMP(200KB+ 单文件 + while-switch 分发循环 + 字节码数组)时,不要走通用流程的第 4-7 步,改走下面这个分支:
|
||||
|
||||
```
|
||||
1. launch_browser(headless=False) + start_network_capture(capture_body=True)
|
||||
↓
|
||||
2. 导航前判断是否首屏挑战
|
||||
├─ 是(瑞数 412 / Akamai 403 等)
|
||||
│ → navigate(url=..., pre_inject_hooks=["xhr","fetch","cookie","jsvmp_probe"], via_blank=True)
|
||||
└─ 否
|
||||
→ navigate(url=...) + 之后装 hook + reload_with_hooks()
|
||||
↓
|
||||
3. find_dispatch_loops(script_url=<VMP_URL>) 确认 VMP(case_count > 50)
|
||||
↓
|
||||
4. 同时装三路探针:
|
||||
├─ 第一板斧(I/O 边界):inject_hook_preset(cookie / xhr / fetch / crypto, persistent=True)
|
||||
├─ 第二板斧(解释器):hook_jsvmp_interpreter(script_url=<VMP basename>)
|
||||
└─ 第四板斧(源码级插桩,核心):instrument_jsvmp_source(
|
||||
url_pattern="**/<VMP 文件>",
|
||||
mode="ast", tag="vmp1")
|
||||
↓
|
||||
5. reload_with_hooks() 让探针先于 VMP 生效 + 清日志
|
||||
↓
|
||||
6. 触发业务操作,让 VMP 生成一次签名
|
||||
↓
|
||||
7. 第三板斧读日志,分 3 个维度看:
|
||||
├─ get_instrumentation_log(tag_filter="vmp1") → hot_keys / hot_methods / hot_functions
|
||||
├─ get_jsvmp_log() → API 调用 + 属性读取摘要
|
||||
├─ analyze_cookie_sources() → cookie 归因(HTTP vs JS)
|
||||
└─ get_console_logs() → xhr/fetch/cookie 预设的输出
|
||||
↓
|
||||
8. 根据 hot_methods 选还原策略:
|
||||
├─ 包含标准加密 API(CryptoJS.MD5 / Subtle.digest 等) → 模式 A 纯算法还原
|
||||
├─ 全自定义 + hot_keys 少 → 模式 B 提取函数 + vm/execjs 沙箱
|
||||
├─ 全自定义 + hot_keys 多 + cookie 走 HTTP → 模式 E jsdom 环境伪装
|
||||
└─ 以上都不行 → 模式 D 浏览器自动化(最后手段)
|
||||
↓
|
||||
9. 实现还原代码
|
||||
↓
|
||||
10. 连续 5+ 次请求验证稳定性
|
||||
↓
|
||||
11. stop_instrumentation() + remove_hooks() 清理
|
||||
```
|
||||
|
||||
参考文档:
|
||||
- `references/jsvmp-analysis.md`:四板斧方法论总纲
|
||||
- `references/jsvmp-source-instrumentation.md`:第四板斧源码级插桩专项(v2.5.0 新)
|
||||
- `references/jsdom-env-patches.md`:模式 E 环境伪装补丁库
|
||||
|
||||
Reference in New Issue
Block a user