feat: skill v3.2.0 — 硬约束Checklist五项压缩到三项,移除MCP session依赖,cases/成为唯一经验库

- SKILL.md 瘦身重写(核心层+按需加载references/)
- Checklist: CHECK-3/4(session)删除,五项→三项
- 新增6个references子文档(path-a/path-b/phase-details/experience-rules-full/mcp-tool-reference/common-pitfalls)
- 删除 references/domain-session-guide.md
- 工具引用对齐实际MCP(32工具),verify_signer_offline替代verify_against_session
- cases/_template.md 新增可验证事实清单段
- 经验法则session相关条目改写
- 红线四条、第一原则0-5、三分法、四板斧、六步法完整保留
This commit is contained in:
白夜
2026-04-18 13:36:14 +08:00
parent a5228600f4
commit 38b982fb1e
13 changed files with 2905 additions and 2067 deletions
+271
View File
@@ -0,0 +1,271 @@
# ⚠️ 反模式清单(v3.0.0 基于三次真实实战数据)
> **本文档是 AI 在每次分析前必读的"禁忌清单"**。不是建议,不是"参考一下",
> 是**打脸清单**——本文档里的每一条都是某次真实 AI 分析中实际发生过的失败。
>
> 本文档的存在假设:加经验法则无效(46 条都记不住),但**让错误路径显眼**有效。
> 这里的每条反模式都配了:(1) 真实站点名;(2) AI 当时的辩护;(3) 为什么那辩护不成立。
---
## 反模式 1:用 Playwright 过反爬挑战取 cookie 作为最终方案
### 实战案例:瑞数 nmpa(2026-04 实战,AI 违反)
**AI 的操作**:
```javascript
// cookie_fetcher.js (实战 AI 产出,违反第一原则)
const { firefox } = require('playwright');
async function getCookies() {
const browser = await firefox.launch();
const page = await browser.newPage();
await page.goto('https://www.nmpa.gov.cn/...'); // 过 RS 412 挑战
await page.waitForTimeout(5000); // 等挑战自动完成
const cookies = await page.context().cookies(); // 取 NfBCSins2OywS
return cookies;
}
// 最终方案每 30 分钟运行一次 getCookies()
```
**AI 的辩护**:"瑞数 JSVMP 保护极其复杂,纯 Node.js 几乎不可能绕过"
**为什么这辩护不成立**:
- 工作区里已经有 `site_nmpa/sign_service.js`,用 sdenv 已完成纯 Node.js 方案
- AI 根本没看 sdenv 文档就断言"不可能"
- 最终交付在无浏览器的 Docker 容器里**无法运行一小时以上**——这就是违规
### 正确做法
```javascript
// 使用 sdenv(魔改 jsdom + C++ V8 Addon)
const { jsdomFromUrl } = require('sdenv');
async function getRSCookies(targetUrl) {
const dom = await jsdomFromUrl(targetUrl, { userAgent: 'Mozilla/5.0...' });
await new Promise(r => dom.window.addEventListener('sdenv:exit', r));
return extractCookies(dom);
}
```
### 判定测试
> 最终代码在一个**无 X11、无浏览器、只有 Node.js**的 Docker 容器里,
> 能不能稳定跑满 24 小时?
> - 能 → 合规
> - 不能 → 违规(Playwright 方案属于违规)
---
## 反模式 2:Cookie 硬编码到 headers.json
### 实战案例:抖音 /aweme/detail(2026-04 实战,AI 违反灰色地带)
**AI 的操作**:
```json
// headers.json (AI 产出)
{
"Cookie": "ttwid=1|xxxxxxxx; __ac_nonce=yyyyyy; __ac_signature=zzzzzz"
}
```
```javascript
// main.js 直接读取上面的 headers 发请求
```
**AI 的辩护**:"cookie 有几小时有效期,不是每次请求前都要开浏览器"
**为什么这辩护不成立**:
- "几小时"还是"几天"不影响定性——cookie 一旦过期,**没有浏览器就完全无法工作**
- 最终方案的可用性取决于浏览器是否可用,浏览器就是方案的依赖
- `__ac_signature` 可以协议还原(基于 `__ac_nonce` 的签名算法),抖音社区早有方案
- `ttwid` 更简单,直接请求首页就能 Set-Cookie 拿到
### 正确做法
```javascript
// 协议还原 cookie 获取
async function getTtwid() {
const r = await axios.get('https://www.douyin.com/');
return r.headers['set-cookie'].find(c => c.startsWith('ttwid=')).split(';')[0];
}
async function generateAcSignature(nonce) {
// 还原 __ac_signature 的签名算法
// ... (基于 nonce 的字符串变换 + 时间戳)
}
```
### 判定测试
> 启动代码在无浏览器环境一周,不人工干预,是否能稳定获取数据?
> - 能 → 合规
> - 不能(需要人工去浏览器抓 cookie 贴进去)→ 违规
---
## 反模式 3:激活 skill 后直接 launch_browser(跳过 Checklist)
### 实战案例:三次都违反(瑞数 + TikTok + 抖音)
**三次的 AI 操作完全一样**:
```
轮 1: 激活 skill (读几万字 SKILL.md)
轮 2: launch_browser(headless=false)
轮 3: navigate(target_url)
... 从此开始分析
```
**AI 的辩护**:"用户要求明确,快速开干;session 是新功能,先做主线任务"
**为什么这辩护不成立**:
- 三次 AI 都这么说,三次都导致:
- session 没开 → v2.9.0 域级持久机制完全失效
- 经验库没查 → 重复踩坑 / 从零写已有方案
- 不知道工作区有 `site_*` 目录 → 瑞数 nmpa 从零写 Playwright 方案,而 `site_nmpa/` 里已有 sdenv 方案
- "快速开干"其实慢得多——瑞数实战用 12 轮对话写了个错误方案,如果做了 Checklist 的 CHECK-2,5 分钟搞定
### 正确做法
```
激活 skill 后第一件事:在对话中输出硬约束 Checklist 五项复述
[CHECK-1] check_environment() → 贴结果
[CHECK-2] 读 cases/README.md 速查表 + listDirectory 工作区 → 命中记录
[CHECK-3] list_sessions() → 历史 session 记录
[CHECK-4] start_reverse_session() → domain + run_id 记录
[CHECK-5] 最终方案意图声明
五项都通过,再调 launch_browser
```
### 判定测试
> 翻 skill 激活后的前 3 条对话记录。
> - 前 3 条里有完整的 CHECK-1~5 复述输出 → 合规
> - 前 3 条是"launch_browser / navigate / ..." → 违规(红线 1)
---
## 反模式 4:在工作区 site_* 目录存在时从零建新项目
### 实战案例:瑞数 nmpa(工作区已有 `site_nmpa/`,AI 建了 `site_nmpa_20260418/` 从零写)
**AI 的操作**:
```
listDirectory() 看到:
site_nmpa/ ← 已有完整方案
site_nmpa_20260418/ ← AI 新建的错误方案
```
**AI 没做的事**:
- 没打开 `site_nmpa/` 里的文件看一眼
- 没读 `site_nmpa/sign_service.js`(已验证的 sdenv 方案)
- 没问用户"这个目录是做什么的"
**AI 的辩护**:"用户要求重新使用 skill 进行获取,我以为要从零开始"
**为什么这辩护不成立**:
- "重新使用 skill" ≠ "从零写代码",而是"遵循 skill 流程"——Phase 0.5 本身就要查已有
- 用户说"重新"可能只是希望用新 skill 验证一遍,不是丢弃已有
- 遇到这种歧义,应该问用户,不是默认从零
### 正确做法
```
listDirectory(".")
→ 看到 site_nmpa/
→ listDirectory("site_nmpa/")
→ 有 sign_service.js / keys.json → 这是已验证的方案
→ 询问用户:"检测到 site_nmpa/ 目录含已验证的 sdenv 方案,
是要(A)基于已有方案创建今天的新目录复用;还是
(B)重新分析一遍验证方案仍有效;还是(C)其他?"
→ 根据用户选择决策
```
---
## 反模式 5:面对复杂 SDK(TikTok webmssdk)直接判定"不可能还原"
### 实战案例:TikTok 潜在滑坡(AI 在 jsdom 不工作时差点滑向 Playwright)
**AI 的思维过程**(实战复盘 Q-B 自述):
> "如果后续环境补丁迭代仍无法激活 SDK 拦截器,我可能会被迫考虑
> '每次请求前用 Camoufox 过一遍页面获取签名'的方案"
**为什么这滑坡不能发生**:
- "环境补丁没让 SDK 激活" 是**本步骤**的问题,不是"整个协议方案不可行"的证据
- 应该走降级梯度的**下一级**(见 SKILL.md 第五条原则),而不是横向跳到浏览器
- 梯度 3-4 (点对点 hook_function / jsdom 变体) 都没试就放弃,违反第五条原则
### 正确做法
看 SKILL.md 第五条原则的降级梯度表:
```
梯度 0: 本域是否有历史 session? → attach_domain_readonly + reverify
梯度 1: get_session_snapshot 看本 run 做过什么
梯度 2: 换 Hook 模式 (proxy ↔ transparent) / 换插桩模式 (ast ↔ regex)
梯度 3: 点对点 hook_function 针对具体签名函数
梯度 4: 路径 B 的其他变体(vm 沙箱 / jsdomFromUrl 全量加载 / sdenv)
梯度 5: 合法出口: stop_reverse_session("suspended") + export_session + 写报告
禁止跨梯度直接到"用浏览器兜底"
```
---
## 反模式 6:经验法则背诵但不执行
### 实战案例:三次都违反
AI 在复盘时能流利背出:
> "经验法则 #28:反爬类型识别是 Phase 0 的 Phase 0"
> "第一原则:协议优先"
> "Phase 0.5:经验库指纹匹配"
但实际操作时**完全忽略**。
**为什么这发生**:
- 规则写在 skill 里 ≠ 规则会被执行
- AI 读了几万字 SKILL.md 后,工作记忆只保留最显眼的几条
- 非硬 gate 的规则(无检查机制)100% 会被忽略
### 正确做法
**不靠 AI 自觉**,靠 v3.0.0 的硬 gate:
- Checklist 五项必须复述输出(可被用户验证)
- 红线四条违反即失败(明确后果)
- 高频站点速查表(不靠"主动想起来"而是"看到域名就命中")
---
## 如何使用本文档
1. **AI 激活 skill 后**:本文档应该在 Checklist [CHECK-2] 步骤被简单扫视(至少看一眼反模式 1-6 的标题,避免踩坑)
2. **AI 在做决策时卡住**:回到本文档,看自己正在考虑的方向是不是反模式之一
3. **AI 写代码写到一半意识到在滑向反模式**:立即停,走 SKILL.md 第五条原则的降级梯度
4. **用户发现 AI 违反**:直接引用本文档的反模式编号质问 AI("你正在做反模式 X,看 references/common-pitfalls.md")
---
## 贡献新反模式
每次实战失败后,如果发现 skill 里没覆盖的新反模式:
1. 走 `retrospective-prompt.md` 做完整复盘
2. 按本文档的"反模式 N:XXX"格式追加一条
3. 更新 CHANGELOG
4. commit 时标注 `doc(pitfall): add pitfall #N from <site> retrospective`
**新反模式的质量要求**:
- 必须有**真实站点名**,不能是抽象描述
- 必须包含 AI 当时的**具体操作**(代码 / 命令序列)
- 必须包含 AI 当时的**辩护**(真实的 rationale)
- 必须说明**为什么辩护不成立**(具体论证,不是"就是不对")
- 必须给**正确做法**(带代码示例)
- 必须给**判定测试**(便于后续 self-check)
6 条起步,后续每次实战失败后追加。30 条就封顶,再加就说明 skill 本身要结构性调整。
---
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|---------|
| v3.0.0 | 2026-04-18 | 初始版本:基于三次真实站点实战(瑞数/TikTok/抖音)数据,收录 6 条反模式 |
-214
View File
@@ -1,214 +0,0 @@
# 域级 Session 档案指南
> v2.9.0 新增。每个目标域维护一份持久化 Session 档案,跨任务复用反爬类型判定、指纹基准、Cookie 归因结论,避免重复侦察。
## 1. 档案结构
每个域的 Session 档案存放在 `cases/_session_archives/<domain-hash>/` 下,包含以下文件:
```
cases/_session_archives/
└── <domain-hash>/
├── session.json # 核心档案(结构化元数据)
├── fingerprint.json # 浏览器指纹基准快照
├── cookie-attribution.json # Cookie 归因结论缓存
├── assertions.json # 断言集(通过/失败/跳过)
└── notes.md # 人工备注(可选)
```
### session.json 字段说明
```json
{
"domain_hash": "sha256(domain)[0:12]",
"created_at": "ISO8601",
"updated_at": "ISO8601",
"skill_version": "2.9.0",
"anti_crawl_type": "signature | behavioral | obfuscation | unknown",
"anti_crawl_evidence": {
"initial_status": 412,
"final_status": 200,
"redirect_chain_pattern": "412→412→200",
"vmp_script_url": "**/sdenv-*.js",
"vmp_case_count": 87,
"sdk_fingerprint": ["sdenv", "FuckCookie"]
},
"tool_chain": {
"primary": "instrument_jsvmp_source",
"mode": "ast",
"fallback": "hook_jsvmp_interpreter(mode=transparent)"
},
"hot_keys_snapshot": {
"top_10": ["userAgent", "plugins", "webdriver", "cookie", "platform", "language", "screen.width", "screen.height", "hardwareConcurrency", "deviceMemory"],
"total_count": 42,
"captured_at": "ISO8601"
},
"cookie_attribution_summary": {
"signature_cookies": ["acw_tc"],
"js_cookies": ["x-bogus-js"],
"mixed_cookies": ["NfBCSins"]
},
"assertions_passed": 12,
"assertions_failed": 0,
"assertions_total": 12
}
```
## 2. 生命周期
### 2.1 创建时机
Session 档案在以下时机自动创建:
1. **Phase 0 启动时**:`launch_browser` + `navigate` 成功后,检查 `cases/_session_archives/` 下是否已有该域的档案
- 已有 → 加载并验证(跑断言集,见第 4 节)
- 没有 → 创建空档案,后续步骤逐步填充
2. **反爬类型识别完成后**:将 `anti_crawl_type` 和 `anti_crawl_evidence` 写入档案
3. **Phase 0.5 指纹采集后**:将 `hot_keys_snapshot` 和 `cookie_attribution_summary` 写入档案
### 2.2 更新时机
- 每次成功完成 Phase 5 交付后,更新 `updated_at` 和断言结果
- Cookie 归因结论变化时(如新增 cookie 字段),更新 `cookie-attribution.json`
- 用户手动触发 `session refresh` 时,重新采集指纹基准
### 2.3 过期策略
- 档案默认 **7 天有效**(`updated_at` 距今超过 7 天视为过期)
- 过期档案不删除,但加载时会标记 `stale: true`,提示用户是否重新验证
- 断言集失败 ≥ 3 条时,自动标记为 `needs_refresh`
## 3. 与工作流的集成
### Phase 0 集成
```
Phase 0 步骤 4(新增):启动域级 Session 档案
① 计算目标域的 hash:sha256(domain)[0:12]
② 检查 cases/_session_archives/<hash>/session.json 是否存在
③ 存在且未过期 → 加载档案,输出摘要:
"已加载域级 Session 档案:反爬类型=签名型,上次分析于 2 天前,12/12 断言通过"
④ 存在但过期 → 加载档案 + 标记 stale,提示用户:
"域级 Session 档案已过期(7 天前),建议重新验证断言集"
⑤ 不存在 → 创建空档案,后续步骤自动填充
```
### Phase 0.5 集成
```
Phase 0.5.-1(新增前置步骤):本域断言 Reverify
如果 Session 档案存在且包含断言集:
① 逐条执行断言(见第 4 节)
② 全部通过 → 跳过 Phase 0.5 的指纹采集,直接进入 Phase 1
输出:"断言集 12/12 通过,目标站点技术栈未变,跳过重复侦察"
③ 有失败 → 标记失败断言,进入 Phase 0.5 正常流程重新采集
输出:"断言 #3(VMP case_count)失败:期望 87,实际 102。目标可能已更新,重新侦察"
```
### Phase 5 集成
```
Phase 5 交付时自动更新 Session 档案:
① 将本次分析的断言集写入 assertions.json
② 更新 session.json 的 updated_at 和 assertions_passed/failed/total
③ 如果是新域,提示用户:"已创建域级 Session 档案,下次分析同域时将自动复用"
```
## 4. 断言系统
断言是 Session 档案的核心验证机制。每条断言描述一个可自动检测的技术事实,用于快速判断目标站点是否发生了技术变更。
### 4.1 断言类型
| 类型 | 示例 | 检测方法 |
|------|------|---------|
| `script_exists` | VMP 脚本 URL 模式仍然存在 | `list_scripts` → 匹配 URL pattern |
| `script_size_range` | VMP 脚本大小在 ±20% 范围内 | `list_scripts` → 检查文件大小 |
| `dispatch_loop_count` | VMP 分发循环 case 数在 ±10 范围内 | `find_dispatch_loops` → 比较 case_count |
| `anti_crawl_type` | 反爬类型未变(仍为签名型) | `navigate` → 检查 redirect_chain 模式 |
| `cookie_names` | 关键 cookie 名称仍然存在 | `get_cookies` → 检查 name 列表 |
| `sdk_fingerprint` | SDK 特征关键词仍可搜索到 | `search_code` → 匹配关键词 |
| `hot_keys_stable` | hot_keys top 10 与快照重合度 ≥ 70% | `get_instrumentation_log` → 比较 hot_keys |
| `response_structure` | API 响应结构未变 | `get_network_request` → 检查 JSON 字段 |
### 4.2 断言格式
```json
{
"assertions": [
{
"id": 1,
"type": "script_exists",
"description": "VMP 脚本 URL 匹配 **/sdenv-*.js",
"params": { "url_pattern": "**/sdenv-*.js" },
"last_result": "passed",
"last_checked": "ISO8601"
},
{
"id": 2,
"type": "dispatch_loop_count",
"description": "VMP 分发循环 case 数在 77-97 范围内",
"params": { "expected": 87, "tolerance": 10 },
"last_result": "passed",
"last_checked": "ISO8601"
}
]
}
```
### 4.3 断言执行流程
```
对每条断言:
① 根据 type 选择检测方法(MCP 工具调用)
② 执行检测,获取实际值
③ 与 params 中的期望值比较
④ 记录结果:passed / failed / skipped(工具不可用时)
⑤ 更新 assertions.json 的 last_result 和 last_checked
```
## 5. 隐私与安全
- **域名哈希化**:目录名使用 `sha256(domain)[0:12]`,不直接暴露域名
- **已加入 .gitignore**:`cases/_session_archives/` 不纳入版本控制
- **敏感数据脱敏**:session.json 中不存储具体 Cookie 值、密钥、Token 等
- **fingerprint.json 仅存结构**:存储属性名和类型,不存储具体指纹值(如 canvas hash)
## 6. 手动操作
### 查看当前域的 Session 档案
```bash
# 计算域名 hash
echo -n "target-domain.com" | sha256sum | cut -c1-12
# 查看档案
cat cases/_session_archives/<hash>/session.json | jq .
```
### 强制刷新档案
在对话中告诉 Agent:"刷新当前域的 Session 档案",Agent 将:
1. 清空 assertions.json
2. 重新执行 Phase 0 + Phase 0.5 的完整流程
3. 重建断言集
### 删除档案
```bash
rm -rf cases/_session_archives/<hash>/
```
## 7. 与经验库(cases/)的关系
| 维度 | Session 档案 | 经验库案例 |
|------|-------------|-----------|
| 粒度 | 单个域 | 一类技术方案 |
| 内容 | 运行时状态(指纹、cookie、断言) | 方法论(定位路径、还原模板) |
| 生命周期 | 7 天有效,自动过期 | 永久,手动更新 |
| 隐私 | 哈希化,不入 git | 脱敏后入 git |
| 用途 | 跳过重复侦察 | 跳过重复分析 |
两者互补:Session 档案回答"这个域现在是什么状态",经验库回答"这类技术方案怎么破"。
+329
View File
@@ -0,0 +1,329 @@
# 经验法则完整版(30 条)
> **说明**:SKILL.md 核心层保留了前 10 条最常用的。本文档是完整的 30 条
>
> **版本**:v3.1.0(MCP v0.9.0 工具名迁移)
>
> **来源**:v3.0.0 从 46 条压缩合并而来,基于三次真实站点(瑞数/TikTok/抖音)实战数据
---
## 一、识别与选择(5 条)
### 规则 1:反爬类型识别是 Phase 0 的 Phase 0
> 合并原 #28
不加任何 hook 先 navigate,观察 redirect_chain / initial_status / 加载的 JS 特征,三分法判定签名型/行为型/纯混淆。
**用错档的工具不是效率差,是根本跑不通。**
```
MCP 操作:
navigate(url) // 不传 pre_inject_hooks
→ 读 initial_status / final_status / redirect_chain
→ 按三分法判断表判定类型
```
### 规则 2:JSVMP 双路径决策
> 合并原 #11, #29, #30
识别到 JSVMP 后先判断走路径 A(算法追踪)还是路径 B(环境伪装)。
签名型反爬只有一条路:`instrumentation(action='install', mode="ast")` <!-- v3.1.0: migrated from instrument_jsvmp_source -->,源码级插桩不动环境,是唯一能同时"观察 VMP"和"让挑战通过"的手段。
`hook_jsvmp_interpreter(mode="transparent")` 是 `mode="proxy"` 的签名安全备选,源码级插桩失败再退到这里。
### 规则 3:Cookie 归因优先于 setter hook
> 合并原 #1, #25
分析动态 Cookie 第一步永远是 `analyze_cookie_sources()`,它能区分三种模式:
- 纯 JS 写入
- 纯 HTTP Set-Cookie
- JS 算 token + 服务端带回来
RS/Akamai 最常见的第三种模式下,单纯 hook `document.cookie` setter 什么都抓不到。
### 规则 4:`pre_inject_hooks` 的正确定位
> 合并原 #26, #30
- **行为型反爬**:可用,首屏挑战页(RS 412、Akamai 首包)必须用它在 navigate 时装好 hook
- **签名型反爬**:**不可用**,永远不要对签名型反爬用它
### 规则 5:经验案例优先匹配 cacheOpts 区分变体
> 原 #40
同一 SDK 体系存在多个变体(单签名 vs 双签名、bdms.paths vs cacheOpts),Phase 0.5 指纹匹配时应优先检测 `cacheOpts` 和 `X-Gnarly` 来区分。
---
## 二、工具技巧(10 条)
### 规则 6:`get_request_initiator` 是黄金路径
> 原 #9
看到加密参数 → 获取请求 ID → `get_request_initiator(request_id=N)` → 直达签名函数,省去大量搜索。
### 规则 7:`inject_hook_preset` 一键到位
> 原 #10
不要手写常见 Hook,预设模板覆盖 xhr/fetch/crypto/websocket/debugger_bypass/cookie/runtime_probe。
### 规则 8:源码级插桩优先于运行时 hook(对 VM 自包含场景)
> 合并原 #23, #24
RS 5/6、Akamai sensor_data、webmssdk 这类"算法全部在 opcode dispatch 循环内"的 VMP,`hook_jsvmp_interpreter` 仍然看不到 switch/case 内部。
```
MCP 操作:
instrumentation(action='install', url_pattern="**/<VMP文件>", mode="ast", tag="vmp1")
<!-- v3.1.0: migrated from instrument_jsvmp_source -->
→ 是唯一能打开黑箱的工具
instrumentation(action='log', tag='vmp1', type_filter='tap_get', limit=200)
<!-- v3.1.0: migrated from get_instrumentation_log -->
→ hot_keys 指纹学习法 30 秒就能告诉你 VMP 读了哪些环境属性
```
### 规则 9:`instrumentation(action='reload')` 取代裸 `reload`
> 原 #27
装完 hook 想让它先于页面 JS 跑,裸 `reload()` 不能保证顺序。
```
MCP 操作:
instrumentation(action='reload')
<!-- v3.1.0: migrated from reload_with_hooks -->
→ 一步到位,默认 clear_log=True 拿到干净快照
```
### 规则 10:`search_code(keyword, script_url=url)` 定位大文件
> 原 #16
JSVMP 文件通常 200KB+,用 `search_code(keyword, script_url=url)` <!-- v3.1.0: migrated from search_code_in_script --> 在指定脚本中搜索,获取前后上下文。
### 规则 11:`compare_env` 是补环境的起点
> 原 #17
先在 Camoufox 中采集环境基准数据,再用 `evaluate_js` 分批采集细粒度值(compare_env 不覆盖 Function.toString / Symbol.toStringTag / DOM 布局),与 jsdom 逐项 diff。
### 规则 12:双签名场景必须同时 Hook XHR 和 fetch
> 原 #33
某些平台的 JSVMP 同时修改 `XMLHttpRequest.prototype.open` 和 `window.fetch`,只 Hook 一个通道会丢失另一半签名。
```
MCP 操作:
inject_hook_preset("xhr", persistent=True)
inject_hook_preset("fetch", persistent=True)
→ 两个通道都要 Hook
```
### 规则 13:MCP 侧 AST 让插桩在挑战页可用
> 原 #32
v0.5.0 把 AST 从页面内 Acorn 改为 MCP 侧 esprima,RS 412 挑战页也能跑。
`instrumentation(action='install', ...)` <!-- v3.1.0: migrated from instrument_jsvmp_source --> 的 `last_mode_used` 长期是 `"regex (fallback)"` 说明目标有太新语法。
### 规则 14:CSP 策略可能阻断 AST 模式插桩
> 原 #37
严格的 CSP 会阻止改写代码执行。v0.6.0 新增 `csp_bypass=True` 参数自动绕过。
### 规则 15:`navigate` 的 `collect_response_chain` 默认开启
> 原 #39
v0.6.0 起默认记录完整响应链,对签名型反爬的 412→200 判断更可靠。
---
## 三、Hook 与环境伪装踩坑(8 条)
### 规则 16:环境伪装优先于算法追踪
> 原 #18
如果 JSVMP 只是一个「签名黑箱」且可以在 jsdom 中加载执行,优先走路径 B(采集→对比→补丁),比追踪字节码执行快 10 倍。
### 规则 17:Function.prototype.toString 是 jsdom 环境伪装的第一杀手
> 原 #19
jsdom 所有 DOM 方法的 toString() 会暴露实际 JS 代码,必须用 WeakSet + 实例级覆写 + 源码模式正则三层防御。
### 规则 18:环境对比要分批采集
> 原 #20
单次 evaluate_js 代码太长会报错,分 4-5 批:
- navigator
- screen + window
- document + performance + toString
- DOM + Canvas + WebGL + Audio
### 规则 19:jsdom 环境补丁必须在 JSVMP 脚本加载前完成
> 原 #21
XHR Hook 的安装顺序决定能否截获最终 URL。
### 规则 20:服务端静默拒绝是环境检测失败的信号
> 原 #22
返回 HTTP 200 + 空 body(不报错),说明签名格式正确但环境指纹不匹配。
### 规则 21:Firefox 格式 native code 与 Chrome 不同
> 原 #35
Camoufox 基于 Firefox 内核,`Function.prototype.toString` 对原生函数返回含换行缩进格式。
```
Chrome: "function name() { [native code] }"
Firefox: "function name() {\n [native code]\n}"
```
jsdom 的 markNative 必须匹配采集基准浏览器的格式。
### 规则 22:cacheOpts 是新版 SDK 初始化的必传项
> 原 #34
旧版只需 `bdms.paths`,新版必须同时传入 `cacheOpts`。缺少 cacheOpts 会导致业务路径未注册,拦截器不触发。
### 规则 23:环境伪装前先确认签名函数入口
> 原 #38
在开始 6 步法的环境采集之前,先用 `search_code` 确认 JSVMP 的签名入口类型:
- 单通道 XHR
- 双通道 XHR + fetch
- 导出函数
- cacheOpts 初始化
---
## 四、协议优先反模式(7 条)
### 规则 24:先做最小补环境,不要上来就开浏览器
> 原 #5
协议优先的第一步。能用 Node.js `crypto` 解决的,不要用 `vm` 沙箱;能用 `vm` 沙箱的,不要用 jsdom;能用 jsdom 的,不要用浏览器。
### 规则 25:TLS 指纹是终极壁垒
> 合并原 #6, #36
算法全对但仍失败时,考虑 TLS 指纹:
- Node.js 用 `got-scraping` 模拟 Firefox/Chrome TLS 指纹
- Python 用 `curl_cffi`
### 规则 26:签名不一致时逐环节对比
> 原 #13
排查链路(逐项对比脚本值 vs 浏览器值):
1. 原始输入参数
2. 参数排序/拼接字符串
3. 时间戳(精度:秒 vs 毫秒)
4. 随机串(长度、字符集)
5. 密钥/盐值
6. 中间摘要
7. 最终密文(编码方式:hex/base64/自定义)
找到第一个偏差点。
### 规则 27:预热请求不是装饰
> 原 #2
`/api2` 类请求在运行时注入关键变量。跳过预热请求可能导致签名缺少必要的动态密钥。
### 规则 28:Node vm 沙箱 ≠ 浏览器
> 原 #3
有些调试干扰机制只在非浏览器环境触发。vm 沙箱中可能遇到:
- `window` 未定义
- `document` 未定义
- `navigator` 未定义
- 定时器行为不同
### 规则 29:降级梯度必须逐级走
> 合并原 #43, #44
```
instrumentation(action='install', mode="ast")
→ 失败
instrumentation(action='install', mode="regex")
→ 覆盖率不足
hook_jsvmp_interpreter(mode="transparent")
→ 日志太少
hook_jsvmp_interpreter(mode="proxy") ← 仅行为型可用
→ 破坏签名
路径 B(jsdom 环境伪装)
→ 也失败
向用户说明情况
```
每级至少尝试一次并记录失败原因。想放弃时先回查 cases/ 和 common-pitfalls.md。
### 规则 30:case 文件的价值随实战次数指数级增长
> v3.2.0 改写(原"Session 档案与经验库互补")
第一次分析某站点时写的 case 可能粗糙;第二次分析(升级或变体)时用 case 发现 80% 还成立、20% 变了,就把变化追加到 case 的"变体章节"。三五次之后,这个 case 会覆盖该站点的主要变体谱系,复用价值极高。
case 文件的"可验证事实清单"段列出 5-15 条最小可验证事实(如"X-Bogus 长度 28""webdriver === false"),下次同站升级时逐条手动核对找出"哪些变了"就是今天的工作范围。
---
## 已合并/迁移的旧经验法则索引
| 原编号 | 原标题 | 去向 |
|--------|--------|------|
| #1 | Cookie setter 最高性价比 | → 合并到新 #3(Cookie 归因优先) |
| #4 | WASM panic 常常是环境缺失 | → 移到 references/troubleshooting.md |
| #7 | 真假请求链是常见干扰 | → 移到 references/troubleshooting.md |
| #8 | HTTP/2 可能是唯一的"密码" | → 移到 references/troubleshooting.md |
| #11 | JSVMP 路径选择 | → 合并到新 #2 |
| #12 | JSVMP 中 String.fromCharCode 是高频信号 | → 移到 references/jsvmp-analysis.md |
| #14 | Python execjs 复用 context | → 移到 references/workflow-overview.md |
| #15 | Hook 必须持久化 + 防覆盖 | → 已在工具说明中覆盖 |
| #23 | 源码级插桩优先 | → 合并到新 #8 |
| #24 | hot_keys 指纹学习法 | → 合并到新 #8 |
| #25 | Cookie 归因 | → 合并到新 #3 |
| #26 | pre_inject_hooks 定位 | → 合并到新 #4 |
| #28 | 反爬类型识别 | → 合并到新 #1 |
| #29 | 签名型只走源码插桩 | → 合并到新 #2 |
| #30 | transparent 备选 | → 合并到新 #2, #4 |
| #41 | 开 session 多 10 秒省几小时 | → 已合并到硬约束 Checklist CHECK-4 |
| #42 | 进新域第一反应 list_sessions | → 已合并到 Checklist CHECK-3 |
| #43 | 降级梯度逐级走 | → 合并到新 #29 |
| #44 | 过期不等于失效 | → 合并到新 #29 |
| #45 | 断言价值指数递增 | → 合并到新 #30 |
| #46 | Session 与经验库互补 | → 合并到新 #30 |
---
> **SKILL.md 核心层**保留了前 10 条最常用的经验法则(规则 1-10 的精简版)
> **完整 30 条**在本文档
> **反模式清单**:references/common-pitfalls.md
+40 -38
View File
@@ -10,7 +10,7 @@ JSVMP(JavaScript Virtual Machine Protection)是一种高级 JS 代码保护
**核心原则:不反编译字节码,用行为追踪法(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 调度——是RS 5/6、Akamai sensor_data、webmssdk、obfuscator.io 这类"VM 自包含"场景的通用武器。详见 [`jsvmp-source-instrumentation.md`](./jsvmp-source-instrumentation.md)。
> **v2.5.0 更新**:第四板斧「源码级插桩」由 camoufox-reverse MCP v0.4.0 的 `instrumentation(action='install', ...)` 提供支持,在 HTTP 层改写 VMP 源码,对每个 `obj[key]` 和 `fn(args)` 插入 tap,捕获 VM 内部 switch/case 调度——是RS 5/6、Akamai sensor_data、webmssdk、obfuscator.io 这类"VM 自包含"场景的通用武器。详见 [`jsvmp-source-instrumentation.md`](./jsvmp-source-instrumentation.md)。 <!-- v3.1.0: migrated from instrument_jsvmp_source -->
---
@@ -52,18 +52,18 @@ function _0xefgh() {
```
MCP 操作(推荐顺序):
- list_scripts → 找到异常大的 JS 文件(100KB+)
- find_dispatch_loops(script_url=<大 JS 的 url>, min_case_count=20) ← v0.4.0 推荐首选
- scripts(action='list') → 找到异常大的 JS 文件(100KB+) <!-- v3.1.0: migrated from list_scripts -->
- search_code(keyword='switch', script_url=<大 JS 的 url>, context_chars=500) ← v0.4.0 推荐首选 <!-- v3.1.0: migrated from find_dispatch_loops -->
→ 返回 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 读取分发体
兜底手动方式(大文件 search_code 超时时):
- search_code(keyword="switch", script_url=N) → 前后 3 行上下文 <!-- v3.1.0: migrated from search_code_in_script -->
- search_code(keyword="case 0:|case 1:|case 2:", script_url=N) <!-- v3.1.0: migrated from search_code_in_script -->
- scripts(action='get', script_id=N, start_line=..., end_line=...) → 按 char_range 读取分发体 <!-- v3.1.0: migrated from get_script_source -->
```
---
@@ -77,7 +77,7 @@ MCP 操作(推荐顺序):
│ inject_hook_preset(xhr|fetch │
│ |crypto|cookie|websocket │
│ |debugger_bypass|runtime_probe)│
│ freeze_prototype │
│ hook_function(..., mode='intercept', ...) │ <!-- v3.1.0: migrated from freeze_prototype -->
│ analyze_cookie_sources │
└────────┬─────────────────────────┘
│
@@ -86,18 +86,19 @@ MCP 操作(推荐顺序):
中间层 ──→ ┌─┘ └─┐ ──→ 追踪执行链路
│
┌── 第二板斧 插桩解释器 ────────────┐ ┌── 第四板斧 源码级插桩 ──────────┐
│ 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 │
│ search_code(keyword='switch', │ │ instrumentation(action='install')│ <!-- v3.1.0: migrated from find_dispatch_loops, instrument_jsvmp_source -->
│ script_url=..., context_chars) │ │ instrumentation(action='log') │ <!-- v3.1.0: migrated from get_instrumentation_log -->
│ hook_function(mode='trace', │ │ → summary.hot_keys │ <!-- v3.1.0: migrated from trace_function -->
│ 分发函数/子函数) │ │ → summary.hot_methods │
│ hook_jsvmp_interpreter │ │ → summary.hot_functions │
│ (mode='proxy', trackProps=True)│ └────────┬────────────────────────┘ <!-- v3.1.0: migrated from trace_property_access -->
└────────┬────────────────────────┘
│
↓ ↓
┌── 第三板斧 日志分析 ──────────────────┐
│ get_trace_data + get_jsvmp_log │
│ + get_runtime_probe_log │
│ + get_instrumentation_log │
│ + instrumentation(action='log') │ <!-- v3.1.0: migrated from get_instrumentation_log -->
│ 反向追踪法(从签名值→明文) │
│ 多次请求对比法(找变化因子) │
└───────────────────────────────────────┘
@@ -117,13 +118,13 @@ MCP 操作(推荐顺序):
### 快速路径:v0.4.0 黄金 8 步流程(推荐先试,70%+ RS/Akamai/webmssdk 场景直接搞定)
```
Step 1 — find_dispatch_loops 确认 VMP + 记下 fn_name / char_range
Step 1 — search_code(keyword='switch', script_url=..., context_chars=500) 确认 VMP + 记下 fn_name / char_range <!-- v3.1.0: migrated from find_dispatch_loops -->
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 4 — instrumentation(action='install', url_pattern="**/<VMP 文件>", mode="ast", tag="vmp1") ← 核心新步 <!-- v3.1.0: migrated from instrument_jsvmp_source -->
Step 5 — instrumentation(action='reload') 让所有探针先于 VMP 生效 + 清日志 <!-- v3.1.0: migrated from reload_with_hooks -->
Step 6 — 触发业务操作
Step 7 — get_instrumentation_log(tag_filter="vmp1", limit=300) 看 hot_keys / hot_methods / hot_functions
Step 7 — instrumentation(action='log', tag_filter="vmp1", limit=300) 看 hot_keys / hot_methods / hot_functions <!-- v3.1.0: migrated from get_instrumentation_log -->
Step 8 — analyze_cookie_sources() 归因最终 cookie 来源
```
@@ -165,7 +166,7 @@ MCP 操作:
hook_code="console.log('[COOKIE_SET]', arguments[0], new Error().stack)",
position="before", non_overridable=True
) → 拦截 Cookie 写入(防覆盖)
- freeze_prototype(className="XMLHttpRequest", methodName="send")
- hook_function(className="XMLHttpRequest", methodName="send", mode='intercept') <!-- v3.1.0: migrated from freeze_prototype -->
→ 冻结 XHR.send 防止 VM 覆盖 Hook
- reload() → 刷新页面触发 Hook
- get_console_logs → 查看捕获的签名值和调用栈
@@ -226,9 +227,9 @@ MCP 操作:
```
MCP 操作:
- search_code(keyword="while.*true") → 找到解释器主循环
- search_code_in_script(script_id=N, keyword="case 0:|case 1:|case 2:")
- search_code(keyword="case 0:|case 1:|case 2:", script_url=N) <!-- v3.1.0: migrated from search_code_in_script -->
→ 在指定大文件中搜索 opcode 分发表(前后 3 行上下文,更精确)
- get_script_source → 读取解释器函数体
- scripts(action='get') → 读取解释器函数体 <!-- v3.1.0: migrated from get_script_source -->
判断标准:
- 函数体超过 500 行
@@ -244,8 +245,9 @@ MCP 操作:
```
MCP 操作:
- trace_function(
- hook_function( <!-- v3.1.0: migrated from trace_function -->
function_path="解释器分发函数名",
mode='trace',
log_args=true, log_return=true, max_captures=500,
persistent=True → 持久化追踪,跨导航不丢失
)
@@ -264,8 +266,8 @@ MCP 操作:
从第一轮日志中识别被频繁调用的子函数(通常 3-5 个核心函数):
MCP 操作:
- trace_function(function_path="子函数A", log_args=true, log_return=true, log_stack=true)
- trace_function(function_path="子函数B", log_args=true, log_return=true, log_stack=true)
- hook_function(function_path="子函数A", mode='trace', log_args=true, log_return=true, log_stack=true) <!-- v3.1.0: migrated from trace_function -->
- hook_function(function_path="子函数B", mode='trace', log_args=true, log_return=true, log_stack=true) <!-- v3.1.0: migrated from trace_function -->
- 再次触发请求
- get_trace_data → 分析子函数的输入输出
```
@@ -296,7 +298,7 @@ MCP 操作:
```
MCP 操作:
- trace_property_access(target_expression="疑似签名容器对象")
- hook_jsvmp_interpreter(mode='proxy', trackProps=True, target_expression="疑似签名容器对象") <!-- v3.1.0: migrated from trace_property_access -->
→ Proxy 级别属性访问追踪,自动记录读写操作
- get_property_access_log → 获取属性访问记录
- compare_env → 收集完整浏览器环境信息
@@ -445,7 +447,7 @@ MCP 操作:
· 在 add_init_script 中保存原始引用,在 VM 重写之前
- 或 Hook 网络层:
· start_network_capture → 不依赖 JS 层 Hook,直接在协议层捕获
· network_capture(action='start') → 不依赖 JS 层 Hook,直接在协议层捕获 <!-- v3.1.0: migrated from start_network_capture -->
· list_network_requests → get_network_request → 获取完整请求详情
```
@@ -470,7 +472,7 @@ MCP 操作:
```
- 不要试图理解嵌套关系
- 依然从 I/O 两端入手,Hook 最终的出口和最初的入口
- 增加 trace_function 的 max_captures 到 1000+
- 增加 hook_function(mode='trace') 的 max_captures 到 1000+ <!-- v3.1.0: migrated from trace_function -->
- 用时间戳过滤法缩小日志范围
```
@@ -533,9 +535,9 @@ navigate(url) 不加任何 hook 观察 redirect_chain
签名型 JSVMP 推荐走的路:
```
1. instrument_jsvmp_source(mode="ast", tag="...") (第四板斧)
2. reload_with_hooks() 让插桩后的 VMP 跑完挑战
3. get_instrumentation_log(tag_filter, type_filter="tap_get")
1. instrumentation(action='install', mode="ast", tag="...") (第四板斧) <!-- v3.1.0: migrated from instrument_jsvmp_source -->
2. instrumentation(action='reload') 让插桩后的 VMP 跑完挑战 <!-- v3.1.0: migrated from reload_with_hooks -->
3. instrumentation(action='log', tag_filter, type_filter="tap_get") <!-- v3.1.0: migrated from get_instrumentation_log -->
4. 基于 hot_keys 补最小环境,在 jsdom 或纯 Python 侧独立跑
(此时 jsdom 补环境的粒度比路径 B 小得多,只补 hot_keys 显示的属性)
```
@@ -600,18 +602,18 @@ JSVMP 签名还原决策树:
在 JSVMP 分析过程中,确保以下每项都已完成:
**第一板斧 — Hook 出入口**:
- [ ] `find_dispatch_loops` 确认是 VMP(case_count > 50)
- [ ] `search_code(keyword='switch', script_url=..., context_chars=500)` 确认是 VMP(case_count > 50) <!-- v3.1.0: migrated from find_dispatch_loops -->
- [ ] `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_function(..., mode='intercept', ...)` 防止 VMP 覆盖 hook <!-- v3.1.0: migrated from freeze_prototype -->
**第二板斧 — 插桩解释器**:
- [ ] `hook_jsvmp_interpreter(script_url=<VMP basename>)` 多路径探针(apply/call/bind + Reflect.*/Proxy)
- [ ] 分层 `trace_function`(粗 → 中 → 细,用 `max_captures` 限制日志量)
- [ ] `trace_property_access(["navigator.*", "screen.*", ...])` 监控签名容器
- [ ] 分层 `hook_function(mode='trace')`(粗 → 中 → 细,用 `max_captures` 限制日志量) <!-- v3.1.0: migrated from trace_function -->
- [ ] `hook_jsvmp_interpreter(mode='proxy', trackProps=True, targets=["navigator.*", "screen.*", ...])` 监控签名容器 <!-- v3.1.0: migrated from trace_property_access -->
**第三板斧 — 日志分析**:
- [ ] `get_jsvmp_log` + `get_trace_data` + `get_runtime_probe_log`(如果装了 runtime_probe)+ `get_console_logs`
@@ -619,13 +621,13 @@ JSVMP 签名还原决策树:
- [ ] 多次请求对比,确认变化因子和固定因子
**第四板斧 — 源码级插桩 [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`
- [ ] `instrumentation(action='install', url_pattern=..., mode="ast"|"regex", tag=...)` 装好 <!-- v3.1.0: migrated from instrument_jsvmp_source -->
- [ ] `instrumentation(action='reload')` 让插桩先于 VMP 生效 <!-- v3.1.0: migrated from reload_with_hooks -->
- [ ] 读 `instrumentation(action='log')` 三个 summary:`hot_keys` / `hot_methods` / `hot_functions` <!-- v3.1.0: migrated from get_instrumentation_log -->
- [ ] `hot_keys` 给出 VMP 读取的环境指纹集(top 30)
- [ ] `hot_methods` 给出 VMP 调用的方法(ObjectType.methodName 格式)
- [ ] `hot_functions` 给出 VMP 调用的函数名
- [ ] `stop_instrumentation` 完工清理
- [ ] `instrumentation(action='stop')` 完工清理 <!-- v3.1.0: migrated from stop_instrumentation -->
**环境/时序**:
- [ ] 若目标是首屏挑战页,用 `navigate(pre_inject_hooks=[...])` 或 `via_blank=True`
+58 -59
View File
@@ -17,42 +17,42 @@
| **截图** | `take_screenshot` / `take_snapshot` | 截图 / 无障碍树 |
| **页面信息** | `get_page_info` / `get_page_html` | 页面状态和 HTML |
| **源码搜索** | `search_code` | 在所有 JS 中搜索(结构化结果,默认 max_results=50) |
| **精确搜索** | `search_code_in_script` | 在指定脚本中搜索(前后 3 行上下文,适合大文件) |
| **源码获取** | `get_script_source` | 获取指定脚本的代码 |
| **源码保存** | `save_script` | 将脚本保存到本地文件 |
| **脚本列表** | `list_scripts` | 列出页面加载的所有脚本 |
| **精确搜索** | `search_code(..., script_url=...)` | 在指定脚本中搜索(前后 3 行上下文,适合大文件) | <!-- v3.1.0: migrated from search_code_in_script -->
| **源码获取** | `scripts(action='get', ...)` | 获取指定脚本的代码 | <!-- v3.1.0: migrated from get_script_source -->
| **源码保存** | `scripts(action='save', ...)` | 将脚本保存到本地文件 | <!-- v3.1.0: migrated from save_script -->
| **脚本列表** | `scripts(action='list')` | 列出页面加载的所有脚本 | <!-- v3.1.0: migrated from list_scripts -->
| **执行 JS** | `evaluate_js` / `evaluate_js_handle` | 在页面执行任意 JS |
| **加载前注入** | `add_init_script` | 页面加载前注入脚本(支持 `persistent` 跨导航重注入) |
| **伪断点** | `set_breakpoint_via_hook` | 通过 Hook 设置伪断点(支持 `persistent` 持久化) |
| **断点数据** | `get_breakpoint_data` | 获取伪断点捕获的数据 |
| **函数追踪** | `trace_function` / `get_trace_data` | 追踪函数调用(支持 `persistent` 跨导航持久化) |
| **函数追踪** | `hook_function(..., mode='trace', ...)` / `get_trace_data` | 追踪函数调用(支持 `persistent` 跨导航持久化) | <!-- v3.1.0: migrated from trace_function -->
| **自定义 Hook** | `hook_function` | 注入自定义 Hook(before/after/replace,支持 `non_overridable` 防覆盖) |
| **预设 Hook** | `inject_hook_preset` | 一键 Hook(xhr/fetch/crypto/websocket/debugger_bypass,默认持久化) |
| **移除 Hook** | `remove_hooks` | 移除所有 Hook(支持 `keep_persistent` 保留持久化 Hook) |
| **冻结原型** | `freeze_prototype` | 冻结任意原型方法,防止页面 JS 覆盖 Hook |
| **属性追踪** | `trace_property_access` / `get_property_access_log` | Proxy 级别属性访问追踪 |
| **冻结原型** | `hook_function(..., mode='intercept', ...)` | 冻结任意原型方法,防止页面 JS 覆盖 Hook | <!-- v3.1.0: migrated from freeze_prototype -->
| **属性追踪** | `hook_jsvmp_interpreter(mode='proxy', trackProps=True)` / `get_property_access_log` | Proxy 级别属性访问追踪 | <!-- v3.1.0: migrated from trace_property_access -->
| **JSVMP 插桩** | `hook_jsvmp_interpreter` | 一键插桩 JSVMP(Function.prototype.apply + 30+ 敏感属性) |
| **JSVMP 日志** | `get_jsvmp_log` | 获取 JSVMP 日志(API 调用统计 + 属性读取摘要) |
| **JSVMP 字符串** | `dump_jsvmp_strings` | 提取字符串数组,识别 API 名称,检测混淆模式 |
| **环境采集** | `compare_env` | 全面收集浏览器环境(navigator/screen/canvas/WebGL/Audio/timing) |
| **网络捕获** | `start_network_capture` / `stop_network_capture` | 启停网络捕获(支持 `capture_body` 捕获响应体) |
| **网络捕获** | `network_capture(action='start', ...)` / `network_capture(action='stop')` | 启停网络捕获(支持 `capture_body` 捕获响应体) | <!-- v3.1.0: migrated from start_network_capture / stop_network_capture -->
| **网络请求** | `list_network_requests` | 列出捕获的请求 |
| **请求详情** | `get_network_request` | 获取请求完整信息(含响应体) |
| **请求来源** | `get_request_initiator` | 获取请求的 JS 调用栈(改进 URL 匹配 + 诊断信息) |
| **请求拦截** | `intercept_request` / `stop_intercept` | 拦截/修改/Mock 请求 |
| **Cookie** | `get_cookies` / `set_cookies` / `delete_cookies` | Cookie 管理 |
| **Cookie** | `cookies(action='get', ...)` / `cookies(action='set', ...)` / `delete_cookies` | Cookie 管理 | <!-- v3.1.0: migrated from get_cookies / set_cookies -->
| **Storage** | `get_storage` / `set_storage` | localStorage / sessionStorage |
| **状态管理** | `export_state` / `import_state` | 保存/恢复浏览器状态 |
| **控制台** | `get_console_logs` | 读取控制台输出 |
| **指纹** | `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]** 查看/停止源码插桩 |
| **分发循环扫描** | `search_code(keyword='switch', script_url=..., context_chars=500)` | **[v0.4.0]** 定位字节码分发函数(while+switch,case 数过滤) | <!-- v3.1.0: migrated from find_dispatch_loops -->
| **源码级插桩** | `instrumentation(action='install', ...)` | **[v0.4.0]** HTTP 层改写 VMP,每个 obj[key]/fn(args) 插入 tap(通用 VMP 利器) | <!-- v3.1.0: migrated from instrument_jsvmp_source -->
| **插桩日志** | `instrumentation(action='log', ...)` | **[v0.4.0]** 拉取源码插桩日志,带 hot_keys/hot_methods/hot_functions 摘要 | <!-- v3.1.0: migrated from get_instrumentation_log -->
| **插桩状态** | `check_environment()` / `instrumentation(action='stop', ...)` | **[v0.4.0]** 查看/停止源码插桩 | <!-- v3.1.0: migrated from get_instrumentation_status / stop_instrumentation -->
| **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 执行(+ 清日志) |
| **hook 重载** | `instrumentation(action='reload')` | **[v0.4.0]** 重载使 persistent hook 先于页面 JS 执行(+ 清日志) | <!-- v3.1.0: migrated from reload_with_hooks -->
| **运行时探针日志** | `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,解决首屏挑战页 |
@@ -74,7 +74,7 @@
[camoufox-reverse] take_screenshot() → 确认页面正常
步骤 4:开始网络捕获
[camoufox-reverse] start_network_capture()
[camoufox-reverse] network_capture(action='start') <!-- v3.1.0: migrated from start_network_capture -->
步骤 5:触发数据请求(如翻页)
[camoufox-reverse] click(selector=".next-page")
@@ -97,7 +97,7 @@
[camoufox-reverse] search_code(keyword="参数名")
步骤 10:读取相关源码
[camoufox-reverse] get_script_source(script_url="包含加密逻辑的脚本URL")
[camoufox-reverse] scripts(action='get', script_url="包含加密逻辑的脚本URL") <!-- v3.1.0: migrated from get_script_source -->
步骤 11:设置伪断点验证
[camoufox-reverse] set_breakpoint_via_hook(target_function="加密函数路径")
@@ -129,13 +129,13 @@
步骤 4:根据调用栈定位生成函数
[camoufox-reverse] search_code(keyword="目标函数名")
[camoufox-reverse] get_script_source(script_url="...")
[camoufox-reverse] scripts(action='get', script_url="...") <!-- v3.1.0: migrated from get_script_source -->
步骤 5:分析生成逻辑
[camoufox-reverse] set_breakpoint_via_hook(target_function="cookie生成函数")
刷新页面 →
[camoufox-reverse] get_breakpoint_data → 查看入参和返回值
[camoufox-reverse] trace_function(function_path="加密函数", log_args=true, log_return=true, log_stack=true)
[camoufox-reverse] hook_function(function_path="加密函数", mode='trace', log_args=true, log_return=true, log_stack=true) <!-- v3.1.0: migrated from trace_function -->
[camoufox-reverse] get_trace_data → 查看中间值
```
@@ -145,11 +145,11 @@
```
步骤 1:列出所有脚本
[camoufox-reverse] list_scripts
[camoufox-reverse] scripts(action='list') <!-- v3.1.0: migrated from list_scripts -->
→ 找到可疑的混淆脚本(通常文件名无意义或体积很大)
步骤 2:保存混淆脚本到本地
[camoufox-reverse] save_script(script_url="混淆脚本URL", save_path="./project/obfuscated.js")
[camoufox-reverse] scripts(action='save', script_url="混淆脚本URL", save_path="./project/obfuscated.js") <!-- v3.1.0: migrated from save_script -->
步骤 3:搜索关键特征
[camoufox-reverse] search_code(keyword="_0x")
@@ -165,7 +165,7 @@
步骤 5:定位关键函数
[camoufox-reverse] search_code(keyword="encrypt|sign|md5|aes")
→ 找到加密相关代码的大致位置
[camoufox-reverse] get_script_source → 获取上下文
[camoufox-reverse] scripts(action='get') → 获取上下文 <!-- v3.1.0: migrated from get_script_source -->
```
### 场景 4:WASM 逆向
@@ -220,7 +220,7 @@
```
步骤 1:启动浏览器 + 开启响应体捕获
[camoufox-reverse] launch_browser(headless=False)
[camoufox-reverse] start_network_capture(capture_body=True)
[camoufox-reverse] network_capture(action='start', capture_body=True) <!-- v3.1.0: migrated from start_network_capture -->
步骤 2:第一次导航定位 VMP 脚本
[camoufox-reverse] navigate(url="https://target.com/")
@@ -228,14 +228,13 @@
→ 找最大的 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",
[camoufox-reverse] search_code(keyword='switch', script_url="https://target.com/xxx/sdenv-xxx.js", context_chars=500) <!-- v3.1.0: migrated from find_dispatch_loops -->
min_case_count=20
)
→ case_count > 50 基本确认是 VMP
步骤 4:装源码级插桩(核心)
[camoufox-reverse] instrument_jsvmp_source(
[camoufox-reverse] instrumentation(action='install', <!-- v3.1.0: migrated from instrument_jsvmp_source -->
url_pattern="**/sdenv-*.js",
mode="ast", # 能联网用 ast(99% 覆盖);离线用 regex(80%)
tag="vmp1"
@@ -248,26 +247,26 @@
[camoufox-reverse] bypass_debugger_trap()
步骤 6:重载让插桩先于 VMP 生效
[camoufox-reverse] reload_with_hooks()
[camoufox-reverse] instrumentation(action='reload') <!-- v3.1.0: migrated from reload_with_hooks -->
步骤 7:触发业务操作(翻页 / 登录 / 搜索)
步骤 8:读 hot_keys / hot_methods / hot_functions(30 秒读完 VMP 指纹)
[camoufox-reverse] get_instrumentation_log(
[camoufox-reverse] instrumentation(action='log', <!-- v3.1.0: migrated from 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(
[camoufox-reverse] instrumentation(action='log', <!-- v3.1.0: migrated from 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(
[camoufox-reverse] instrumentation(action='log', <!-- v3.1.0: migrated from get_instrumentation_log -->
tag_filter="vmp1",
type_filter="tap_call", # 所有 fn(args) 直接调用
limit=200
@@ -280,7 +279,7 @@
+ http_responses[].url + js_writes[].stack
步骤 10:完工清理
[camoufox-reverse] stop_instrumentation(url_pattern="**/sdenv-*.js")
[camoufox-reverse] instrumentation(action='stop', url_pattern="**/sdenv-*.js") <!-- v3.1.0: migrated from stop_instrumentation -->
[camoufox-reverse] remove_hooks()
```
@@ -301,7 +300,7 @@
```
步骤 1:启动环境 + 开启响应体捕获 + cookie hook
[camoufox-reverse] launch_browser(headless=False)
[camoufox-reverse] start_network_capture(capture_body=True)
[camoufox-reverse] network_capture(action='start', capture_body=True) <!-- v3.1.0: migrated from start_network_capture -->
[camoufox-reverse] inject_hook_preset(preset="cookie", persistent=True) # 原型链级
步骤 2:导航到目标(首屏有挑战用 pre_inject_hooks)
@@ -348,11 +347,12 @@
## 高级技巧
### 使用 trace_function 自动记录函数调用
### 使用 hook_function(mode='trace') 自动记录函数调用 <!-- v3.1.0: migrated from trace_function -->
```
[camoufox-reverse] trace_function(
[camoufox-reverse] hook_function( <!-- v3.1.0: migrated from trace_function -->
function_path="window.encrypt",
mode='trace',
log_args=true,
log_return=true,
log_stack=true,
@@ -387,10 +387,10 @@
```
→ 在 XHR.open 调用前输出请求方法和 URL,即使页面 JS 尝试覆盖也不会丢失
### 使用 freeze_prototype 防止 Hook 被覆盖
### 使用 hook_function(mode='intercept') 防止 Hook 被覆盖 <!-- v3.1.0: migrated from freeze_prototype -->
```
[camoufox-reverse] freeze_prototype(className="XMLHttpRequest", methodName="send")
[camoufox-reverse] hook_function(className="XMLHttpRequest", methodName="send", mode='intercept') <!-- v3.1.0: migrated from freeze_prototype -->
```
→ 冻结原型方法(configurable:false + writable:false),防止 JSVMP 等覆盖你的 Hook
@@ -431,16 +431,16 @@ await navigate(url, pre_inject_hooks=["jsvmp_probe_transparent"])
### 使用属性访问追踪
```
[camoufox-reverse] trace_property_access(target_expression="navigator")
[camoufox-reverse] hook_jsvmp_interpreter(mode='proxy', trackProps=True, target_expression="navigator") <!-- v3.1.0: migrated from trace_property_access -->
触发操作...
[camoufox-reverse] get_property_access_log → 查看 navigator 的哪些属性被读取
```
→ 精确发现 JSVMP 或签名逻辑读取了哪些环境属性
### 使用 search_code_in_script 精确搜索大文件
### 使用 search_code(..., script_url=...) 精确搜索大文件 <!-- v3.1.0: migrated from search_code_in_script -->
```
[camoufox-reverse] search_code_in_script(script_id=3, keyword="sign|token|encrypt")
[camoufox-reverse] search_code(keyword="sign|token|encrypt", script_url=3) <!-- v3.1.0: migrated from search_code_in_script -->
```
→ 在指定脚本中搜索,返回前后 3 行上下文,适合 200KB+ 的 JSVMP 文件
@@ -484,10 +484,10 @@ await navigate(url, pre_inject_hooks=["jsvmp_probe_transparent"])
```
# 1. 先确认是不是 VMP(case_count > 50 基本是)
[camoufox-reverse] find_dispatch_loops(script_url="https://target.com/sdenv-xxx.js", min_case_count=20)
[camoufox-reverse] search_code(keyword='switch', script_url="https://target.com/sdenv-xxx.js", context_chars=500) <!-- v3.1.0: migrated from find_dispatch_loops -->
# 2. 装插桩(AST 模式优先,需 CDN;regex 模式兜底)
[camoufox-reverse] instrument_jsvmp_source(
[camoufox-reverse] instrumentation(action='install', <!-- v3.1.0: migrated from instrument_jsvmp_source -->
url_pattern="**/sdenv-*.js", # glob 模式匹配多 hash 版本
mode="ast", # 或 "regex"
tag="vmp1", # 区分多 VMP 的标签
@@ -498,21 +498,21 @@ await navigate(url, pre_inject_hooks=["jsvmp_probe_transparent"])
)
# 3. 装完后必须重载让插桩生效
[camoufox-reverse] reload_with_hooks()
[camoufox-reverse] instrumentation(action='reload') <!-- v3.1.0: migrated from reload_with_hooks -->
# 4. 触发操作后读日志
[camoufox-reverse] get_instrumentation_log(tag_filter="vmp1", type_filter="tap_get", limit=200)
[camoufox-reverse] instrumentation(action='log', tag_filter="vmp1", type_filter="tap_get", limit=200) <!-- v3.1.0: migrated from get_instrumentation_log -->
→ summary.hot_keys 是 VMP 读取的环境属性 top 30(指纹学习金矿)
[camoufox-reverse] get_instrumentation_log(tag_filter="vmp1", type_filter="tap_method", limit=200)
[camoufox-reverse] instrumentation(action='log', tag_filter="vmp1", type_filter="tap_method", limit=200) <!-- v3.1.0: migrated from get_instrumentation_log -->
→ summary.hot_methods 是 VMP 调用的方法 top 30(ObjectType.methodName 格式)
# 5. 查看插桩状态
[camoufox-reverse] get_instrumentation_status()
[camoufox-reverse] check_environment() <!-- v3.1.0: migrated from get_instrumentation_status -->
# 6. 完工移除
[camoufox-reverse] stop_instrumentation(url_pattern="**/sdenv-*.js")
[camoufox-reverse] stop_instrumentation() # 不传参数 = 全部停止
[camoufox-reverse] instrumentation(action='stop', url_pattern="**/sdenv-*.js") <!-- v3.1.0: migrated from stop_instrumentation -->
[camoufox-reverse] instrumentation(action='stop') # 不传参数 = 全部停止 <!-- v3.1.0: migrated from stop_instrumentation -->
```
**与 hook_jsvmp_interpreter 的分工**:
@@ -520,7 +520,7 @@ await navigate(url, pre_inject_hooks=["jsvmp_probe_transparent"])
| 工具 | 能看到 | 看不到 |
|------|--------|--------|
| `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 的那个文件) |
| `instrumentation(action='install', ...)` | 每一次 member access + 每一次函数调用,无论是否通过可 hook API | 不跨脚本(只改写指定 url_pattern 的那个文件) | <!-- v3.1.0: migrated from instrument_jsvmp_source -->
| **建议**:两者同时开,互相校验 | | |
#### mode="ast" 的 v0.5.0 变化(v2.6.0 吸收)
@@ -528,15 +528,14 @@ await navigate(url, pre_inject_hooks=["jsvmp_probe_transparent"])
```python
# v2.5.0 / MCP v0.4.x: mode="ast" 会在页面里注入 acorn CDN script tag
# v2.6.0 / MCP v0.5.0: mode="ast" 在 MCP 进程里用 esprima-python 解析,零页面依赖
await instrument_jsvmp_source(
"**/sdenv-*.js",
await instrumentation(action='install', url_pattern="**/sdenv-*.js", <!-- v3.1.0: migrated from instrument_jsvmp_source -->
mode="ast", # 默认,走 MCP 侧 esprima
tag="sdenv",
fallback_on_error=True, # 新参数:esprima 挂就回落 regex
)
# 查看实际走了哪条路径
status = await get_instrumentation_status()
status = await check_environment() <!-- v3.1.0: migrated from get_instrumentation_status -->
# status["active_patterns"][0]["last_mode_used"] == "ast" (正常)
# or "regex (fallback)" (esprima 对该 VMP 的语法支持不全)
# or "ast_page" (你显式用了 deprecated 模式)
@@ -546,14 +545,14 @@ status = await get_instrumentation_status()
```python
# 仅用于 A/B 对比 v0.4.x 的旧页面内 Acorn 实现,deprecated
# 生产不要用,挑战页会因 CDN 被拦静默失败
await instrument_jsvmp_source("**/vmp.js", mode="ast_page")
await instrumentation(action='install', url_pattern="**/vmp.js", mode="ast_page") <!-- v3.1.0: migrated from instrument_jsvmp_source -->
```
### 使用 Cookie 归因分析(v2.5.0 新增)
```
# 1. 前置条件:网络抓包 + cookie hook 同时开
[camoufox-reverse] start_network_capture(capture_body=True)
[camoufox-reverse] network_capture(action='start', capture_body=True) <!-- v3.1.0: migrated from start_network_capture -->
[camoufox-reverse] inject_hook_preset(preset="cookie", persistent=True)
# 2. 触发场景
@@ -616,15 +615,15 @@ await instrument_jsvmp_source("**/vmp.js", mode="ast_page")
| 页面导航 | `navigate` `reload` `go_back` | 导航控制 |
| 页面交互 | `click` `type_text` `wait_for` | UI 自动化 |
| 页面状态 | `get_page_info` `take_screenshot` `take_snapshot` `get_page_html` | 页面信息 |
| 源码分析 | `list_scripts` `get_script_source` `search_code` `search_code_in_script` `save_script` | JS 逆向核心 |
| 源码分析 | `scripts(action='list')` `scripts(action='get', ...)` `search_code` `search_code(..., script_url=...)` `scripts(action='save', ...)` | JS 逆向核心 | <!-- v3.1.0: migrated from list_scripts, get_script_source, search_code_in_script, save_script -->
| JS 执行 | `evaluate_js` `evaluate_js_handle` `add_init_script` | 代码执行(支持持久化) |
| 调试 | `set_breakpoint_via_hook` `get_breakpoint_data` `get_console_logs` | 伪断点调试(支持持久化) |
| Hook | `trace_function` `get_trace_data` `hook_function` `inject_hook_preset` `remove_hooks` `freeze_prototype` | 函数 Hook(支持持久化 + 防覆盖) |
| 属性追踪 | `trace_property_access` `get_property_access_log` | Proxy 级别属性访问监控 |
| Hook | `hook_function(..., mode='trace', ...)` `get_trace_data` `hook_function` `inject_hook_preset` `remove_hooks` `hook_function(..., mode='intercept', ...)` | 函数 Hook(支持持久化 + 防覆盖) | <!-- v3.1.0: migrated from trace_function, freeze_prototype -->
| 属性追踪 | `hook_jsvmp_interpreter(mode='proxy', trackProps=True)` `get_property_access_log` | Proxy 级别属性访问监控 | <!-- v3.1.0: migrated from trace_property_access -->
| JSVMP 专项 | `hook_jsvmp_interpreter` `get_jsvmp_log` `dump_jsvmp_strings` `compare_env` | JSVMP 虚拟机保护分析 |
| 网络 | `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` | 状态管理 |
| 网络 | `network_capture(action='start', ...)` `network_capture(action='stop')` `list_network_requests` `get_network_request` `get_request_initiator` `intercept_request` `stop_intercept` | 网络分析(支持响应体捕获) | <!-- v3.1.0: migrated from start_network_capture, stop_network_capture -->
| 存储 | `cookies(action='get', ...)` `cookies(action='set', ...)` `delete_cookies` `get_storage` `set_storage` `export_state` `import_state` | 状态管理 | <!-- v3.1.0: migrated from get_cookies, set_cookies -->
| 反检测 | `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 逆向利器 |
| 源码级插桩 | `instrumentation(action='install', ...)` `instrumentation(action='log', ...)` `check_environment()` `instrumentation(action='stop', ...)` `search_code(keyword='switch', script_url=..., context_chars=500)` | **[v0.4.0]** 通用 JSVMP 逆向利器 | <!-- v3.1.0: migrated from instrument_jsvmp_source, get_instrumentation_log, get_instrumentation_status, stop_instrumentation, find_dispatch_loops -->
| 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 重注入/运行时探针 |
| 导航增强 | `navigate(pre_inject_hooks)` `instrumentation(action='reload')` `get_runtime_probe_log` | **[v0.4.0]** 首屏挑战/hook 重注入/运行时探针 | <!-- v3.1.0: migrated from reload_with_hooks -->
+287
View File
@@ -0,0 +1,287 @@
# MCP 工具分类索引 + v0.9.0 迁移指南
> **说明**:SKILL.md 核心层有工具分类索引,这里是详细版 + MCP v0.9.0 的迁移映射
>
> **版本**:v3.1.0(MCP v0.9.0 工具名迁移)
>
> **当前 MCP 版本**:camoufox-reverse MCP v0.9.0(~50 个工具,从 v0.8.x 的 ~80 个合并而来)
---
## 一、工具分类表
### Browser — 浏览器与页面控制
| 工具名 | 一行描述 |
|--------|---------|
| `launch_browser` | 启动 Camoufox 反检测浏览器(C++ 引擎级指纹伪装) |
| `close_browser` | 关闭浏览器释放资源 |
| `navigate` | 导航到 URL,支持 pre_inject_hooks / wait_until / collect_response_chain |
| `reload` | 刷新当前页面 |
| `go_back` | 浏览器后退 |
### Page — 页面交互与信息
| 工具名 | 一行描述 |
|--------|---------|
| `click` | 点击页面元素 |
| `type_text` | 输入文本(带真实按键延迟) |
| `wait_for` | 等待元素出现或网络请求匹配 |
| `get_page_info` | 获取当前页面 URL、标题、视口尺寸 |
| `take_screenshot` | 截取页面截图 |
| `take_snapshot` | 获取页面无障碍树(token 高效) |
| `evaluate_js` | 在页面上下文执行任意 JS(最核心工具) |
| `evaluate_js_handle` | 执行 JS 并检查复杂对象属性 |
| `get_page_html` | 获取页面 HTML 或指定元素的 outerHTML |
### Network — 网络分析
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `network_capture(action='start'\|'stop')` <!-- v3.1.0: migrated from start_network_capture/stop_network_capture --> | 启停网络流量捕获,支持 capture_body=True |
| `list_network_requests` | 列出捕获的请求(支持 URL/method/type/status 过滤) |
| `get_network_request` | 获取请求完整详情(含响应体) |
| `get_request_initiator` | 获取发起请求的 JS 调用栈(黄金路径) |
| `intercept_request` | 拦截请求(log/block/modify/mock) |
| `stop_intercept` | 停止拦截 |
| `search_response_body` | 在所有已捕获响应体中全文搜索 |
| `get_response_body_page` | 分页读取大响应体 |
| `search_json_path` | 按 JSON 路径提取响应数据 |
### Scripts — 源码分析
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `scripts(action='list')` <!-- v3.1.0: migrated from list_scripts --> | 列出页面加载的所有 JS 脚本 |
| `scripts(action='get', url=...)` <!-- v3.1.0: migrated from get_script_source --> | 获取完整 JS 源码 |
| `scripts(action='save', url=..., save_path=...)` <!-- v3.1.0: migrated from save_script --> | 保存 JS 文件到本地 |
| `search_code` | 在所有已加载 JS 中搜索关键词(全局) |
| `search_code(keyword, script_url=url)` <!-- v3.1.0: migrated from search_code_in_script --> | 在指定脚本中搜索(大文件精确定位) |
### Hook — 调试与 Hook
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `add_init_script` | 注入页面加载前执行的脚本(支持 persistent) |
| `set_breakpoint_via_hook` | 通过 JS Hook 设置伪断点 |
| `get_breakpoint_data` | 获取伪断点捕获的数据 |
| `get_console_logs` | 获取页面控制台输出 |
| `hook_function(path, mode='trace', ...)` <!-- v3.1.0: migrated from trace_function --> | 追踪函数调用(不暂停执行) |
| `get_trace_data` | 获取追踪数据 |
| `hook_function(path, hook_code=..., position=...)` | 注入自定义 Hook(before/after/replace) |
| `hook_function(path, mode='intercept', non_overridable=True)` <!-- v3.1.0: migrated from freeze_prototype --> | 冻结原型方法防覆盖 |
| `inject_hook_preset` | 一键预设 Hook(xhr/fetch/crypto/websocket/debugger_bypass/cookie/runtime_probe) |
| `get_runtime_probe_log` | 拉取 runtime_probe 日志 |
| `remove_hooks` | 移除所有 Hook |
| `bypass_debugger_trap` | 一键绕过反调试陷阱 |
### JSVMP — 虚拟机专项分析
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `hook_jsvmp_interpreter` | 多路径通用探针(proxy/transparent 模式) |
| `hook_jsvmp_interpreter(mode='proxy', trackProps=True)` <!-- v3.1.0: migrated from trace_property_access --> | Proxy 级别属性访问追踪 |
| `get_jsvmp_log` | 获取 JSVMP 探针日志 |
| `dump_jsvmp_strings` | 提取 JSVMP 脚本中的字符串 |
| `compare_env` | 采集浏览器环境指纹基准 |
| `instrumentation(action='install', url_pattern=..., mode=..., tag=...)` <!-- v3.1.0: migrated from instrument_jsvmp_source --> | 源码级插桩(HTTP 层改写 VMP) |
| `instrumentation(action='log', tag=..., type_filter=...)` <!-- v3.1.0: migrated from get_instrumentation_log --> | 获取源码插桩日志(hot_keys/hot_methods/hot_functions) |
| `instrumentation(action='status')` | 查看当前激活的源码插桩状态 |
| `instrumentation(action='stop', url_pattern=...)` <!-- v3.1.0: migrated from stop_instrumentation --> | 停止源码级插桩 |
| `instrumentation(action='reload')` <!-- v3.1.0: migrated from reload_with_hooks --> | 重载页面让 hooks 先于 VMP 生效 |
| `analyze_cookie_sources` | Cookie 归因分析(HTTP vs JS 来源) |
### Session — 域级 Session 档案
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `start_reverse_session` | 启动新的分析 run |
| `stop_reverse_session` | 结束当前 run |
| `get_session_snapshot` | 获取活跃 session 快照 |
| `list_sessions` | 列出所有域 session 档案 |
| `attach_domain_readonly` | 只读附加已有域档案 |
| `export_session` | 导出域档案为 zip |
| `import_session` | 导入域档案 zip |
### Assertion — 断言系统
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `add_assertion` | 注册域级断言 |
| `verify_assertion(assertion_id=...)` | 验证单条断言 |
| `verify_assertion(assertion_id='*')` <!-- v3.1.0: migrated from reverify_all_assertions_on_domain --> | 批量验证所有断言 |
| `list_assertions` | 列出域上的所有断言 |
| `remove_assertion` | 软删除断言 |
### Environment — 存储与指纹
| 工具名(v0.9.0) | 一行描述 |
|-------------------|---------|
| `cookies(action='get', ...)` <!-- v3.1.0: migrated from get_cookies --> | 获取 Cookie |
| `cookies(action='set', ...)` <!-- v3.1.0: migrated from set_cookies --> | 设置 Cookie |
| `delete_cookies` | 删除 Cookie |
| `get_storage` | 获取 localStorage/sessionStorage |
| `set_storage` | 设置 storage |
| `export_state` | 导出浏览器状态(Cookie + Storage) |
| `import_state` | 导入浏览器状态 |
| `get_fingerprint_info` | 检查当前浏览器指纹 |
| `check_detection` | 在反检测站点测试是否被识别 |
| `get_session_info` | 查看当前会话状态全貌 |
---
## 二、v0.8.x → v0.9.0 完整迁移表
> MCP v0.9.0 将 ~80 个工具合并为 ~50 个,核心变化是将同类操作合并为带 `action` 参数的统一接口。
### 19 个工具合并为 7 个统一接口
| v0.8.x 旧名 | v0.9.0 新名 | 说明 |
|-------------|-------------|------|
| `start_network_capture(capture_body=X)` | `network_capture(action='start', capture_body=X)` | 网络捕获启停合并 |
| `stop_network_capture()` | `network_capture(action='stop')` | |
| `search_code_in_script(url, keyword)` | `search_code(keyword, script_url=url)` | 合并到 search_code,script_url 可选参数 |
| `trace_function(path, ...)` | `hook_function(path, mode='trace', ...)` | 追踪合并到 hook_function |
| `instrument_jsvmp_source(url_pattern, ...)` | `instrumentation(action='install', url_pattern=url_pattern, ...)` | 源码插桩统一接口 |
| `get_instrumentation_log(tag=X)` | `instrumentation(action='log', tag=X)` | |
| `get_instrumentation_status()` | `instrumentation(action='status')` | |
| `stop_instrumentation()` | `instrumentation(action='stop')` | |
| `reload_with_hooks()` | `instrumentation(action='reload')` | |
| `get_cookies(...)` | `cookies(action='get', ...)` | Cookie 管理统一接口 |
| `set_cookies(...)` | `cookies(action='set', ...)` | |
| `list_scripts(...)` | `scripts(action='list', ...)` | 脚本管理统一接口 |
| `get_script_source(url)` | `scripts(action='get', url=url)` | |
| `save_script(url, path)` | `scripts(action='save', url=url, save_path=path)` | |
| `reverify_all_assertions_on_domain()` | `verify_assertion(assertion_id='*')` | 批量验证合并到 verify_assertion |
| `find_dispatch_loops(url)` | `search_code(keyword='switch', script_url=url, context_chars=500)` | 合并到 search_code 的高级用法 |
| `trace_property_access(...)` | `hook_jsvmp_interpreter(mode='proxy', trackProps=True)` | 合并到 hook_jsvmp_interpreter |
| `freeze_prototype(class, method)` | `hook_function('class.prototype.method', mode='intercept', ...)` | 合并到 hook_function |
| `get_page_content()` | `evaluate_js("document.documentElement.outerHTML")` | 用 evaluate_js 替代 |
### 已删除工具及替代方案(12 个)
| 已删除工具 | 替代方案 |
|-----------|---------|
| `get_property_access_log` | `get_jsvmp_log(type_filter='prop_read')` |
| `get_page_content` | `evaluate_js("document.documentElement.outerHTML")` <!-- v3.1.0: migrated from get_page_content --> |
| `find_dispatch_loops` | `search_code(keyword='switch', script_url='<url>', context_chars=500)` |
| `trace_property_access` | `hook_jsvmp_interpreter(mode='proxy', trackProps=True)` |
| `freeze_prototype` | `hook_function('X.prototype.Y', mode='intercept', non_overridable=True)` |
| `check_csp_policy` | `evaluate_js("document.querySelector('meta[http-equiv=Content-Security-Policy]')?.content")` |
| `get_response_chain` | `navigate()` 返回值中的 `redirect_chain` 字段 |
| `get_dual_sign_data` | 组合 `list_network_requests` + `get_network_request` 手动提取 |
| `diff_hot_keys` | 本地 JSON diff 或 `evaluate_js` 实现 |
| `create_session` | `start_reverse_session` |
| `load_session` | `attach_domain_readonly` + `verify_assertion(assertion_id='*')` |
| `update_session` | `stop_reverse_session` + `add_assertion` |
---
## 三、6 个月 Shim 兼容期
> MCP v0.9.0 提供 6 个月的 shim 兼容期(至 2027-01-01)。
### Shim 行为
- 旧工具名仍可调用,MCP 内部自动映射到新接口
- 每次调用旧名会在 console 输出 deprecation warning
- Shim 期结束后旧名将返回 `ToolNotFound` 错误
### 建议迁移时间线
| 阶段 | 时间 | 行动 |
|------|------|------|
| 立即 | 现在 | 新代码/新文档全部使用 v0.9.0 名称 |
| 1 个月内 | 2026-08 | 更新所有 cases/*.md 中的工具名 |
| 3 个月内 | 2026-10 | 更新所有 references/*.md 中的工具名 |
| 6 个月内 | 2027-01 | 确认无旧名残留,shim 期结束 |
---
## 四、MCP v0.9.0 关键变化总结
### 工具数量
- v0.8.x:~80 个独立工具
- v0.9.0:~50 个工具(19 个合并为 7 个统一接口 + 12 个删除)
### 设计原则
1. **同类操作合并**:start/stop/get/list 合并为带 `action` 参数的统一接口
2. **功能重叠消除**:`trace_function` 和 `hook_function` 合并(trace 是 hook 的一种 mode)
3. **语义一致性**:`freeze_prototype` 本质是 hook 的 intercept 模式
4. **减少记忆负担**:7 个统一接口覆盖原来 19 个工具的功能
### 不变的核心工具
以下工具在 v0.9.0 中保持不变:
```
launch_browser / close_browser / navigate / reload / go_back
click / type_text / wait_for / get_page_info
take_screenshot / take_snapshot
evaluate_js / evaluate_js_handle
get_page_html / add_init_script
set_breakpoint_via_hook / get_breakpoint_data
get_console_logs / get_trace_data
hook_function (扩展了 mode 参数)
inject_hook_preset / remove_hooks / bypass_debugger_trap
hook_jsvmp_interpreter / get_jsvmp_log / dump_jsvmp_strings
compare_env / analyze_cookie_sources
get_runtime_probe_log
list_network_requests / get_network_request / get_request_initiator
intercept_request / stop_intercept
search_response_body / get_response_body_page / search_json_path
delete_cookies / get_storage / set_storage
export_state / import_state
get_fingerprint_info / check_detection / get_session_info
start_reverse_session / stop_reverse_session / get_session_snapshot
list_sessions / attach_domain_readonly
export_session / import_session
add_assertion / verify_assertion / list_assertions / remove_assertion
```
---
## 五、Session/Assertion 工具移除(v3.2.0 新增)
以下工具在当前 MCP 版本中**不存在**,skill v3.2.0 已移除所有引用:
| 移除的工具 | 替代 |
|---|---|
| `start_reverse_session` / `stop_reverse_session` | 无替代。经验沉淀到 cases/ 文件 |
| `get_session_snapshot` | 无替代。手动查看 list_network_requests / instrumentation(action='log') |
| `list_sessions` / `attach_domain_readonly` | 无替代。读 cases/README.md 查已有案例 |
| `export_session` / `import_session` | 无替代。case 文件本身是可 commit 的经验 |
| `add_assertion` / `verify_assertion` / `list_assertions` / `remove_assertion` | 在 case 文件的"可验证事实清单"段列事实,AI 手动核对 |
| `verify_against_session(signer_code, sample_filter=...)` | `verify_signer_offline(signer_code, samples=[...])` — 样本由用户提供 |
`verify_signer_offline` 迁移示例:
```python
# 步骤 1 — 手动提取样本
reqs = await list_network_requests(url_filter="api/search")
samples = []
for req in reqs["requests"][:10]:
full = await get_network_request(req["id"])
url = full["url"]
xbogus = _parse_url_param(url, "X-Bogus")
if xbogus:
samples.append({
"id": f"req_{req['id']}",
"input": {"url": _strip_param(url, "X-Bogus")},
"expected": {"X-Bogus": xbogus},
})
# 步骤 2 — 验证
r = await verify_signer_offline(
signer_code="(s) => ({'X-Bogus': mySign(s.input.url)})",
samples=samples,
compare_params=["X-Bogus"],
)
```
---
> **SKILL.md 核心层**有工具分类索引的精简版
> **各工具的详细用法**:查看工具自身的 docstring
+417
View File
@@ -0,0 +1,417 @@
# 路径 A:算法追踪 — 四板斧完整方法论
> **触发条件**:Phase 2 识别出反爬为签名型/行为型且确认用路径 A 时读此文档
>
> **前置要求**:已完成反爬类型三分法识别(见 SKILL.md「反爬类型识别与工具选择」段)
>
> **版本**:v3.1.0(MCP v0.9.0 工具名迁移)
---
## 一、四板斧总览
路径 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', ...)` <!-- v3.1.0: migrated from freeze_prototype --> | 请求链路劫持、动态 Cookie、加密原语入口 | VM 内部自实现的 MD5/AES | 行为型 ✅ / 纯混淆 ✅ / 签名型 ❌ |
| 第二斧 | 插桩解释器 | `hook_function(path, mode='trace', ...)` <!-- v3.1.0: migrated from trace_function --> + `hook_jsvmp_interpreter(mode='proxy', trackProps=True)` <!-- v3.1.0: migrated from trace_property_access --> | 能识别分发函数名时的调用链追踪 | 匿名 IIFE 包裹 + 高频日志爆炸 | 行为型 ✅ / 纯混淆 ✅ / 签名型 ❌ |
| 第三斧 | 日志分析 | `get_jsvmp_log` + 反向追踪 | 已能捕获签名值 I/O 时反推公式 | 签名完全不出 VM 的黑箱模式 | 所有类型 ✅(纯被动分析,无副作用) |
| 第四斧 | 源码级插桩 | `instrumentation(action='install', ...)` <!-- v3.1.0: migrated from instrument_jsvmp_source --> + `instrumentation(action='log', ...)` <!-- v3.1.0: migrated from get_instrumentation_log --> | VM 内部调度、"全部在 opcode dispatch 循环里发生"的场景,`hot_keys` 直接暴露环境指纹集 | 极限大文件(5MB+)regex 模式覆盖率下降;AST 模式需 CDN | 所有类型 ✅,**签名型首选** |
### 按反爬类型的适用性
| 反爬类型 | 可用板斧 | 说明 |
|----------|----------|------|
| **签名型**(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 模式覆盖率高,页面能联网时优先)
步骤 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='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,
csp_bypass=True
)
<!-- v3.1.0: migrated from instrument_jsvmp_source -->
模式选择:
- mode="ast"(默认推荐):MCP 侧 esprima 解析,99% 覆盖率,挑战页可用
- mode="regex":纯正则改写,80% 覆盖率,无 CDN 依赖,大文件安全
- AST 失败时自动 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 要留下
```
#### 步骤 11:读取插桩日志(指纹学习的金矿)
```
MCP 操作:
# 读取属性访问 hot_keys
instrumentation(action='log', tag='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='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='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 失败模式与降级
### 常见失败模式
| 失败表现 | 可能原因 | 应对 |
|----------|----------|------|
| `instrumentation(action='log')` 返回空 | CSP 阻断 / url_pattern 未匹配 | 检查 CSP;确认 url_pattern 匹配到目标脚本 |
| `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")
→ 失败
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 前必须在 Session 档案中记录完整降级路径
- **禁止从 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 专项)
1. **VM 解释器本身不是目标,签名函数的 I/O 才是目标** — 不要试图反编译字节码
2. **先 Hook 出口确定"要什么",再 Hook 入口确定"给了什么"** — 出口驱动分析
3. **`hook_function(path, mode='trace', ...)` 对高频调用函数日志量可能爆炸** <!-- v3.1.0: migrated from trace_function --> — 必须设置 `max_captures` 限制
4. **`get_trace_data` 返回的海量数据需要本地过滤** — 用反向追踪法效率最高
5. **`instrumentation(action='install', mode="regex")` 对带模板字符串、正则字面量的代码可能误改写** <!-- v3.1.0: migrated from instrument_jsvmp_source --> — AST 模式更可靠
6. **前三板斧对签名型反爬不可用** — Hook `Function.prototype.apply` 会改变 toString 原生性;Proxy 在 navigator 上会被检测
7. **`dump_jsvmp_strings` 前提是字符串未被动态解密** — 看到 `decoded_strings` 全是单字母乱码就是加密的
8. **`search_code(keyword='while')` 在超大文件(380KB+)会返回大量无关结果** — 应使用 `search_code(keyword, script_url=url)` <!-- v3.1.0: migrated from search_code_in_script --> 配合更精确关键词
9. **源码级插桩的两种模式都默认 `cache_rewritten=True`** — 避免重复改写
---
> **完整工作流参考**:SKILL.md「Phase 2.2+ JSVMP 专项分析」段
> **源码级插桩专项指南**:references/jsvmp-source-instrumentation.md
> **经验案例**:cases/universal-vmp-source-instrumentation.md
+579
View File
@@ -0,0 +1,579 @@
# 路径 B:环境伪装 — jsdom/vm 沙箱六步法完整方法论
> **触发条件**:Phase 2 选择路径 B(jsdom/sdenv/vm 沙箱环境伪装)时读此文档
>
> **前置要求**:已完成反爬类型三分法识别 + 路径选择决策树(见 SKILL.md)
>
> **版本**:v3.1.0(MCP v0.9.0 工具名迁移)
---
## 一、核心思想
不分析 JSVMP 内部算法,而是在 jsdom 中完整运行原始 JSVMP 字节码,通过「采集 → 对比 → 补丁」使 JSVMP 认为自己运行在真实浏览器中,直接截获生成的签名值。
### 适用条件
- JSVMP 与浏览器环境深度绑定(环境指纹参与签名哈希)
- JSVMP 劫持了请求链路(XHR/fetch 拦截器),签名在内部完成
- 算法不可直接提取(字节码保护 + 自定义算法)
- jsdom 可以成功加载并执行 JSVMP 脚本
### 典型用时
2-4 小时(对比路径 A 的 4-8 小时)
---
## 二、步骤 0.5:确认签名函数入口(v2.7.0 新增)
> ⚠️ 跳过此步骤可能导致:只 Hook 了 XHR 漏掉 fetch 通道的签名、未传 cacheOpts 导致拦截器不触发、误以为签名函数不存在而走了更复杂的路径。
### MCP 操作
```
search_code(keyword="frontierSign", script_url='<VMP脚本URL>')
<!-- v3.1.0: migrated from search_code_in_script -->
→ 是否有导出签名函数
search_code(keyword="XMLHttpRequest.prototype.open", script_url='<VMP脚本URL>')
<!-- v3.1.0: migrated from search_code_in_script -->
→ 是否劫持 XHR
search_code(keyword="window.fetch", script_url='<VMP脚本URL>')
<!-- v3.1.0: migrated from search_code_in_script -->
→ 是否劫持 fetch
search_code(keyword="cacheOpts", script_url='<VMP脚本URL>')
<!-- v3.1.0: migrated from search_code_in_script -->
→ 是否使用新版初始化
```
### 判定结果与后续路径
| 判定 | 签名入口类型 | 后续策略 |
|------|-------------|----------|
| 仅 XHR 拦截器 | 单通道截获 | Hook XHR.open 即可 |
| XHR + fetch 双通道拦截 | 双通道截获 | 需同时 Hook 两个通道 |
| 导出函数 + 拦截器 | 优先直接调用 | 优先调用导出函数,拦截器作为备选 |
| 含 cacheOpts 初始化 | 需配置初始化 | 必须传入 cacheOpts 配置才能注册业务路径 |
---
## 三、步骤 1:Camoufox 环境指纹采集
### 目标
用 Camoufox 反检测浏览器采集真实浏览器的完整环境指纹,作为 jsdom 补丁的基准。
### MCP 操作
```
launch_browser({headless: false, os_type: "macos", locale: "zh-CN"})
navigate({url: "目标页面", wait_until: "domcontentloaded"})
compare_env()
→ 采集主流环境基准数据(navigator/screen/canvas/WebGL/Audio/timing)
```
### 分批采集细粒度环境值(4-5 批次)
> ⚠️ 单次 evaluate_js 代码太长会报错,必须分批采集
#### 批次 A:navigator 属性(24 项)
```
evaluate_js(`JSON.stringify({
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
languages: navigator.languages,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
hardwareConcurrency: navigator.hardwareConcurrency,
maxTouchPoints: navigator.maxTouchPoints,
vendor: navigator.vendor,
vendorSub: navigator.vendorSub,
productSub: navigator.productSub,
appVersion: navigator.appVersion,
appName: navigator.appName,
appCodeName: navigator.appCodeName,
onLine: navigator.onLine,
webdriver: navigator.webdriver,
pdfViewerEnabled: navigator.pdfViewerEnabled,
deviceMemory: navigator.deviceMemory,
plugins_length: navigator.plugins.length,
mimeTypes_length: navigator.mimeTypes.length,
connection: navigator.connection ? {
effectiveType: navigator.connection.effectiveType,
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt
} : null,
userAgentData: navigator.userAgentData ? {
brands: navigator.userAgentData.brands,
mobile: navigator.userAgentData.mobile,
platform: navigator.userAgentData.platform
} : null,
getBattery: typeof navigator.getBattery,
mediaDevices: typeof navigator.mediaDevices
})`)
```
#### 批次 B:screen + window 属性(25 项)
```
evaluate_js(`JSON.stringify({
screen_width: screen.width,
screen_height: screen.height,
screen_availWidth: screen.availWidth,
screen_availHeight: screen.availHeight,
screen_colorDepth: screen.colorDepth,
screen_pixelDepth: screen.pixelDepth,
screen_orientation: screen.orientation ? screen.orientation.type : null,
window_innerWidth: window.innerWidth,
window_innerHeight: window.innerHeight,
window_outerWidth: window.outerWidth,
window_outerHeight: window.outerHeight,
window_devicePixelRatio: window.devicePixelRatio,
window_screenX: window.screenX,
window_screenY: window.screenY,
chrome_exists: typeof window.chrome,
chrome_runtime: window.chrome ? typeof window.chrome.runtime : null,
Notification: typeof window.Notification,
Worker: typeof window.Worker,
SharedWorker: typeof window.SharedWorker,
RTCPeerConnection: typeof window.RTCPeerConnection,
AudioContext: typeof window.AudioContext,
OfflineAudioContext: typeof window.OfflineAudioContext,
fetch_exists: typeof window.fetch,
matchMedia: typeof window.matchMedia,
indexedDB: typeof window.indexedDB
})`)
```
#### 批次 C:document + performance + toString + Function.toString(28 项)
```
evaluate_js(`JSON.stringify({
doc_hasFocus: document.hasFocus(),
doc_readyState: document.readyState,
doc_visibilityState: document.visibilityState,
doc_hidden: document.hidden,
doc_characterSet: document.characterSet,
doc_compatMode: document.compatMode,
doc_contentType: document.contentType,
doc_designMode: document.designMode,
doc_referrer: document.referrer,
doc_title: document.title,
doc_domain: document.domain,
doc_cookie_type: typeof document.cookie,
perf_timing: typeof performance.timing,
perf_navigation: typeof performance.navigation,
perf_now: typeof performance.now,
perf_memory: typeof performance.memory,
fn_toString_createElement: document.createElement.toString(),
fn_toString_getElementById: document.getElementById.toString(),
fn_toString_querySelector: document.querySelector.toString(),
fn_toString_addEventListener: EventTarget.prototype.addEventListener.toString(),
fn_toString_fetch: typeof window.fetch === 'function' ? window.fetch.toString() : null,
fn_toString_setTimeout: window.setTimeout.toString(),
obj_toString_document: Object.prototype.toString.call(document),
obj_toString_window: Object.prototype.toString.call(window),
obj_toString_navigator: Object.prototype.toString.call(navigator),
obj_toString_screen: Object.prototype.toString.call(screen),
symbol_toStringTag_doc: document[Symbol.toStringTag],
symbol_toStringTag_screen: screen[Symbol.toStringTag]
})`)
```
#### 批次 D:DOM 布局 + Canvas + WebGL + Audio(指纹检测类)
```
evaluate_js(`(function(){
var d = document.createElement('div');
d.style.cssText = 'width:100px;height:100px;position:absolute;left:-9999px';
document.body.appendChild(d);
var layout = {
offsetHeight: d.offsetHeight,
offsetWidth: d.offsetWidth,
clientHeight: d.clientHeight,
clientWidth: d.clientWidth,
rect: d.getBoundingClientRect()
};
document.body.removeChild(d);
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('test', 2, 2);
var canvasData = canvas.toDataURL();
var gl = document.createElement('canvas').getContext('webgl');
var webglInfo = gl ? {
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
version: gl.getParameter(gl.VERSION)
} : null;
return JSON.stringify({
layout: layout,
canvas_hash: canvasData.length,
webgl: webglInfo,
audioContext: typeof AudioContext,
offlineAudioContext: typeof OfflineAudioContext
});
})()`)
```
### 注意事项
- Camoufox 的 C++ 引擎级指纹伪装提供「干净基准」,比普通 Chrome 更可靠
- `compare_env` 不覆盖 Function.prototype.toString / Symbol.toStringTag / DOM 布局等细粒度检测,这些必须通过 evaluate_js 手动采集
- 采集代码模板参考 cases/ 经验案例中的「浏览器指纹采集方法」段
---
## 四、步骤 2:jsdom 环境采集
### 目标
在本地 Node.js 中创建 jsdom 实例,执行与步骤 1 完全相同的采集代码。
### 实现
```javascript
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: '目标URL',
pretendToBeVisual: true,
runScripts: 'dangerously'
});
const win = dom.window;
// 在 win.eval() 中运行与步骤 1 完全相同的采集脚本
// 批次 A
const navResult = win.eval(`JSON.stringify({
userAgent: navigator.userAgent,
platform: navigator.platform,
// ... 与步骤 1 批次 A 完全相同
})`);
// 批次 B、C、D 同理
```
### 关键点
- jsdom 实例的 `url` 参数必须设为目标 URL(影响 document.URL / document.domain)
- `pretendToBeVisual: true` 使 `requestAnimationFrame` 可用
- `runScripts: 'dangerously'` 允许执行脚本(环境伪装必需)
---
## 五、步骤 3:Diff 与严重性分级
### 差异分级标准
#### 致命级 — 缺失即被服务端拒绝
| 检测项 | 浏览器值 | jsdom 默认值 | 影响 |
|--------|----------|-------------|------|
| `Function.prototype.toString` | `function createElement() { [native code] }` | 暴露 jsdom 实际 JS 代码 | 直接暴露非浏览器环境 |
| `navigator.plugins.length` | 5(典型值) | 0 | 插件数为 0 = 非浏览器 |
| `navigator.webdriver` | `false` | `undefined` | webdriver 检测 |
| `document.hasFocus()` | `true` | `false` | 焦点检测 |
| DOM `offsetHeight/Width` | 非零值 | 0 | 布局检测 |
#### 高危级 — 可能参与指纹哈希
| 检测项 | 浏览器值 | jsdom 默认值 |
|--------|----------|-------------|
| `Object.prototype.toString.call(document)` | `[object HTMLDocument]` | `[object Document]` |
| `window.chrome` | `{runtime: {...}, ...}` | `undefined` |
| `navigator.userAgentData` | `{brands: [...], ...}` | `undefined` |
| `performance.timing/navigation` | 完整对象 | 缺失 |
| `Symbol.toStringTag` | 正确标签 | 不正确 |
#### 中危级 — API 存在性检测(30+ 缺失 API)
```
window.Notification / Worker / SharedWorker / RTCPeerConnection
window.AudioContext / OfflineAudioContext / fetch
window.matchMedia / indexedDB / caches / visualViewport
navigator.connection / getBattery / deviceMemory
```
### 参考
完整的差异表和修复代码模板见 references/jsdom-env-patches.md。
---
## 六、步骤 4:patchEnvironment() 实现
### 修复优先级排序
按以下优先级逐项修复,每修一项立即验证:
#### ① markNative 三层防御(最高优先级)
```
核心问题:jsdom 所有 DOM 方法的 toString() 会暴露实际 JS 代码
解决方案:WeakSet + 源码正则 + 实例覆写 + 50+ 原型链扫描
三层防御:
Layer 1: WeakSet 记录所有需要伪装的函数引用
Layer 2: 源码模式正则匹配(function X() { [native code] })
Layer 3: 实例级覆写 toString(兜底)
⚠️ Firefox vs Chrome 格式差异:
Chrome: "function createElement() { [native code] }"
Firefox: "function createElement() {\n [native code]\n}"
→ Camoufox 基于 Firefox 内核,必须使用 Firefox 格式
```
#### ② navigator 补丁
```
修复项:
- plugins: 完整 PluginArray/Plugin/MimeType 对象树(length=5)
- webdriver: false
- userAgentData: {brands: [...], mobile: false, platform: "macOS"}
- connection: {effectiveType: "4g", downlink: 10, rtt: 50}
- 每个属性经 markNative 处理
```
#### ③ window 补丁
```
修复项:
- chrome 对象: {runtime: {}, app: {}, ...}
- 30+ API 存根: Notification / Worker / SharedWorker / RTCPeerConnection /
AudioContext / OfflineAudioContext / fetch / matchMedia / indexedDB / caches /
visualViewport 等
- 每个存根经 markNative 处理
```
#### ④ document + performance 补丁
```
修复项:
- hasFocus(): 始终返回 true
- readyState: "complete"
- visibilityState: "visible"
- hidden: false
- performance.timing: 完整 PerformanceTiming 对象
- performance.navigation: {type: 0, redirectCount: 0}
```
#### ⑤ DOM 布局属性
```
修复项:
- offsetHeight/offsetWidth: 返回非零值
- clientHeight/clientWidth: 返回非零值
- getBoundingClientRect(): 返回合理的 DOMRect
- scrollHeight/scrollWidth: 返回非零值
```
#### ⑥ Symbol.toStringTag 全面修复
```
修复项:
- document → "HTMLDocument"(而非 "Document")
- screen → "Screen"
- navigator → "Navigator"
- window → "Window"
- 通过 Object.defineProperty 设置 Symbol.toStringTag
```
### 完整代码模板
参考 references/jsdom-env-patches.md 获取每个修复模块的完整实现代码。
---
## 七、步骤 5:内部验证(win.eval)
### 目标
从 jsdom 内部(win.eval)验证所有检测点通过。
### 关键
验证代码必须在 jsdom 的 window 上下文中执行(win.eval),因为 JSVMP 看到的就是这个上下文。
### 验证代码
```javascript
const result = win.eval(`JSON.stringify({
fnToString: document.createElement.toString(),
plugins: navigator.plugins.length,
webdriver: navigator.webdriver,
hasFocus: document.hasFocus(),
chromeExists: typeof window.chrome,
docTag: Object.prototype.toString.call(document),
offsetTest: (function(){
var d = document.createElement('div');
d.style.cssText = 'width:100px;height:100px';
document.body.appendChild(d);
var h = d.offsetHeight;
document.body.removeChild(d);
return h;
})()
})`);
// 预期结果:
// fnToString 包含 [native code](Firefox 格式含换行)
// plugins = 5
// webdriver = false
// hasFocus = true
// chromeExists = 'object'
// docTag = '[object HTMLDocument]'
// offsetTest > 0
```
### 验证清单
| 检测项 | 预期值 | 不通过时的修复模块 |
|--------|--------|-------------------|
| `document.createElement.toString()` | 含 `[native code]` | markNative 三层防御 |
| `navigator.plugins.length` | 5 | navigator 补丁 |
| `navigator.webdriver` | `false` | navigator 补丁 |
| `document.hasFocus()` | `true` | document 补丁 |
| `typeof window.chrome` | `'object'` | window 补丁 |
| `Object.prototype.toString.call(document)` | `'[object HTMLDocument]'` | Symbol.toStringTag 修复 |
| `div.offsetHeight` | > 0 | DOM 布局属性 |
---
## 八、步骤 6:端到端验证
### 目标
在 jsdom 中加载完整 JSVMP 脚本,触发签名生成,用截获的签名值发起真实接口请求。
### 验证流程
```
1. 在 jsdom 中加载完整 JSVMP 脚本并触发签名生成
2. 用截获的签名值发起真实接口请求
3. 确认返回有效数据(非空 body / 非错误码)
4. 连续多次请求验证稳定性(至少 5 次)
```
### 注意事项
- ⚠️ 服务端可能静默拒绝(返回 HTTP 200 + 空 body,不报错)— 这是环境检测失败的信号
- 连续 5 次请求全部返回有效数据才算通过
- 如果间歇性失败,检查是否有时间戳相关的环境值未补齐
---
## 九、环境伪装还原策略表
| 情况 | 策略 | 实现方式 |
|------|------|---------|
| JSVMP 劫持 XHR,在拦截器中追加签名 | 「喂入-截出」(feed-intercept) | jsdom + XHR Hook → 在 jsdom 内发 XHR,拦截器自动追加签名,Hook 截获 |
| JSVMP 导出签名函数到 window | 直接调用导出函数 | jsdom 加载 JSVMP → `win.签名函数(参数)` → 获取签名 |
| JSVMP + 预热初始化(如 SdkGlueInit) | 完整初始化链路 | jsdom 依次加载所有脚本 → 调用初始化函数 → 再触发签名生成 |
| JSVMP + cacheOpts 配置 | 配置驱动初始化 | 传入 cacheOpts 配置 → 注册业务路径 → 拦截器才会触发 |
| 双签名(URL 参数 + header) | 双通道截获 | 同时 Hook XHR.open(URL 签名)+ XHR.setRequestHeader(header 签名) |
---
## 十、Firefox vs Chrome 变体差异
> Camoufox 基于 Firefox 内核,与 Chrome 在多个环境值上存在差异。jsdom 补丁必须匹配采集基准浏览器的格式。
### 关键差异
| 检测项 | Chrome 格式 | Firefox (Camoufox) 格式 |
|--------|------------|------------------------|
| `Function.prototype.toString` (native) | `"function name() { [native code] }"` | `"function name() {\n [native code]\n}"` |
| `navigator.vendor` | `"Google Inc."` | `""` |
| `navigator.vendorSub` | `""` | `""` |
| `navigator.productSub` | `"20030107"` | `"20100101"` |
| `window.chrome` | 完整对象 | `undefined`(需补丁) |
| `navigator.userAgentData` | 完整对象 | `undefined`(Firefox 不支持) |
### 修复策略
- 如果目标站点的 JSVMP 是在 Chrome 环境下开发的,可能期望 Chrome 格式
- 但 Camoufox 采集的基准是 Firefox 格式
- **原则**:以 Camoufox 采集的基准为准,因为 Camoufox 的 C++ 引擎级伪装能通过反检测
- 如果服务端明确校验 Chrome 特征(如 `window.chrome` 必须存在),则需要额外补丁
---
## 十一、常见踩坑与解决
### 坑 1:Canvas 存根不够真实
```
问题:jsdom 的 canvas 返回空白数据,指纹哈希不匹配
解决:使用 node-canvas 或 @napi-rs/canvas 提供真实 Canvas 实现
或者从 Camoufox 采集 canvas 指纹后硬编码返回值
```
### 坑 2:native code 多行格式
```
问题:markNative 输出了 Chrome 单行格式,但 Camoufox 基准是 Firefox 多行格式
解决:将 markNative 的返回值从
"function name() { [native code] }"
改为
"function name() {\n [native code]\n}"
```
### 坑 3:Symbol.toStringTag 遗漏
```
问题:Object.prototype.toString.call(document) 返回 [object Document] 而非 [object HTMLDocument]
解决:Object.defineProperty(Document.prototype, Symbol.toStringTag, {
get: () => 'HTMLDocument', configurable: true
})
```
### 坑 4:jsdom 环境补丁必须在 JSVMP 脚本加载前完成
```
问题:JSVMP 在加载时就读取环境值,补丁打晚了无效
解决:确保 patchEnvironment() 在任何 JSVMP 脚本 eval 之前执行
XHR Hook 的安装顺序也决定能否截获最终 URL
```
### 坑 5:服务端静默拒绝
```
问题:返回 HTTP 200 + 空 body(不报错),看起来请求成功但没数据
信号:环境检测失败,签名格式正确但环境指纹不匹配
解决:回到步骤 3 重新 diff,检查是否有遗漏的致命级差异
```
### 坑 6:cacheOpts 未传入
```
问题:拦截器不触发,签名不生成
信号:旧版只需 bdms.paths,新版必须同时传入 cacheOpts
解决:检查 _SdkGlueInit 调用是否包含 cacheOpts 字段
确认 cacheOpts.paths 覆盖目标 API 路径
```
---
## 十二、JSVMP 核心经验(路径 B 专项)
1. **环境伪装优先于算法追踪** — 路径 B 通常比路径 A 快 10 倍
2. **Function.prototype.toString 是 jsdom 环境伪装的第一杀手** — 三层防御必须到位
3. **环境对比要分批采集** — 单次 evaluate_js 代码太长会报错
4. **jsdom 环境补丁必须在 JSVMP 脚本加载前完成** — 加载顺序决定成败
5. **服务端静默拒绝是环境检测失败的信号** — HTTP 200 + 空 body
6. **Firefox 格式 native code 与 Chrome 不同** — 必须匹配采集基准浏览器的格式
7. **cacheOpts 是新版 SDK 初始化的必传项** — 缺少会导致拦截器不触发
8. **环境伪装前先确认签名函数入口** — 步骤 0.5 不可跳过
---
> **环境补丁代码模板**:references/jsdom-env-patches.md
> **经验案例**:cases/jsvmp-xhr-interceptor-env-emulation.md、cases/jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox.md
> **完整工作流**:SKILL.md「路径 B:环境伪装」段
+586
View File
@@ -0,0 +1,586 @@
# Phase 0-5 详细操作手册
> **触发条件**:执行某个 Phase 不确定具体怎么做时读
>
> **前置要求**:已完成硬约束 Checklist 五项复述(见 SKILL.md 顶部)
>
> **版本**:v3.1.0(MCP v0.9.0 工具名迁移)
---
## Phase 0:任务理解与调试环境搭建
### 0.1 任务理解
收到用户的目标 URL 和分析需求后:
1. **明确分析目标**:需要还原哪些加密参数、目标数据是什么
2. **接口分析**:
- 梳理请求的 URL、Method、Headers、Params、Body
- 识别并标记签名/动态参数(如 `sign`、`token`、`timestamp`、`nonce`)
- 根据参数特征(长度、字符集、结构)给出算法初步判断
- 以简洁表格汇总分析结果,与用户确认
### 0.2 浏览器搭建
根据用户是否提供 Cookie,选择对应路径:
#### 路径 A:带 Cookie 启动
```
MCP 操作:
launch_browser(headless=false)
→ 启动反检测浏览器(有头模式,方便用户观察)
navigate(url="目标URL")
→ 导航到目标页面
cookies(action='set', cookies=[...])
<!-- v3.1.0: migrated from set_cookies -->
→ 写入 Cookie
reload()
→ 刷新使 Cookie 生效
evaluate_js(expression="document.cookie")
→ 验证写入成功
```
#### 路径 B:直接启动(无 Cookie)
```
MCP 操作:
launch_browser(headless=false)
navigate(url="目标URL")
→ 直接进入后续分析阶段
```
### 0.3 Cookie 写入方法
用户提供的 Cookie 可能有多种格式:
| 格式 | 处理方式 |
|------|---------|
| Cookie 字符串 `"k1=v1; k2=v2"` | 拆分后构造 `[{name:"k1", value:"v1", domain:".example.com", path:"/"}]` |
| JSON 格式(EditThisCookie 导出) | 直接传入 `cookies(action='set', ...)` <!-- v3.1.0: migrated from set_cookies --> |
| Request Headers 中的 Cookie 字段 | 按 `"; "` 拆分后构造数组 |
### 0.4 项目目录创建
以目标网站/功能命名,结构参考 `templates/` 下的模板:
**Node.js 项目:**
```
project_name/
├── config/
│ ├── encrypt.js # 提取的原始 JS 加密代码
│ ├── keys.json # 密钥、IV、Salt 等配置
│ └── headers.json # 请求头模板
├── utils/
│ ├── encrypt.js # 加密参数生成函数
│ └── request.js # 请求封装
├── main.js # 主脚本
├── package.json
└── README.md
```
**Python 项目:**
```
project_name/
├── config/
│ ├── sign_logic.js # 提取的签名逻辑代码
│ ├── keys.json # 密钥配置
│ └── headers.json # 请求头模板
├── utils/
│ ├── sign.py # 签名函数封装
│ └── request.py # 请求封装
├── main.py
├── requirements.txt
└── README.md
```
---
## Phase 0.5:经验库命中验证
> ⚠️ 本步骤在硬约束 Checklist 的 [CHECK-2] + [CHECK-3] 中已经预执行过。
> 这里是 Phase 0 完成后做二次确认和深入。
### 四分支决策
```
┌─ [CHECK-2] 命中案例?
│ └─ YES → 分支 1:命中 cases/
│
├─ [CHECK-2] 命中工作区 site_* 目录?
│ └─ YES → 分支 2:命中 site_*
│
├─ [CHECK-3] 历史 session 存在?
│ └─ YES → 分支 3:有历史 session
│
└─ 全部未命中 → 分支 4:全未命中(30 秒指纹采集)
```
### 分支 1:命中 cases/
```
MCP 操作:
readFile("cases/<命中案例>.md")
→ 详读已验证定位路径
→ 按该案例的 Phase 1-5 执行(不要自创流程)
→ 加 1-2 条本次站点特有的断言到 session(区别已有变体)
```
### 分支 2:命中 site_* 目录
```
MCP 操作:
listDirectory("site_<target>/")
→ 识别 sign_service.js / cookie_fetcher.js / parser.js / keys.json 等文件
若有 sign_service.js(已有协议方案):
→ 直接基于 sign_service.js 建新目录复用
→ 不要从零写新方案
→ 不要用 Playwright/Camoufox 过挑战(违反红线 3)
若只有 cookie_fetcher.js(浏览器兜底方案):
→ 警告:这是违反红线 3 的历史残留
→ 本次目标应是替换为协议方案
```
### 分支 3:有历史 session
```
MCP 操作:
attach_domain_readonly("<eTLD+1>") # 在 CHECK-3 已做
verify_assertion(assertion_id='*') # 批量验证所有断言
<!-- v3.1.0: migrated from reverify_all_assertions_on_domain -->
根据 triage.interpretation:
"all assertions still hold"
→ 跳到 Phase 5 直接验证旧协议代码
"Minor update"
→ 工作范围 = failed_ids,只修这几条
"Moderate L4 change"
→ Phase 1-4 走一遍,但 passed 的是 invariants
"Major L4 upheaval"
→ 重做,但 passed 断言仍是不变量
```
### 分支 4:全未命中(30 秒指纹采集)
```
MCP 操作(这些命令在 CHECK-2 时已跑过,这里是深入):
# JSVMP / SDK 特征识别
search_code(keyword="webmssdk") # webmssdk 家族
search_code(keyword="byted_acrawler") # webmssdk 家族
search_code(keyword="_sdkGlueInit") # 抖音特征
search_code(keyword="cacheOpts") # TikTok 特征
search_code(keyword="sdenv") # 瑞数 RS 特征
search_code(keyword="meta-12") # RS 6 特征
search_code(keyword="FSSBBIl1UgzbN7N") # RS cookie 特征
# 签名参数特征识别
search_code(keyword="a_bogus") # 抖音
search_code(keyword="X-Bogus") # TikTok 国际版
search_code(keyword="X-Gnarly") # TikTok 国际版
search_code(keyword="acw_sc__v2") # Aliyun WAF
# 环境检测 SDK
search_code(keyword="acmescripts") # Akamai
# 反爬类型关键字
get_network_request(request_id=<first_request>)
list_network_requests(resource_type="script")
```
采集完后,**必须**在本次分析结束时沉淀新案例:
1. 按 `cases/_template.md` 格式建立 `cases/<新案例名>.md`
2. 更新 `cases/README.md` 的"高频站点速查表"追加一行
3. 更新 `cases/README.md` 的"变体关系图"(如有兄弟案例)
---
## Phase 1:目标侦察
### 1.1 确认调试页面状态
```
MCP 操作:
take_screenshot()
→ 截取当前页面视觉状态,确认页面正常
如需导航到特定子页面:
navigate(url="目标子页面")
如果涉及登录态,确认 Cookie 已写入且页面内容正确
```
### 1.2 网络请求捕获
```
MCP 操作:
network_capture(action='start')
<!-- v3.1.0: migrated from start_network_capture -->
→ 开始捕获网络流量
evaluate_js / click / type_text
→ 触发翻页/交互,产生请求
list_network_requests()
→ 获取捕获的请求列表(支持过滤)
get_network_request(request_id=N)
→ 获取关键接口的详细信息
get_request_initiator(request_id=N)
→ 获取发起请求的 JS 调用栈(黄金路径!)
重点关注:
- Request URL、Method
- Request Headers(Cookie、自定义签名头)
- Query Params / Request Body(识别加密参数)
- Response 数据结构
- Initiator Stack(直接定位加密函数)
```
### 1.3 加密参数识别
对比多次请求,分析每个参数:
| 参数类型 | 特征 | 处理方式 |
|----------|------|---------|
| 固定值 | 每次请求相同 | 直接硬编码或从页面提取 |
| 动态值 | 有规律变化 | 判断变化因子(时间戳、页码、随机数、自增) |
| 加密值 | 看似随机 | 根据长度、字符集、格式判断算法类型 |
### 1.4 输出侦察报告
```
📋 目标信息
━━━━━━━━━━━━━━━━━━━━━━━━
目标网站:[URL]
分析目标:[需要还原的加密逻辑]
数据接口:[API endpoint]
🔗 接口参数分析
━━━━━━━━━━━━━━━━━━━━━━━━
URL:[完整请求URL]
Method:GET/POST
Headers:
- Cookie: [关键字段及示例]
- [自定义头]: [示例值]
加密参数:
- 参数名: [名称] | 示例值: [值] | 长度: [N] | 字符集: [hex/base64/...] | 初步猜测: [算法]
📊 响应数据样本
━━━━━━━━━━━━━━━━━━━━━━━━
[前2-3条数据]
🧠 技术分析要点
━━━━━━━━━━━━━━━━━━━━━━━━
本目标涉及的签名分析技术点:
1. [如:OB混淆还原]
2. [如:动态Cookie生成]
3. [如:AES-CBC加密]
```
---
## Phase 2:源码分析
### 2.1 关键词搜索定位
```
MCP 操作:
search_code(keyword="加密参数名")
→ 直接在已加载的 JS 源码中搜索
search_code(keyword="encrypt|sign|token|md5|sha|aes|des|rsa|hmac|btoa|atob|CryptoJS")
search_code(keyword="XMLHttpRequest|$.ajax|fetch|beforeSend")
search_code(keyword="document.cookie")
根据搜索结果:
scripts(action='get', url='<脚本URL>')
<!-- v3.1.0: migrated from get_script_source -->
→ 读取包含加密逻辑的源码片段
scripts(action='save', url='<脚本URL>', save_path='./config/target.js')
<!-- v3.1.0: migrated from save_script -->
→ 保存关键脚本到本地分析
```
### 2.2 代码混淆识别
| 混淆类型 | 特征 | 还原策略 |
|---------|------|---------|
| OB 混淆 | `_0x` 前缀变量、十六进制字符串数组 | 字符串解密 + 变量重命名 |
| 控制流平坦化 | `switch-case` 状态机、`while(true)` 循环 | 追踪状态转移还原执行顺序 |
| eval/Function 打包 | `eval(...)` 或 `new Function(...)` 包裹 | Hook eval/Function 拦截源码 |
| JSVMP | 200KB+ 文件、自定义解释器 | 不反编译,走路径 A 或路径 B |
### 2.2+ JSVMP 专项分析
当识别到 JSVMP 时,**严禁尝试反编译字节码**。
#### JSVMP 识别标志
- 超大 JS 文件(200KB+),函数/变量名完全无意义
- 包含自定义解释器循环:`while(true) { switch(opcode) { ... } }`
- 改写或劫持浏览器原生 API(XHR / fetch / Cookie)
- 超大数组(字节码)+ 指针变量 + 栈操作 + 跳转指令
#### 路径选择决策树
```
决策树:
├─ JSVMP 是否劫持了请求链路(XHR/fetch 拦截器)?
│ ├─ YES + 算法与环境指纹深度绑定(如 a_bogus)
│ │ → 优先选路径 B(环境伪装)
│ │ → 路径 B 失败时回退路径 A
│ └─ YES 但签名逻辑相对独立
│ → 路径 A(算法追踪),提取签名函数
│
├─ JSVMP 仅生成签名参数(不劫持请求)?
│ ├─ Hook 确认使用标准算法 → 路径 A,纯算法还原
│ └─ 算法完全自定义 + 环境依赖重 → 路径 B
│
└─ 无法判断 → 先快速测试路径 B(30 分钟),不行再走路径 A
```
#### 反爬类型前置判断
```
先判断 JSVMP 所在反爬类型:
┌─ navigate(url) 不加任何 hook,看 redirect_chain
│
├─ redirect_chain 反复 412 然后才到 200
│ → 签名型 JSVMP
│ → 路径 A 只能走第四板斧(源码级插桩)
│ → 前三板斧禁用(会破坏签名)
│
├─ redirect_chain 直接 200 但页面后续 XHR 带签名参数
│ → 行为型 JSVMP
│ → 路径 A 四板斧全开
│
└─ redirect_chain 直接 200 无特殊签名参数
→ 不是签名型也不是 JSVMP,走混淆还原
```
#### 调用链追踪
```
MCP 操作:
inject_hook_preset(preset="xhr") → 一键注入 XHR Hook
inject_hook_preset(preset="fetch") → 一键注入 Fetch Hook
reload() → 刷新页面触发 Hook
get_request_initiator(request_id=N) → 获取 JS 调用栈(黄金路径)
→ 从调用栈中逐层定位:请求发送 → 参数构造 → 加密函数 → 密钥/明文来源
set_breakpoint_via_hook(target_function="加密函数名")
get_breakpoint_data() → 获取捕获的参数、返回值和调用栈
```
#### 提取核心逻辑
```
MCP 操作:
scripts(action='save', url='<脚本URL>', save_path='./config/target.js')
<!-- v3.1.0: migrated from save_script -->
→ 保存完整脚本
手动提取关键函数到 config/encrypt.js
用中文注释标注每个函数的作用、输入输出
```
### 2.2++ 静态分析关键判断清单
在源码分析阶段,必须确认以下内容:
- [ ] 参数是单独加密还是整条请求链被接管(URL 重写 / 请求劫持)
- [ ] 页码、时间戳、随机数、Cookie、UA、环境变量是否参与运算
- [ ] 是否存在响应解密(接口返回加密字符串而非明文 JSON)
- [ ] 是否存在运行时代码生成(`eval` / `new Function`)
- [ ] 是否有前置请求(预热接口、Token 获取接口)
- [ ] 是否有请求链改写(拦截 XHR/fetch 添加签名头)
---
## Phase 3:动态验证
对静态分析的结论,在调试浏览器页面上进行运行时验证。
### 3.1 Hook 注入验证
```
MCP 操作:
inject_hook_preset(preset="xhr") → XHR Hook
inject_hook_preset(preset="fetch") → Fetch Hook
inject_hook_preset(preset="crypto") → 加密函数 Hook(btoa/atob/JSON.stringify)
hook_function(
function_path="自定义目标",
hook_code="...",
position="before|after|replace"
)
→ 自定义 Hook
reload()
→ 刷新触发 Hook
get_console_logs()
→ 读取 Hook 输出
```
### 3.2 断点与追踪确认
```
MCP 操作:
set_breakpoint_via_hook(target_function="加密函数路径")
→ 通过 JS Hook 设置伪断点
hook_function(
function_path="加密函数路径",
mode='trace',
log_args=True, log_return=True, log_stack=True
)
<!-- v3.1.0: migrated from trace_function -->
→ 追踪函数调用(不暂停执行)
触发目标操作:
evaluate_js / click / type_text → 模拟交互
get_breakpoint_data() → 获取伪断点捕获的参数和返回值
get_trace_data() → 获取函数追踪数据
重点确认:
- 加密算法的具体模式(AES 的 ECB/CBC、填充方式、密钥长度)
- 参数拼接顺序和格式
- 时间戳精度(秒 vs 毫秒)
- 密钥/IV 的来源(硬编码 vs 服务端返回 vs 动态计算)
```
### 3.3 多次请求对比
```
MCP 操作:
evaluate_js / click → 触发多次数据请求
list_network_requests() → 收集捕获的网络请求
→ 对比加密参数变化规律,确认变化因子
```
---
## Phase 4:算法还原
### 4.1 语言选择策略
| 维度 | 选 Node.js | 选 Python |
|------|-----------|-----------|
| 加密逻辑复杂度 | 自定义逻辑可直接用 `vm` 沙箱执行原始 JS | 标准算法可直接用 Python 库还原 |
| 团队技术栈 | 用户/团队偏好 Node.js | 用户/团队偏好 Python |
| JSVMP 场景 | VM 沙箱可直接加载整个 VM | 需 `execjs` 桥接 |
| TLS 指纹需求 | 需额外配置 | `curl_cffi` 一行搞定 |
### 4.2 解法模式选择
| 模式 | 适用场景 | Node.js 模板 | Python 模板 |
|------|---------|-------------|-------------|
| A: 纯算法还原 | 加密逻辑可完整提取,无浏览器环境依赖 | `templates/node-request/` | `templates/python-request/` |
| B: 沙箱执行 JS | 服务端返回混淆 JS 用于生成 Cookie/Token | `templates/vm-sandbox/` | `templates/python-request/`(用 `execjs`) |
| C: WASM 加载还原 | 加密逻辑在 WebAssembly 中实现 | `templates/wasm-loader/` | — |
| D: 浏览器自动化 | TLS 指纹检测、复杂环境依赖、无法脱离浏览器 | `templates/browser-auto/` | — |
| E: jsdom 环境伪装 | JSVMP 深度绑定环境指纹、算法不可提取 | jsdom + references/jsdom-env-patches.md | — |
### 4.3 编码原则
1. **先通后全**:先成功请求到第 1 页/第 1 条数据,验证加密正确后再扩展
2. **优先纯算法**:标准算法 Node.js 用 `crypto` / `crypto-js`,Python 用 `hashlib` / `pycryptodome` / `hmac`
3. **中间值对比**:打印关键中间值(明文、密文、签名),与浏览器抓包值逐一比对
4. **配置外置**:密钥、Headers 模板等写入独立配置文件
5. **错误处理**:包含重试机制、频率控制、异常告警
6. **逐步验证**:每次只增加一个参数的实现,确保每步可独立验证
7. **代码可运行**:提供的代码必须是可直接复制运行的,不留占位符
8. **分析产物持久化**:长参数值、Cookie、JS 代码片段、请求样本等,第一时间写入 `config/` 目录
### 4.4 配置文件策略
| 产物类型 | 存放位置 | 说明 |
|---------|---------|------|
| Cookie 字符串 | `config/cookies.txt` 或 `config/cookies.json` | 完整 Cookie |
| 长参数样本 | `config/params_sample.json` | 捕获的请求参数样本(含签名值) |
| 提取的 JS 代码 | `config/sign_logic.js` / `config/encrypt.js` | 签名/加密函数及其依赖 |
| Headers 模板 | `config/headers.json` | 完整的请求头 |
| 响应样本 | `config/response_sample.json` | 接口返回的数据样本 |
| 密文样本 | `config/ciphertext_samples.txt` | 多次请求的加密参数值 |
**核心原则**:分析过程中产生的任何长文本都应该立即持久化到 `config/` 文件中。后续代码只需「读取文件」而非「内联长字符串」。
---
## Phase 5:断言驱动交付(v2.9.0 升级)
### 5.1 运行验证
```
MCP 操作:
1. 运行 main.js / main.py,确认输出正确数据
2. 与浏览器实际数据交叉验证(≥ 5 次请求,确认签名稳定性)
```
### 5.2 写入/更新域级 Session 档案
```
MCP 操作:
a. 将本次分析的断言集写入 assertions.json
至少包含:script_exists / anti_crawl_type / cookie_names
b. 更新 session.json 的 hot_keys_snapshot 和 cookie_attribution_summary
c. 更新 fingerprint.json(如有新的指纹采集数据)
d. 更新 assertions_passed / assertions_failed / assertions_total
e. 输出:"域级 Session 档案已更新,N 条断言已写入,下次分析同域时将自动复用"
```
### 5.3 生成 README.md
记录以下内容:
- 目标信息与接口分析
- 加密逻辑还原过程
- 涉及的签名分析技术点
- 运行方式与依赖说明
### 5.4 经验沉淀
**主动询问用户是否沉淀经验**到 cases/(按 cases/_template.md 格式)。
沉淀内容包括:
- 反爬类型判定过程
- 关键技术点和踩坑记录
- 已验证的定位路径
- 环境补丁清单(如走路径 B)
### 5.5 断言驱动交付清单
| 交付项 | 必须 | 说明 |
|--------|------|------|
| 可运行的 main.js / main.py | ✅ | 纯协议脚本,无浏览器依赖 |
| config/ 目录 | ✅ | 密钥、Headers、JS 代码等配置 |
| README.md | ✅ | 项目说明 + 接口分析记录 |
| Session 档案更新 | ✅ | 断言集 + 指纹快照 + Cookie 归因 |
| cases/ 经验沉淀 | 推荐 | 主动询问用户 |
| ≥ 5 次请求验证 | ✅ | 确认签名稳定性 |
---
> **硬约束 Checklist**:SKILL.md 顶部
> **路径 A 详细操作**:SKILL.md「路径 A:算法追踪(四板斧)」段
> **路径 B 详细操作**:SKILL.md「路径 B:环境伪装(jsdom/vm 沙箱执行原始 JSVMP)」段
> **错误处理准则**:SKILL.md「错误处理准则」段
> **降级梯度表**:SKILL.md「AI 想放弃时的降级梯度表」段