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:
@@ -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 条反模式 |
|
||||
@@ -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 档案回答"这个域现在是什么状态",经验库回答"这类技术方案怎么破"。
|
||||
@@ -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
|
||||
@@ -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
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:环境伪装」段
|
||||
@@ -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 想放弃时的降级梯度表」段
|
||||
Reference in New Issue
Block a user