diff --git a/README.md b/README.md index 9249e25..283948d 100644 --- a/README.md +++ b/README.md @@ -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 归因分析) diff --git a/SKILL.md b/SKILL.md index f5bfc8d..6fcdf03 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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=)` 一键装通用探针 +> 3. `inject_hook_preset("cookie")` + `inject_hook_preset("xhr", persistent=True)` +> 4. `instrument_jsvmp_source(url_pattern="**/", 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=) → 一键装多路径探针(快速路径,推荐先试) + 步骤 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="**/", 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="**/") → 完工后移除 ``` -**路径 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` | diff --git a/cases/README.md b/cases/README.md index 6952d24..07da741 100644 --- a/cases/README.md +++ b/cases/README.md @@ -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 归因(**骨架模板**,由使用者按实际站点填充) | ## 私有映射 diff --git a/cases/jsvmp-ruishu6-cookie-412-sdenv.md b/cases/jsvmp-ruishu6-cookie-412-sdenv.md new file mode 100644 index 0000000..b6e9e7c --- /dev/null +++ b/cases/jsvmp-ruishu6-cookie-412-sdenv.md @@ -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] 页面底部存在 `` 格式的入口函数调用,函数名每次不同 +- [x] HTML 中包含 `` 标签,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 结构为: + 标签(动态 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 未模拟的检测点) | diff --git a/cases/universal-vmp-source-instrumentation.md b/cases/universal-vmp-source-instrumentation.md new file mode 100644 index 0000000..66a1369 --- /dev/null +++ b/cases/universal-vmp-source-instrumentation.md @@ -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 为 + find_dispatch_loops(script_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 等已知模式字段 \ No newline at end of file diff --git a/references/hook-techniques.md b/references/hook-techniques.md index 3c21b42..8f9b322 100644 --- a/references/hook-techniques.md +++ b/references/hook-techniques.md @@ -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 的计数 diff --git a/references/jsvmp-analysis.md b/references/jsvmp-analysis.md index cd73ebf..531c754 100644 --- a/references/jsvmp-analysis.md +++ b/references/jsvmp-analysis.md @@ -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=) 装多路径探针 +Step 3 — inject_hook_preset("cookie", persistent=True) + inject_hook_preset("xhr", persistent=True) +Step 4 — instrument_jsvmp_source(url_pattern="**/", 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=)` 多路径探针(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+ 次请求稳定) diff --git a/references/jsvmp-source-instrumentation.md b/references/jsvmp-source-instrumentation.md new file mode 100644 index 0000000..920bd0a --- /dev/null +++ b/references/jsvmp-source-instrumentation.md @@ -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"`(默认,零依赖) + +- **原理**:纯正则匹配 `[]`,替换为 `__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,下面称为 +``` + +### Step 3 — 确认是 VMP(而不是普通混淆) + +``` +Actions: + find_dispatch_loops(script_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="", # 多路径运行时探针 + 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+) \ No newline at end of file diff --git a/references/mcp-cookbook.md b/references/mcp-cookbook.md index a922e17..1314450 100644 --- a/references/mcp-cookbook.md +++ b/references/mcp-cookbook.md @@ -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 重注入/运行时探针 | diff --git a/references/troubleshooting.md b/references/troubleshooting.md index 271dded..e32c1ac 100644 --- a/references/troubleshooting.md +++ b/references/troubleshooting.md @@ -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` | 快速摸底页面在读什么 / 调什么(低开销) | diff --git a/references/workflow-overview.md b/references/workflow-overview.md index 9282679..a849a47 100644 --- a/references/workflow-overview.md +++ b/references/workflow-overview.md @@ -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(case_count > 50) + ↓ +4. 同时装三路探针: + ├─ 第一板斧(I/O 边界):inject_hook_preset(cookie / xhr / fetch / crypto, persistent=True) + ├─ 第二板斧(解释器):hook_jsvmp_interpreter(script_url=) + └─ 第四板斧(源码级插桩,核心):instrument_jsvmp_source( + url_pattern="**/", + 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 环境伪装补丁库