16 KiB
16 KiB
路径 A:算法追踪 — 四板斧完整方法论
触发条件:Phase 2 识别出反爬为签名型/行为型且确认用路径 A 时读此文档
前置要求:已完成反爬类型三分法识别(见 SKILL.md「反爬类型识别与工具选择」段)
版本:v3.4.1(MCP v1.1.1,增加 AST 插桩执行健康门禁)
一、四板斧总览
路径 A 的核心方法论是「从 I/O 两端夹逼 + 中间层插桩 + 源码级全量 tap」。
四板斧的关系:
- 第一 / 二 / 三板斧:诊断"VM 看外界"(入口)和"外界看 VM"(出口)——适合签名通过 CryptoJS/atob/MD5 等可 hook 原语走的 VMP
- 第四板斧:诊断"VM 看自己"——适合 VMP 算法全部封装在字节码分发循环
switch(opcode) { case N: obj[key](args); ... }里的场景
四板斧概览表
| 板斧 | 名称 | 工具 | 擅长 | 不擅长 | 适用反爬类型 |
|---|---|---|---|---|---|
| 第一斧 | Hook I/O | inject_hook_preset(xhr/fetch/crypto/cookie) + hook_function('X.prototype.Y', mode='intercept', ...) |
请求链路劫持、动态 Cookie、加密原语入口 | VM 内部自实现的 MD5/AES | 行为型 ✅ / 纯混淆 ✅ / 签名型 ❌ |
| 第二斧 | 插桩解释器 | hook_function(path, mode='trace', ...) + hook_jsvmp_interpreter(mode='proxy', trackProps=True) |
能识别分发函数名时的调用链追踪 | 匿名 IIFE 包裹 + 高频日志爆炸 | 行为型 ✅ / 纯混淆 ✅ / 签名型 ❌ |
| 第三斧 | 日志分析 | get_jsvmp_log + 反向追踪 |
已能捕获签名值 I/O 时反推公式 | 签名完全不出 VM 的黑箱模式 | 所有类型 ✅(纯被动分析,无副作用) |
| 第四斧 | 源码级插桩 | instrumentation(action='install', ...) + instrumentation(action='log', ...) |
VM 内部调度、"全部在 opcode dispatch 循环里发生"的场景,hot_keys 直接暴露环境指纹集 |
极限大文件(5MB+)开销高;改写后必须验证 runtime 已执行 | 所有类型 ✅,签名型首选 |
按反爬类型的适用性
| 反爬类型 | 可用板斧 | 说明 |
|---|---|---|
| 签名型(RS / Akamai / Shape) | 仅第四斧 | 前三板斧会改变环境,破坏签名。源码级插桩不动环境,是唯一通用解 |
| 行为型(短视频平台 / JY) | 全部四斧 | 不校验浏览器原生性,所有工具可用 |
| 纯混淆(obfuscator.io / 自研 VMP) | 全部四斧 | 只是代码难读,不检测观察者 |
二、快速路径(推荐优先试)
快速路径足以解决 70%+ 的 RS/Akamai/webmssdk 场景。无法解决时再走手动四板斧流程。
8 步快速流程
步骤 1:确认是否 VMP
search_code(keyword='switch', script_url='<VMP脚本URL>', context_chars=500)
<!-- v3.1.0: migrated from find_dispatch_loops -->
→ case_count > 50 基本确认是 VMP
步骤 2:一键装通用探针
hook_jsvmp_interpreter(script_url=<VMP basename>)
步骤 3:装出口 Hook
inject_hook_preset("cookie") + inject_hook_preset("xhr", persistent=True)
步骤 4:装源码级插桩(核心)
instrumentation(action='install', url_pattern="**/<VMP 文件>", mode="ast", tag="vmp1")
<!-- v3.1.0: migrated from instrument_jsvmp_source -->
(AST 在 MCP 侧运行,不依赖页面联网)
步骤 5:让所有探针先于 VMP 生效
instrumentation(action='reload')
<!-- v3.1.0: migrated from reload_with_hooks -->
→ 清日志,获得干净快照
步骤 6:触发目标操作
evaluate_js / click / type_text → 翻页、搜索、登录等
步骤 7:读 hot_keys(30 秒定位环境指纹集)
instrumentation(action='log', tag_filter='vmp1', type_filter='tap_get', limit=300)
<!-- v3.1.0: migrated from get_instrumentation_log -->
→ 看 hot_keys / hot_methods / hot_functions 三个摘要
步骤 8:交叉印证
get_jsvmp_log() + analyze_cookie_sources()
三、第一板斧:Hook I/O(确定 I/O 边界)
目标
确定 JSVMP 的输入(读了什么环境值、接收了什么参数)和输出(生成了什么签名、写了什么 Cookie)。
详细步骤
步骤 0:一键装多路径探针(快速路径,推荐先试)
MCP 操作:
hook_jsvmp_interpreter(script_url=<VMP basename>)
→ 自动覆盖 apply/call/bind + Reflect.*/Proxy 全局对象 + timing/random
步骤 1:Hook 出口 — 请求与 Cookie
MCP 操作:
inject_hook_preset("xhr", persistent=True) → XHR 请求出口
inject_hook_preset("fetch", persistent=True) → fetch 请求出口
inject_hook_preset("cookie", persistent=True) → 原型链级 cookie hook
hook_function('XMLHttpRequest.prototype.open', mode='intercept', non_overridable=True)
<!-- v3.1.0: migrated from freeze_prototype -->
→ 冻结 XHR.open 防止页面 JS 覆盖 Hook
analyze_cookie_sources()
→ 辨识每个 Cookie 是 HTTP Set-Cookie / JS document.cookie / 混合写入
步骤 2:Hook 入口 — 加密原语
MCP 操作:
inject_hook_preset("crypto")
→ 自动捕获 btoa/atob/JSON.stringify I/O
hook_function(
function_path="String.fromCharCode",
hook_code="console.log('[MCP] fromCharCode:', JSON.stringify([...arguments]))",
position="before"
)
→ 捕获字符编码操作(JSVMP 高频信号)
步骤 3:关联出入口数据
分析逻辑:
- 出口捕获到的签名值(如 sign=abc123)
- 入口捕获到的加密原语调用(如 MD5("timestamp+key"))
- 关联两者 → 推断签名公式
四、第二板斧:插桩解释器(追踪执行链路)
目标
在 JSVMP 解释器层面追踪执行链路,定位字节码分发函数和关键调用。
详细步骤
步骤 3:定位字节码分发函数
MCP 操作:
search_code(keyword='switch', script_url='<VMP脚本URL>', context_chars=500)
<!-- v3.1.0: migrated from find_dispatch_loops -->
→ 定位 case_count > 20 的 switch 语句
→ case_count > 50 基本确认是 VMP 解释器
步骤 4:分层追踪函数调用
MCP 操作(粗粒度 → 中粒度 → 细粒度):
# 粗粒度:追踪解释器主函数
hook_function(
function_path="<解释器主函数>",
mode='trace',
log_args=True, log_return=True, log_stack=False,
max_captures=100
)
<!-- v3.1.0: migrated from trace_function -->
# 中粒度:追踪子 handler
hook_function(
function_path="<子handler函数>",
mode='trace',
log_args=True, log_return=True, log_stack=True,
max_captures=50
)
<!-- v3.1.0: migrated from trace_function -->
# 细粒度:追踪特定加密函数
hook_function(
function_path="<加密函数路径>",
mode='trace',
log_args=True, log_return=True, log_stack=True,
max_captures=30
)
<!-- v3.1.0: migrated from trace_function -->
⚠️ 必须设置 max_captures 限制日志量,高频调用函数(每秒数千次)会爆炸
步骤 5:监控签名容器 + 采集环境基准
MCP 操作:
hook_jsvmp_interpreter(mode='proxy', trackProps=True)
<!-- v3.1.0: migrated from trace_property_access -->
→ 监控 navigator.*/screen.*/document.cookie 等签名容器的属性读取
compare_env()
→ 采集浏览器环境基准数据(navigator/screen/canvas/WebGL/Audio/timing)
五、第三板斧:日志分析(从海量数据提取签名链路)
目标
从多维度日志中提取签名生成的完整链路。
详细步骤
步骤 6:多维度日志采集
MCP 操作:
get_trace_data() → 函数追踪数据
get_jsvmp_log() → JSVMP 探针日志
get_console_logs() → 控制台输出
get_runtime_probe_log() → 运行时探针日志
步骤 7:反向追踪法
分析方法:
1. 从已知签名值(如 sign=abc123)出发
2. 在所有日志中搜索该值首次出现的位置
3. 从该位置反向追踪:
- 该值由哪个函数生成?
- 该函数的输入是什么?
- 输入又来自哪里?
4. 逐层追踪直到找到原始明文输入
这是处理海量日志数据效率最高的方法。
步骤 8:验证提取的算法
MCP 操作:
evaluate_js("提取的签名函数(已知输入)")
→ 对比输出与实际请求中的签名值
→ 一致则算法提取成功
六、第四板斧:源码级插桩(通用 VMP 利器)
v2.5.0 新增,签名型反爬的唯一通用解法
目标
在 HTTP 层改写 VMP 源码,对每个 obj[key] 读取和 fn(args) 调用插入 tap,不改变运行时环境。
为什么需要第四板斧
前三板斧(Hook/插桩/日志)在以下场景失效:
- RS 5/6:VM 完全自包含,不路由到可 hook API
- Akamai sensor_data v2/v3:算法全部在 opcode dispatch 循环内
- webmssdk:签名逻辑封装在字节码中
- 这些场景下
hook_jsvmp_interpreter仍然看不到 switch/case 内部的opcode_table[code]调度
详细步骤
步骤 9:安装源码级插桩
MCP 操作:
instrumentation(
action='install',
url_pattern="**/<VMP 文件>",
mode="ast",
tag="vmp1",
rewrite_member_access=True,
rewrite_calls=True
)
<!-- v3.1.0: migrated from instrument_jsvmp_source -->
模式选择:
- mode="ast"(默认推荐):MCP 侧 esprima 解析,挑战页可用
- mode="regex":轻量正则改写,只覆盖 bracket member access
- 原始源码 AST parse 失败时自动 fallback 到 regex(fallback_on_error=True)
步骤 10:让插桩先于 VMP 生效
MCP 操作:
instrumentation(action='reload')
<!-- v3.1.0: migrated from reload_with_hooks -->
→ 清空 __mcp_vmp_log / __mcp_jsvmp_log / __mcp_cookie_log
→ 获得干净的一次执行捕获
⚠️ 签名型反爬:不要清 cookie!第一次挑战拿到的 cookie 要留下
步骤 10.5:验证改写产物实际执行
MCP 操作:
instrumentation(action='status')
evaluate_js(expression="(() => ({
tapInstalled: window.__mcp_tap_installed === true,
logReady: Array.isArray(window.__mcp_vmp_log)
}))()")
判定:
- files_rewritten > 0 且 tapInstalled=false
→ 改写文本已下发,但浏览器未执行;优先怀疑嵌套 AST 节点改写损坏
- tapInstalled=true 且日志为空
→ runtime 已执行;继续检查触发动作和 tag_filter
步骤 11:读取插桩日志(指纹学习的金矿)
MCP 操作:
# 读取属性访问 hot_keys
instrumentation(action='log', tag_filter='vmp1', type_filter='tap_get', limit=200)
<!-- v3.1.0: migrated from get_instrumentation_log -->
→ summary.hot_keys 告诉你 VMP 读取了哪些属性,按频次倒排
→ 典型 RS 输出:{"userAgent":120, "plugins":98, "webdriver":77, "cookie":43, ...}
→ 这就是 VMP 参与签名哈希的完整环境指纹集
# 读取方法调用 hot_methods
instrumentation(action='log', tag_filter='vmp1', type_filter='tap_method', limit=200)
<!-- v3.1.0: migrated from get_instrumentation_log -->
→ summary.hot_methods 格式 ObjectType.methodName
→ 能否看到 MD5/AES/HMAC 就是算法是否用标准加密的核心判据
# 读取函数调用 hot_functions
instrumentation(action='log', tag_filter='vmp1', type_filter='tap_call', limit=200)
<!-- v3.1.0: migrated from get_instrumentation_log -->
→ 看有没有 btoa/atob/encodeURIComponent 等熟识函数
步骤 12:完工清理
MCP 操作:
instrumentation(action='stop', url_pattern="**/<VMP 文件>")
<!-- v3.1.0: migrated from stop_instrumentation -->
→ 关闭源码级 route
七、路径 A 失败模式与降级
常见失败模式
| 失败表现 | 可能原因 | 应对 |
|---|---|---|
files_rewritten > 0 但 __mcp_tap_installed=false |
改写产物未解析/未执行,常见于嵌套 call/member/new 链 | 停止 AST route,按 regex → transparent 降级 |
runtime 已安装但 instrumentation(action='log') 返回空 |
VMP 未触发 / tag_filter 不匹配 | 重新触发业务动作并检查 tag_filter |
hot_keys 只有 < 5 个属性 |
regex 模式覆盖率不足 | 切换 mode="ast" 或增大 context_chars |
| 签名值始终不一致 | 环境指纹参与哈希但未补齐 | 根据 hot_keys 逐项补齐环境 |
navigate 反复 412 |
观察者效应——Hook 破坏了签名 | 移除所有 pre_inject_hooks,改用源码级插桩 |
| AST 模式持续 fallback 到 regex | esprima 无法解析目标语法 | 接受 regex 80% 覆盖率,或退到 transparent 模式 |
降级梯度(必须逐级走)
L1: instrumentation(action='install', mode="ast")
→ status + runtime 标记健康检查失败
L2: instrumentation(action='install', mode="regex")
→ 覆盖率不足
L3: hook_jsvmp_interpreter(mode="transparent")
→ 日志太少
L4: hook_jsvmp_interpreter(mode="proxy") ← 仅行为型/纯混淆可用!
→ 破坏签名
L5: 路径 B(jsdom 环境伪装)
→ 也失败
L6: 向用户说明情况,建议浏览器自动化或 sdenv
关键规则:
- L1→L2→L3 是标准降级路径,每级必须尝试
- L3→L4 仅对行为型反爬开放,签名型必须走 L3→L5
- 到达 L6 前必须在当前 case 记录完整降级路径
- 禁止从 L1 直接跳到 L6
八、路径 A 还原策略选择
四板斧完成后,根据收集到的信息选择还原策略:
| 情况 | 策略 | 实现方式 |
|---|---|---|
签名使用标准算法(MD5/HMAC/AES),get_jsvmp_log 能看到对应 API 调用 |
纯算法还原 | Node.js crypto / Python hashlib + pycryptodome |
| 签名逻辑是标准算法但拼接规则复杂 | 还原拼接逻辑 + 标准算法 | 提取拼接顺序和格式,手动实现 |
签名逻辑完全定制化,但 hot_keys 清晰暴露输入域 |
提取最小 JS 片段执行 | Node.js vm 沙箱 / Python execjs |
VM 劫持了整个请求链路,analyze_cookie_sources 显示 cookie 来自 HTTP Set-Cookie |
纯算法还原不现实 | 转路径 B:jsdom 环境伪装 |
VM 算法全部内联在 dispatch 循环,即便源码插桩 hot_keys 也无法还原 |
加载完整 VM + 最小环境 | 转路径 B:优先 jsdom 运行原始脚本 |
后续还原路径决策
根据 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 环境伪装(见 SKILL.md 路径 B 段或 references/jsdom-env-patches.md)
九、JSVMP 核心经验(路径 A 专项)
- VM 解释器本身不是目标,签名函数的 I/O 才是目标 — 不要试图反编译字节码
- 先 Hook 出口确定"要什么",再 Hook 入口确定"给了什么" — 出口驱动分析
hook_function(path, mode='trace', ...)对高频调用函数日志量可能爆炸 — 必须设置max_captures限制get_trace_data返回的海量数据需要本地过滤 — 用反向追踪法效率最高instrumentation(action='install', mode="regex")对带模板字符串、正则字面量的代码可能误改写 — AST 具备语法感知,但仍必须验证改写产物已执行- 前三板斧对签名型反爬不可用 — Hook
Function.prototype.apply会改变 toString 原生性;Proxy 在 navigator 上会被检测 dump_jsvmp_strings前提是字符串未被动态解密 — 看到decoded_strings全是单字母乱码就是加密的search_code(keyword='while')在超大文件(380KB+)会返回大量无关结果 — 应使用search_code(keyword, script_url=url)配合更精确关键词- 源码级插桩会在 MCP 内部按 URL 缓存改写结果 — 当前统一接口不需要传缓存参数
完整工作流参考:SKILL.md「Phase 2.2+ JSVMP 专项分析」段 源码级插桩专项指南:references/jsvmp-source-instrumentation.md 经验案例:cases/universal-vmp-source-instrumentation.md