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:
白夜
2026-04-17 22:36:50 +08:00
parent 8d078d6aad
commit 871c58381e
11 changed files with 1625 additions and 97 deletions
+17 -6
View File
@@ -1,13 +1,14 @@
# Camoufox 逆向分析 Skill
# JS 逆向分析 Skill
面向逆向分析与爬虫对抗场景的 Skill,围绕 `camoufox-reverse` MCP 构建单一工作流:先用 Camoufox 反检测浏览器完成网络捕获、源码定位、Hook 调试与反检测验证,再按需落地到 Node.js 或 Python 算法还原与自动化调用。
面向 Web 逆向分析与接口签名还原场景的 Skill,围绕 `camoufox-reverse` MCP 构建单一工作流:先用 Camoufox 反检测浏览器完成网络捕获、源码定位、Hook 调试与反检测验证,再按需落地到 Node.js 或 Python 算法还原与自动化调用。
## 核心能力
- **双语言算法还原**: Node.js (`crypto` / `crypto-js`) 与 Python (`hashlib` / `pycryptodome`) 双路径实现
- **加密算法还原**: MD5/SHA/AES/DES/RSA/HMAC/Base64 等常见加密算法的纯算法复现
- **JS 混淆还原**: OB混淆、控制流平坦化、eval打包、自定义VM/JSVMP 等混淆类型的分析策略
- **JSVMP 行为追踪**: Hook/插桩/日志分析三板斧,从行为层面追踪 JS 虚拟机保护的签名生成链路(详见专项指南)
- **JSVMP 行为追踪**: Hook/插桩/日志/源码级插桩四板斧(v2.5.0 新增第四板斧),从行为层面追踪 JS 虚拟机保护的签名生成链路,对瑞数 5/6、Akamai sensor_data、webmssdk、obfuscator.io 通用有效(详见专项指南)
- **Cookie 归因分析**: `analyze_cookie_sources` 融合 HTTP Set-Cookie 与 JS document.cookie 日志,一次性解答"这个 Cookie 到底是谁写的"(v2.5.0 新增)
- **动态 Cookie 逆向**: eval首包、api预热、浏览器指纹等 Cookie 生成机制的破解
- **WASM 逆向**: WebAssembly 加密模块的加载、环境补全和调用
- **协议层对抗**: TLS 指纹检测、HTTP/2 强制、频率限制等协议层防护的应对
@@ -50,7 +51,7 @@ git clone https://github.com/WhiteNightShadow/hello_js_reverse_skill.git ~/.code
git clone https://github.com/WhiteNightShadow/hello_js_reverse_skill.git ~/.vscode/skills/hello_js_reverse_skill
```
> 安装完成后,AI Agent 会自动读取 `SKILL.md` 获取 Camoufox 逆向分析能力。当你在对话中涉及接口签名分析、反爬对抗、动态 Cookie、混淆 JS、WASM 或浏览器环境调试等场景时,Skill 会自动激活。
> 安装完成后,AI Agent 会自动读取 `SKILL.md` 获取 JS 逆向分析能力。当你在对话中涉及接口签名分析、反爬对抗、动态 Cookie、混淆 JS、WASM 或浏览器环境调试等场景时,Skill 会自动激活。
## 项目结构
@@ -170,7 +171,7 @@ python main.py
## MCP 工具集成
本 Skill 只围绕 [`camoufox-reverse` MCP](https://github.com/WhiteNightShadow/camoufox-reverse-mcp) 服务器组织能力(Camoufox 反检测浏览器,52 个工具):
本 Skill 只围绕 [`camoufox-reverse` MCP](https://github.com/WhiteNightShadow/camoufox-reverse-mcp) 服务器组织能力(Camoufox 反检测浏览器,**65 个工具,v0.4.0+**):
| 核心工具 | 用途 |
|---------|------|
@@ -192,6 +193,14 @@ python main.py
| `check_detection` / `get_fingerprint_info` | 反检测验证与指纹查看 |
| `bypass_debugger_trap` | 一键绕过反调试陷阱 |
| `export_state` / `import_state` | 保存/恢复浏览器状态 |
| `find_dispatch_loops` | **[v2.5.0 新]** 扫描脚本定位字节码分发函数(while+switch,case 数过滤) |
| `instrument_jsvmp_source` | **[v2.5.0 新]** 源码级插桩:在 HTTP 层改写 VMP,对每个 obj[key]/fn(args) 插入 tap(通用 VMP 利器) |
| `get_instrumentation_log` / `get_instrumentation_status` / `stop_instrumentation` | **[v2.5.0 新]** 源码插桩日志(hot_keys/hot_methods/hot_functions)/ 状态 / 停止 |
| `reload_with_hooks` | **[v2.5.0 新]** 重载让 persistent hook 先于页面 JS 执行(+ 清日志) |
| `analyze_cookie_sources` | **[v2.5.0 新]** Cookie 归因:HTTP Set-Cookie vs JS document.cookie 融合分析 |
| `get_runtime_probe_log` | **[v2.5.0 新]** 获取 runtime_probe 预设的广谱事件日志(xhr/fetch/canvas/WebGL/nav) |
| `navigate(pre_inject_hooks=[...], via_blank=True)` | **[v2.5.0 增强]** 首屏挑战页预注入 hook + 返回 initial_status/final_status/redirect_chain |
| `inject_hook_preset("cookie" | "runtime_probe")` | **[v2.5.0 增强]** 新增两个预设 |
详见 `references/mcp-cookbook.md`。
@@ -214,6 +223,8 @@ python main.py
| 环境检测 | webdriver/蜜罐/指纹 | `environment-patch.md` |
| 反检测站点 | Cloudflare/瑞数/极验绕过 | SKILL.md |
| **请求失败排查** | **Cookie/Header/时间戳/签名对比** | **`troubleshooting.md`** |
| **通用 JSVMP 源码级插桩** | **HTTP 层源码改写 + hot_keys 指纹学习法(v2.5.0 新增)** | **`jsvmp-source-instrumentation.md`** |
| **Cookie 归因分析** | **HTTP Set-Cookie vs JS document.cookie 融合分析** | **`mcp-cookbook.md` 场景 7** |
## 技术栈
@@ -237,4 +248,4 @@ python main.py
- **curl_cffi**: 带浏览器 TLS 指纹模拟的 HTTP 客户端
### 调试工具
- **[camoufox-reverse MCP](https://github.com/WhiteNightShadow/camoufox-reverse-mcp)**: 反检测浏览器逆向分析(52 个工具,含 JSVMP 专项分析)
- **[camoufox-reverse MCP](https://github.com/WhiteNightShadow/camoufox-reverse-mcp) v0.4.0+**: 反检测浏览器逆向分析(65 个工具,含 JSVMP 源码级插桩 + Cookie 归因分析)
+216 -41
View File
@@ -4,8 +4,9 @@ description: >
Node.js / Python 接口自动化与签名还原工程技能:对自有平台或已授权平台的 Web API 进行签名分析与接口对接,
通过 Camoufox 反检测浏览器动态调试与静态源码分析,定位并还原前端加密/签名逻辑,
使用 Node.js 或 Python 实现算法复现与自动化接口调用。
深度集成 camoufox-reverse MCP(C++ 引擎级指纹伪装,52 个逆向分析工具)。
擅长 JSVMP 虚拟机保护的双路径攻克:路径 A 算法追踪(Hook / 插桩 / 日志分析三板斧)、
深度集成 camoufox-reverse MCP(C++ 引擎级指纹伪装,65 个逆向分析工具,v0.4.0+)。
擅长 JSVMP 虚拟机保护的双路径攻克:路径 A 算法追踪(Hook / 插桩 / 日志分析 / 源码级插桩四板斧,
通用对瑞数 5/6、Akamai sensor_data、webmssdk、obfuscator.io)、
路径 B 环境伪装(jsdom/vm 沙箱 + 浏览器环境采集对比 + 全量补丁)。
argument-hint: "<目标URL> [需要分析的加密参数名, 如 sign, m, token]"
---
@@ -99,7 +100,7 @@ argument-hint: "<目标URL> [需要分析的加密参数名, 如 sign, m, token]
## 核心武器:camoufox-reverse MCP
Camoufox 反检测浏览器 + Playwright 协议,C++ 引擎级指纹伪装,57 个工具覆盖逆向分析全链路。
Camoufox 反检测浏览器 + Playwright 协议,C++ 引擎级指纹伪装,65 个工具覆盖逆向分析全链路(v0.4.0+)。
**核心优势:**
- Camoufox 在 **C++ 引擎层** 修改指纹信息,而非 JS 补丁
@@ -112,8 +113,12 @@ Camoufox 反检测浏览器 + Playwright 协议,C++ 引擎级指纹伪装,57
- `launch_browser`:启动 Camoufox 反检测浏览器(可配置 headless/proxy/os_type/humanize/block_images/block_webrtc)。**已启动时返回完整会话状态**(页面 URL、上下文列表、抓包状态),不再只返回 `already_running`
- `close_browser`:关闭浏览器释放资源
- `navigate`:导航到目标 URL(支持 wait_until: load/domcontentloaded/networkidle)
- `navigate`:导航到目标 URL(支持 wait_until: load/domcontentloaded/networkidle)。**v0.4.0 增强**:
- `pre_inject_hooks=["xhr", "fetch", "crypto", "websocket", "debugger_bypass", "cookie_hook", "runtime_probe", "jsvmp_probe"]`——先走 `about:blank` 把 hook 装好,再 goto 目标 URL,保证 hook 在首屏 JS 之前生效(对瑞数 412 挑战页、Akamai 首包检测至关重要)
- `via_blank=True`——不装新 hook 也强制经 about:blank,让之前注册的 persistent scripts 一定生效
- 返回 `initial_status` / `final_status` / `redirect_chain`,解决"412 挑战页 → 通过后 200"的状态歧义
- `reload` / `go_back`:刷新 / 后退
- `reload_with_hooks`:**[v0.4.0 新]** 重载当前页面,使 persistent hooks 在页面 JS 之前执行。默认 `clear_log=True` 会清空 `__mcp_jsvmp_log` / `__mcp_prop_access_log` / `__mcp_cookie_log`,获得干净的一次执行捕获。**典型用法**:先 navigate 到目标页定位 VMP 脚本 URL,再 `instrument_jsvmp_source` / `hook_jsvmp_interpreter`,最后 `reload_with_hooks()` 让探针先于 VMP 生效
- `click` / `type_text`:点击元素 / 输入文本
- `wait_for`:等待元素或 URL 匹配
- `get_page_info`:获取当前页面 URL、标题、视口尺寸
@@ -144,18 +149,54 @@ Camoufox 反检测浏览器 + Playwright 协议,C++ 引擎级指纹伪装,57
- `trace_function`:追踪函数调用(不暂停执行,记录入参、返回值、可选调用栈,支持 **`persistent=True` 跨导航持久化**)
- `get_trace_data`:获取追踪数据(自动合并页面数据和 Python 端持久化数据)
- `hook_function`:注入自定义 Hook(before/after/replace 三种模式,支持 `non_overridable` 防覆盖)
- `inject_hook_preset`:一键预设 Hook(xhr/fetch/crypto/websocket/debugger_bypass,**默认 persistent=True 持久化**)
- `inject_hook_preset`:一键预设 Hook,**默认 persistent=True 持久化**。v0.4.0 可选预设扩展为 7 个:
- `xhr` / `fetch` / `crypto` / `websocket` / `debugger_bypass`——与旧版一致
- `cookie`——**[v0.4.0 新]** 基于 `cookie_hook.js`,**原型链级 hook**(沿 `document` 原型链找到 `cookie` 描述符所在的 `Document.prototype` / `HTMLDocument.prototype`,在那里替换 getter/setter,避免直接 `Object.defineProperty(document, 'cookie', ...)` 被浏览器忽略)。写入 `window.__mcp_cookie_log`(含 op/value/stack/ts)
- `runtime_probe`——**[v0.4.0 新]** 低开销广谱运行时探针,**非 Proxy 模式**,只覆写具体热点 API:XHR.open/send、fetch、canvas.toDataURL/getContext、WebGL.getParameter、navigator getters(userAgent/platform/language/webdriver/plugins 等 10 项)、EventTarget.addEventListener(鼠标/键盘/设备运动)。写入 `window.__mcp_runtime_log`
- `get_runtime_probe_log`:**[v0.4.0 新]** 拉取 runtime_probe 日志,支持 `type_filter`("xhr_open"/"xhr_send"/"fetch"/"canvas_toDataURL"/"canvas_getContext"/"webgl_getParameter"/"nav_read"/"addEventListener")和按类型聚合的 `by_type`
- `remove_hooks`:移除所有 Hook(支持 `keep_persistent` 参数保留持久化 Hook)
- `freeze_prototype`:**冻结任意原型方法**(`Object.defineProperty` configurable:false + writable:false,防止页面 JS 覆盖 Hook)
- `trace_property_access`:**Proxy 级别属性访问追踪**(监控对象属性的读写操作,使用 `.*` 后缀监控全部属性,如 `navigator.*`、`screen.*`)
- `get_property_access_log`:获取属性访问记录
### JSVMP 专项分析
### JSVMP 专项分析(v0.4.0 从 4 个工具扩展为 10 个,新增源码级插桩全套)
- `hook_jsvmp_interpreter`:**一键插桩 JSVMP 解释器**(Hook Function.prototype.apply/call + 追踪 30+ 敏感属性读取,自动识别 VM 行为)。**适用边界**:JSVMP 通过 apply/call 调用子函数时最有效;如果 JSVMP 使用内部函数表 + 直接调用,需要自定义 Hook 点(通过 `hook_function` 手动指定)。对高频调用函数(每秒数千次),trace 日志量可能爆炸,建议配合 `max_captures` 限制
- `get_jsvmp_log`:获取 JSVMP 分析日志(含 API 调用统计 + 属性读取摘要)。**注意**:返回的海量数据需要本地过滤,用「反向追踪法」从已知签名值反向搜索效率最高
- `dump_jsvmp_strings`:**提取 JSVMP 字符串数组**(识别 API 名称、检测混淆模式)。**局限**:前提是字符串未被动态解密,部分 JSVMP 的字符串表本身也是加密的
- `compare_env`:**全面收集浏览器环境指纹**,返回分类结构化数据(navigator/screen/canvas/WebGL/Audio/timing 等),用于与 Node.js/jsdom 环境对比差异。**覆盖范围**:默认采集主流检测项,但不包含 `Function.prototype.toString` 原生性检测、`Symbol.toStringTag` 标签、DOM `offsetHeight` 布局属性等细粒度检测。对于 JSVMP 深度环境检测场景(如 58 项差异修复),建议配合 `evaluate_js` 分批采集更细粒度的环境值(参考 `cases/` 经验案例中的采集批次模板)
**A. 运行时探针(通用,不改写源码)**
- `hook_jsvmp_interpreter`:**v0.4.0 重写为多路径通用探针**(不再只是 apply hook)。覆盖 4 条 VMP 访问宿主环境的通道:
- `track_calls=True`:Hook `Function.prototype.apply/call/bind` + `Date.now/performance.now` + `Math.random/crypto.getRandomValues`
- `track_reflect=True`:Hook `Reflect.apply/get/set/construct`(ES6 VMP 常用)
- `track_props=True`:在全局对象上装 Proxy——默认 `["navigator","screen","history","localStorage","sessionStorage","performance"]`(`document` **默认不装** 因为容易破坏页面;document.cookie 用 `cookie` 预设)
- 参数 `script_url` 做栈过滤(传 VMP 文件 basename),`max_entries=10000`
- **对瑞数 5/6、Akamai sensor_data v2/v3、TikTok webmssdk、obfuscator.io 通用有效**
- `get_jsvmp_log`:获取探针日志,支持 `type_filter`("api_call"/"prop_read")、`property_filter`、`func_filter`。返回自带 `summary.api_calls` / `summary.property_reads`(按调用次数倒排)
- `dump_jsvmp_strings`:**v0.4.0 修复**——用手动括号匹配替代嵌套正则,不再死循环。返回 `string_arrays`(大数组) / `decoded_strings`(去重后 top 500) / `api_names`(命中 navigator/screen/encrypt/md5 等关键词的串) / `suspicious_patterns`(JSVMP 循环、eval、new Function、XOR 解密、Base64+fromCharCode 解码器)
- `find_dispatch_loops`:**[v0.4.0 新]** 扫描脚本定位字节码分发函数。遍历每个 `switch(…)`,配对 `{…}` 做手动括号匹配,统计 case 数,返回 `case_count >= min_case_count(默认 20)` 的候选。输出 `{fn_name, case_count, char_range, preview}`——`case_count > 50` 基本可以确认是 VMP 解释器
- `compare_env`:**全面收集浏览器环境指纹**,结构化返回 navigator/screen/canvas/WebGL/Audio/timing/misc 分类数据。**覆盖范围**:默认采集主流检测项,**不包含** `Function.prototype.toString` 原生性检测、`Symbol.toStringTag` 标签、DOM `offsetHeight` 布局等细粒度项——这些需配合 `evaluate_js` 分批采集(见 `cases/jsvmp-xhr-interceptor-env-emulation.md` 4 批次模板)
**B. 源码级插桩(通用 VMP 利器,v0.4.0 新增核心能力)**
- `instrument_jsvmp_source(url_pattern, mode, tag, rewrite_member_access=True, rewrite_calls=True, max_rewrites=5000, cache_rewritten=True)`:**[v0.4.0 新]** 在 HTTP 层拦截 VMP 脚本响应,对源码做改写,在**每个 `obj[key]` 读取**(`__mcp_tap_get`)、**每个 `obj.method(args)` 方法调用**(`__mcp_tap_method`)、**每个 `fn(args)` 直接调用**(`__mcp_tap_call`)前插入运行时 tap。改写后的源码继续正常执行,但每次对宿主环境的交互都写入 `window.__mcp_vmp_log`。
- **`mode="regex"`**:纯正则改写,无 CDN 依赖,只改写 member access(80% 覆盖),速度快,大文件安全
- **`mode="ast"`**:通过页面内 Acorn 做精确 AST 改写,99% 覆盖,需加载 `cdnjs.cloudflare.com/ajax/libs/acorn`
- `url_pattern` 用 glob(如 `"**/webmssdk.es5.js"`、`"**/sdenv-*.js"`、`"https://target.com/FuckCookie_*.js"`)
- `tag` 作为过滤键(一次插桩多个 VMP 时区分)
- **与 runtime 探针互补**:runtime 探针只看 VMP 路由到可 hook API 的部分;源码插桩能看到 VMP 在 switch/case 内部每一次 `opcode_table[code]` 访问和每一次子 handler 调用,是瑞数 6 / Akamai 这类"VM 完全自包含"场景的唯一有效手段
- `get_instrumentation_log(tag_filter, type_filter, key_filter, limit=500, clear=False)`:**[v0.4.0 新]** 拉取源码插桩日志。`type_filter` 可选 `"tap_get"` / `"tap_call"` / `"tap_method"` / `"tap_call_err"`。返回内置 `summary`:
- `hot_keys`:访问频次 top 30 的属性名——**指纹学习的金矿**,瑞数 VMP 会在这里暴露所有它读取的环境属性
- `hot_methods`:调用频次 top 30 的方法(格式 `ObjectType.methodName`)
- `hot_functions`:调用频次 top 30 的函数名
- `get_instrumentation_status`:**[v0.4.0 新]** 查看当前激活的源码插桩,每个 pattern 的 `files_rewritten`、`total_edits`、`last_url`、`cached_urls`
- `stop_instrumentation(url_pattern=None)`:**[v0.4.0 新]** 停止一个 pattern(传路径)或全部(不传),底层调用 `page.unroute`
**C. Cookie 归因**
- `analyze_cookie_sources(name_filter=None)`:**[v0.4.0 新]** 解答"某个 Cookie 到底是谁写的"。融合三路数据源:
- 所有捕获响应的 `Set-Cookie` header(需 `start_network_capture` 在先)
- `window.__mcp_cookie_log`(需 `inject_hook_preset("cookie")` 在先)
- `page.context.cookies()`——当前 jar 状态
- 对每个 cookie 名返回:`sources: ["http_set_cookie" | "js_document_cookie"]` / `first_set_ts` / `http_responses: [{url, ts, header}]` / `js_writes: [{value, stack, ts}]` / `current_value`
- **适用场景**:瑞数/Akamai 里 JS 端计算 token 但最终由服务端 `Set-Cookie` 写入的混合模式——没这个工具就会在 JS Hook 上找不到写入而一筹莫展
### 网络分析(逆向核心)
@@ -489,48 +530,83 @@ Actions:
---
##### 路径 A:算法追踪(三板斧:Hook → 插桩 → 日志分析)
##### 路径 A:算法追踪(四板斧:Hook → 插桩 → 日志 → 源码级插桩)
**核心方法论 — 从 I/O 两端夹逼 + 中间层插桩**:
**核心方法论 — 从 I/O 两端夹逼 + 中间层插桩 + 源码级全量 tap**:
> **快速路径**:对于典型 JSVMP,可直接使用 `hook_jsvmp_interpreter` 一键插桩,再用 `get_jsvmp_log` 获取结构化分析结果,省去手动 Hook 的步骤 1-4。
> **v2.5.0 更新**:v2.4.0 的"三板斧"在瑞数 5/6、Akamai sensor_data、webmssdk 这类"VM 完全自包含、不路由到可 hook API"的场景上失效——因为 Hook `Function.prototype.apply` 看不到 VM 内部 `opcode_table[code]` 调度。v0.4.0 MCP 新增的 **源码级插桩(`instrument_jsvmp_source`)** 作为第四板斧补齐这一缺口。四板斧的关系是:
>
> - **第一 / 二 / 三板斧**:诊断"VM 看外界"(入口)和"外界看 VM"(出口)——适合签名通过 CryptoJS/atob/MD5 等可 hook 原语走的 VMP
> - **第四板斧**:诊断"VM 看自己"——适合 VMP 算法全部封装在字节码分发循环 `switch(opcode) { case N: obj[key](args); ... }` 里的场景
**三板斧适用边界与局限**:
- `hook_jsvmp_interpreter` 的 Hook `Function.prototype.apply` 策略——JSVMP 不一定通过 apply 调用子函数,很多 JSVMP 使用内部函数表 + 直接调用,此时需通过 `hook_function` 手动指定 Hook 点
- 「追踪 30+ 敏感属性读取」——如果 JSVMP 用 Proxy 或 eval 间接读取属性,追踪可能遗漏
- `dump_jsvmp_strings` 提取字符串数组——前提是字符串未被动态解密,很多 JSVMP 的字符串表本身也是加密的
- `search_code(keyword="while")` 在超大文件(380KB+)中会返回大量无关结果,应使用 `search_code_in_script` 配合更精确的关键词
- `trace_function` 对高频调用函数(每秒数千次)的日志量可能爆炸,必须设置 `max_captures` 限制
> **快速路径(推荐优先试)**:
>
> 1. `find_dispatch_loops(script_url)` 确认是不是 VMP(`case_count > 50` 基本是)
> 2. `hook_jsvmp_interpreter(script_url=<VMP basename>)` 一键装通用探针
> 3. `inject_hook_preset("cookie")` + `inject_hook_preset("xhr", persistent=True)`
> 4. `instrument_jsvmp_source(url_pattern="**/<VMP 文件>", mode="ast", tag="vmp1")`(AST 模式覆盖率高,页面能联网时优先)
> 5. `reload_with_hooks()` — 让所有探针先于 VMP 生效,同时清日志
> 6. 触发目标操作
> 7. `get_instrumentation_log(tag_filter="vmp1", limit=300)` — 看 `hot_keys` / `hot_methods` / `hot_functions` 三个摘要,通常 30 秒内就能定位到 VMP 读取的环境指纹集和调用的加密原语
> 8. `get_jsvmp_log()` + `analyze_cookie_sources()` 交叉印证
>
> 快速路径足以解决 70%+ 的瑞数/Akamai/webmssdk 场景。**无法解决时**(算法全部内联、无可识别 hot_keys 模式)再走手动四板斧流程 ↓
**四板斧适用边界与局限**:
| 板斧 | 工具 | 擅长 | 不擅长 |
|------|------|------|-------|
| 第一斧(Hook I/O) | `inject_hook_preset(xhr/fetch/crypto/cookie)` + `freeze_prototype` | 请求链路劫持、动态 Cookie、加密原语入口 | VM 内部自实现的 MD5/AES |
| 第二斧(插桩解释器) | `trace_function` + `trace_property_access` | 能识别分发函数名时的调用链追踪 | 匿名 IIFE 包裹 + 高频日志爆炸 |
| 第三斧(日志分析) | `get_jsvmp_log` + 反向追踪 | 已能捕获签名值 I/O 时反推公式 | 签名完全不出 VM 的黑箱模式 |
| 第四斧(**源码级插桩**) | `instrument_jsvmp_source` + `get_instrumentation_log` | VM 内部调度、"全部在 opcode dispatch 循环里发生"的场景,`hot_keys` 直接暴露环境指纹集 | 极限大文件(5MB+)regex 模式覆盖率下降;AST 模式需 CDN |
- `hook_jsvmp_interpreter` **v0.4.0 重写后**覆盖 apply/call/bind + Reflect.*/Proxy 全局对象 + timing/random,适用面从"只对 apply 型 VMP"扩展到"绝大多数 VMP",但仍看不到 VMP 在 switch/case 内 obj[key] 访问
- `dump_jsvmp_strings` 前提是字符串未被动态解密,很多 VMP 的字符串表本身也是加密的(看到 `results.decoded_strings` 里全是单字母乱码就是这种情况)
- `search_code(keyword="while")` 在超大文件(380KB+)会返回大量无关结果,应使用 `search_code_in_script` 配合更精确关键词,或直接 `find_dispatch_loops`
- `trace_function` 对高频调用函数(每秒数千次)日志量可能爆炸,必须设置 `max_captures` 限制
- `get_trace_data` 返回的海量数据需要本地过滤工具,用「反向追踪法」(从已知签名值反向搜索)效率最高
- `instrument_jsvmp_source` 的 regex 模式对带模板字符串、正则字面量的代码可能误改写,AST 模式更可靠;两种模式都默认 `cache_rewritten=True`,避免重复改写
```
三板斧摘要(详细步骤见 references/jsvmp-analysis.md):
四板斧摘要(详细步骤见 references/jsvmp-analysis.md 及 references/jsvmp-source-instrumentation.md):
第一板斧:Hook 出入口(确定 I/O 边界)
步骤 0:hook_jsvmp_interpreter → 一键插桩(快速路径,推荐先试)
步骤 1:Hook 出口 — inject_hook_preset(xhr/fetch, persistent=True) + Cookie Hook + freeze_prototype
步骤 0:hook_jsvmp_interpreter(script_url=<VMP basename>) → 一键装多路径探针(快速路径,推荐先试)
步骤 1:Hook 出口 — inject_hook_preset(xhr/fetch, persistent=True) + inject_hook_preset("cookie") + freeze_prototype
分析 Cookie 来源 — analyze_cookie_sources() 辨识 HTTP vs JS 写入
步骤 2:Hook 入口 — inject_hook_preset(crypto) + String.fromCharCode + CryptoJS 函数
→ 关联出入口数据,推断签名公式
第二板斧:插桩解释器(追踪执行链路)
步骤 3:search_code_in_script 定位解释器核心分发函数(while-switch 循环)
步骤 3:find_dispatch_loops(script_url) → 一键定位字节码分发函数(case_count > 20 的 switch)
步骤 4:分层 trace_function(粗→中→细,max_captures 限制日志量)
步骤 5:trace_property_access 监控签名容器 + compare_env 采集环境基准
第三板斧:日志分析(从海量数据提取签名链路)
步骤 6:get_trace_data + get_jsvmp_log + get_console_logs → 多维度过滤
步骤 6:get_trace_data + get_jsvmp_log + get_console_logs + get_runtime_probe_log → 多维度过滤
步骤 7:反向追踪法 — 从已知签名值反向搜索首次出现位置 → 追踪到原始明文
步骤 8:evaluate_js 验证提取的算法,对比签名结果
第四板斧:源码级插桩(通用 VMP 利器,v2.5.0 新增)
步骤 9:instrument_jsvmp_source(url_pattern="**/<VMP 文件>", mode="ast", tag="vmp1")
→ 在 HTTP 层改写 VMP 源码,每个 obj[key]/fn(args) 都被 tap
步骤 10:reload_with_hooks() → 让插桩先于 VMP 生效,同时清日志
步骤 11:get_instrumentation_log(tag_filter="vmp1", limit=300)
→ 看 hot_keys(被读取属性 top 30) / hot_methods(被调用方法 top 30) / hot_functions(被调用函数 top 30)
→ 这三张表直接告诉你 VMP 读了哪些环境指纹、调了哪些 API,即使算法在 switch/case 内部
步骤 12:stop_instrumentation(url_pattern="**/<VMP 文件>") → 完工后移除
```
**路径 A 还原策略选择**:
**路径 A 还原策略选择(四板斧完整决策树见 references/jsvmp-analysis.md):**
| 情况 | 策略 | 实现方式 |
|------|------|---------|
| 签名使用标准算法(MD5/HMAC/AES) | 直接用目标语言还原 | Node.js `crypto` / Python `hashlib` + `pycryptodome` |
| 签名使用标准算法(MD5/HMAC/AES),`get_jsvmp_log` 能看到对应 API 调用 | 直接用目标语言还原 | Node.js `crypto` / Python `hashlib` + `pycryptodome` |
| 签名逻辑是标准算法但拼接规则复杂 | 还原拼接逻辑 + 标准算法 | 提取拼接顺序和格式,手动实现 |
| 签名逻辑完全定制化 | 提取最小 JS 片段执行 | Node.js `vm` 沙箱 / Python `execjs` |
| VM 劫持了整个请求链路 | 提取 VM 核心 + 最小环境 | 加载完整 VM 文件但只调用签名入口 |
| 签名逻辑完全定制化,但 `hot_keys` 清晰暴露输入域 | 提取最小 JS 片段执行 | Node.js `vm` 沙箱 / Python `execjs` |
| VM 劫持了整个请求链路,`analyze_cookie_sources` 显示 cookie 来自 HTTP Set-Cookie | 纯算法还原不现实,走路径 B | jsdom 环境伪装,让 VM 在沙箱里自己生成 |
| VM 算法全部内联在 dispatch 循环,即便源码插桩 `hot_keys` 也无法还原 | 加载完整 VM + 最小环境 | 路径 B,优先 jsdom 运行原始脚本 |
---
@@ -903,20 +979,29 @@ MCP 操作:
- trace_function(function_path="签名函数名", log_stack=true)
```
### 场景 2:动态 Cookie 生成
### 场景 2:动态 Cookie 生成 + Cookie 归因
```
特征:Cookie 中有频繁变化的字段,页面 JS 动态写入
定位:Hook document.cookie setter → 追踪写入来源
类型:
a. eval 首包:请求返回混淆 JS → eval 执行 → 写入 Cookie
b. 预热请求:/api2 等接口返回 JS → 注入 window 变量 → 计算 Cookie
c. 指纹 Cookie:收集浏览器信息 → base64 编码 → 写入
MCP 操作:
- hook_function(function_path="Document.prototype.cookie", hook_code="console.log('Cookie set:', arguments)", position="before")
- inject_hook_preset(preset="crypto") → 捕获加密 I/O
- evaluate_js(expression="document.cookie")
- list_network_requests → 识别预热请求
特征:Cookie 中有频繁变化的字段,可能是服务端 Set-Cookie / JS document.cookie / 两者混合
定位三步走:
Step 1 — 先归因(v2.5.0 新增,关键步骤):
inject_hook_preset(preset="cookie", persistent=True) → 注入原型链级 cookie_hook(替代手写 Document.prototype.cookie hook)
start_network_capture(capture_body=True)
触发操作 / reload_with_hooks()
analyze_cookie_sources() → 返回每个 cookie 的 sources(http_set_cookie / js_document_cookie)
→ 这一步直接告诉你目标 cookie 是「纯服务端发的」「纯 JS 写的」还是「JS 算 token + 服务端带回来」
→ 瑞数/Akamai 常见的是第三种,单纯 Hook document.cookie setter 什么都抓不到
Step 2 — 按归因结果走对应路径:
a. 纯 JS 写入 → 看 js_writes[].stack 定位写入函数 → search_code + trace_function
b. 纯 HTTP Set-Cookie → 看 http_responses[].url 定位发 token 的接口 → 分析请求体里的签名参数
c. 混合(JS 算 token POST 给服务端,服务端 Set-Cookie 回来)→ 两步都要做
Step 3 — 还原 token 计算:
inject_hook_preset(preset="crypto") → 捕获 btoa/atob/JSON.stringify I/O
若涉及 VMP → 走场景 11(通用 JSVMP 源码级插桩)
若涉及预热请求 → list_network_requests 看主请求之前的调用链
若涉及 eval 首包 → hook_function(Function, ...) 截获动态生成的函数源码
```
### 场景 3:响应数据加密
@@ -1032,6 +1117,90 @@ MCP 操作:
参考:references/jsdom-env-patches.md(补丁模板)、cases/ 经验案例
```
### 场景 11:通用 JSVMP 源码级插桩(瑞数 5/6 / Akamai sensor_data / webmssdk / obfuscator.io)
```
特征:JSVMP 算法完全封装在 opcode 分发循环 switch(...) { case N: obj[key](args); ... } 中,
常规 Hook Function.prototype.apply 看不见 VM 内部动作
识别:
- 文件 100KB+,通常是 sdenv-*.js / FuckCookie_*.js / webmssdk.es5.js / sensor_data v2/v3
- find_dispatch_loops(script_url) 返回 case_count > 50 的候选
- 传统 hook_jsvmp_interpreter 日志里 Function.apply 调用次数远远少于 VM 行数
核心方法论:源码级插桩 + hot_keys 指纹学习法
黄金 8 步流程(照抄即可,瑞数/Akamai/webmssdk 通吃):
Step 1 — 定位 VMP 脚本 URL
launch_browser(headless=False)
start_network_capture(capture_body=True)
navigate(url="https://target.com/", wait_until="load")
list_network_requests(resource_type="script")
→ 找最大的 JS(通常 200KB+),记下 URL
Step 2 — 确认是 VMP
find_dispatch_loops(script_url="https://target.com/xxx/sdenv-xxx.js", min_case_count=20)
→ case_count > 50 基本确认;记下 fn_name / char_range
Step 3 — 装源码级插桩(核心)
instrument_jsvmp_source(
url_pattern="**/sdenv-*.js", # glob 模式,能覆盖 CDN 不同 hash 版本
mode="ast", # 页面能联网时优先 ast(99% 覆盖);否则 regex(80%,无 CDN)
tag="vmp1", # 一次插桩多个 VMP 时用来过滤
rewrite_member_access=True, # 每个 obj[key] 都 tap
rewrite_calls=True # 每个 fn(args) / obj.method(args) 都 tap
)
Step 4 — 兜底 hook(与源码插桩互补)
inject_hook_preset("cookie", persistent=True) # 原型链级 cookie hook
inject_hook_preset("xhr", persistent=True) # XHR 请求出口
inject_hook_preset("fetch", persistent=True) # fetch 请求出口
hook_jsvmp_interpreter(script_url="sdenv-") # 通用运行时探针(apply/call/bind/Reflect.*)
bypass_debugger_trap()
Step 5 — 让插桩先于 VMP 生效
reload_with_hooks()
→ 这一步会清 __mcp_vmp_log / __mcp_jsvmp_log / __mcp_cookie_log,获得干净捕获
→ 若 navigate 一开始就是首屏挑战(412),用 navigate(url=..., pre_inject_hooks=["xhr","fetch","cookie","jsvmp_probe"]) 替代
Step 6 — 触发目标行为(翻页 / 登录 / 搜索),让 VMP 生成一次签名
Step 7 — 读 hot_keys(指纹学习的金矿)
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_get", limit=200)
→ summary.hot_keys 告诉你 VMP 读取了哪些属性,按频次倒排
→ 典型瑞数输出:{"userAgent":120, "plugins":98, "webdriver":77, "cookie":43, ...}
→ 这就是 VMP 参与签名哈希的完整环境指纹集!直接对齐到 Node.js/jsdom 侧即可
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_method", limit=200)
→ summary.hot_methods 告诉你 VMP 调用了哪些方法(格式 ObjectType.methodName)
→ 典型:{"Object.defineProperty":45, "Array.prototype.join":38, "CryptoJS.MD5":12, ...}
→ 能否看到 MD5/AES/HMAC 就是算法是否用标准加密的核心判据
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_call", limit=200)
→ summary.hot_functions 里看有没有熟识的函数名(btoa/atob/encodeURIComponent)
Step 8 — 归因 Cookie 最终落地
analyze_cookie_sources(name_filter="acw_tc|NfBCSins|x-bogus") # 按关键词过滤
→ 对每个目标 cookie 返回 sources、first_set_ts、http_responses、js_writes
→ 决定还原方向:
· sources = ["http_set_cookie"] → 看 http_responses[].url 的请求体里有什么 token,再从 VMP tap 日志里找 token 生成
· sources = ["js_document_cookie"] → 看 js_writes[].stack,直接定位写入函数
· sources = 两者都有 → 两步都做,这是瑞数标配模式
后续还原:
根据 hot_keys / hot_methods 制定策略:
路径 A-1 — hot_methods 里出现 CryptoJS.MD5 / SubtleCrypto.digest → 纯算法还原
路径 A-2 — hot_methods 里全是自定义 fn 名 → 提取 VMP 子片段 + Node.js vm 沙箱运行
路径 B — hot_keys 里环境指纹很多(navigator/screen/webgl 40+)
+ analyze_cookie_sources 显示 cookie 来自 HTTP Set-Cookie
→ 走 jsdom 环境伪装(见 references/jsdom-env-patches.md)
完工后:
stop_instrumentation(url_pattern="**/sdenv-*.js") # 关闭源码级 route
remove_hooks() # 清 hook
工具参考:references/jsvmp-source-instrumentation.md(完整方法论)
```
## 调试环境保护策略
参考 `references/anti-debug.md` 处理以下常见调试干扰手段:
@@ -1157,7 +1326,7 @@ launch_browser set_cookies start_ search_ set_break hook_ trace_
### 调试工具
- **camoufox-reverse MCP** — 反检测浏览器 + Hook + 源码搜索 + 网络分析 + 指纹伪装 + 请求拦截 + JSVMP 专项分析(52 个工具,一站式逆向分析)
- **camoufox-reverse MCP** v0.4.0+ — 反检测浏览器 + Hook + 源码搜索 + 网络分析 + 指纹伪装 + 请求拦截 + JSVMP 源码级插桩 + Cookie 归因分析(65 个工具,一站式逆向分析)
## 经验法则
@@ -1183,6 +1352,11 @@ launch_browser set_cookies start_ search_ set_break hook_ trace_
20. **环境对比要分批采集**:单次 evaluate_js 代码太长会报错,分 4-5 批(navigator / screen+window / document+performance+toString / DOM+Canvas+WebGL+Audio)
21. **jsdom 环境补丁必须在 JSVMP 脚本加载前完成**:XHR Hook 的安装顺序决定能否截获最终 URL(我方 Hook → JSVMP 加载 → JSVMP 保存 Hook 后的引用)
22. **服务端静默拒绝是环境检测失败的信号**:返回 HTTP 200 + 空 body(不报错),说明签名格式正确但环境指纹不匹配
23. **源码级插桩优先于运行时 hook(对 VM 自包含场景)**:瑞数 5/6、Akamai sensor_data、webmssdk 这类"算法全部在 opcode dispatch 循环内"的 VMP,`hook_jsvmp_interpreter` 的多路径探针仍然看不到 switch/case 内部,这时 `instrument_jsvmp_source` 是唯一能打开黑箱的工具——对这类场景应直接跳过前三板斧,从第四板斧开始
24. **hot_keys 指纹学习法**:`get_instrumentation_log` 返回的 `summary.hot_keys` 按访问频次倒排属性名,是 VMP 环境指纹集的完整画像——30 秒就能告诉你"这个 VMP 会读 navigator 的哪 20 个属性、screen 的哪 6 个属性",直接对齐到 Node.js/jsdom 侧比肉眼扫字节码快 100 倍
25. **Cookie 归因优先于 setter hook**:分析动态 Cookie 第一步永远是 `analyze_cookie_sources`,它能区分"纯 JS 写""纯 HTTP Set-Cookie""JS 算 token + 服务端带回来"三种模式——瑞数/Akamai 最常见的第三种模式下,单纯 hook `document.cookie` setter 什么都抓不到,会浪费几小时白忙
26. **首屏挑战页必须用 `pre_inject_hooks`**:瑞数 412 挑战、Akamai 首包检测是在"你还没来得及装 hook"的那一瞬间发生的;旧流程"launch_browser → navigate → 装 hook"完全漏掉首屏——正确做法是 `navigate(url=..., pre_inject_hooks=["xhr", "fetch", "cookie", "jsvmp_probe"])`,先走 about:blank 装好 hook 再 goto,或者用 `via_blank=True` 让之前 persistent scripts 生效
27. **`reload_with_hooks` 取代裸 `reload`**:装完 hook 想让它先于页面 JS 跑,裸 `reload()` 不能保证顺序;用 `reload_with_hooks()` 一步到位——它不仅按 context-level init_script 的方式重载,还默认 `clear_log=True` 清空 `__mcp_jsvmp_log`/`__mcp_vmp_log`/`__mcp_prop_access_log`/`__mcp_cookie_log`,拿到的是这次重载的干净快照
---
@@ -1197,3 +1371,4 @@ launch_browser set_cookies start_ search_ set_break hook_ trace_
| v2.2.0 | 2026-04-01 | 适配 camoufox-reverse MCP v1.1(44→52 个工具):新增 JSVMP 专项工具(`hook_jsvmp_interpreter`/`get_jsvmp_log`/`dump_jsvmp_strings`/`compare_env`);新增 Hook 增强工具(`freeze_prototype`/`trace_property_access`/`get_property_access_log`);新增 `search_code_in_script` 大文件精确搜索;所有 Hook/trace 工具支持 `persistent=True` 跨导航持久化 + `non_overridable` 防覆盖;`start_network_capture` 支持 `capture_body=True` 捕获响应体 |
| v2.3.0 | 2026-04-01 | 新增经验库系统(`cases/` 目录):Phase 0.5 指纹快速匹配阶段(30 秒内完成技术指纹采集 → 经验库匹配 → 命中走快速路径 / 未命中走标准流程);案例模板 `_template.md` + 知识提取提示词;首个案例:JSVMP 字节码虚拟机 + XHR 拦截器 + jsdom 环境伪装(含 58 项环境检测差异全表 + markNative 三层防御方案 + 完整浏览器指纹采集流程) |
| v2.4.0 | 2026-04-02 | **JSVMP 双路径攻克体系**:基于实战经验重构 JSVMP 分析框架,新增路径选择决策树(路径 A 算法追踪 vs 路径 B 环境伪装);路径 B「环境伪装六步法」完整方法论(采集→对比→分级→补丁→验证→端到端);新增场景 10「JSVMP + 环境伪装」;标注三板斧适用边界与局限(apply 限制 / 高频日志爆炸 / 字符串表加密);`compare_env` 覆盖范围说明及细粒度采集建议;新增 `references/jsdom-env-patches.md` 环境补丁知识库(Function.toString 三层防御 / plugins 完整模拟 / DOM 布局属性 / 30+ API 存根 / Symbol.toStringTag);经验法则扩充至 22 条(+环境伪装优先 / Function.toString 第一杀手 / 分批采集 / Hook 顺序 / 静默拒绝信号);解法模式新增 E: jsdom 环境伪装 |
| v2.5.0 | 2026-04-17 | **JSVMP 路径 A 升级为四板斧(新增源码级插桩)+ 对齐 MCP v0.4.0**:适配 camoufox-reverse MCP v0.4.0(52→65 个工具);新增第四板斧「源码级插桩」——在 HTTP 层对 VMP 脚本改写,每个 `obj[key]` / `fn(args)` 都插入 tap,对瑞数 5/6、Akamai sensor_data v2/v3、webmssdk、obfuscator.io 通用有效;新增 8 个 MCP 工具清单(`instrument_jsvmp_source`/`get_instrumentation_log`/`get_instrumentation_status`/`stop_instrumentation`/`find_dispatch_loops`/`reload_with_hooks`/`analyze_cookie_sources`/`get_runtime_probe_log`);`hook_jsvmp_interpreter` 从单路径 apply 升级为多路径(apply/call/bind + Reflect.*/Proxy 全局对象 + timing/random);`inject_hook_preset` 新增 `cookie`(原型链级 cookie hook)和 `runtime_probe`(低开销广谱运行时探针)两个预设;`navigate` 支持 `pre_inject_hooks` + `via_blank` + 返回 `initial_status`/`final_status`/`redirect_chain`,解决首屏挑战页 hook 失效;新增场景 11「通用 JSVMP 源码级插桩」(黄金 8 步流程);场景 2「动态 Cookie」引入 `analyze_cookie_sources` 作为第一步归因;新增 `references/jsvmp-source-instrumentation.md` 源码级插桩专项指南(regex vs ast 模式选择、hot_keys 指纹学习法、与 hook 探针互补);经验法则扩充至 27 条(+源码级插桩优先 / hot_keys 指纹学习法 / Cookie 归因优先于 setter hook / 首屏 pre_inject_hooks / reload_with_hooks 取代裸 reload);新增骨架案例 `cases/universal-vmp-source-instrumentation.md` |
+2
View File
@@ -13,6 +13,8 @@
| 案例文件 | 技术特征 | 难度 | 核心方案 |
|---------|---------|------|---------|
| [jsvmp-xhr-interceptor-env-emulation.md](jsvmp-xhr-interceptor-env-emulation.md) | JSVMP 字节码虚拟机 + XHR 拦截器 + 多层 SDK 联动 + jsdom 全量环境伪装 | ★★★★★ | jsdom 沙箱 + 58 项环境补丁 + XHR Hook 截出 a_bogus |
| [jsvmp-ruishu6-cookie-412-sdenv.md](jsvmp-ruishu6-cookie-412-sdenv.md) | JSVMP 瑞数6 + Cookie 生成 + 412 挑战 + sdenv 补环境 | ★★★★★ | sdenv(魔改 jsdom + C++ V8 Addon)让瑞数 JSVMP 真实执行生成 Cookie |
| [universal-vmp-source-instrumentation.md](universal-vmp-source-instrumentation.md) | **[v2.5.0 新]** 通用 VMP(瑞数 5/6、Akamai sensor_data、webmssdk、obfuscator.io)+ 首屏挑战 + 混合 Cookie 模式 | ★★★★ | **源码级插桩** + hot_keys 指纹学习 + analyze_cookie_sources 归因(**骨架模板**,由使用者按实际站点填充) |
## 私有映射
+218
View File
@@ -0,0 +1,218 @@
# JSVMP-瑞数6+Cookie生成+412挑战+sdenv补环境
> 难度:★★★★★
> 还原方案:E: jsdom 环境模拟(sdenv 魔改 jsdom + C++ V8 Addon)
> 实现语言:Node.js
> 最后验证日期:2026-04-17
---
## 技术指纹(供 Phase 0.5 自动匹配)
### JS 特征
- [x] 单文件 230KB+,变量名为 `_$` 前缀加2-3位随机字母(如 `_$bV`, `_$ku`),存在 `if($_ts.cd){` 入口判断,函数名每次请求动态变化
- [x] 页面底部存在 `<script>_$xx();</script>` 格式的入口函数调用,函数名每次不同
- [x] HTML 中包含 `<meta id="固定ID" content="动态token" r='m'>` 标签,content 值约 100 字符,每次请求变化
- [x] 内联脚本设置 `$_ts` 全局变量,包含 `nsd`(数字种子)和 `cd`(约 1800 字符的配置字符串),每次请求动态变化
### 参数特征
- [x] Cookie 名称格式为 `XxxYyyZzz2Aaaa` + `S`/`T` 后缀(如 `NfBCSins2OywS` 和 `NfBCSins2OywT`),S 由服务端 Set-Cookie 下发,T 由客户端 JS 生成
### 请求特征
- [x] 首次请求返回 HTTP 412(非标准状态码),响应体为精简 HTML(含 meta + 内联 $_ts 配置 + 外部 JS 引用 + 入口函数调用)
- [x] 412 响应的 Set-Cookie 头同时下发 `acw_tc`(会话标识)和 `XxxS`(服务端标识,HttpOnly,过期时间 10 年)
- [x] 成功请求需携带 3 个 Cookie:`acw_tc` + `XxxS`(服务端下发)+ `XxxT`(客户端生成)
- [x] 瑞数 JS 文件 URL 路径包含随机目录名和文件名,但版本号后缀(如 `.e17ed02.js`)在一段时间内固定
### 反调试特征
- [x] JSVMP 内部使用函数表+直接调用(非 `Function.prototype.apply/call`),导致标准 JSVMP Hook 工具无法拦截
- [x] `$_ts` 全局变量在 JS 执行完毕后被清理,运行时无法通过 console 访问
- [x] 检测 `typeof document.all` 必须为 `"undefined"`(浏览器特有行为,纯 JS 无法模拟)
### 混淆类型
- [x] JSVMP(三层嵌套虚拟机),外层解析配置生成 eval 代码,中层执行 Cookie 生成和 XHR 劫持,内层执行 AES/CRC32/Huffman/Base64 加密
### 指纹检测规则(Agent 执行)
```
快速检测命令(30秒内完成):
- search_code(keyword="$_ts") → 命中 + 存在 nsd/cd 字段 → 高置信度
- search_code(keyword="_$") → 大量 _$ 前缀变量 → 辅助确认
- list_scripts → 存在 230KB+ 文件 → 辅助确认
- list_network_requests → 首次请求返回 412 → 高置信度
- 检查 Set-Cookie 头 → 存在 acw_tc + XxxS 格式 Cookie → 直接定位本案例
匹配判定:412 响应 + $_ts 配置 + _$ 前缀变量 → 瑞数6 高置信度匹配
```
---
## 加密方案
- **算法**:Huffman 编码 → XOR → AES-128-CBC → CRC32 校验 → AES-128-CBC → Base64(URL-safe 变体)
- **密钥来源**:从 `$_ts.cd` 配置字符串中通过 XOR offset 推导提取 45 组密钥,密钥随每次 412 响应动态变化
- **加密流程**:
1. 收集浏览器指纹(Canvas、WebGL、UA、屏幕尺寸、navigator 属性等)
2. 组装 basearr(约 154 字节 TLV 结构,包含 8 种 type 的环境数据)
3. basearr → Huffman 编码 → XOR 加密 → AES-128-CBC 加密 → 追加 CRC32 校验 → 再次 AES-128-CBC → Base64 编码
4. Cookie T = `"0"` + Base64 结果
5. 写入 `document.cookie`,然后通过 `location.replace` 或 XHR 重新请求页面
- **签名公式**:Cookie T 不是简单的签名,而是完整的加密数据包,包含浏览器指纹、时间戳、随机数等信息,服务端解密后验证指纹一致性
---
## 已验证定位路径(Phase 0.5 命中后直接执行)
```
步骤 1: Camoufox 反检测浏览器 + 网络捕获
启动 Camoufox(C++ 引擎级指纹伪装),注入 XHR/Fetch/Crypto 持久化 Hook,导航到目标页面。
通过 list_network_requests 发现请求链路:
请求 #1 返回 412 + Set-Cookie + HTML body
请求 #2 加载 230KB JS 文件
请求 #3 携带 3 个 Cookie 返回 200
步骤 2: 412 响应体分析
通过 get_network_request(id=1, include_body=true) 获取完整的 412 响应。
发现 HTML body 结构为:
<meta> 标签(动态 token)+ $_ts 内联配置(nsd + cd)+ 外部 JS 引用 + 入口函数调用 _$xx()
同时从 Set-Cookie 头提取到 acw_tc 和 XxxS 两个服务端 Cookie。
步骤 3: 成功请求对比
通过 get_network_request(id=3, include_headers=true) 对比成功请求的 Cookie 头,
发现多了一个 XxxT Cookie — 这就是客户端 JS 生成的。
同时发现请求头中 Referer 和 Sec-Fetch-Site: same-origin 是必需的。
步骤 4: Cookie setter Hook 验证
注入 document.cookie setter Hook,发现 Cookie 日志为空 —
说明瑞数 JS 不是通过标准 document.cookie setter 写入的
(可能是通过 jsdom 内部机制或 location.replace 触发的重新请求时由浏览器自动携带)。
步骤 5: JSVMP 插桩尝试
使用 hook_jsvmp_interpreter 对瑞数 JS 进行插桩,发现日志为空 —
确认瑞数6使用内部函数表+直接调用,不经过 Function.prototype.apply/call,
标准 JSVMP Hook 无效。
步骤 6: 环境对比
使用 compare_env 采集 Camoufox 中的真实浏览器环境数据,
发现 document.cookie 中包含 enable_XxxYyy=true 标记,
说明瑞数 JS 执行成功后会设置一个启用标记。
步骤 7: 瑞数 JS 保存
通过 save_script 将 230KB 的瑞数 JS 文件保存到本地,分析其结构:
以 if($_ts.cd){ 开头,内部是 JSVMP 解释器,
window 仅出现 3 次(环境访问全部通过 JSVMP 字节码间接进行)。
步骤 8: 补环境方案选型
参考 ctl456/ruishu6-yaojianju(手动补环境)和 warterbili/ruishu-re(sdenv/纯算/JsRpc 多方案),
确定 sdenv(魔改 jsdom + C++ V8 Addon)是瑞数6的最优纯 Node.js 方案。
```
---
## 还原代码模板
### 核心函数:瑞数6 Cookie 生成 — 基于 sdenv
```javascript
/**
* 瑞数6 Cookie 生成 — 基于 sdenv (魔改 jsdom + C++ V8 Addon)
* 依赖: npm install sdenv (需要 pnpm 安装 + node-gyp 编译原生模块)
* 原理:
* sdenv 的核心是 documentAll.node (51行C++),用 V8 的
* ObjectTemplate::MarkAsUndetectable() 实现 document.all 的浏览器特有行为
* (typeof === "undefined" 但可调用),加上完整的浏览器环境模拟,
* 让瑞数 JSVMP 在 Node.js 中真实执行并生成有效 Cookie。
*/
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const { jsdomFromUrl } = require('sdenv');
const https = require('https');
class RuishuClient {
constructor(config = {}) {
this.host = config.host; // 目标主机名
this.entryPath = config.entryPath; // 入口页面路径(返回 412 的页面)
this.ua = config.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36';
this.dom = null;
this.cookies = '';
this.ready = false;
}
async init() {
const url = `https://${this.host}${this.entryPath}`;
this.dom = await jsdomFromUrl(url, {
userAgent: this.ua,
consoleConfig: { error: () => {} },
});
// 等待瑞数 JS 执行完毕
await new Promise(resolve => {
this.dom.window.addEventListener('sdenv:exit', () => resolve());
setTimeout(resolve, 8000);
});
this.cookies = this.dom.cookieJar.getCookieStringSync(`https://${this.host}`);
this.ready = true;
return this;
}
get(path) {
if (!this.ready) throw new Error('请先调用 init()');
return new Promise((resolve, reject) => {
https.request({
hostname: this.host, port: 443, path, method: 'GET',
headers: {
'User-Agent': this.ua,
'Host': this.host,
'Cookie': this.cookies,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Referer': `https://${this.host}/`,
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Dest': 'document',
},
}, res => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => resolve({ status: res.statusCode, body }));
}).on('error', reject).end();
});
}
close() {
if (this.dom) {
try { this.dom.window.close(); } catch(e) {}
this.dom = null;
this.ready = false;
}
}
}
```
---
## 踩坑记录
| # | 坑 | 现象 | 解决方法 |
|---|---|------|---------|
| 1 | 误判反爬类型 | 通过 webFetch 直接请求返回 412,猜测是 JSL 加速乐(`__jsl_clearance_s` cookie),实际是瑞数信息(RS) | 不要猜,用 Camoufox 实际抓包看 412 响应体结构和 Set-Cookie 头 |
| 2 | Cookie 有效但请求返回 400 | Camoufox 获取的 Cookie 有效(curl 测试返回 200),但 axios 请求返回 400 | 瑞数不仅检查 Cookie,还检查请求头指纹 — UA 必须与生成 Cookie 时一致,且必须包含 `Sec-Fetch-Site: same-origin` 等 Fetch Metadata 头 |
| 3 | jsdom 执行瑞数 JS 卡死(超时) | 原生 jsdom + `runScripts: 'dangerously'` 加载 412 页面,瑞数 JS 执行后进入死循环 | `typeof document.all` 在 jsdom 中返回 `"object"` 而非 `"undefined"`,瑞数检测到非浏览器环境后故意卡死。必须用 sdenv(C++ 层面实现 `MarkAsUndetectable()`)或真实浏览器 |
| 4 | V8 `%GetUndetectable()` 不够 | 用 V8 内部函数获取的 undetectable 对象通过了 `typeof === "undefined"` 检测,但它是空对象,没有 `HTMLAllCollection` 的方法 | 瑞数 JSVMP 后续尝试调用该对象的方法时报错 `_$xx[_$yy[49]] is not a function`。`document.all` 不仅要通过 typeof 检测,还要有完整的集合行为 |
| 5 | 瑞数入口函数名每次不同 | 412 响应体底部的入口函数调用(如 `_$bV()`, `_$l1()`, `_$jH()`)每次请求都不同 | 正则匹配时需要用 `_\$[a-zA-Z0-9]+\(\)` 而非固定函数名 |
| 6 | Node.js v24 的 Navigator 原型有只读属性 | `Navigator.prototype` 上的 `language` 等属性已经有了 getter,用 `Object.assign` 赋值报错 | 必须用 `Object.defineProperty` 逐个覆盖 |
| 7 | sdenv 原生模块编译 | sdenv 的核心 `documentAll.node` 是 C++ 原生模块,需要 node-gyp 编译 | pnpm 安装时默认不执行 build scripts,需要手动 `pnpm approve-builds` 或 `npx node-gyp rebuild` |
| 8 | JSVMP Hook 工具对瑞数6无效 | Camoufox 的 `hook_jsvmp_interpreter` 通过 Hook `Function.prototype.apply/call` 追踪,但瑞数6使用内部函数表+直接调用,日志为空 | 不是所有 JSVMP 都能用标准 Hook 工具分析,需要根据具体实现选择分析方法 |
---
## 变体说明
| 变体 | 差异点 | 影响 |
|------|--------|------|
| 瑞数 4/5 代 vs 6 代 | 瑞数6 JS 文件约 230KB,比 4/5 代(约 200KB)更大,环境检测项更多 | 4/5 代可通过手动补环境成功,6 代手动补环境极其困难,建议直接用 sdenv |
| Cookie-only vs Cookie+URL后缀 | 大部分瑞数站点只需要 Cookie 即可访问。少数站点还需要 URL 后缀签名 | sdenv 只能生成 Cookie,不能生成 URL 后缀。需要后缀的站点建议用 JsRpc 方案 |
| HTTP vs HTTPS | 部分瑞数站点使用 HTTP,其他使用 HTTPS | sdenv 的 `jsdomFromUrl` 两种协议都支持,但 HTTPS 站点需要设置 `NODE_TLS_REJECT_UNAUTHORIZED=0`(某些政府站点的证书链不完整) |
| 静态页面 vs API 接口 | 列表页和详情页可能是纯静态 HTML(服务端渲染),用 cheerio 解析即可。其他站点可能有 JSON API 接口 | API 请求可能需要额外的签名参数(如 `sign = MD5(itemId + searchValue + timestamp)`) |
| `$_ts` 配置差异 | 不同站点的 `$_ts` 配置结构可能不同,有的只有 `nsd` 和 `cd`,其他可能还有 `cp`、`aebi` 等字段 | sdenv 方案不需要关心这些差异,因为它让瑞数 JS 自己解析配置 |
| 服务端动态更新 | 瑞数服务端可以随时更新 `basearr` 中的环境检测值 | 纯算方案会因此失效,sdenv 方案通常不受影响(除非瑞数新增了 sdenv 未模拟的检测点) |
@@ -0,0 +1,208 @@
# 案例:通用 VMP 源码级插桩(骨架模板)
> **v2.5.0 骨架案例**。本文是一个**方法论模板**,不是某个具体站点的案例。适用于:瑞数 5/6、Akamai sensor_data v2/v3、TikTok webmssdk、obfuscator.io 等所有"算法封装在字节码 dispatch 循环里"的 VMP。
>
> 使用方式:
>
> 1. 在 Phase 0.5 指纹匹配时,若检测到 VMP 特征(`find_dispatch_loops` 返回 case_count > 50),直接走本案例的「已验证定位路径」
> 2. 完成一次具体站点逆向后,复制本文件重命名为 `vmp-<具体技术特征>.md`,按真实站点数据填充 `[填入...]` 占位符,加入 cases/README.md 索引
---
## 指纹检测规则
- **JS 特征**:
- 单 JS 文件 100KB+,通常命名模式为 `sdenv-*.js` / `FuckCookie_*.js` / `webmssdk.es5.js` / `sensor_data.js` / `akam/xxx.js` / `a_bogus.js`
- `find_dispatch_loops(script_url=<该文件>)` 返回 `case_count > 50` 的候选
- 含超大字节码数组(`var X = [3,15,7,22,...]`,长度 1000+)
- 含 `while(true){switch(...)}` 或 `while(!![]){switch(...)}` 解释器循环
- **参数特征**:
- 固定长度签名参数(128/192/256 字符) + Base64 变体(有 `-_` 替换 `+/`) 或 hex
- 典型参数名:`a_bogus` / `_signature` / `X-Bogus` / `msToken` / `_m_h5_tk` / `acw_tc` / `FSSBBIl1UgzbN7N`
- **请求特征**:
- 首包返回 412(瑞数)或 403(Akamai)+ 动态 cookie 写入后通过
- 存在预热接口(如 `/challenge` / `/akam/11/*` / `/v1/gen_token`)
- Cookie 中有多个动态字段(JS 写的 + HTTP Set-Cookie 发的)
- **反调试特征**:
- debugger 陷阱(可 `bypass_debugger_trap` 绕过)
- navigator.webdriver / Function.prototype.toString 检测
- **混淆类型**:JSVMP(字节码虚拟机)
---
## 加密方案
- **算法**:[填入真实案例时填写,常见:HMAC-SHA256 / 自实现 MD5 / AES-CBC / 自定义哈希表 + 异或 / CRC32 变体]
- **密钥来源**:[填入:硬编码字节码中 / 服务端预热下发 / 浏览器指纹哈希派生]
- **加密流程**:
1. VMP 收集浏览器环境指纹(`hot_keys` 暴露的属性集合)
2. 拼接业务参数 + 时间戳 + 指纹
3. 通过 dispatch 循环内的 handler 系列完成哈希/加密
4. 输出 Base64/hex 编码的签名
- **签名公式**:[填入真实公式]
---
## 已验证定位路径(源码级插桩黄金 8 步)
```
Step 1 — launch + 抓包
launch_browser(headless=False)
start_network_capture(capture_body=True)
Step 2 — 首次导航
若是首屏挑战站点:
navigate(
url="[填入目标URL]",
pre_inject_hooks=["xhr", "fetch", "cookie", "jsvmp_probe", "runtime_probe"],
via_blank=True,
wait_until="networkidle"
)
否则:
navigate(url="[填入]", wait_until="load")
Step 3 — 定位 VMP 脚本
list_network_requests(resource_type="script")
→ 找 size 最大的 JS,记 URL 为 <VMP_URL>
find_dispatch_loops(script_url=<VMP_URL>, min_case_count=20)
→ 确认 case_count > 50
Step 4 — 装源码级插桩
instrument_jsvmp_source(
url_pattern="[填入 glob,如 **/sdenv-*.js]",
mode="ast",
tag="vmp1",
rewrite_member_access=True,
rewrite_calls=True
)
Step 5 — 装兜底 hook(若 Step 2 没走 pre_inject_hooks)
inject_hook_preset("cookie", persistent=True)
inject_hook_preset("xhr", persistent=True)
inject_hook_preset("fetch", persistent=True)
inject_hook_preset("crypto", persistent=True)
hook_jsvmp_interpreter(script_url="[VMP basename]")
bypass_debugger_trap()
Step 6 — reload_with_hooks 让探针先于 VMP 生效
reload_with_hooks(clear_log=True)
Step 7 — 触发业务操作
[填入具体操作]
Step 8 — 读日志
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_get", limit=200)
→ 记录 hot_keys top 30 的环境属性
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_method", limit=200)
→ 记录 hot_methods 是否含 CryptoJS.* / SubtleCrypto.*
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_call", limit=200)
→ 记录 hot_functions 里的高频自定义函数名
analyze_cookie_sources()
→ 对每个目标 cookie 确认 sources
清理:
stop_instrumentation(url_pattern="[glob]")
remove_hooks()
```
---
## 还原代码(按 hot_methods 分支)
### 分支 A:hot_methods 包含标准加密 API → 纯算法还原
```javascript
// Node.js 示例
const crypto = require('crypto');
function genSign(params, ts, fingerprint) {
const payload = [
// [填入实际字段顺序]
params.url,
params.body || '',
ts,
fingerprint,
].join('|');
return crypto
.createHmac('sha256', '[填入密钥]')
.update(payload)
.digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
```
```python
# Python 等价实现
import hmac, hashlib, base64
def gen_sign(params, ts, fingerprint):
payload = '|'.join([
params['url'], params.get('body', ''),
str(ts), fingerprint
])
raw = hmac.new(b'[填入密钥]', payload.encode(), hashlib.sha256).digest()
return base64.urlsafe_b64encode(raw).rstrip(b'=').decode()
```
### 分支 B:hot_methods 全自定义 + hot_keys 少 → vm/execjs 沙箱执行提取 JS
```javascript
// Node.js + vm
const vm = require('vm');
const fs = require('fs');
const sandboxCode = fs.readFileSync('./config/sign_extracted.js', 'utf8');
const sandbox = {
// 最小环境:把 hot_keys 里出现的全部属性放进来
navigator: { userAgent: '...', platform: '...', language: 'zh-CN' },
// ... 其他 hot_keys 暴露的属性
};
vm.createContext(sandbox);
vm.runInContext(sandboxCode, sandbox);
function genSign(input) {
return sandbox.__export_sign(input);
}
```
### 分支 C:hot_keys 多(30+) + cookie 来自 HTTP Set-Cookie → 路径 B jsdom 环境伪装
见 `references/jsdom-env-patches.md` + `cases/jsvmp-xhr-interceptor-env-emulation.md`。
将本案例中采集到的 `hot_keys` 清单作为 "要对齐的环境属性集"——省去 58 项盲目对齐。
---
## 踩坑记录
- **坑 1:instrument_jsvmp_source 必须在 navigate/reload 之前调用**——route 注册晚了就抓不到 VMP 脚本加载
- **坑 2:AST 模式需要 cdnjs 访问**——代理环境下要保证 `cdnjs.cloudflare.com` 可访问,否则降级到 `mode="regex"`
- **坑 3:reload_with_hooks 只刷当前页,不改变 URL**——首屏挑战场景要用 `navigate(pre_inject_hooks=[...])` 而不是 reload_with_hooks
- **坑 4:hot_keys 里 "[key of Symbol()]"**——这是 Symbol 键被 preview 化的表现,不是真实属性名,可以忽略
- **坑 5:同一个 VMP 被多次加载(HMR / 页面跳转)** → 确保 `cache_rewritten=True`,否则每次重写开销巨大
- **坑 6:VMP 通过 eval / new Function 动态生成子 VMP** → 源码级插桩看不到动态生成的部分,需要额外 `hook_function(Function, ...)` 截获
- **坑 7:Proxy(jsvmp_hook)与源码级插桩叠加时页面崩溃** → 尝试 `hook_jsvmp_interpreter(track_props=False)` 关闭 Proxy,只留 apply/Reflect.*
---
## 变体说明
| 变体 | 差异点 | 调整策略 |
|------|-------|---------|
| 瑞数 5 | 固定 `sdenv-*.js` 命名 | url_pattern 用 `**/sdenv-*.js` |
| 瑞数 6 | 脚本名每次不同 + 多层 VMP | 先 `list_network_requests` 找所有 100KB+ JS,逐个 instrument(tag 区分) |
| Akamai sensor_data | `_abck` / `bm_sz` cookie | 重点看 `hot_keys` 中 touch/mouse 事件相关属性 |
| webmssdk(TikTok) | 配合 msToken 预热 | 需先让 `/v1/generate_token` 预热接口完成再触发业务 |
| obfuscator.io(开源) | case 数可能只有 20-30 | `find_dispatch_loops(min_case_count=15)` |
---
## 指纹匹配时的权重建议
作为 Phase 0.5 指纹匹配的备选路径,本案例应命中以下任一条件即触发"尝试源码级插桩":
- **高权重(直接走)**:`find_dispatch_loops` 返回 case_count > 50
- **中权重(优先试)**:单 JS > 200KB + 含 while-switch + 参数长度 128/192/256 + Base64 变体
- **低权重(参考)**:Cookie 中有 acw_tc / FSSBBIl1 / _abck / ak_bmsc / ttwid / msToken 等已知模式字段
+124 -3
View File
@@ -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
View File
@@ -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+ 次请求稳定)
+378
View File
@@ -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
View File
@@ -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 重注入/运行时探针 |
+5
View File
@@ -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` | 快速摸底页面在读什么 / 调什么(低开销) |
+56 -2
View File
@@ -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 环境伪装补丁库