feat(v2.4.0): JSVMP 双路径攻克体系 + jsdom 环境补丁知识库

基于实战经验重构 JSVMP 分析框架:

- SKILL.md: 新增路径选择决策树(路径 A 算法追踪 vs 路径 B 环境伪装)
- SKILL.md: 路径 B「环境伪装六步法」完整方法论(采集→对比→分级→补丁→验证→端到端)
- SKILL.md: 新增场景 10「JSVMP + jsdom 环境伪装」、解法模式 E
- SKILL.md: 标注三板斧适用边界与局限,经验法则扩充至 22 条
- references/jsdom-env-patches.md: 新增 jsdom 环境补丁知识库(Function.toString 三层防御 / plugins 完整模拟 / DOM 布局属性 / 30+ API 存根)
- cases/: 新增经验库系统(模板 + 首个 JSVMP 实战案例含 58 项环境检测差异全表)
- references/jsvmp-analysis.md: JSVMP 专项分析详细指南
- references/troubleshooting.md: 常见问题排查指南
- templates/python-request/: Python 项目模板
- 各 references 文档增强、Python 技术栈支持、MCP v1.1 适配(44→52 工具)

Made-with: Cursor
This commit is contained in:
白夜
2026-04-02 18:10:05 +08:00
parent fd6e51c825
commit b5cf8b121a
30 changed files with 4045 additions and 433 deletions
+4
View File
@@ -1,3 +1,7 @@
.DS_Store .DS_Store
node_modules/ node_modules/
*.log *.log
.idea/
# 私有域名映射(不纳入版本控制)
cases/_private_mapping.json
+78 -30
View File
@@ -1,16 +1,20 @@
# JS 逆向工程 Skill # Camoufox 逆向分析 Skill
通用 JavaScript 逆向工程技能,通过 Node.js 实现算法还原和模拟请求。深度集成 `js-reverse` 和 `chrome-devtools` 两个 MCP 工具,覆盖主流 JS 逆向场景。 面向逆向分析与爬虫对抗场景的 Skill,围绕 `camoufox-reverse` MCP 构建单一工作流:先用 Camoufox 反检测浏览器完成网络捕获、源码定位、Hook 调试与反检测验证,再按需落地到 Node.js 或 Python 算法还原与自动化调用。
## 核心能力 ## 核心能力
- **加密算法还原**: MD5/SHA/AES/DES/RSA/HMAC/Base64 等常见加密算法的 Node.js 纯算法复现 - **双语言算法还原**: Node.js (`crypto` / `crypto-js`) 与 Python (`hashlib` / `pycryptodome`) 双路径实现
- **加密算法还原**: MD5/SHA/AES/DES/RSA/HMAC/Base64 等常见加密算法的纯算法复现
- **JS 混淆还原**: OB混淆、控制流平坦化、eval打包、自定义VM/JSVMP 等混淆类型的分析策略 - **JS 混淆还原**: OB混淆、控制流平坦化、eval打包、自定义VM/JSVMP 等混淆类型的分析策略
- **JSVMP 行为追踪**: Hook/插桩/日志分析三板斧,从行为层面追踪 JS 虚拟机保护的签名生成链路(详见专项指南)
- **动态 Cookie 逆向**: eval首包、api预热、浏览器指纹等 Cookie 生成机制的破解 - **动态 Cookie 逆向**: eval首包、api预热、浏览器指纹等 Cookie 生成机制的破解
- **WASM 逆向**: WebAssembly 加密模块的加载、环境补全和调用 - **WASM 逆向**: WebAssembly 加密模块的加载、环境补全和调用
- **协议层对抗**: TLS 指纹检测、HTTP/2 强制、频率限制等协议层防护的应对 - **协议层对抗**: TLS 指纹检测、HTTP/2 强制、频率限制等协议层防护的应对
- **MCP 工具深度集成**: 源码搜索、断点调试、Hook注入、网络监听全自动化 - **反检测浏览器**: Camoufox C++ 引擎级指纹伪装,绕过 Cloudflare/瑞数/极验等反爬检测
- **Codex/Claude Code 完整适配**: 内置 Agent 授权指令框架,解决 AI 编码工具在签名分析、接口调试等场景下的安全策略限制,确保 AI 全力协助完成分析任务 - **单 MCP 工作流**: 统一使用 `camoufox-reverse` 完成源码搜索、Hook 注入、函数追踪、网络分析、请求拦截与反检测验证
- **错误排查体系**: 完整的请求失败排查流程和签名值不一致排查方法论
- **Codex/Claude Code 完整适配**: 内置 Agent 授权指令框架,确保 AI 全力协助完成分析任务
## 快速使用 ## 快速使用
@@ -46,7 +50,7 @@ git clone https://github.com/WhiteNightShadow/hello_js_reverse_skill.git ~/.code
git clone https://github.com/WhiteNightShadow/hello_js_reverse_skill.git ~/.vscode/skills/hello_js_reverse_skill git clone https://github.com/WhiteNightShadow/hello_js_reverse_skill.git ~/.vscode/skills/hello_js_reverse_skill
``` ```
> 安装完成后,AI Agent 会自动读取 `SKILL.md` 获取 JS 逆向分析能力。当你在对话中涉及 JS 加密分析、网站逆向、参数签名还原等场景时,Skill 会自动激活。 > 安装完成后,AI Agent 会自动读取 `SKILL.md` 获取 Camoufox 逆向分析能力。当你在对话中涉及接口签名分析、反爬对抗、动态 Cookie、混淆 JS、WASM 或浏览器环境调试等场景时,Skill 会自动激活。
## 项目结构 ## 项目结构
@@ -58,26 +62,35 @@ hello_js_reverse_skill/
│ │
├── references/ # 参考文档(知识库) ├── references/ # 参考文档(知识库)
│ ├── workflow-overview.md # 工作流总览与解法模式决策树 │ ├── workflow-overview.md # 工作流总览与解法模式决策树
│ ├── crypto-patterns.md # 加密算法识别与 Node.js 还原代码 │ ├── crypto-patterns.md # 加密算法识别与 Node.js/Python 双语言还原代码
│ ├── obfuscation-guide.md # JS 混淆类型识别与还原策略 │ ├── obfuscation-guide.md # JS 混淆类型识别与还原策略
│ ├── hook-techniques.md # Hook 技术大全(13种 Hook 模板) │ ├── hook-techniques.md # Hook 技术大全(13种 Hook 模板)
│ ├── anti-debug.md # 反调试对抗手册(7种绕过方案) │ ├── anti-debug.md # 反调试对抗手册(7种绕过方案)
│ ├── environment-patch.md # 环境补全指南(VM沙箱/WASM/jQuery/XHR) │ ├── environment-patch.md # 环境补全指南(VM沙箱/WASM/jQuery/XHR)
│ ├── protocol-analysis.md # 协议层分析(TLS/HTTP2/UA/频率限制) │ ├── protocol-analysis.md # 协议层分析(TLS/HTTP2/UA/频率限制)
│ └── mcp-cookbook.md # MCP 工具使用手册(5大场景操作步骤) │ ├── mcp-cookbook.md # MCP 工具使用手册(5大场景操作步骤)
│ ├── jsvmp-analysis.md # JSVMP 虚拟机保护专项分析指南(三板斧方法论)
│ └── troubleshooting.md # 错误排查指南(请求失败/签名不一致/环境问题)
│ │
├── scripts/ # 工具脚本 ├── scripts/ # 工具脚本
│ ├── check-deps.sh # 环境依赖检查 │ ├── check-deps.sh # 环境依赖检查(Node.js + Python)
│ ├── sandbox-runner.js # VM 沙箱执行器(CLI工具) │ ├── sandbox-runner.js # VM 沙箱执行器(CLI工具)
│ ├── hook-generator.js # Hook 代码生成器(10种Hook模板) │ ├── hook-generator.js # Hook 代码生成器(10种Hook模板)
│ └── crypto-identifier.js # 加密算法识别器(密文特征分析) │ └── crypto-identifier.js # 加密算法识别器(密文特征分析)
│ │
├── templates/ # 项目模板(按解法模式分类) ├── templates/ # 项目模板(按解法模式和语言分类)
│ ├── node-request/ # 模式A:纯 Node.js 算法还原 │ ├── node-request/ # 模式A (Node.js):纯算法还原
│ │ ├── main.js │ │ ├── main.js
│ │ ├── utils/encrypt.js │ │ ├── utils/encrypt.js
│ │ ├── utils/request.js │ │ ├── utils/request.js
│ │ └── package.json │ │ └── package.json
│ ├── python-request/ # 模式A (Python):纯算法还原
│ │ ├── main.py
│ │ ├── utils/sign.py
│ │ ├── utils/request.py
│ │ ├── config/headers.json
│ │ ├── config/keys.json
│ │ └── requirements.txt
│ ├── vm-sandbox/ # 模式B:VM 沙箱执行 │ ├── vm-sandbox/ # 模式B:VM 沙箱执行
│ │ ├── main.js │ │ ├── main.js
│ │ ├── utils/sandbox.js │ │ ├── utils/sandbox.js
@@ -131,13 +144,15 @@ node scripts/sandbox-runner.js obfuscated.js --extract-cookie --verbose
根据分析结果选择合适的模板: 根据分析结果选择合适的模板:
| 场景 | 模板 | 何时使用 | | 场景 | 语言 | 模板 | 何时使用 |
|------|------|---------| |------|------|------|---------|
| 标准加密算法 | `templates/node-request/` | 加密可用 crypto 库还原 | | 标准加密算法 | Node.js | `templates/node-request/` | 加密可用 crypto 库还原 |
| 动态 Cookie | `templates/vm-sandbox/` | 服务端返回 JS 生成 Cookie | | 标准加密算法 | Python | `templates/python-request/` | 加密可用 hashlib/pycryptodome 还原 |
| WASM 加密 | `templates/wasm-loader/` | 加密在 .wasm 中实现 | | 动态 Cookie | Node.js | `templates/vm-sandbox/` | 服务端返回 JS 生成 Cookie |
| TLS/环境依赖 | `templates/browser-auto/` | 无法脱离浏览器环境 | | WASM 加密 | Node.js | `templates/wasm-loader/` | 加密在 .wasm 中实现 |
| TLS/环境依赖 | Node.js | `templates/browser-auto/` | 无法脱离浏览器环境 |
**Node.js 项目:**
```bash ```bash
cp -r templates/node-request/ my_project/ cp -r templates/node-request/ my_project/
cd my_project && npm install cd my_project && npm install
@@ -145,20 +160,38 @@ cd my_project && npm install
node main.js node main.js
``` ```
**Python 项目:**
```bash
cp -r templates/python-request/ my_project/
cd my_project && pip install -r requirements.txt
# 修改 main.py 中的配置和 utils/sign.py 中的签名逻辑
python main.py
```
## MCP 工具集成 ## MCP 工具集成
本 Skill 深度集成 js-reverse MCP 服务器: 本 Skill 只围绕 `camoufox-reverse` MCP 服务器组织能力(Camoufox 反检测浏览器,52 个工具):
| 核心工具 | 用途 | | 核心工具 | 用途 |
|---------|------| |---------|------|
| `list_pages` / `select_page` | 连接已打开的真实浏览器页面 | | `launch_browser` / `navigate` | 启动反检测浏览器并导航到目标页面 |
| `search_in_sources` | 在所有 JS 源码中搜索加密关键词 | | `search_code` / `search_code_in_script` | 在所有 JS 中搜索关键词 / 在指定脚本中精确搜索 |
| `set_breakpoint_on_text` | 按代码文本设置断点 | | `set_breakpoint_via_hook` | 通过 JS Hook 设置伪断点,捕获参数和返回值(支持持久化) |
| `trace_function` | 追踪函数调用,自动记录参数和返回值 | | `trace_function` | 追踪函数调用,自动记录参数和返回值(支持持久化) |
| `inject_before_load` | 页面加载前注入 Hook 脚本 | | `hook_function` | 注入自定义 Hook(before/after/replace,支持 non_overridable 防覆盖) |
| `break_on_xhr` | XHR 请求断点 | | `inject_hook_preset` | 一键预设 Hook(xhr/fetch/crypto/websocket/debugger_bypass,默认持久化) |
| `get_paused_info` | 获取断点处的调用栈和变量 | | `freeze_prototype` | 冻结原型方法防止页面 JS 覆盖 Hook |
| `save_script_source` | 保存脚本到本地分析 | | `hook_jsvmp_interpreter` | **一键插桩 JSVMP 解释器**(追踪 Function.prototype.apply + 30+ 敏感属性) |
| `get_jsvmp_log` / `dump_jsvmp_strings` | 获取 JSVMP 分析日志 / 提取字符串数组 |
| `compare_env` | 全面收集浏览器环境,用于补环境对照 |
| `trace_property_access` | Proxy 级别属性访问追踪 |
| `add_init_script` | 页面加载前注入脚本(支持 persistent 跨导航重注入) |
| `get_request_initiator` | 获取发起请求的 JS 调用栈(改进 URL 匹配 + 诊断信息) |
| `start_network_capture` | 启动网络捕获(支持 capture_body 捕获响应体) |
| `intercept_request` | 拦截/修改/Mock 网络请求 |
| `check_detection` / `get_fingerprint_info` | 反检测验证与指纹查看 |
| `bypass_debugger_trap` | 一键绕过反调试陷阱 |
| `export_state` / `import_state` | 保存/恢复浏览器状态 |
详见 `references/mcp-cookbook.md`。 详见 `references/mcp-cookbook.md`。
@@ -170,18 +203,21 @@ node main.js
| 动态 Cookie | eval首包/api预热/指纹Cookie | `hook-techniques.md` | | 动态 Cookie | eval首包/api预热/指纹Cookie | `hook-techniques.md` |
| 响应数据加密 | AES/DES/RC4解密 | `crypto-patterns.md` | | 响应数据加密 | AES/DES/RC4解密 | `crypto-patterns.md` |
| OB混淆 | 字符串还原/CFF/变量重命名 | `obfuscation-guide.md` | | OB混淆 | 字符串还原/CFF/变量重命名 | `obfuscation-guide.md` |
| 自定义VM/JSVMP | I/O追踪/Hook解释器 | `obfuscation-guide.md` | | **JSVMP 虚拟机保护** | **Hook/插桩/日志分析三板斧** | **`jsvmp-analysis.md`** |
| 自定义VM | I/O追踪/Hook解释器 | `obfuscation-guide.md` |
| WASM加密 | 加载/环境补全/导出函数调用 | `environment-patch.md` | | WASM加密 | 加载/环境补全/导出函数调用 | `environment-patch.md` |
| TLS指纹 | 浏览器自动化/curl-impersonate | `protocol-analysis.md` | | TLS指纹 | Camoufox 反检测/curl-impersonate/curl_cffi | `protocol-analysis.md` |
| HTTP/2 | Node.js http2模块 | `protocol-analysis.md` | | HTTP/2 | Node.js http2模块/Python httpx | `protocol-analysis.md` |
| 字体反爬 | 字体文件解析/CMAP映射 | SKILL.md | | 字体反爬 | 字体文件解析/CMAP映射 | SKILL.md |
| WebSocket | WS消息分析/协议解析 | `mcp-cookbook.md` | | WebSocket | WS消息分析/协议解析 | `mcp-cookbook.md` |
| 反调试 | debugger绕过/检测对抗 | `anti-debug.md` | | 反调试 | debugger绕过/检测对抗 | `anti-debug.md` |
| 环境检测 | webdriver/蜜罐/指纹 | `environment-patch.md` | | 环境检测 | webdriver/蜜罐/指纹 | `environment-patch.md` |
| 反检测站点 | Cloudflare/瑞数/极验绕过 | SKILL.md |
| **请求失败排查** | **Cookie/Header/时间戳/签名对比** | **`troubleshooting.md`** |
## 技术栈 ## 技术栈
- **Node.js**: 运行时环境 ### Node.js
- **crypto** (内置): MD5/SHA/AES/DES/HMAC - **crypto** (内置): MD5/SHA/AES/DES/HMAC
- **crypto-js**: CryptoJS 兼容实现 - **crypto-js**: CryptoJS 兼容实现
- **node-forge**: RSA/PKI - **node-forge**: RSA/PKI
@@ -190,3 +226,15 @@ node main.js
- **WebAssembly** (内置): WASM 加载 - **WebAssembly** (内置): WASM 加载
- **http2** (内置): HTTP/2 请求 - **http2** (内置): HTTP/2 请求
- **playwright-core**: 浏览器自动化 - **playwright-core**: 浏览器自动化
### Python
- **requests** / **httpx**: HTTP 请求(httpx 支持 HTTP/2)
- **pycryptodome**: AES/DES/RSA/3DES 加密
- **hashlib** (内置): MD5/SHA 系列
- **hmac** (内置): HMAC 签名
- **base64** (内置): Base64 编解码
- **execjs**: 执行 JS 代码
- **curl_cffi**: 带浏览器 TLS 指纹模拟的 HTTP 客户端
### 调试工具
- **camoufox-reverse MCP**: 反检测浏览器逆向分析(52 个工具,含 JSVMP 专项分析)
+758 -184
View File
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
# 逆向经验库(Cases)
本目录存放已验证的逆向分析经验案例,供 Phase 0.5 指纹匹配阶段自动检索。
## 使用方式
- Agent 在 Phase 0.5 采集目标站点的技术指纹后,遍历本目录下的 `.md` 文件,匹配「指纹检测规则」段
- 命中后按案例中的「已验证定位路径」和「还原代码模板」快速执行
- 每个案例以**技术特征**命名,不以具体域名命名
## 案例索引
| 案例文件 | 技术特征 | 难度 | 核心方案 |
|---------|---------|------|---------|
| [jsvmp-xhr-interceptor-env-emulation.md](jsvmp-xhr-interceptor-env-emulation.md) | JSVMP 字节码虚拟机 + XHR 拦截器 + 多层 SDK 联动 + jsdom 全量环境伪装 | ★★★★★ | jsdom 沙箱 + 58 项环境补丁 + XHR Hook 截出 a_bogus |
## 私有映射
`_private_mapping.json` 可用于本地私有标注域名与案例的对应关系(已加入 `.gitignore`,不会被提交):
```json
{
"某短视频平台": {
"pattern": "jsvmp-xhr-interceptor-env-emulation",
"domain_hint": "短视频",
"notes": "Cookie 字段 ttwid/__ac_nonce/__ac_signature"
}
}
```
## 新增案例
1. 复制 `_template.md` 为新文件,以技术特征命名
2. 按模板格式填写各段
3. 更新本文件的案例索引表
4. 可选:在 `_private_mapping.json` 中添加私有域名映射
### 跨端知识提取提示词
当你在其他大模型端完成了一个站点的逆向分析,可以使用以下提示词让它输出可沉淀的结构化信息:
```
请将本次逆向分析的经验总结为结构化的技术案例,按以下格式输出。注意:不要包含具体域名、URL、真实密钥等敏感信息,用抽象描述代替。
---
## 案例名称
(用技术特征命名,如"OB混淆+AES-CBC+动态密钥"、"JSVMP+Cookie生成")
## 技术指纹
(列出可用于自动检测的稳定特征,每条写成可搜索的模式)
- JS特征: (如"_0x前缀变量大量出现"、"单文件200KB+"、"存在while-switch解释器循环")
- 参数特征: (如"sign参数,32位hex,疑似MD5"、"token参数,Base64格式")
- 请求特征: (如"存在/api/init预热请求"、"Cookie中有动态字段__ac_xxx")
- 反调试特征: (如"debugger定时器"、"console.log检测")
- 混淆类型: (如"OB混淆v2"、"JSVMP"、"webpack打包+变量混淆")
## 加密方案
- 算法: (如AES-CBC、MD5、HMAC-SHA256、RSA等)
- 密钥来源: (硬编码/接口下发/动态计算/从页面DOM提取)
- 加密流程: (明文如何组装 → 如何加密 → 如何拼接到请求中)
- 签名公式: (如 sign = MD5(path + timestamp + nonce + secret).toLowerCase())
## 定位路径
(还原过程中最高效的定位方法,按执行顺序)
1. 第一步做了什么,搜索了什么关键词
2. 第二步怎么找到的关键函数
3. 第三步怎么确认的算法
## 还原代码
(脱敏后的核心还原函数,可直接复用)
## 踩坑记录
(遇到的坑和解决方法,每条一个)
## 变体说明
(同类站点的已知变体差异)
---
```
将输出内容发给本 Skill 的使用者,即可按 `_template.md` 格式沉淀到本目录。
+120
View File
@@ -0,0 +1,120 @@
# [案例名称:用技术特征命名,如 OB混淆+AES-CBC+动态密钥]
> 难度:★☆☆☆☆ ~ ★★★★★
> 还原方案:[A: 纯算法还原 / B: 沙箱执行 / C: WASM 加载 / D: 浏览器自动化 / E: jsdom 环境模拟]
> 实现语言:[Node.js / Python / 双语言]
> 最后验证日期:YYYY-MM-DD
---
## 技术指纹(供 Phase 0.5 自动匹配)
### JS 特征
- [ ] 特征1:描述 + 可搜索的关键词/正则
- [ ] 特征2:...
### 参数特征
- [ ] 参数名: [名称] | 长度: [N] | 字符集: [hex/base64/自定义] | 编码: [描述]
- [ ] ...
### 请求特征
- [ ] 特征1:如预热接口、XHR 拦截、多层 SDK 加载顺序
- [ ] ...
### 反调试特征
- [ ] 特征1:如 debugger 陷阱、Function.toString 检测
- [ ] ...
### 混淆类型
- [ ] 类型:OB混淆 / JSVMP / 控制流平坦化 / eval打包 / 其他
### 指纹检测规则(Agent 执行)
```
快速检测命令(30秒内完成):
- search_code(keyword="关键词1") → 命中条件
- search_code(keyword="关键词2") → 命中条件
- list_scripts → 文件体积条件
匹配判定:以上 N 项中命中 M 项以上 → 高置信度匹配
```
---
## 加密方案
- **算法**:[具体算法名称和模式]
- **密钥来源**:[硬编码 / 接口下发 / 动态计算 / 环境指纹]
- **加密流程**:
1. [步骤1]
2. [步骤2]
3. ...
- **签名公式**:`sign = 算法(参数1 + 参数2 + ... + 密钥)`(如可提取)
---
## 已验证定位路径(Phase 0.5 命中后直接执行)
```
步骤 1: [具体 MCP 操作和关键词]
步骤 2: [...]
步骤 3: [...]
...
```
---
## 还原代码模板
### 核心函数
```javascript
// Node.js 还原代码
```
```python
# Python 还原代码(如适用)
```
### 环境补丁(如需要)
```javascript
// 环境模拟代码
```
---
## 踩坑记录
| # | 坑 | 现象 | 解决方法 |
|---|---|------|---------|
| 1 | | | |
| 2 | | | |
---
## 变体说明
| 变体 | 差异点 | 影响 |
|------|--------|------|
| A | | |
| B | | |
---
## 浏览器指纹采集方法(如涉及环境伪装)
> 仅当还原方案涉及环境模拟(jsdom / vm 沙箱)时需要此段
```javascript
// 使用 camoufox-reverse MCP 在真实浏览器中采集指纹的代码
```
---
## 环境检测差异表(如涉及环境伪装)
> 仅当还原方案涉及环境模拟时需要此段
| 检测点 | 真实浏览器值 | 模拟环境原始值 | 修复方式 |
|--------|-------------|---------------|---------|
| | | | |
@@ -0,0 +1,515 @@
# JSVMP 字节码虚拟机 + XHR 拦截器 + 多层 SDK 联动 + jsdom 全量环境伪装
> 难度:★★★★★
> 还原方案:E: jsdom 环境模拟(喂入-截出策略)
> 实现语言:Node.js
> 最后验证日期:2026-04-01
> 平台类型:短视频平台
---
## 技术指纹(供 Phase 0.5 自动匹配)
### JS 特征
- [x] `window['_$webrt_1668687510']` — JSVMP 解释器入口函数,接受十六进制字节码字符串
- [x] 单文件 380KB+,包含 `'HNOJ@?RC'` 魔数校验
- [x] `_0x` 前缀变量大量出现(构造函数、工厂函数、闭包变量)
- [x] UMD 模式导出到 `window.byted_acrawler`(浏览器)或 `module.exports`(Node.js)
- [x] 存在 while-switch 风格的 JSVMP 解释器循环(`_0x1218ef` / `_0x5f1fc4` 函数)
- [x] 内部状态对象含 `bogusIndex`、`msNewTokenList`、`moveList`、`clickList`、`activeState`
- [x] 三层 SDK 脚本联动:`sdk-glue.js`(100KB)→ `bdms.js`(147KB)→ `webmssdk.es5.js`(387KB)
### 参数特征
- [x] `a_bogus`:约 180~192 字符,Base64 变体编码(含 `+`、`/`、`=`)
- [x] `X-Bogus`(旧版):16 字符,字母数字(由 `frontierSign()` 生成,非 JSVMP 拦截器)
- [x] `msToken`:约 140 字符,Base64 编码,来自目标域名的 Cookie
- [x] `verifyFp` / `fp`:格式 `verify_xxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx`
- [x] `UIFID`:约 256 字符 hex 编码,浏览器指纹标识
### 请求特征
- [x] 页面 inline script 调用 `window._SdkGlueInit(config, resources)` 完成初始化
- [x] 初始化配置含 `bdms.paths` 数组(正则路径列表,如 `'^/aweme/v1/'`)
- [x] JSVMP 通过修改 `XMLHttpRequest.prototype.open` 实现请求拦截
- [x] 拦截器仅对匹配 `enablePathList` 的 URL 追加 `a_bogus`
- [x] 默认仅拦截 `/web/report`,需通过 `_SdkGlueInit` 注入业务路径
- [x] Cookie 中包含动态字段:`ttwid`、`__ac_nonce`、`__ac_signature`、`s_v_web_id`、`odin_tt`
### 反调试特征
- [x] JSVMP 字节码执行,无法直接设置断点
- [x] debugger 定时器陷阱(可通过 `bypass_debugger_trap` 绕过)
- [x] 环境检测(58 项):`Function.prototype.toString` 原生性检查、`navigator.webdriver`、`navigator.plugins.length`、`document.hasFocus()`、DOM `offsetHeight/Width` 零值检测、`Object.prototype.toString` 标签、`window.chrome` 存在性、`Performance.timing` 对象、Canvas/WebGL 指纹、AudioContext 指纹等
- [x] 非真实浏览器环境生成的签名会被服务端**静默拒绝**(返回 HTTP 200 + 空 body,不报错)
### 混淆类型
- [x] JSVMP(JavaScript Virtual Machine Protection),字节码存储为十六进制字符串,由解释器 `_$webrt_1668687510` 运行时执行
### 指纹检测规则(Agent 执行)
```
快速检测命令(30秒内完成):
- search_code(keyword="webmssdk") → 命中 → 直接定位本案例
- search_code(keyword="byted_acrawler") → 命中 → 直接定位本案例
- search_code(keyword="_SdkGlueInit") → 命中 → 直接定位本案例
- search_code(keyword="a_bogus") → 命中 + 参数长度 180-192 → 高置信度
- list_scripts → 存在 380KB+ 文件 → 辅助确认
匹配判定:以上前 3 项命中任意 1 项 → 高置信度匹配
```
---
## 加密方案
- **算法**:JSVMP 自定义字节码虚拟机,内部算法不可直接读取。签名结果为 `a_bogus`(~192 字符 Base64 变体)或 `X-Bogus`(16 字符)
- **密钥来源**:动态计算 — 由浏览器环境指纹(Canvas/WebGL/navigator/screen 等数十项数据)+ 请求 URL query string + User-Agent + 时间戳共同生成
- **加密流程**:
1. `sdk-glue` 通过 `_SdkGlueInit(config, resources)` 初始化,注入拦截路径列表
2. `webmssdk` JSVMP 构造函数执行字节码,修改 `XMLHttpRequest.prototype.open`
3. 当 XHR 请求 URL 匹配 `enablePathList` 中的正则时,拦截器触发
4. JSVMP 读取完整 URL query string、`document.cookie`、`navigator.*` 属性、`screen.*` 属性等数十项环境指纹
5. JSVMP 内部通过字节码计算生成 `a_bogus` 值(含环境指纹哈希)
6. 拦截器将 `&a_bogus=xxx` 追加到 URL 末尾,再调用原始 `open`
- **签名公式**:无法提取(JSVMP 字节码保护,算法封装在虚拟机内部)。采用「喂入-截出」策略:在 jsdom 中执行原始 JSVMP 字节码,通过 XHR Hook 截获最终签名值
---
## 已验证定位路径(Phase 0.5 命中后直接执行)
### Phase 1:网络捕获定位接口
```
步骤 1: start_network_capture + list_network_requests → 捕获带 a_bogus 的完整请求 URL
步骤 2: search_code(keyword="bdms") → 找到三个关键脚本的 CDN 地址:
- webmssdk.es5.js (v1.0.0.20, 387KB) — JSVMP 签名引擎
- bdms.js (v1.0.1.19, 147KB) — ByteDance Monitoring System
- sdk-glue.js (v1.0.0.64, 100KB) — SDK 胶水层
```
### Phase 2:初始化链路还原
```
步骤 3: search_code(keyword="enablePathList") → 定位到 sdk-glue 中的路径配置机制
步骤 4: 页面 inline script 中找到 _SdkGlueInit 调用及完整的 paths 配置
步骤 5: search_code(keyword="frontierSign") → 找到 webmssdk 的导出结构:init() / frontierSign() / setConfig()
步骤 6: search_code(keyword="bogusIndex") → 确认 a_bogus 由 JSVMP 的 XHR 拦截器生成(不同于 frontierSign 生成的 X-Bogus)
```
### Phase 3:jsdom 沙箱验证
```
步骤 7: 在 jsdom (runScripts: 'dangerously') 中依次加载三个脚本并调用 _SdkGlueInit,
成功触发 JSVMP 的 XHR 拦截器生成 192 字符的 a_bogus
步骤 8: 浏览器生成的 a_bogus 用 Node.js axios 请求成功 → 确认非 TLS 问题
步骤 9: jsdom 生成的 a_bogus 被服务端拒绝(返回空 body)→ 确认是环境指纹差异
```
### Phase 4:环境指纹对比(核心突破点)
```
步骤 10: 用 camoufox-reverse MCP 启动反检测浏览器,navigate 到目标页面
步骤 11: 通过 evaluate_js 分批采集浏览器完整环境指纹
(navigator 24项 / screen 10项 / window 40项 / document 16项 /
performance 12项 / toString 7项 / Function.toString 6项 /
DOM布局 6项 / Canvas+WebGL+Audio 各项)
步骤 12: 在 jsdom 中运行完全相同的采集代码
步骤 13: 逐项对比发现 58 项差异,按影响分级修复
```
### Phase 5:环境补丁与验证
```
步骤 14: 编写 patchEnvironment() 函数修复全部 58 项差异
步骤 15: 从 jsdom 内部验证所有检测点通过(win.eval() 内执行 Function.prototype.toString 等)
步骤 16: 连续 5 次请求同一视频 ID,全部返回完整数据(35KB+),确认稳定性
```
---
## 还原代码模板
### 核心函数:markNative — 将 jsdom 函数伪装为浏览器原生函数
```javascript
const _origFnToString = win.Function.prototype.toString;
const nativeFnSet = new WeakSet();
function markNative(fn) {
if (typeof fn === 'function') {
nativeFnSet.add(fn);
try {
const name = fn.name || '';
Object.defineProperty(fn, 'toString', {
value: function () { return `function ${name}() { [native code] }`; },
writable: true, configurable: true,
});
} catch (e) {}
}
return fn;
}
// jsdom 内部函数的通用源码特征(不在 WeakSet 中的漏网之鱼)
const jsdomPatterns = [
/^\s*\w+\s*\([^)]*\)\s*\{[\s\S]*?const\s+esValue\s*=/,
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*?this\._globalObject/,
/^\s*\w+\s*\([^)]*\)\s*\{\s*const\s+\w+\s*=\s*this\s*!==/,
];
win.Function.prototype.toString = function () {
if (nativeFnSet.has(this)) {
return `function ${this.name || ''}() { [native code] }`;
}
let src;
try { src = _origFnToString.call(this); } catch (e) {
return 'function () { [native code] }';
}
for (const pat of jsdomPatterns) {
if (pat.test(src)) return `function ${this.name || ''}() { [native code] }`;
}
return src;
};
```
### 核心函数:深度扫描原型链标记所有 jsdom 内置方法
```javascript
function scanPrototypeChain(obj, maxDepth) {
let proto = obj;
for (let d = 0; d < (maxDepth || 10) && proto; d++) {
for (const name of Object.getOwnPropertyNames(proto)) {
try {
const desc = Object.getOwnPropertyDescriptor(proto, name);
if (desc && typeof desc.value === 'function') markNative(desc.value);
if (desc && typeof desc.get === 'function') markNative(desc.get);
if (desc && typeof desc.set === 'function') markNative(desc.set);
} catch (e) {}
}
proto = Object.getPrototypeOf(proto);
if (proto === Object.prototype || proto === null) break;
}
}
// 扫描 50+ 原型链,覆盖 jsdom 所有内置类
const protoTargets = [
win.Document?.prototype, win.HTMLDocument?.prototype,
win.Element?.prototype, win.HTMLElement?.prototype,
win.Node?.prototype, win.EventTarget?.prototype,
win.XMLHttpRequest?.prototype, win.HTMLCanvasElement?.prototype,
// ... 扫描 50+ 个 DOM 类的原型链
];
for (const proto of protoTargets) {
if (proto) scanPrototypeChain(proto, 3);
}
scanPrototypeChain(doc, 5); // document 实例自身
scanPrototypeChain(nav, 5); // navigator 实例自身
```
### 核心函数:navigator.plugins 完整模拟
```javascript
const pluginData = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
];
const mimeData = [
{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
{ type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
];
// 构建完整的 PluginArray → Plugin → MimeType 对象树
// 包含 item()/namedItem()/refresh() 方法 + Symbol.toStringTag
```
### 核心函数:a_bogus 生成(喂入-截出策略)
```javascript
function generateABogus(fullUrl, cookieStr) {
const dom = initSdk(); // jsdom + 环境补丁 + SDK 加载(单例)
const win = dom.window;
// 写入 Cookie 供 JSVMP 读取
if (cookieStr) {
cookieStr.split(';').forEach(c => {
try { win.document.cookie = c.trim(); } catch (e) {}
});
}
return new Promise((resolve) => {
win.__capturedUrls = [];
// 在 jsdom 内发起 XHR,触发 JSVMP 拦截器自动追加 a_bogus
win.eval(`(function(){
var x = new XMLHttpRequest();
x.open('GET','${fullUrl.replace(/'/g, "\\'")}',true);
x.send();
})()`);
setTimeout(() => {
let aBogus = null;
for (const url of win.__capturedUrls) {
const m = url.match(/[&?]a_bogus=([^&]+)/);
if (m) { aBogus = decodeURIComponent(m[1]); break; }
}
resolve(aBogus);
}, 500);
});
}
```
---
## 踩坑记录
| # | 坑 | 现象 | 解决方法 |
|---|---|------|---------|
| 1 | Function.prototype.toString 暴露 jsdom | JSVMP 检测到非原生函数,生成无效签名 | 三层防御:WeakSet 精确标记 + jsdom 源码模式正则 + 实例级 Object.defineProperty |
| 2 | navigator.plugins 结构不完整 | 不是简单的 `length=5`,JSVMP 遍历 `item()`/`namedItem()` 并检查 `Symbol.toStringTag` | 构建完整 PluginArray→Plugin→MimeType 对象树 |
| 3 | DOM 布局属性全为 0 | jsdom 无渲染引擎,`offsetHeight`/`Width`/`getBoundingClientRect` 返回 0 | HTMLElement.prototype getter 覆写,解析 style 或返回默认非零值 |
| 4 | Object.prototype.toString 标签错误 | jsdom 的 document 原型链是 `Document` 而非 `HTMLDocument`,screen 是普通对象 | `Symbol.toStringTag` 修复 + 原型链 `constructor.name` 修改 |
| 5 | XHR Hook 顺序导致截获失败 | webmssdk 保存的是 Hook 之前的原始 `open`,我方 Hook 截获不到最终 URL | 必须在 webmssdk 加载前完成 XHR Hook 安装 |
| 6 | 服务端静默拒绝 | 环境不完整时返回 HTTP 200 + 空 body,不返回错误码 | 通过对比 Camoufox 真实环境逐项排查 58 项差异 |
---
## 变体说明
| 变体 | 差异点 | 影响 |
|------|--------|------|
| X-Bogus(旧版) | 由 `frontierSign()` 生成的 16 字符签名,非 JSVMP 拦截器 | 较简单,可通过 Hook `frontierSign` 入参直接还原 |
| 不同业务线 | `enablePathList` 配置不同,不同业务接口使用不同路径正则 | 需从 `_SdkGlueInit` 调用中提取完整 paths 配置 |
| SDK 版本迭代 | `webmssdk` 版本号更新,字节码变化 | 环境检测项可能增加,需重新对比 `compare_env` |
---
## 浏览器指纹采集方法
使用 camoufox-reverse MCP 在真实浏览器中采集指纹的完整流程:
```
步骤 1: launch_browser({headless: false, os_type: "macos", locale: "zh-CN"})
步骤 2: navigate({url: "目标页面", wait_until: "domcontentloaded"})
步骤 3: 分批采集环境指纹(通过 evaluate_js 在页面上下文中执行)
```
### 批次 A: navigator 属性
```javascript
JSON.stringify({
nav_userAgent: navigator.userAgent,
nav_platform: navigator.platform,
nav_language: navigator.language,
nav_languages: navigator.languages,
nav_cookieEnabled: navigator.cookieEnabled,
nav_doNotTrack: navigator.doNotTrack,
nav_onLine: navigator.onLine,
nav_hardwareConcurrency: navigator.hardwareConcurrency,
nav_maxTouchPoints: navigator.maxTouchPoints,
nav_webdriver: navigator.webdriver,
nav_vendor: navigator.vendor,
nav_productSub: navigator.productSub,
nav_appVersion: navigator.appVersion,
nav_deviceMemory: navigator.deviceMemory,
nav_plugins_length: navigator.plugins ? navigator.plugins.length : -1,
nav_mimeTypes_length: navigator.mimeTypes ? navigator.mimeTypes.length : -1,
nav_pdfViewerEnabled: navigator.pdfViewerEnabled,
nav_connection_type: typeof navigator.connection,
nav_getBattery_type: typeof navigator.getBattery,
nav_userAgentData_type: typeof navigator.userAgentData
})
```
### 批次 B: screen + window 属性
```javascript
JSON.stringify({
scr_width: screen.width, scr_height: screen.height,
scr_availWidth: screen.availWidth, scr_availHeight: screen.availHeight,
scr_colorDepth: screen.colorDepth, scr_pixelDepth: screen.pixelDepth,
scr_availLeft: screen.availLeft, scr_availTop: screen.availTop,
scr_orientation_type: screen.orientation ? screen.orientation.type : undefined,
win_innerWidth: window.innerWidth, win_innerHeight: window.innerHeight,
win_outerWidth: window.outerWidth, win_outerHeight: window.outerHeight,
win_devicePixelRatio: window.devicePixelRatio,
win_chrome: typeof window.chrome,
win_Notification: typeof window.Notification,
win_SharedWorker: typeof window.SharedWorker,
win_WebSocket: typeof window.WebSocket,
win_RTCPeerConnection: typeof window.RTCPeerConnection,
win_AudioContext: typeof window.AudioContext,
win_OfflineAudioContext: typeof window.OfflineAudioContext,
win_fetch: typeof window.fetch,
win_Worker: typeof window.Worker,
win_isSecureContext: window.isSecureContext,
win_matchMedia: typeof window.matchMedia,
win_indexedDB: typeof window.indexedDB,
win_visualViewport: typeof window.visualViewport,
win_clientInformation: typeof window.clientInformation
})
```
### 批次 C: document + performance + toString + Function.toString
```javascript
JSON.stringify({
doc_hidden: document.hidden,
doc_visibilityState: document.visibilityState,
doc_hasFocus: document.hasFocus(),
doc_characterSet: document.characterSet,
doc_compatMode: document.compatMode,
doc_readyState: document.readyState,
doc_fullscreenEnabled: document.fullscreenEnabled,
doc_fonts_type: typeof document.fonts,
doc_timeline_type: typeof document.timeline,
perf_timing_type: typeof performance.timing,
perf_navigation_type: typeof performance.navigation,
perf_memory_type: typeof performance.memory,
toString_nav: Object.prototype.toString.call(navigator),
toString_doc: Object.prototype.toString.call(document),
toString_win: Object.prototype.toString.call(window),
toString_scr: Object.prototype.toString.call(screen),
toString_perf: Object.prototype.toString.call(performance),
fn_eval: eval.toString().substring(0, 60),
fn_setTimeout: setTimeout.toString().substring(0, 60),
fn_createElement: document.createElement.toString().substring(0, 80),
FnProto_toString: Function.prototype.toString.call(Function.prototype.toString).substring(0, 80),
nav_proto: Object.getPrototypeOf(navigator).constructor.name,
doc_proto: Object.getPrototypeOf(document).constructor.name,
nav_toStringTag: navigator[Symbol.toStringTag],
doc_toStringTag: document[Symbol.toStringTag]
})
```
### 批次 D: DOM 布局 + Canvas + WebGL + Audio
```javascript
JSON.stringify({
dom_test: (function(){
var d = document.createElement('div');
d.style.cssText = 'width:100px;height:100px;position:absolute;left:-9999px';
document.body.appendChild(d);
var r = {offsetHeight: d.offsetHeight, offsetWidth: d.offsetWidth,
clientHeight: d.clientHeight, clientWidth: d.clientWidth};
var rect = d.getBoundingClientRect();
r.rect_width = rect.width; r.rect_height = rect.height;
document.body.removeChild(d);
return r;
})(),
canvas_test: (function(){
var c = document.createElement('canvas'); c.width = 200; c.height = 50;
var ctx = c.getContext('2d');
ctx.textBaseline = 'top'; ctx.font = '14px Arial';
ctx.fillStyle = '#f60'; ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069'; ctx.fillText('abcdef', 2, 15);
return {dataURL_len: c.toDataURL().length};
})(),
webgl_test: (function(){
try {
var c = document.createElement('canvas');
var gl = c.getContext('webgl');
var ext = gl.getExtension('WEBGL_debug_renderer_info');
return {
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
unmasked_vendor: ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : 'N/A',
unmasked_renderer: ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : 'N/A'
};
} catch(e) { return {error: e.message}; }
})(),
audio_test: (function(){
try { var ctx = new OfflineAudioContext(1,44100,44100);
return {sampleRate: ctx.sampleRate, available: true};
} catch(e) { return {error: e.message}; }
})()
})
```
---
## 环境检测差异全表(58 项)
### 致命级(缺失即被拒)
| 检测点 | 真实浏览器值 | jsdom 原始值 | 修复方式 |
|--------|-------------|-------------|---------|
| `Function.prototype.toString` | `function X() { [native code] }` | 暴露实际实现代码(如 `createElement(localName) { const esValue = ...`) | WeakSet 精确标记 + jsdom 源码模式正则 + 实例级 `Object.defineProperty(fn, 'toString')` |
| `navigator.webdriver` | `false` | `undefined` | `Object.defineProperty` 强制设为 `false` |
| `navigator.plugins.length` | `5` | `0` | 模拟 Chrome 5 个 PDF 插件(含完整 PluginArray/Plugin/MimeType 结构) |
| `document.hasFocus()` | `true` | `false` | 方法覆写,始终返回 `true` |
| DOM `offsetHeight/Width` | 非零值(如 100) | `0` | `HTMLElement.prototype` getter 覆写,解析 style 或返回默认值 |
### 高危级(可能参与指纹哈希)
| 检测点 | 真实浏览器值 | jsdom 原始值 | 修复方式 |
|--------|-------------|-------------|---------|
| `Object.prototype.toString(document)` | `[object HTMLDocument]` | `[object Document]` | `Symbol.toStringTag` 修复 + 原型链 `constructor.name` 修改 |
| `Object.prototype.toString(screen)` | `[object Screen]` | `[object Object]` | 重建 screen 对象并设置 `Symbol.toStringTag` |
| `window.chrome` | `{app:{...}, runtime:{...}, csi, loadTimes}` | `undefined` | 完整 Chrome 对象模拟(含 `runtime.OnInstalledReason` 等枚举) |
| `navigator.userAgentData` | `NavigatorUAData` 对象 | `undefined` | 模拟 Chrome brands/mobile/platform + `getHighEntropyValues()` |
| `navigator.platform` | `MacIntel` | `""` | `Object.defineProperty` 覆盖 |
| `navigator.language` | `zh-CN` | `en-US` | `Object.defineProperty` 覆盖 |
| `navigator.languages` | `["zh-CN", "zh"]` | `["en-US", "en"]` | `Object.defineProperty` 覆盖 |
| `performance.timing` | 完整 `PerformanceTiming` 对象(20+ 字段) | `undefined` | 构造合理的 timing 对象(时间戳递增) |
| `performance.navigation` | `PerformanceNavigation` 对象 | `undefined` | 构造 `{type:0, redirectCount:0}` 对象 |
| `document.readyState` | `complete` | `loading` | getter 覆写返回 `'complete'` |
| `screen.colorDepth/pixelDepth` | `30` | `24` | 重建 screen 对象 |
| `screen.orientation` | `{type: 'landscape-primary', angle: 0}` | `undefined` | 重建 screen 对象含 orientation |
### 中危级(API 存在性检测)
| 检测点 | jsdom 原始值 | 修复方式 |
|--------|-------------|---------|
| `window.Notification` | `undefined` | 构造函数存根 + `permission`/`requestPermission` |
| `window.Worker` / `SharedWorker` | `undefined` | 构造函数存根 |
| `window.RTCPeerConnection` | `undefined` | 构造函数存根 |
| `window.AudioContext` / `OfflineAudioContext` | `undefined` | 完整 Web Audio API 存根(含 `createOscillator`/`createDynamicsCompressor`/`startRendering`) |
| `window.fetch` | `undefined` | 函数存根 + `Request`/`Response`/`Headers`/`AbortController` |
| `window.matchMedia` | `undefined` | 返回 `{matches:false, media:query}` 存根 |
| `window.requestIdleCallback` | `undefined` | `setTimeout` 包装 |
| `window.indexedDB` | `undefined` | `IDBFactory` 存根 |
| `window.caches` | `undefined` | `CacheStorage` 存根 |
| `window.speechSynthesis` | `undefined` | `SpeechSynthesis` 存根 |
| `window.customElements` | `undefined` | `CustomElementRegistry` 存根 |
| `window.visualViewport` | `undefined` | `VisualViewport` 存根 |
| `window.isSecureContext` | `undefined` | 设为 `true` |
| `window.clientInformation` | `undefined` | 指向 `navigator` |
| `navigator.connection` | `undefined` | `NetworkInformation` 存根 |
| `navigator.getBattery` | `undefined` | 返回 `BatteryManager` Promise |
| `navigator.deviceMemory` | `undefined` | 设为 `8` |
| `navigator.maxTouchPoints` | `undefined` | 设为 `0` |
| `navigator.pdfViewerEnabled` | `undefined` | 设为 `true` |
| `navigator.mimeTypes.length` | `0` | 模拟 2 个 MIME 类型 |
| `document.fonts` | `undefined` | `FontFaceSet` 存根 |
| `document.timeline` | `undefined` | `DocumentTimeline` 存根 |
| `document.fullscreenEnabled` | `undefined` | 设为 `true` |
---
## 关键经验总结
### 1. 为什么不反编译 JSVMP 字节码
JSVMP 用自定义指令集执行,反编译成本极高且版本迭代频繁。「喂入-截出」策略(在模拟环境中运行原始字节码,只截获 I/O)是性价比最高的方案。
### 2. jsdom 环境伪装的核心难度排序
| 排名 | 难度 | 检测项 | 为什么难 |
|------|------|--------|---------|
| 1 | ★★★★★ | `Function.prototype.toString` | jsdom 所有 DOM 方法都是 JS 实现,`toString()` 会暴露完整源码。需要三层防御:WeakSet 精确匹配 + jsdom 源码正则模式 + 实例级 toString 覆写 |
| 2 | ★★★★ | `navigator.plugins` 结构 | 不是简单的 `length=5`,JSVMP 可能遍历 `item()`/`namedItem()` 并检查 `Symbol.toStringTag` |
| 3 | ★★★ | DOM 布局属性 | jsdom 无渲染引擎,所有 `offsetHeight`/`Width`/`getBoundingClientRect` 返回 0 |
| 4 | ★★★ | `Object.prototype.toString` 标签 | jsdom 的 document 原型链是 `Document` 而非 `HTMLDocument`,screen 是普通对象 |
| 5 | ★★ | 30+ 缺失 API | 需要逐个补全,且每个都要 `markNative` 处理 toString |
### 3. XHR Hook 顺序至关重要
```
我方 Hook → webmssdk 加载(保存我方 Hook 后的 open 引用)→ JSVMP 执行时调用保存的 open
↓
我方 Hook 拦截到带 a_bogus 的 URL
```
如果 webmssdk 先加载再 Hook,JSVMP 保存的是原始 `open`,我方 Hook 无法截获最终 URL。
### 4. 对比方法论
用 Camoufox 反检测浏览器(C++ 引擎级指纹伪装)作为「干净基准」,在目标页面上下文中执行环境采集代码,与 jsdom 输出逐项 diff。分批采集(navigator / screen+window / document+performance+toString / DOM+Canvas+WebGL+Audio)避免单次执行过长。
+27 -14
View File
@@ -15,11 +15,21 @@
使用 MCP 工具捕获请求: 使用 MCP 工具捕获请求:
``` ```
[js-reverse] list_network_requests(resourceTypes=["XHR"]) [camoufox-reverse] launch_browser()
[camoufox-reverse] navigate(url="https://example.com/data")
[camoufox-reverse] start_network_capture()
→ 触发翻页交互
[camoufox-reverse] click(selector=".next-page")
[camoufox-reverse] list_network_requests
→ 发现数据接口: /api/v1/products → 发现数据接口: /api/v1/products
[js-reverse] get_network_request(reqid=42) [camoufox-reverse] get_network_request(request_id=42)
→ 获取完整请求信息 → 获取完整请求信息
[camoufox-reverse] get_request_initiator(request_id=42)
→ 直接定位签名函数 generateSign
``` ```
**请求详情**: **请求详情**:
@@ -42,17 +52,17 @@ Headers:
### 搜索加密入口 ### 搜索加密入口
``` ```
[js-reverse] search_in_sources(query="sign=") [camoufox-reverse] search_code(keyword="sign=")
→ 在 /static/js/app.js 第 1234 行找到: params.sign = generateSign(page, timestamp) → 在 /static/js/app.js 第 1234 行找到: params.sign = generateSign(page, timestamp)
[js-reverse] search_in_sources(query="generateSign") [camoufox-reverse] search_code(keyword="generateSign")
→ 在 /static/js/utils.js 第 567 行找到函数定义 → 在 /static/js/utils.js 第 567 行找到函数定义
``` ```
### 读取关键代码 ### 读取关键代码
``` ```
[js-reverse] get_script_source(url="https://example.com/static/js/utils.js", startLine=560, endLine=590) [camoufox-reverse] get_script_source(script_url="https://example.com/static/js/utils.js")
``` ```
还原后的关键代码: 还原后的关键代码:
@@ -69,16 +79,18 @@ function generateSign(page, timestamp) {
### 注入 Hook 验证 ### 注入 Hook 验证
``` ```
[js-reverse] inject_before_load(script="XHR Hook + generateSign Hook") [camoufox-reverse] inject_hook_preset(preset="xhr")
[js-reverse] navigate_page(type="reload") [camoufox-reverse] set_breakpoint_via_hook(target_function="window.generateSign")
[js-reverse] list_console_messages() [camoufox-reverse] reload()
[camoufox-reverse] get_breakpoint_data
``` ```
Hook 输出: 伪断点捕获:
``` ```
[Hook:Custom] generateSign 调用 target: window.generateSign
[Hook:Custom] 参数: [1, 1710000000] arguments: [1, 1710000000]
[Hook:Custom] 返回: "e10adc3949ba59abbe56e057f20f883e" return_value: "e10adc3949ba59abbe56e057f20f883e"
call_stack: [...]
``` ```
### 验证 MD5 ### 验证 MD5
@@ -148,5 +160,6 @@ async function fetchPage(page) {
1. Network 抓包分析 1. Network 抓包分析
2. JS 源码搜索定位 2. JS 源码搜索定位
3. MD5 签名还原 3. MD5 签名还原
4. XHR Hook 验证 4. `get_request_initiator` 黄金路径定位
5. Node.js 纯算法模拟请求 5. `inject_hook_preset` + `set_breakpoint_via_hook` 动态验证
6. Node.js 纯算法模拟请求
+10 -6
View File
@@ -27,9 +27,13 @@ setInterval(function() { debugger; }, 100);
### 绕过方案 ### 绕过方案
**方案 A:MCP 注入绕过** **方案 A:MCP 一键绕过**
``` ```
[js-reverse] inject_before_load(script=反debugger脚本) [camoufox-reverse] bypass_debugger_trap()
或
[camoufox-reverse] inject_hook_preset(preset="debugger_bypass")
或手动注入:
[camoufox-reverse] add_init_script(script=反debugger脚本)
``` ```
```javascript ```javascript
@@ -57,9 +61,9 @@ setInterval = function(fn, ms) {
}; };
``` ```
**方案 B:断点条件设置 false** **方案 B:加载前拦截 debugger 构造**
``` ```
[js-reverse] set_breakpoint_on_text(text="debugger", condition="false") [camoufox-reverse] add_init_script(script=反debugger脚本)
``` ```
## 2. 开发者工具检测 ## 2. 开发者工具检测
@@ -158,7 +162,7 @@ if (someFunction.toString().length !== 1234) {
} }
``` ```
**绕过**:不修改原始函数,使用 `inject_before_load` 在执行之前收集所需数据。 **绕过**:不修改原始函数,使用 `add_init_script` 在页面脚本执行之前收集所需数据。
## 4. 时间差检测 ## 4. 时间差检测
@@ -284,7 +288,7 @@ try {
## 通用反反调试注入脚本 ## 通用反反调试注入脚本
以下脚本适合通过 `inject_before_load` 在页面加载前注入: 以下脚本适合通过 `add_init_script` 在页面加载前注入:
```javascript ```javascript
(function() { (function() {
+178 -9
View File
@@ -1,4 +1,4 @@
# 常见加密模式识别与还原 # 常见加密模式识别与还原(Node.js / Python 双语言)
## 快速识别表 ## 快速识别表
@@ -20,12 +20,21 @@
### 标准 MD5 ### 标准 MD5
```javascript ```javascript
// Node.js
const crypto = require('crypto'); const crypto = require('crypto');
function md5(str) { function md5(str) {
return crypto.createHash('md5').update(str).digest('hex'); return crypto.createHash('md5').update(str).digest('hex');
} }
``` ```
```python
# Python
import hashlib
def md5(text: str) -> str:
return hashlib.md5(text.encode('utf-8')).hexdigest()
```
### 自定义 MD5(非标准实现) ### 自定义 MD5(非标准实现)
某些网站修改了 MD5 的内部参数(如 chrsz=16),导致输出与标准 MD5 不同。 某些网站修改了 MD5 的内部参数(如 chrsz=16),导致输出与标准 MD5 不同。
@@ -51,12 +60,22 @@ sign = md5(md5(password) + timestamp)
## 2. HMAC ## 2. HMAC
```javascript ```javascript
// Node.js
const crypto = require('crypto'); const crypto = require('crypto');
function hmacSha256(message, secret) { function hmacSha256(message, secret) {
return crypto.createHmac('sha256', secret).update(message).digest('hex'); return crypto.createHmac('sha256', secret).update(message).digest('hex');
} }
``` ```
```python
# Python
import hmac
import hashlib
def hmac_sha256(message: str, secret: str) -> str:
return hmac.new(secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
```
**识别特征**:搜索 `HMAC`、`createHmac`、`CryptoJS.HmacSHA256` **识别特征**:搜索 `HMAC`、`createHmac`、`CryptoJS.HmacSHA256`
## 3. AES ## 3. AES
@@ -64,6 +83,7 @@ function hmacSha256(message, secret) {
### AES-CBC ### AES-CBC
```javascript ```javascript
// Node.js
const crypto = require('crypto'); const crypto = require('crypto');
function aesEncrypt(plaintext, key, iv) { function aesEncrypt(plaintext, key, iv) {
@@ -83,9 +103,27 @@ function aesDecrypt(ciphertext, key, iv) {
} }
``` ```
```python
# Python
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def aes_cbc_encrypt(plaintext: str, key: str, iv: str) -> str:
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
padded = pad(plaintext.encode('utf-8'), AES.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode('utf-8')
def aes_cbc_decrypt(ciphertext_b64: str, key: str, iv: str) -> str:
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
decrypted = cipher.decrypt(base64.b64decode(ciphertext_b64))
return unpad(decrypted, AES.block_size).decode('utf-8')
```
### AES-ECB ### AES-ECB
```javascript ```javascript
// Node.js
function aesEcbEncrypt(plaintext, key) { function aesEcbEncrypt(plaintext, key) {
const cipher = crypto.createCipheriv('aes-128-ecb', key, null); const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
cipher.setAutoPadding(true); cipher.setAutoPadding(true);
@@ -95,6 +133,14 @@ function aesEcbEncrypt(plaintext, key) {
} }
``` ```
```python
# Python
def aes_ecb_encrypt(plaintext: str, key: str) -> str:
cipher = AES.new(key.encode('utf-8'), AES.MODE_ECB)
padded = pad(plaintext.encode('utf-8'), AES.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode('utf-8')
```
### CryptoJS 兼容 ### CryptoJS 兼容
```javascript ```javascript
@@ -118,6 +164,7 @@ const encrypted = CryptoJS.AES.encrypt(plaintext, CryptoJS.enc.Utf8.parse(key),
## 4. DES / 3DES ## 4. DES / 3DES
```javascript ```javascript
// Node.js
function desEncrypt(plaintext, key) { function desEncrypt(plaintext, key) {
const cipher = crypto.createCipheriv('des-ecb', key, null); // 8字节key const cipher = crypto.createCipheriv('des-ecb', key, null); // 8字节key
cipher.setAutoPadding(true); cipher.setAutoPadding(true);
@@ -135,9 +182,26 @@ function tripleDesEncrypt(plaintext, key, iv) {
} }
``` ```
```python
# Python
from Crypto.Cipher import DES, DES3
from Crypto.Util.Padding import pad
def des_ecb_encrypt(plaintext: str, key: str) -> str:
cipher = DES.new(key.encode('utf-8'), DES.MODE_ECB)
padded = pad(plaintext.encode('utf-8'), DES.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode('utf-8')
def triple_des_cbc_encrypt(plaintext: str, key: str, iv: str) -> str:
cipher = DES3.new(key.encode('utf-8'), DES3.MODE_CBC, iv.encode('utf-8'))
padded = pad(plaintext.encode('utf-8'), DES3.block_size)
return base64.b64encode(cipher.encrypt(padded)).decode('utf-8')
```
## 5. RSA ## 5. RSA
```javascript ```javascript
// Node.js
const crypto = require('crypto'); const crypto = require('crypto');
function rsaEncrypt(plaintext, publicKey) { function rsaEncrypt(plaintext, publicKey) {
@@ -162,6 +226,27 @@ function rsaEncrypt(plaintext, publicKeyPem) {
} }
``` ```
```python
# Python
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
def rsa_encrypt(plaintext: str, public_key_pem: str) -> str:
key = RSA.import_key(public_key_pem)
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(plaintext.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
# OAEP 模式
from Crypto.Cipher import PKCS1_OAEP
def rsa_encrypt_oaep(plaintext: str, public_key_pem: str) -> str:
key = RSA.import_key(public_key_pem)
cipher = PKCS1_OAEP.new(key)
encrypted = cipher.encrypt(plaintext.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
```
**常见 RSA 公钥格式**: **常见 RSA 公钥格式**:
- PEM 格式(`-----BEGIN PUBLIC KEY-----`) - PEM 格式(`-----BEGIN PUBLIC KEY-----`)
- 模数(n) + 指数(e) 格式(需要手动构造 PEM) - 模数(n) + 指数(e) 格式(需要手动构造 PEM)
@@ -169,7 +254,7 @@ function rsaEncrypt(plaintext, publicKeyPem) {
## 6. Base64 变体 ## 6. Base64 变体
```javascript ```javascript
// 标准 Base64 // Node.js
const encoded = Buffer.from(str).toString('base64'); const encoded = Buffer.from(str).toString('base64');
const decoded = Buffer.from(encoded, 'base64').toString(); const decoded = Buffer.from(encoded, 'base64').toString();
@@ -190,9 +275,28 @@ function customBase64(str, table) {
} }
``` ```
```python
# Python
import base64
encoded = base64.b64encode(text.encode('utf-8')).decode('utf-8')
decoded = base64.b64decode(encoded).decode('utf-8')
# Base64url(URL安全变体)
encoded_url = base64.urlsafe_b64encode(text.encode('utf-8')).decode('utf-8').rstrip('=')
# 自定义字符表 Base64
def custom_base64(text: str, table: str) -> str:
std = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
b64 = base64.b64encode(text.encode('utf-8')).decode('utf-8')
trans = str.maketrans(std, table)
return b64.translate(trans)
```
## 7. 异或加密(XOR) ## 7. 异或加密(XOR)
```javascript ```javascript
// Node.js
function xorEncrypt(plaintext, key) { function xorEncrypt(plaintext, key) {
const result = []; const result = [];
for (let i = 0; i < plaintext.length; i++) { for (let i = 0; i < plaintext.length; i++) {
@@ -202,9 +306,20 @@ function xorEncrypt(plaintext, key) {
} }
``` ```
```python
# Python
def xor_encrypt(plaintext: str, key: str) -> str:
result = bytes([
ord(plaintext[i]) ^ ord(key[i % len(key)])
for i in range(len(plaintext))
])
return result.hex()
```
## 8. RC4 ## 8. RC4
```javascript ```javascript
// Node.js
function rc4(data, key) { function rc4(data, key) {
const s = Array.from({ length: 256 }, (_, i) => i); const s = Array.from({ length: 256 }, (_, i) => i);
let j = 0; let j = 0;
@@ -224,21 +339,53 @@ function rc4(data, key) {
} }
``` ```
```python
# Python(使用 pycryptodome)
from Crypto.Cipher import ARC4
def rc4_encrypt(data: str, key: str) -> bytes:
cipher = ARC4.new(key.encode('utf-8'))
return cipher.encrypt(data.encode('utf-8'))
# 或手动实现(与 JS 版本逻辑一致)
def rc4_manual(data: str, key: str) -> bytes:
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + ord(key[i % len(key)])) % 256
s[i], s[j] = s[j], s[i]
i = j = 0
result = []
for char in data:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
result.append(ord(char) ^ s[(s[i] + s[j]) % 256])
return bytes(result)
```
## 9. 时间戳处理 ## 9. 时间戳处理
```javascript ```javascript
// 毫秒级(13位) // Node.js
const tsMs = Date.now(); // 1710000000000 const tsMs = Date.now(); // 毫秒级(13位)
const tsSec = Math.floor(Date.now() / 1000); // 秒级(10位)
// 秒级(10位)
const tsSec = Math.floor(Date.now() / 1000); // 1710000000
// 注意:某些网站使用服务端时间戳,需要从响应头或接口获取
``` ```
```python
# Python
import time
ts_ms = int(time.time() * 1000) # 毫秒级(13位)
ts_sec = int(time.time()) # 秒级(10位)
```
**注意**:某些网站使用服务端时间戳,需要从响应头或接口获取。
## 10. 参数签名常见拼接模式 ## 10. 参数签名常见拼接模式
```javascript ```javascript
// Node.js
// 模式1:固定格式 // 模式1:固定格式
sign = md5(`page=${page}&t=${timestamp}&key=${secret}`) sign = md5(`page=${page}&t=${timestamp}&key=${secret}`)
@@ -253,3 +400,25 @@ sign = md5(JSON.stringify(data) + secret)
// 模式4:管道分隔 // 模式4:管道分隔
sign = md5(`${page}|${timestamp}|${secret}`) sign = md5(`${page}|${timestamp}|${secret}`)
``` ```
```python
# Python
import hashlib, json, time
def md5(text: str) -> str:
return hashlib.md5(text.encode('utf-8')).hexdigest()
# 模式1:固定格式
sign = md5(f"page={page}&t={timestamp}&key={secret}")
# 模式2:所有参数排序拼接
params = {"page": 1, "size": 10, "t": int(time.time())}
param_str = "&".join(f"{k}={params[k]}" for k in sorted(params))
sign = md5(param_str + secret)
# 模式3:JSON 字符串(注意 separators 控制格式)
sign = md5(json.dumps(data, separators=(',', ':'), sort_keys=True) + secret)
# 模式4:管道分隔
sign = md5(f"{page}|{timestamp}|{secret}")
```
+9 -6
View File
@@ -387,12 +387,15 @@ function createFullBrowserEnv(options = {}) {
## MCP 辅助确定环境需求 ## MCP 辅助确定环境需求
``` ```
1. [js-reverse] search_in_sources(query="window.|document.|navigator.") 1. [camoufox-reverse] search_code(keyword="window|document|navigator|location|screen")
→ 识别代码访问了哪些浏览器 API → 先在已加载脚本中识别代码访问了哪些浏览器 API
2. [js-reverse] evaluate_script(function="列出所有被访问的全局变量") 2. [camoufox-reverse] add_init_script(script="记录被访问的全局变量和属性")
→ 确定需要补全的范围 + reload()
+ get_console_logs
→ 确定运行时真正访问了哪些环境字段
3. [js-reverse] set_breakpoint_on_text(text="typeof window") 3. [camoufox-reverse] set_breakpoint_via_hook(target_function="环境检测函数路径")
→ 在环境检测处断点,确认检测逻辑 + get_breakpoint_data
→ 在环境检测函数入口捕获入参、返回值和调用栈,确认需要补全的关键分支
``` ```
+13 -12
View File
@@ -10,18 +10,17 @@ Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函
### 通过 MCP 注入(推荐) ### 通过 MCP 注入(推荐)
``` ```
[js-reverse] inject_before_load(script=HookScript) [camoufox-reverse] add_init_script(script=HookScript)
→ 在页面脚本加载前注入,确保 Hook 在目标代码之前生效 → 在页面脚本加载前注入,确保 Hook 在目标代码之前生效
[js-reverse] evaluate_script(function=HookScript) [camoufox-reverse] evaluate_js(expression=HookScript)
→ 在当前页面上下文执行,适合页面已加载后的动态注入 → 在当前页面上下文执行,适合页面已加载后的动态注入
```
### 通过浏览器控制台注入 [camoufox-reverse] inject_hook_preset(preset="xhr|fetch|crypto|websocket|debugger_bypass")
→ 一键注入预设 Hook,覆盖常见逆向场景
在 chrome-devtools MCP 中使用: [camoufox-reverse] hook_function(function_path="目标函数", hook_code="...", position="before|after|replace")
``` → 对指定函数注入自定义 Hook
[chrome-devtools] evaluate_script(function=HookScript)
``` ```
## Hook 模板库 ## Hook 模板库
@@ -390,8 +389,10 @@ Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函
## MCP 注入最佳实践 ## MCP 注入最佳实践
1. **使用 `inject_before_load`**:确保 Hook 在目标代码之前生效 1. **使用 `add_init_script`**:确保 Hook 在目标代码之前生效
2. **使用 `console.log` 输出**:通过 `list_console_messages` 收集结果 2. **优先使用 `inject_hook_preset`**:一键注入 xhr/fetch/crypto/websocket/debugger_bypass 预设 Hook
3. **使用 `console.trace`**:在关键点输出调用栈 3. **使用 `hook_function`**:对特定函数注入 before/after/replace Hook
4. **避免覆写原型链的 toString**:防止被反检测代码发现 4. **使用 `console.log` 输出**:通过 `get_console_logs` 收集结果
5. **使用 Proxy 代替直接覆写**:更隐蔽,不改变 `typeof` 结果 5. **使用 `console.trace`**:在关键点输出调用栈
6. **Camoufox 优势**:Juggler 协议沙箱隔离,Hook 不会被页面 JS 检测到
7. **使用 Proxy 代替直接覆写**:更隐蔽,不改变 `typeof` 结果
+915
View File
@@ -0,0 +1,915 @@
# jsdom 环境补丁知识库
## 概述
当 JSVMP 选择路径 B(环境伪装)在 jsdom 中运行时,jsdom 与真实浏览器存在大量差异。
本文档提供系统化的环境补丁方案,按检测影响分级,所有代码模板可直接复用。
## 使用方法
```javascript
const { JSDOM } = require('jsdom');
function createPatchedJsdom(url, options = {}) {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: url,
referrer: options.referrer || '',
contentType: 'text/html',
pretendToBeVisual: true,
runScripts: 'dangerously',
});
const win = dom.window;
const doc = win.document;
const nav = win.navigator;
patchEnvironment(win, doc, nav, options);
return dom;
}
```
---
## 致命级补丁(缺失即被服务端拒绝)
### 1. Function.prototype.toString 三层防御
**问题**:jsdom 所有 DOM 方法都是 JS 实现,`toString()` 会暴露完整源码(如 `createElement(localName) { const esValue = ...`),
JSVMP 通过此检测判断是否在真实浏览器中。
**难度**:★★★★★ — 这是 jsdom 环境伪装的第一杀手
```javascript
const _origFnToString = win.Function.prototype.toString;
const nativeFnSet = new WeakSet();
// 第一层:精确标记 — WeakSet 注册已知 jsdom 内置函数
function markNative(fn) {
if (typeof fn === 'function') {
nativeFnSet.add(fn);
try {
const name = fn.name || '';
// 第三层:实例级覆写 — 直接在函数对象上定义 toString
Object.defineProperty(fn, 'toString', {
value: function () { return `function ${name}() { [native code] }`; },
writable: true, configurable: true,
});
} catch (e) {}
}
return fn;
}
// 第二层:源码模式正则 — 捕获 WeakSet 遗漏的 jsdom 函数
const jsdomPatterns = [
/^\s*\w+\s*\([^)]*\)\s*\{[\s\S]*?const\s+esValue\s*=/, // jsdom 接口方法模式
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*?this\._globalObject/, // jsdom 全局对象访问模式
/^\s*\w+\s*\([^)]*\)\s*\{\s*const\s+\w+\s*=\s*this\s*!==/, // jsdom this 检查模式
];
// 覆写全局 Function.prototype.toString
win.Function.prototype.toString = function () {
// 第一层:WeakSet 精确匹配
if (nativeFnSet.has(this)) {
return `function ${this.name || ''}() { [native code] }`;
}
let src;
try { src = _origFnToString.call(this); } catch (e) {
return 'function () { [native code] }';
}
// 第二层:源码模式正则检测
for (const pat of jsdomPatterns) {
if (pat.test(src)) return `function ${this.name || ''}() { [native code] }`;
}
return src;
};
// 深度扫描原型链,批量标记所有 jsdom 内置方法
function scanPrototypeChain(obj, maxDepth) {
let proto = obj;
for (let d = 0; d < (maxDepth || 10) && proto; d++) {
for (const name of Object.getOwnPropertyNames(proto)) {
try {
const desc = Object.getOwnPropertyDescriptor(proto, name);
if (desc && typeof desc.value === 'function') markNative(desc.value);
if (desc && typeof desc.get === 'function') markNative(desc.get);
if (desc && typeof desc.set === 'function') markNative(desc.set);
} catch (e) {}
}
proto = Object.getPrototypeOf(proto);
if (proto === Object.prototype || proto === null) break;
}
}
// 扫描 50+ 原型链
const protoTargets = [
win.Document?.prototype, win.HTMLDocument?.prototype,
win.Element?.prototype, win.HTMLElement?.prototype,
win.Node?.prototype, win.EventTarget?.prototype,
win.XMLHttpRequest?.prototype, win.HTMLCanvasElement?.prototype,
win.HTMLInputElement?.prototype, win.HTMLFormElement?.prototype,
win.HTMLAnchorElement?.prototype, win.HTMLImageElement?.prototype,
win.HTMLDivElement?.prototype, win.HTMLSpanElement?.prototype,
win.HTMLBodyElement?.prototype, win.HTMLHeadElement?.prototype,
win.HTMLScriptElement?.prototype, win.HTMLStyleElement?.prototype,
win.HTMLLinkElement?.prototype, win.HTMLMetaElement?.prototype,
win.Window?.prototype, win.Location?.prototype,
win.DOMParser?.prototype, win.URL?.prototype,
win.Event?.prototype, win.CustomEvent?.prototype,
win.MutationObserver?.prototype, win.ResizeObserver?.prototype,
win.DOMTokenList?.prototype, win.NamedNodeMap?.prototype,
win.NodeList?.prototype, win.HTMLCollection?.prototype,
win.CSSStyleDeclaration?.prototype, win.Text?.prototype,
win.Comment?.prototype, win.DocumentFragment?.prototype,
win.Range?.prototype, win.Selection?.prototype,
win.TreeWalker?.prototype, win.NodeIterator?.prototype,
win.Attr?.prototype, win.CharacterData?.prototype,
win.Storage?.prototype, win.FormData?.prototype,
win.Headers?.prototype,
];
for (const proto of protoTargets) {
if (proto) scanPrototypeChain(proto, 3);
}
scanPrototypeChain(doc, 5);
scanPrototypeChain(nav, 5);
```
### 2. navigator.webdriver
**问题**:jsdom 中 `navigator.webdriver` 为 `undefined`,真实浏览器为 `false`(Camoufox 在 C++ 层已处理)。
```javascript
Object.defineProperty(nav, 'webdriver', {
get: () => false, configurable: true, enumerable: true,
});
```
### 3. navigator.plugins 完整结构模拟
**问题**:jsdom `plugins.length = 0`,且无 `item()`/`namedItem()` 方法。JSVMP 不只检查长度,还会遍历结构和检查 `Symbol.toStringTag`。
```javascript
const pluginData = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
];
const mimeData = [
{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
{ type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
];
function buildPluginArray(win) {
const mimeTypes = mimeData.map((m, i) => {
const mt = Object.create(null);
Object.defineProperties(mt, {
type: { get: markNative(function type() { return m.type; }), enumerable: true },
description: { get: markNative(function description() { return m.description; }), enumerable: true },
suffixes: { get: markNative(function suffixes() { return m.suffixes; }), enumerable: true },
enabledPlugin: { get: markNative(function enabledPlugin() { return plugins[0]; }), enumerable: true },
});
mt[Symbol.toStringTag] = 'MimeType';
return mt;
});
const plugins = pluginData.map((p, i) => {
const plugin = Object.create(null);
Object.defineProperties(plugin, {
name: { get: markNative(function name() { return p.name; }), enumerable: true },
filename: { get: markNative(function filename() { return p.filename; }), enumerable: true },
description: { get: markNative(function description() { return p.description; }), enumerable: true },
length: { get: markNative(function length() { return mimeTypes.length; }), enumerable: true },
});
mimeTypes.forEach((mt, mi) => { plugin[mi] = mt; });
plugin.item = markNative(function item(idx) { return mimeTypes[idx] || null; });
plugin.namedItem = markNative(function namedItem(name) {
return mimeTypes.find(m => m.type === name) || null;
});
plugin[Symbol.toStringTag] = 'Plugin';
plugin[Symbol.iterator] = markNative(function* () { yield* mimeTypes; });
return plugin;
});
const pluginArray = Object.create(null);
Object.defineProperty(pluginArray, 'length', {
get: markNative(function length() { return plugins.length; }),
enumerable: true,
});
plugins.forEach((p, i) => { pluginArray[i] = p; });
pluginArray.item = markNative(function item(idx) { return plugins[idx] || null; });
pluginArray.namedItem = markNative(function namedItem(name) {
return plugins.find(p => p.name === name) || null;
});
pluginArray.refresh = markNative(function refresh() {});
pluginArray[Symbol.toStringTag] = 'PluginArray';
pluginArray[Symbol.iterator] = markNative(function* () { yield* plugins; });
Object.defineProperty(nav, 'plugins', {
get: markNative(function plugins() { return pluginArray; }),
configurable: true, enumerable: true,
});
// mimeTypes
const mimeTypeArray = Object.create(null);
Object.defineProperty(mimeTypeArray, 'length', {
get: markNative(function length() { return mimeTypes.length; }),
enumerable: true,
});
mimeTypes.forEach((m, i) => { mimeTypeArray[i] = m; });
mimeTypeArray.item = markNative(function item(idx) { return mimeTypes[idx] || null; });
mimeTypeArray.namedItem = markNative(function namedItem(name) {
return mimeTypes.find(m => m.type === name) || null;
});
mimeTypeArray[Symbol.toStringTag] = 'MimeTypeArray';
mimeTypeArray[Symbol.iterator] = markNative(function* () { yield* mimeTypes; });
Object.defineProperty(nav, 'mimeTypes', {
get: markNative(function mimeTypes() { return mimeTypeArray; }),
configurable: true, enumerable: true,
});
}
```
### 4. document.hasFocus()
**问题**:jsdom 的 `document.hasFocus()` 始终返回 `false`,真实浏览器活动标签页返回 `true`。
```javascript
doc.hasFocus = markNative(function hasFocus() { return true; });
```
### 5. DOM 布局属性(offsetHeight/Width/getBoundingClientRect)
**问题**:jsdom 无渲染引擎,所有布局属性返回 0。JSVMP 创建不可见元素测量布局值来检测。
```javascript
function patchDOMLayout(win) {
const HTMLElement = win.HTMLElement;
if (!HTMLElement) return;
function parseStyleDimension(el, prop) {
try {
const style = el.getAttribute && el.getAttribute('style');
if (style) {
const m = style.match(new RegExp(prop + '\\s*:\\s*(\\d+)'));
if (m) return parseInt(m[1], 10);
}
} catch (e) {}
return 0;
}
const layoutProps = ['offsetHeight', 'offsetWidth', 'clientHeight', 'clientWidth',
'scrollHeight', 'scrollWidth', 'offsetTop', 'offsetLeft'];
for (const prop of layoutProps) {
Object.defineProperty(HTMLElement.prototype, prop, {
get: markNative(function () {
const dim = prop.includes('Height') || prop.includes('Top') ? 'height' : 'width';
const fromStyle = parseStyleDimension(this, dim);
if (fromStyle > 0) return fromStyle;
const defaults = { offsetHeight: 150, offsetWidth: 300, clientHeight: 150,
clientWidth: 300, scrollHeight: 150, scrollWidth: 300, offsetTop: 0, offsetLeft: 0 };
return defaults[prop] || 0;
}),
configurable: true, enumerable: true,
});
}
HTMLElement.prototype.getBoundingClientRect = markNative(function getBoundingClientRect() {
const w = parseStyleDimension(this, 'width') || 300;
const h = parseStyleDimension(this, 'height') || 150;
return { x: 0, y: 0, top: 0, left: 0, right: w, bottom: h, width: w, height: h,
toJSON: markNative(function toJSON() { return this; }) };
});
HTMLElement.prototype.getClientRects = markNative(function getClientRects() {
return [this.getBoundingClientRect()];
});
}
```
---
## 高危级补丁(可能参与指纹哈希)
### 6. Object.prototype.toString 标签(Symbol.toStringTag)
**问题**:jsdom 的 document 原型链是 `Document` 而非 `HTMLDocument`,screen/performance 是普通对象。
```javascript
function patchToStringTags(win, doc, nav) {
const tagMap = [
[doc, 'HTMLDocument'],
[nav, 'Navigator'],
[win.screen, 'Screen'],
[win.performance, 'Performance'],
[win.location, 'Location'],
];
for (const [obj, tag] of tagMap) {
if (obj) {
try { obj[Symbol.toStringTag] = tag; } catch (e) {}
}
}
// 修复 document 的原型链构造函数名称
try {
const docProto = Object.getPrototypeOf(doc);
if (docProto && docProto.constructor && docProto.constructor.name !== 'HTMLDocument') {
Object.defineProperty(docProto.constructor, 'name', { value: 'HTMLDocument' });
}
} catch (e) {}
}
```
### 7. window.chrome 对象
**问题**:jsdom 中 `window.chrome` 为 `undefined`,Chrome 浏览器必定存在此对象。
```javascript
function patchChrome(win) {
win.chrome = {
app: {
isInstalled: false,
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
getDetails: markNative(function getDetails() { return null; }),
getIsInstalled: markNative(function getIsInstalled() { return false; }),
},
runtime: {
OnInstalledReason: {
CHROME_UPDATE: 'chrome_update', INSTALL: 'install',
SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update',
},
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
PlatformNaclArch: { ARM: 'arm', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
PlatformOs: { ANDROID: 'android', CROS: 'cros', FUCHSIA: 'fuchsia', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
connect: markNative(function connect() { return { onMessage: { addListener: () => {} }, onDisconnect: { addListener: () => {} } }; }),
sendMessage: markNative(function sendMessage() {}),
id: undefined,
},
csi: markNative(function csi() { return {}; }),
loadTimes: markNative(function loadTimes() {
return { commitLoadTime: Date.now() / 1000, connectionInfo: 'h2', finishDocumentLoadTime: 0,
finishLoadTime: 0, firstPaintAfterLoadTime: 0, firstPaintTime: 0,
navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now() / 1000 - 0.5,
startLoadTime: Date.now() / 1000 - 0.3, wasAlternateProtocolAvailable: false,
wasFetchedViaSpdy: true, wasNpnNegotiated: true };
}),
};
}
```
### 8. navigator.userAgentData
**问题**:jsdom 中不存在 `NavigatorUAData`,Chrome 86+ 浏览器默认提供。
```javascript
function patchUserAgentData(nav, uaBrands) {
const brands = uaBrands || [
{ brand: 'Chromium', version: '120' },
{ brand: 'Google Chrome', version: '120' },
{ brand: 'Not_A Brand', version: '8' },
];
const uaData = {
brands: brands,
mobile: false,
platform: 'macOS',
getHighEntropyValues: markNative(function getHighEntropyValues(hints) {
return Promise.resolve({
brands, mobile: false, platform: 'macOS',
architecture: 'arm', bitness: '64', model: '',
platformVersion: '14.2.0', uaFullVersion: '120.0.0.0',
fullVersionList: brands.map(b => ({ ...b })),
wow64: false,
});
}),
toJSON: markNative(function toJSON() { return { brands, mobile: false, platform: 'macOS' }; }),
};
uaData[Symbol.toStringTag] = 'NavigatorUAData';
Object.defineProperty(nav, 'userAgentData', {
get: markNative(function userAgentData() { return uaData; }),
configurable: true, enumerable: true,
});
}
```
### 9. performance.timing / navigation / memory
**问题**:jsdom 中 `performance.timing` 和 `performance.navigation` 为 `undefined`。
```javascript
function patchPerformance(win) {
const perf = win.performance;
if (!perf) return;
const now = Date.now();
const timing = {
navigationStart: now - 1200, unloadEventStart: now - 1100, unloadEventEnd: now - 1099,
redirectStart: 0, redirectEnd: 0, fetchStart: now - 1050,
domainLookupStart: now - 1000, domainLookupEnd: now - 990,
connectStart: now - 980, connectEnd: now - 900, secureConnectionStart: now - 950,
requestStart: now - 890, responseStart: now - 500, responseEnd: now - 400,
domLoading: now - 380, domInteractive: now - 200, domContentLoadedEventStart: now - 190,
domContentLoadedEventEnd: now - 185, domComplete: now - 50,
loadEventStart: now - 40, loadEventEnd: now - 30,
};
timing[Symbol.toStringTag] = 'PerformanceTiming';
Object.defineProperty(perf, 'timing', {
get: markNative(function timing() { return timing; }),
configurable: true, enumerable: true,
});
const navigation = { type: 0, redirectCount: 0 };
navigation[Symbol.toStringTag] = 'PerformanceNavigation';
Object.defineProperty(perf, 'navigation', {
get: markNative(function navigation() { return navigation; }),
configurable: true, enumerable: true,
});
const memory = { jsHeapSizeLimit: 4294705152, totalJSHeapSize: 35194260, usedJSHeapSize: 24486084 };
Object.defineProperty(perf, 'memory', {
get: markNative(function memory() { return memory; }),
configurable: true, enumerable: true,
});
}
```
### 10. navigator 基础属性覆盖
```javascript
function patchNavigatorBasics(nav, options) {
const overrides = {
platform: options.platform || 'MacIntel',
language: options.language || 'zh-CN',
languages: options.languages || ['zh-CN', 'zh'],
vendor: options.vendor || 'Google Inc.',
productSub: '20030107',
appVersion: options.appVersion || '5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
hardwareConcurrency: options.hardwareConcurrency || 8,
maxTouchPoints: options.maxTouchPoints || 0,
deviceMemory: options.deviceMemory || 8,
pdfViewerEnabled: true,
cookieEnabled: true,
onLine: true,
doNotTrack: null,
};
for (const [key, value] of Object.entries(overrides)) {
try {
Object.defineProperty(nav, key, {
get: markNative(function () { return value; }),
configurable: true, enumerable: true,
});
} catch (e) {}
}
}
```
### 11. document 属性补丁
```javascript
function patchDocument(doc) {
Object.defineProperty(doc, 'hidden', {
get: markNative(function hidden() { return false; }),
configurable: true, enumerable: true,
});
Object.defineProperty(doc, 'visibilityState', {
get: markNative(function visibilityState() { return 'visible'; }),
configurable: true, enumerable: true,
});
// readyState 在 jsdom 中默认为 'loading',JSVMP 运行时应为 'complete'
Object.defineProperty(doc, 'readyState', {
get: markNative(function readyState() { return 'complete'; }),
configurable: true, enumerable: true,
});
Object.defineProperty(doc, 'fullscreenEnabled', {
get: markNative(function fullscreenEnabled() { return true; }),
configurable: true, enumerable: true,
});
}
```
### 12. screen 对象重建
**问题**:jsdom 的 screen 是普通对象,缺少 `orientation`、`availLeft`/`availTop`,且 `colorDepth` 可能不正确。
```javascript
function patchScreen(win, options) {
const screenData = {
width: options.width || 1920, height: options.height || 1080,
availWidth: options.availWidth || 1920, availHeight: options.availHeight || 1055,
availLeft: 0, availTop: 25,
colorDepth: options.colorDepth || 30, pixelDepth: options.pixelDepth || 30,
orientation: {
type: 'landscape-primary', angle: 0,
onchange: null,
[Symbol.toStringTag]: 'ScreenOrientation',
},
};
screenData[Symbol.toStringTag] = 'Screen';
for (const [key, value] of Object.entries(screenData)) {
try {
Object.defineProperty(win.screen, key, {
get: markNative(function () { return value; }),
configurable: true, enumerable: true,
});
} catch (e) {}
}
}
```
---
## 中危级补丁(API 存在性检测)
JSVMP 可能通过 `typeof window.XXX` 检测 API 是否存在。以下是 jsdom 缺失但浏览器存在的 API 存根。
**核心原则**:每个存根函数必须经过 `markNative()` 处理,否则 `toString()` 检测会暴露。
### 13. 批量 API 存根
```javascript
function patchWindowAPIs(win) {
// Notification
if (!win.Notification) {
win.Notification = markNative(function Notification() {});
win.Notification.permission = 'default';
win.Notification.requestPermission = markNative(function requestPermission() {
return Promise.resolve('default');
});
win.Notification.maxActions = 2;
}
// Worker / SharedWorker
if (!win.Worker) {
win.Worker = markNative(function Worker() {
return { postMessage: markNative(function() {}), terminate: markNative(function() {}),
addEventListener: markNative(function() {}), onmessage: null, onerror: null };
});
}
if (!win.SharedWorker) {
win.SharedWorker = markNative(function SharedWorker() {
return { port: { postMessage: markNative(function() {}), start: markNative(function() {}),
addEventListener: markNative(function() {}), onmessage: null } };
});
}
// RTCPeerConnection
if (!win.RTCPeerConnection) {
win.RTCPeerConnection = markNative(function RTCPeerConnection() {
return { createDataChannel: markNative(function() { return {}; }),
createOffer: markNative(function() { return Promise.resolve({}); }),
setLocalDescription: markNative(function() { return Promise.resolve(); }),
close: markNative(function() {}), onicecandidate: null };
});
}
// AudioContext / OfflineAudioContext
if (!win.AudioContext) {
win.AudioContext = markNative(function AudioContext() {
return { sampleRate: 44100, state: 'suspended', currentTime: 0, destination: {},
createOscillator: markNative(function() {
return { type: 'sine', frequency: { value: 440 },
connect: markNative(function() {}), start: markNative(function() {}),
stop: markNative(function() {}) };
}),
createDynamicsCompressor: markNative(function() {
return { threshold: { value: -24 }, knee: { value: 30 },
ratio: { value: 12 }, reduction: 0,
connect: markNative(function() {}) };
}),
createAnalyser: markNative(function() { return { connect: markNative(function() {}) }; }),
createGain: markNative(function() { return { gain: { value: 1 }, connect: markNative(function() {}) }; }),
close: markNative(function() { return Promise.resolve(); }),
resume: markNative(function() { return Promise.resolve(); }) };
});
}
if (!win.OfflineAudioContext) {
win.OfflineAudioContext = markNative(function OfflineAudioContext(channels, length, sampleRate) {
const ctx = new win.AudioContext();
ctx.startRendering = markNative(function startRendering() {
return Promise.resolve({ duration: length / sampleRate, length: length,
numberOfChannels: channels, sampleRate: sampleRate,
getChannelData: markNative(function() { return new Float32Array(length); }) });
});
return ctx;
});
}
// fetch + Request/Response/Headers/AbortController
if (!win.fetch) {
win.fetch = markNative(function fetch() { return Promise.reject(new Error('Network error')); });
win.Request = markNative(function Request() {});
win.Response = markNative(function Response() {});
if (!win.AbortController) {
win.AbortController = markNative(function AbortController() {
return { signal: { aborted: false }, abort: markNative(function() {}) };
});
}
}
// matchMedia
if (!win.matchMedia) {
win.matchMedia = markNative(function matchMedia(query) {
return { matches: false, media: query, onchange: null,
addListener: markNative(function() {}), removeListener: markNative(function() {}),
addEventListener: markNative(function() {}), removeEventListener: markNative(function() {}) };
});
}
// requestIdleCallback / cancelIdleCallback
if (!win.requestIdleCallback) {
win.requestIdleCallback = markNative(function requestIdleCallback(cb) {
return win.setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 1);
});
win.cancelIdleCallback = markNative(function cancelIdleCallback(id) { win.clearTimeout(id); });
}
// requestAnimationFrame(jsdom pretendToBeVisual=true 已提供,但检查一下)
if (!win.requestAnimationFrame) {
win.requestAnimationFrame = markNative(function requestAnimationFrame(cb) {
return win.setTimeout(() => cb(Date.now()), 16);
});
win.cancelAnimationFrame = markNative(function cancelAnimationFrame(id) { win.clearTimeout(id); });
}
// indexedDB
if (!win.indexedDB) {
win.indexedDB = { open: markNative(function open() { return {}; }) };
win.indexedDB[Symbol.toStringTag] = 'IDBFactory';
}
// caches
if (!win.caches) {
win.caches = {
open: markNative(function open() { return Promise.resolve({}); }),
has: markNative(function has() { return Promise.resolve(false); }),
delete: markNative(function _delete() { return Promise.resolve(false); }),
keys: markNative(function keys() { return Promise.resolve([]); }),
match: markNative(function match() { return Promise.resolve(undefined); }),
};
win.caches[Symbol.toStringTag] = 'CacheStorage';
}
// speechSynthesis
if (!win.speechSynthesis) {
win.speechSynthesis = {
speaking: false, pending: false, paused: false,
getVoices: markNative(function getVoices() { return []; }),
speak: markNative(function speak() {}),
cancel: markNative(function cancel() {}),
};
win.speechSynthesis[Symbol.toStringTag] = 'SpeechSynthesis';
}
// customElements
if (!win.customElements) {
win.customElements = {
define: markNative(function define() {}),
get: markNative(function get() { return undefined; }),
whenDefined: markNative(function whenDefined() { return Promise.resolve(); }),
};
win.customElements[Symbol.toStringTag] = 'CustomElementRegistry';
}
// visualViewport
if (!win.visualViewport) {
win.visualViewport = {
width: win.innerWidth || 1920, height: win.innerHeight || 1080,
offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, scale: 1,
onresize: null, onscroll: null,
addEventListener: markNative(function() {}), removeEventListener: markNative(function() {}),
};
win.visualViewport[Symbol.toStringTag] = 'VisualViewport';
}
// isSecureContext
if (win.isSecureContext === undefined) {
Object.defineProperty(win, 'isSecureContext', {
get: markNative(function isSecureContext() { return true; }),
configurable: true, enumerable: true,
});
}
// clientInformation
if (!win.clientInformation) {
Object.defineProperty(win, 'clientInformation', {
get: markNative(function clientInformation() { return nav; }),
configurable: true, enumerable: true,
});
}
}
```
### 14. navigator 扩展 API 存根
```javascript
function patchNavigatorAPIs(nav) {
// connection (NetworkInformation)
if (!nav.connection) {
const conn = {
effectiveType: '4g', downlink: 10, rtt: 50, saveData: false,
onchange: null, type: 'wifi',
addEventListener: markNative(function() {}), removeEventListener: markNative(function() {}),
};
conn[Symbol.toStringTag] = 'NetworkInformation';
Object.defineProperty(nav, 'connection', {
get: markNative(function connection() { return conn; }),
configurable: true, enumerable: true,
});
}
// getBattery
if (!nav.getBattery) {
nav.getBattery = markNative(function getBattery() {
return Promise.resolve({
charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1,
onchargingchange: null, onchargingtimechange: null,
ondischargingtimechange: null, onlevelchange: null,
});
});
}
}
```
### 15. document 扩展 API 存根
```javascript
function patchDocumentAPIs(doc) {
// fonts (FontFaceSet)
if (!doc.fonts) {
const fonts = {
ready: Promise.resolve(),
status: 'loaded',
check: markNative(function check() { return true; }),
load: markNative(function load() { return Promise.resolve([]); }),
forEach: markNative(function forEach() {}),
entries: markNative(function entries() { return [][Symbol.iterator](); }),
keys: markNative(function keys() { return [][Symbol.iterator](); }),
values: markNative(function values() { return [][Symbol.iterator](); }),
[Symbol.iterator]: markNative(function () { return [][Symbol.iterator](); }),
size: 0,
};
fonts[Symbol.toStringTag] = 'FontFaceSet';
Object.defineProperty(doc, 'fonts', {
get: markNative(function fonts() { return fonts; }),
configurable: true, enumerable: true,
});
}
// timeline (DocumentTimeline)
if (!doc.timeline) {
const timeline = { currentTime: win.performance?.now?.() || 0 };
timeline[Symbol.toStringTag] = 'DocumentTimeline';
Object.defineProperty(doc, 'timeline', {
get: markNative(function timeline() { return timeline; }),
configurable: true, enumerable: true,
});
}
}
```
---
## window 尺寸属性
```javascript
function patchWindowDimensions(win, options) {
const dims = {
innerWidth: options.innerWidth || 1920,
innerHeight: options.innerHeight || 969,
outerWidth: options.outerWidth || 1920,
outerHeight: options.outerHeight || 1055,
devicePixelRatio: options.devicePixelRatio || 2,
scrollX: 0, scrollY: 0, pageXOffset: 0, pageYOffset: 0,
};
for (const [key, value] of Object.entries(dims)) {
try {
Object.defineProperty(win, key, {
get: markNative(function () { return value; }),
configurable: true, enumerable: true,
});
} catch (e) {}
}
}
```
---
## Node.js 特征清除
在 jsdom 的 window 上下文中隐藏 Node.js 运行时特征:
```javascript
function hideNodeFeatures(win) {
const nodeGlobals = ['process', 'module', 'exports', 'require',
'global', '__filename', '__dirname', 'Buffer'];
for (const g of nodeGlobals) {
try { delete win[g]; } catch (e) {
try { win[g] = undefined; } catch (e2) {}
}
}
}
```
---
## 完整 patchEnvironment 调用顺序
**顺序很重要**:markNative 必须在所有补丁之前定义,所有补丁函数必须经过 markNative 处理。
```javascript
function patchEnvironment(win, doc, nav, options = {}) {
// ① 定义 markNative(三层防御)— 必须最先
setupMarkNative(win);
// ② 扫描并标记所有 jsdom 内置原型链方法
scanAllPrototypes(win, doc, nav);
// ③ 致命级补丁
patchNavigatorBasics(nav, options); // webdriver / platform / language
buildPluginArray(win); // plugins 完整结构
doc.hasFocus = markNative(function hasFocus() { return true; });
patchDOMLayout(win); // offsetHeight/Width/getBoundingClientRect
// ④ 高危级补丁
patchToStringTags(win, doc, nav); // Symbol.toStringTag
patchChrome(win); // window.chrome
patchUserAgentData(nav); // userAgentData
patchPerformance(win); // timing/navigation/memory
patchDocument(doc); // hasFocus/readyState/visibility
patchScreen(win, options); // colorDepth/orientation
patchWindowDimensions(win, options); // innerWidth/outerWidth/devicePixelRatio
// ⑤ 中危级补丁
patchWindowAPIs(win); // 30+ API 存根
patchNavigatorAPIs(nav); // connection/getBattery
patchDocumentAPIs(doc); // fonts/timeline
// ⑥ 清除 Node.js 特征
hideNodeFeatures(win);
// ⑦ 最后一次全局扫描,标记新增的所有函数
scanAllPrototypes(win, doc, nav);
}
```
---
## 环境采集对比方法
### 浏览器端采集(通过 Camoufox MCP)
分 4 批次通过 `evaluate_js` 在真实浏览器中执行采集脚本,
采集代码模板参考 `cases/` 中的「浏览器指纹采集方法」段。
### jsdom 端采集
在 Node.js 中用 `win.eval()` 执行完全相同的采集代码:
```javascript
const dom = createPatchedJsdom('https://target.com');
const win = dom.window;
const jsdomResult = JSON.parse(win.eval(`JSON.stringify({
nav_userAgent: navigator.userAgent,
nav_plugins_length: navigator.plugins ? navigator.plugins.length : -1,
nav_webdriver: navigator.webdriver,
// ... 与浏览器端完全相同的采集代码
})`));
```
### Diff 生成
```javascript
function diffEnv(browserData, jsdomData) {
const diffs = [];
for (const key of Object.keys(browserData)) {
const bVal = JSON.stringify(browserData[key]);
const jVal = JSON.stringify(jsdomData[key]);
if (bVal !== jVal) {
diffs.push({ key, browser: browserData[key], jsdom: jsdomData[key] });
}
}
return diffs;
}
```
---
## MCP 辅助工作流
```
1. compare_env → 快速采集浏览器基准环境数据(覆盖主流检测项)
→ 适合快速判断差异范围
2. evaluate_js → 分批采集细粒度环境值(4-5 批次)
→ 覆盖 Function.toString / Symbol.toStringTag / DOM 布局等 compare_env 不包含的项
→ 每批代码不要太长,避免执行超时
3. 本地 Node.js 脚本 → 在 jsdom 中运行相同采集代码 + 生成 diff 报告
4. 迭代修复 → 每次修复一批差异 → 重新 diff → 直到差异归零
5. 端到端验证 → jsdom 生成签名 → 真实接口请求 → 确认数据有效
```
+486
View File
@@ -0,0 +1,486 @@
# JSVMP 虚拟机保护分析指南
## 什么是 JSVMP
JSVMP(JavaScript Virtual Machine Protection)是一种高级 JS 代码保护技术,将原始 JS 源码编译为自定义字节码,运行时由内嵌的虚拟机解释器逐条执行。这意味着:
- 原始代码逻辑不再以 JS AST 形式存在,无法通过传统反混淆手段还原
- 代码以字节码数组 + 解释器循环的形式运行
- 常见于瑞数、极验、某数等商业级反爬方案
**核心原则:不反编译字节码,用行为追踪法(Hook / 插桩 / 日志分析)从 I/O 两端夹逼定位签名逻辑。**
---
## 识别 JSVMP
### 文件特征
| 特征 | 描述 |
|------|------|
| 文件大小 | 200KB+ 的单文件,通常 500KB~2MB |
| 变量命名 | 完全无意义:单字母(`a`, `b`, `c`)或 `_0x` 前缀 |
| 超大数组 | 包含数千个数字元素的数组(字节码) |
| 解释器循环 | `while(true) { switch(opcode) { case 0: ... case 1: ... } }` |
| 栈操作 | 频繁出现 `push`、`pop`、`shift` 操作 |
| API 劫持 | 重写 `XMLHttpRequest`、`fetch`、`document.cookie` 等原生 API |
### 代码模式示例
```javascript
// 典型的 JSVMP 解释器结构
var _0x1234 = [3, 15, 7, 22, ...]; // 字节码数组
var _0x5678 = []; // 操作栈
var _0xabcd = 0; // 指令指针
function _0xefgh() {
while (true) {
var _0x9999 = _0x1234[_0xabcd++];
switch (_0x9999) {
case 0: _0x5678.push(_0x1234[_0xabcd++]); break; // PUSH
case 1: var a = _0x5678.pop(), b = _0x5678.pop(); _0x5678.push(a + b); break; // ADD
case 2: /* ... */ break;
// 数十到数百个 case
}
}
}
```
### 如何确认是 JSVMP
```
MCP 操作:
- list_scripts → 找到异常大的 JS 文件
- get_script_source(script_id=N, start_line=1, end_line=50) → 查看文件头部结构
- search_code(keyword="while") → 搜索解释器循环
- search_code_in_script(script_id=N, keyword="switch") → 在指定大文件中精确搜索(前后 3 行上下文)
- search_code(keyword="push|pop|shift") → 搜索栈操作
- dump_jsvmp_strings → 提取字符串数组,识别 API 名称和混淆模式(快速判断)
```
---
## 三板斧方法论
### 快速路径:一键 JSVMP 分析(推荐先试)
对于典型 JSVMP,可以跳过手动 Hook 直接使用专项工具:
```
MCP 操作:
- hook_jsvmp_interpreter → 自动 Hook Function.prototype.apply + 追踪 30+ 敏感属性读取
- dump_jsvmp_strings → 提取字符串数组,识别 API 名称,检测混淆模式
- 触发目标操作(翻页、提交等)
- get_jsvmp_log → 获取结构化分析结果:
· API 调用统计(哪些原生函数被 VM 调用)
· 属性读取摘要(哪些环境属性被访问)
· 调用时序(帮助定位签名生成节点)
- compare_env → 收集完整浏览器环境(navigator/screen/canvas/WebGL/Audio/timing)
· 用于后续 Node.js/Python 补环境时对照
快速路径无法解决时,再进入手动三板斧流程 ↓
```
### 第一板斧:Hook 出入口(确定 I/O 边界)
**目标**:不看 VM 内部实现,只关心"什么进去了"和"什么出来了"。
#### 1.1 Hook 出口 — 签名值去了哪里
```
MCP 操作:
- inject_hook_preset(preset="xhr", persistent=True) → 持久化拦截 XHR(跨导航不丢失)
- inject_hook_preset(preset="fetch", persistent=True) → 持久化拦截 Fetch
- hook_function(
function_path="Document.prototype.cookie",
hook_code="console.log('[COOKIE_SET]', arguments[0], new Error().stack)",
position="before", non_overridable=True
) → 拦截 Cookie 写入(防覆盖)
- freeze_prototype(className="XMLHttpRequest", methodName="send")
→ 冻结 XHR.send 防止 VM 覆盖 Hook
- reload() → 刷新页面触发 Hook
- get_console_logs → 查看捕获的签名值和调用栈
```
**关键信息提取**:
- 签名参数名(如 `sign`、`m`、`_signature`)
- 签名值的格式(长度、字符集、编码方式)
- 签名值出现在请求的哪个位置(URL params / Body / Header / Cookie)
#### 1.2 Hook 入口 — 明文从哪里来
```
MCP 操作:
- inject_hook_preset(preset="crypto", persistent=True)
→ 持久化 Hook btoa/atob/JSON.stringify/JSON.parse
- hook_function(function_path="String.fromCharCode",
hook_code="if(arguments.length > 1) console.log('[fromCharCode]', Array.from(arguments).map(Number), String.fromCharCode(...arguments))",
position="before")
→ JSVMP 高频使用 fromCharCode 构造字符串
- add_init_script(script=`
// Hook 常见加密库入口
const _origMD5 = window.CryptoJS?.MD5;
if (_origMD5) {
window.CryptoJS.MD5 = function() {
console.log('[CryptoJS.MD5] input:', arguments[0]?.toString());
const result = _origMD5.apply(this, arguments);
console.log('[CryptoJS.MD5] output:', result.toString());
return result;
};
}
`)
```
#### 1.3 I/O 关联分析
拿到出口和入口的数据后,进行关联:
```
已知信息:
出口:sign=a1b2c3d4e5f6... (32位hex → 疑似MD5)
入口:CryptoJS.MD5 输入 = "page=1&ts=1680000000&key=secret123"
关联结论:
sign = MD5("page=" + page + "&ts=" + timestamp + "&key=" + secret_key)
```
---
### 第二板斧:插桩解释器(追踪执行链路)
当出入口 Hook 无法直接关联(例如 VM 使用自实现的 MD5 而非 CryptoJS)时,需要插桩解释器。
#### 2.1 定位解释器核心函数
```
MCP 操作:
- search_code(keyword="while.*true") → 找到解释器主循环
- search_code_in_script(script_id=N, keyword="case 0:|case 1:|case 2:")
→ 在指定大文件中搜索 opcode 分发表(前后 3 行上下文,更精确)
- get_script_source → 读取解释器函数体
判断标准:
- 函数体超过 500 行
- 包含 20+ 个 case 分支
- 有数组索引递增操作(指令指针移动)
```
#### 2.2 分层插桩策略
**原则:由粗到细,逐步缩小范围。**
**第一轮 — 分发器级别(粗粒度)**
```
MCP 操作:
- trace_function(
function_path="解释器分发函数名",
log_args=true, log_return=true, max_captures=500,
persistent=True → 持久化追踪,跨导航不丢失
)
- 触发一次包含签名参数的请求
- get_trace_data → 获取 500 条执行记录(自动合并页面数据 + Python 端持久化数据)
分析要点:
- 观察每次调用的参数模式
- 找到签名值首次出现的调用序号
- 缩小时间窗口
```
**第二轮 — 子函数级别(中粒度)**
```
从第一轮日志中识别被频繁调用的子函数(通常 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)
- 再次触发请求
- get_trace_data → 分析子函数的输入输出
```
**第三轮 — 字符串操作级别(细粒度)**
```
JSVMP 中字符串操作是签名生成的关键信号:
MCP 操作:
- hook_function(function_path="String.prototype.charAt",
hook_code="console.log('[charAt]', this.substring(0,50), arguments[0])",
position="before")
- hook_function(function_path="String.prototype.charCodeAt", ...)
- hook_function(function_path="String.prototype.substring", ...)
- hook_function(function_path="String.prototype.concat", ...)
- hook_function(function_path="Array.prototype.join", ...)
分析要点:
- 追踪字符串如何被逐步拼接
- 观察 charCodeAt 的输入字符和输出编码
- 关注 join('') 调用 —— 通常是最终拼接点
```
#### 2.3 关键变量监控
**方法 1:使用 MCP 专用工具(推荐)**
```
MCP 操作:
- trace_property_access(target_expression="疑似签名容器对象")
→ Proxy 级别属性访问追踪,自动记录读写操作
- get_property_access_log → 获取属性访问记录
- compare_env → 收集完整浏览器环境信息
→ 用于后续补环境时精确对照 Node.js/Python 差异
```
**方法 2:手动注入监控脚本**
```javascript
// 通过 add_init_script(persistent=True) 注入
(function() {
// 方法1:监控全局变量写入
const watched = ['sign', 'm', '_signature', 'token'];
const origDefineProperty = Object.defineProperty;
watched.forEach(key => {
let val;
try {
origDefineProperty(window, key, {
get() { return val; },
set(v) {
console.log(`[GLOBAL_SET] window.${key} =`, v);
console.trace();
val = v;
},
configurable: true
});
} catch(e) {}
});
// 方法2:监控对象属性赋值
const _origAssign = Object.assign;
Object.assign = function() {
const result = _origAssign.apply(this, arguments);
for (let i = 1; i < arguments.length; i++) {
const src = arguments[i];
if (src && typeof src === 'object') {
Object.keys(src).forEach(k => {
if (watched.includes(k.toLowerCase())) {
console.log(`[ASSIGN] ${k} =`, src[k]);
console.trace();
}
});
}
}
return result;
};
})();
```
---
### 第三板斧:日志分析(从海量数据中提取签名链路)
JSVMP 的 trace 日志通常有数百甚至数千条,需要系统化的分析方法。
#### 3.1 日志采集
```
MCP 操作:
- get_trace_data → 获取函数追踪数据(自动合并页面数据 + Python 端持久化数据)
- get_jsvmp_log → 获取 JSVMP 专项日志(含 API 调用统计 + 属性读取摘要)
- get_console_logs → 获取 Hook 输出
- get_breakpoint_data → 获取伪断点捕获数据
- get_property_access_log → 获取属性访问记录
```
#### 3.2 过滤策略
| 过滤维度 | 方法 | 说明 |
|---------|------|------|
| 关键词过滤 | 搜索签名值片段 | 在日志中搜索已知签名值的前8位 |
| 时间窗口过滤 | 只看请求前 1-2 秒 | 签名通常在请求发送前即时生成 |
| 类型过滤 | 只看字符串参数 | 数值型操作大多是 VM 内部调度 |
| 长度过滤 | 关注长度 > 8 的字符串 | 排除短字符串噪声 |
| 变化过滤 | 对比多次请求日志 | 每次都变的是动态参数,不变的是密钥 |
#### 3.3 反向追踪法(核心技巧)
从已知的签名值反向追踪到原始明文:
```
步骤 1:在日志中搜索签名值
→ 搜索 "a1b2c3d4" (签名值的前几位)
→ 找到:[trace#247] return "a1b2c3d4e5f6..."
步骤 2:查看该次调用的输入
→ [trace#247] args: ["page=1&ts=1680000000&key=secret123"]
→ 这是签名函数的输入明文
步骤 3:继续追踪输入来源
→ 搜索 "page=1&ts=1680000000"
→ [trace#245] return "page=1&ts=1680000000&key=secret123"
→ [trace#245] args: ["page=1", "ts=1680000000", "key=secret123"]
→ 这是参数拼接函数
步骤 4:确认各参数来源
→ "page=1" → 用户输入
→ "ts=1680000000" → Date.now() / 1000
→ "key=secret123" → 硬编码密钥
结论:sign = MD5("page=" + page + "&ts=" + Math.floor(Date.now()/1000) + "&key=secret123")
```
#### 3.4 多次请求对比法
```
请求 1: sign=aaa111, ts=1680000000, page=1
请求 2: sign=bbb222, ts=1680000005, page=2
请求 3: sign=ccc333, ts=1680000010, page=1
对比分析:
- page 相同但 sign 不同(请求1 vs 请求3)→ ts 参与签名
- ts 不同且 sign 不同 → ts 是变化因子之一
- 请求1 和 请求3 page 相同但 sign 不同 → 不仅仅是 page 参与
结论:sign 至少由 page + ts 共同决定
```
#### 3.5 算法指纹识别
根据 I/O 特征反推算法类型:
| 输出特征 | 可能算法 |
|---------|---------|
| 32 位 hex | MD5 |
| 40 位 hex | SHA-1 |
| 64 位 hex | SHA-256 |
| 44 位 Base64(含 `=` 填充) | HMAC-SHA256 + Base64 |
| 24/32/44 位 Base64 | AES 加密(CBC/ECB) |
| 输出长度随输入变化 | 非固定哈希,可能是加密或自定义编码 |
| 输出包含特殊分隔符 | 自定义拼接格式 |
---
## 常见 JSVMP 变体及应对
### 变体 1:VM 劫持 XHR/Fetch
**特征**:VM 重写了 `XMLHttpRequest` 或 `fetch`,请求在 VM 内部完成,外部 Hook 抓不到。
**应对**:
```
- Hook 更底层的接口:
· hook_function(function_path="XMLHttpRequest.prototype.send", ...)
· 在 add_init_script 中保存原始引用,在 VM 重写之前
- 或 Hook 网络层:
· start_network_capture → 不依赖 JS 层 Hook,直接在协议层捕获
· list_network_requests → get_network_request → 获取完整请求详情
```
### 变体 2:VM 动态生成加密函数
**特征**:加密算法不是硬编码在字节码中,而是 VM 运行时通过 `eval` 或 `new Function` 动态构造。
**应对**:
```
- inject_hook_preset(preset="crypto") → 包含 eval/Function Hook
- hook_function(function_path="Function",
hook_code="console.log('[new Function]', arguments[arguments.length-1].substring(0,200))",
position="before")
- 从 Hook 日志中提取动态生成的函数源码
```
### 变体 3:多层 VM 嵌套
**特征**:外层 VM 解密出内层 VM 的字节码,再由内层 VM 执行签名逻辑。
**应对**:
```
- 不要试图理解嵌套关系
- 依然从 I/O 两端入手,Hook 最终的出口和最初的入口
- 增加 trace_function 的 max_captures 到 1000+
- 用时间戳过滤法缩小日志范围
```
### 变体 4:VM + WASM 混合
**特征**:VM 负责流程控制,核心加密算法调用 WASM 导出函数。
**应对**:
```
- search_code(keyword="WebAssembly|wasm|instantiate")
- Hook WASM 导出函数:
evaluate_js(expression=`
// 等 WASM 实例化后 Hook 导出函数
const origInstantiate = WebAssembly.instantiate;
WebAssembly.instantiate = async function() {
const result = await origInstantiate.apply(this, arguments);
const exports = result.instance?.exports || result.exports;
Object.keys(exports).forEach(name => {
if (typeof exports[name] === 'function') {
const orig = exports[name];
exports[name] = function() {
console.log('[WASM]', name, 'args:', Array.from(arguments));
const ret = orig.apply(this, arguments);
console.log('[WASM]', name, 'return:', ret);
return ret;
};
}
});
return result;
};
`)
- 确认 WASM 函数的 I/O 后,下载 .wasm 文件用 Node.js 直接加载调用
```
---
## 还原策略决策
```
JSVMP 签名还原决策树:
├─ Hook 确认使用标准算法(MD5/HMAC/AES)?
│ ├─ YES → 纯算法还原(Node.js crypto / Python hashlib+pycryptodome)
│ │ · 还原拼接顺序和密钥即可
│ │ · 这是最理想的结果
│ └─ NO → 继续分析
│
├─ 能提取出独立的签名函数?
│ ├─ YES → 沙箱执行(Node.js vm / Python execjs)
│ │ · 提取函数及其依赖
│ │ · 构造最小执行环境
│ └─ NO → 继续分析
│
├─ VM 劫持了整个请求链路?
│ ├─ YES → 加载完整 VM(仅调用签名入口)
│ │ · 下载完整 JS 文件
│ │ · 在 Node.js vm 中加载
│ │ · 补最小浏览器环境
│ │ · 调用签名函数获取结果
│ └─ NO → 浏览器自动化兜底
│
└─ 无法脱离浏览器
→ 使用 Camoufox 浏览器自动化(最后手段)
```
---
## 实战检查清单
在 JSVMP 分析过程中,确保以下每项都已完成:
- [ ] 确认文件是 JSVMP(解释器循环 + 字节码数组 + 栈操作)
- [ ] Hook XHR/Fetch 出口,捕获签名值的最终形态
- [ ] Hook Cookie setter,确认是否有动态 Cookie
- [ ] Hook 加密原语入口(btoa/atob/CryptoJS/MD5/SHA)
- [ ] Hook String.fromCharCode(JSVMP 高频信号)
- [ ] 定位解释器核心分发函数
- [ ] 分层插桩(粗 → 中 → 细)
- [ ] 日志反向追踪,找到签名值首次出现的位置
- [ ] 多次请求对比,确认变化因子和固定因子
- [ ] 根据输出特征判断算法类型
- [ ] 选择还原策略(纯算法 / 沙箱 / 完整 VM / 浏览器)
- [ ] 实现并验证签名计算结果
+194 -125
View File
@@ -2,46 +2,51 @@
## 概述 ## 概述
本手册详细说明 `js-reverse` 和 `chrome-devtools` 两个 MCP 服务器的协作使用方式, 本手册详细说明 `camoufox-reverse` MCP 服务器的使用方式,
提供常见逆向场景下的具体操作步骤。 提供常见逆向场景下的具体操作步骤。
## 工具速查表 ## 工具速查表
### js-reverse MCP(逆向分析核心) ### camoufox-reverse MCP(52 个工具)
| 类别 | 工具 | 核心用途 | | 类别 | 工具 | 核心用途 |
|------|------|---------| |------|------|---------|
| **源码搜索** | `search_in_sources` | 在所有 JS 中搜索关键词 | | **浏览器** | `launch_browser` / `close_browser` | 启动/关闭反检测浏览器 |
| **源码获取** | `get_script_source` | 获取指定脚本的代码片段 | | **导航** | `navigate` / `reload` / `go_back` | 页面导航 |
| **源码保存** | `save_script_source` | 将脚本保存到本地文件 | | **页面交互** | `click` / `type_text` / `wait_for` | 元素交互 |
| **截图** | `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` | 列出页面加载的所有脚本 | | **脚本列表** | `list_scripts` | 列出页面加载的所有脚本 |
| **断点设置** | `set_breakpoint_on_text` | 按代码文本设置断点 | | **执行 JS** | `evaluate_js` / `evaluate_js_handle` | 在页面执行任意 JS |
| **XHR 断点** | `break_on_xhr` | 在 XHR 请求时暂停 | | **加载前注入** | `add_init_script` | 页面加载前注入脚本(支持 `persistent` 跨导航重注入) |
| **函数追踪** | `trace_function` | 追踪函数调用,记录参数 | | **伪断点** | `set_breakpoint_via_hook` | 通过 Hook 设置伪断点(支持 `persistent` 持久化) |
| **暂停信息** | `get_paused_info` | 获取断点处的栈和变量 | | **断点数据** | `get_breakpoint_data` | 获取伪断点捕获的数据 |
| **单步调试** | `step_into/over/out` | 单步执行 | | **函数追踪** | `trace_function` / `get_trace_data` | 追踪函数调用(支持 `persistent` 跨导航持久化) |
| **脚本注入** | `inject_before_load` | 页面加载前注入 JS | | **自定义 Hook** | `hook_function` | 注入自定义 Hook(before/after/replace,支持 `non_overridable` 防覆盖) |
| **脚本执行** | `evaluate_script` | 在页面执行 JS | | **预设 Hook** | `inject_hook_preset` | 一键 Hook(xhr/fetch/crypto/websocket/debugger_bypass,默认持久化) |
| **网络请求** | `list_network_requests` | 列出网络请求 | | **移除 Hook** | `remove_hooks` | 移除所有 Hook(支持 `keep_persistent` 保留持久化 Hook) |
| **请求详情** | `get_network_request` | 获取请求完整信息 | | **冻结原型** | `freeze_prototype` | 冻结任意原型方法,防止页面 JS 覆盖 Hook |
| **请求来源** | `get_request_initiator` | 获取请求的 JS 调用栈 | | **属性追踪** | `trace_property_access` / `get_property_access_log` | Proxy 级别属性访问追踪 |
| **WebSocket** | `list_websocket_connections` | 列出 WS 连接 | | **JSVMP 插桩** | `hook_jsvmp_interpreter` | 一键插桩 JSVMP(Function.prototype.apply + 30+ 敏感属性) |
| **WS 分析** | `analyze_websocket_messages` | 分析 WS 消息模式 | | **JSVMP 日志** | `get_jsvmp_log` | 获取 JSVMP 日志(API 调用统计 + 属性读取摘要) |
| **控制台** | `list_console_messages` | 读取控制台输出 | | **JSVMP 字符串** | `dump_jsvmp_strings` | 提取字符串数组,识别 API 名称,检测混淆模式 |
| **环境采集** | `compare_env` | 全面收集浏览器环境(navigator/screen/canvas/WebGL/Audio/timing) |
### chrome-devtools MCP(浏览器自动化) | **网络捕获** | `start_network_capture` / `stop_network_capture` | 启停网络捕获(支持 `capture_body` 捕获响应体) |
| **网络请求** | `list_network_requests` | 列出捕获的请求 |
| 类别 | 工具 | 核心用途 | | **请求详情** | `get_network_request` | 获取请求完整信息(含响应体) |
|------|------|---------| | **请求来源** | `get_request_initiator` | 获取请求的 JS 调用栈(改进 URL 匹配 + 诊断信息) |
| **页面管理** | `new_page` / `navigate_page` / `close_page` | 页面操作 | | **请求拦截** | `intercept_request` / `stop_intercept` | 拦截/修改/Mock 请求 |
| **UI 交互** | `click` / `fill` / `type_text` / `press_key` | 元素交互 | | **Cookie** | `get_cookies` / `set_cookies` / `delete_cookies` | Cookie 管理 |
| **DOM 快照** | `take_snapshot` | 获取页面文本结构 | | **Storage** | `get_storage` / `set_storage` | localStorage / sessionStorage |
| **截图** | `take_screenshot` | 页面截图 | | **状态管理** | `export_state` / `import_state` | 保存/恢复浏览器状态 |
| **脚本执行** | `evaluate_script` | 执行 JS | | **控制台** | `get_console_logs` | 读取控制台输出 |
| **设备模拟** | `emulate` | 模拟 UA/视口/网络 | | **指纹** | `get_fingerprint_info` | 检查浏览器指纹 |
| **网络请求** | `list_network_requests` / `get_network_request` | 请求监控 | | **反检测** | `check_detection` | 测试是否被反爬识别 |
| **等待** | `wait_for` | 等待文本出现 | | **反调试** | `bypass_debugger_trap` | 绕过 debugger 陷阱 |
| **对话框** | `handle_dialog` | 处理弹窗 |
## 场景化操作手册 ## 场景化操作手册
@@ -50,40 +55,46 @@
**目标**:找到接口中加密参数的生成逻辑 **目标**:找到接口中加密参数的生成逻辑
``` ```
步骤 1:打开目标页面 步骤 1:启动反检测浏览器
[chrome-devtools] new_page(url="https://target.com") [camoufox-reverse] launch_browser()
步骤 2:获取页面结构 步骤 2:导航到目标页面
[chrome-devtools] take_snapshot() → 确认页面正常加载 [camoufox-reverse] navigate(url="https://target.com")
步骤 3:触发数据请求(如翻页) 步骤 3:确认页面加载
[chrome-devtools] click(uid="翻页按钮的uid") [camoufox-reverse] take_screenshot() → 确认页面正常
步骤 4:开始网络捕获
[camoufox-reverse] start_network_capture()
步骤 5:触发数据请求(如翻页)
[camoufox-reverse] click(selector=".next-page")
或 或
[chrome-devtools] evaluate_script(function="document.querySelector('.next').click()") [camoufox-reverse] evaluate_js(expression="document.querySelector('.next').click()")
步骤 4:查看网络请求 步骤 6:查看网络请求
[js-reverse] list_network_requests(resourceTypes=["XHR","Fetch"]) [camoufox-reverse] list_network_requests
步骤 5:获取目标请求详情 步骤 7:获取目标请求详情
[js-reverse] get_network_request(reqid=目标请求ID) [camoufox-reverse] get_network_request(request_id=N)
→ 记录 URL、Headers、Params、Response → 记录 URL、Headers、Params、Response
步骤 6:获取请求调用栈 步骤 8:获取请求调用栈(黄金路径)
[js-reverse] get_request_initiator(requestId=目标请求ID) [camoufox-reverse] get_request_initiator(request_id=N)
→ 找到发起请求的 JS 函数 → 直接定位发起请求的 JS 函数
步骤 7:搜索加密参数 步骤 9:搜索加密参数
[js-reverse] search_in_sources(query="参数名=") [camoufox-reverse] search_code(keyword="参数名=")
[js-reverse] search_in_sources(query="参数名") [camoufox-reverse] search_code(keyword="参数名")
步骤 8:读取相关源码 步骤 10:读取相关源码
[js-reverse] get_script_source(url="包含加密逻辑的脚本URL", startLine=X, endLine=Y) [camoufox-reverse] get_script_source(script_url="包含加密逻辑的脚本URL")
步骤 9:设置断点验证 步骤 11:设置伪断点验证
[js-reverse] set_breakpoint_on_text(text="加密函数调用") [camoufox-reverse] set_breakpoint_via_hook(target_function="加密函数路径")
触发请求 → 触发请求 →
[js-reverse] get_paused_info(includeScopes=true) [camoufox-reverse] get_breakpoint_data
→ 查看变量值确认加密逻辑 → 查看捕获的参数和返回值
``` ```
### 场景 2:动态 Cookie 逆向 ### 场景 2:动态 Cookie 逆向
@@ -92,25 +103,31 @@
``` ```
步骤 1:注入 Cookie Hook 步骤 1:注入 Cookie Hook
[js-reverse] inject_before_load(script="Cookie setter Hook 代码") [camoufox-reverse] hook_function(
function_path="Document.prototype.cookie",
hook_code="console.log('[Cookie Set]', arguments[0]); console.trace('Cookie Stack');",
position="before"
)
或使用预设 Hook:
[camoufox-reverse] inject_hook_preset(preset="crypto")
步骤 2:刷新页面触发 Cookie 生成 步骤 2:刷新页面触发 Cookie 生成
[js-reverse] navigate_page(type="reload") [camoufox-reverse] reload()
步骤 3:读取 Hook 输出 步骤 3:读取 Hook 输出
[js-reverse] list_console_messages(types=["log"]) [camoufox-reverse] get_console_logs
→ 找到 Cookie 设置记录和调用栈 → 找到 Cookie 设置记录和调用栈
步骤 4:根据调用栈定位生成函数 步骤 4:根据调用栈定位生成函数
[js-reverse] search_in_sources(query="目标函数名") [camoufox-reverse] search_code(keyword="目标函数名")
[js-reverse] get_script_source(url="...", startLine=X, endLine=Y) [camoufox-reverse] get_script_source(script_url="...")
步骤 5:分析生成逻辑 步骤 5:分析生成逻辑
[js-reverse] set_breakpoint_on_text(text="cookie生成代码片段") [camoufox-reverse] set_breakpoint_via_hook(target_function="cookie生成函数")
刷新页面 → 刷新页面 →
[js-reverse] get_paused_info → 查看入参 [camoufox-reverse] get_breakpoint_data → 查看入参和返回值
[js-reverse] step_into → 进入加密函数 [camoufox-reverse] trace_function(function_path="加密函数", log_args=true, log_return=true, log_stack=true)
[js-reverse] get_paused_info → 查看中间值 [camoufox-reverse] get_trace_data → 查看中间值
``` ```
### 场景 3:混淆代码分析 ### 场景 3:混淆代码分析
@@ -119,27 +136,27 @@
``` ```
步骤 1:列出所有脚本 步骤 1:列出所有脚本
[js-reverse] list_scripts() [camoufox-reverse] list_scripts
→ 找到可疑的混淆脚本(通常文件名无意义或体积很大) → 找到可疑的混淆脚本(通常文件名无意义或体积很大)
步骤 2:保存混淆脚本到本地 步骤 2:保存混淆脚本到本地
[js-reverse] save_script_source(url="混淆脚本URL", filePath="./project/obfuscated.js") [camoufox-reverse] save_script(script_url="混淆脚本URL", save_path="./project/obfuscated.js")
步骤 3:搜索关键特征 步骤 3:搜索关键特征
[js-reverse] search_in_sources(query="_0x", urlFilter="混淆脚本URL") [camoufox-reverse] search_code(keyword="_0x")
→ 确认混淆类型(OB混淆) → 确认混淆类型(OB混淆)
[js-reverse] search_in_sources(query="switch|while.*true", urlFilter="混淆脚本URL") [camoufox-reverse] search_code(keyword="switch|while.*true")
→ 检查是否有控制流平坦化 → 检查是否有控制流平坦化
步骤 4:在浏览器中还原字符串 步骤 4:在浏览器中还原字符串
[js-reverse] evaluate_script(function="返回字符串数组的代码") [camoufox-reverse] evaluate_js(expression="返回字符串数组的代码")
→ 获取解密后的字符串映射 → 获取解密后的字符串映射
步骤 5:定位关键函数 步骤 5:定位关键函数
[js-reverse] search_in_sources(query="encrypt|sign|md5|aes", urlFilter="混淆脚本URL") [camoufox-reverse] search_code(keyword="encrypt|sign|md5|aes")
→ 找到加密相关代码的大致位置 → 找到加密相关代码的大致位置
[js-reverse] get_script_source → 获取上下文 [camoufox-reverse] get_script_source → 获取上下文
``` ```
### 场景 4:WASM 逆向 ### 场景 4:WASM 逆向
@@ -148,29 +165,24 @@
``` ```
步骤 1:搜索 WASM 加载代码 步骤 1:搜索 WASM 加载代码
[js-reverse] search_in_sources(query="WebAssembly|.wasm|instantiate") [camoufox-reverse] search_code(keyword="WebAssembly|.wasm|instantiate")
步骤 2:找到 WASM 文件 步骤 2:找到 WASM 文件
[js-reverse] list_network_requests(resourceTypes=["Other"]) [camoufox-reverse] list_network_requests
→ 找到 .wasm 文件的请求 → 找到 .wasm 文件的请求
步骤 3:分析 WASM 调用 步骤 3:分析 WASM 调用
[js-reverse] search_in_sources(query="exports.|instance.exports") [camoufox-reverse] search_code(keyword="exports.|instance.exports")
→ 找到导出函数的调用代码 → 找到导出函数的调用代码
步骤 4:追踪输入输出 步骤 4:追踪输入输出
[js-reverse] set_breakpoint_on_text(text="wasm导出函数调用") [camoufox-reverse] set_breakpoint_via_hook(target_function="wasm导出函数调用路径")
触发请求 → 触发请求 →
[js-reverse] get_paused_info → 查看入参 [camoufox-reverse] get_breakpoint_data → 查看入参和返回值
[js-reverse] step_over → 执行函数
[js-reverse] get_paused_info → 查看返回值
步骤 5:在浏览器中测试 步骤 5:在浏览器中测试
[js-reverse] evaluate_script(function="调用wasm函数并返回结果") [camoufox-reverse] evaluate_js(expression="调用wasm函数并返回结果")
→ 验证 I/O 关系 → 验证 I/O 关系
步骤 6:下载 WASM 文件
[chrome-devtools] get_network_request(reqid=WASM请求ID, responseFilePath="./project/main.wasm")
``` ```
### 场景 5:WebSocket 通信分析 ### 场景 5:WebSocket 通信分析
@@ -178,18 +190,18 @@
**目标**:分析 WebSocket 消息格式和加密 **目标**:分析 WebSocket 消息格式和加密
``` ```
步骤 1:列出 WS 连接 步骤 1:注入 WebSocket Hook
[js-reverse] list_websocket_connections() [camoufox-reverse] inject_hook_preset(preset="websocket")
步骤 2:分析消息模式 步骤 2:读取 WS 消息日志
[js-reverse] analyze_websocket_messages(wsid=连接ID, direction="sent") [camoufox-reverse] get_console_logs
[js-reverse] analyze_websocket_messages(wsid=连接ID, direction="received") → 获取 WS 发送/接收消息
步骤 3:获取具体消息 步骤 3:搜索 WS 相关代码
[js-reverse] get_websocket_messages(wsid=连接ID, show_content=true) [camoufox-reverse] search_code(keyword="WebSocket|ws.send|onmessage")
步骤 4:搜索 WS 相关代码 步骤 4:手动发送 WS 消息
[js-reverse] search_in_sources(query="WebSocket|ws.send|onmessage") [camoufox-reverse] evaluate_js(expression="ws.send(JSON.stringify({...}))")
``` ```
## 高级技巧 ## 高级技巧
@@ -197,30 +209,79 @@
### 使用 trace_function 自动记录函数调用 ### 使用 trace_function 自动记录函数调用
``` ```
[js-reverse] trace_function( [camoufox-reverse] trace_function(
functionName="encrypt", function_path="window.encrypt",
logArgs=true, log_args=true,
logThis=false, log_return=true,
pause=false log_stack=true,
max_captures=50,
persistent=True → 跨导航持久化,页面刷新/跳转后自动重注入
) )
``` ```
→ 每次 `encrypt` 被调用时自动记录参数,不暂停执行 → 每次 `encrypt` 被调用时自动记录参数、返回值和调用栈,不暂停执行
→ `get_trace_data` 会自动合并页面数据和 Python 端持久化数据
### 使用条件断点 ### 使用 inject_hook_preset 一键 Hook
``` ```
[js-reverse] set_breakpoint_on_text( 默认 persistent=True(持久化,跨导航自动重注入):
text="fetch(",
condition="arguments[0].indexOf('/api/data') !== -1" [camoufox-reverse] inject_hook_preset(preset="xhr") → Hook 所有 XHR 请求
[camoufox-reverse] inject_hook_preset(preset="fetch") → Hook 所有 fetch 请求
[camoufox-reverse] inject_hook_preset(preset="crypto") → Hook btoa/atob/JSON.stringify
[camoufox-reverse] inject_hook_preset(preset="websocket") → Hook WebSocket 消息
[camoufox-reverse] inject_hook_preset(preset="debugger_bypass") → 绕过反调试
```
### 使用 hook_function 自定义 Hook
```
[camoufox-reverse] hook_function(
function_path="XMLHttpRequest.prototype.open",
hook_code="console.log('[XHR]', arguments[0], arguments[1])",
position="before",
non_overridable=True → 防覆盖:Object.defineProperty 冻结 + toString 伪装 native code
) )
``` ```
→ 只在请求特定接口时暂停 → 在 XHR.open 调用前输出请求方法和 URL,即使页面 JS 尝试覆盖也不会丢失
### 使用 inject_before_load 进行预分析 ### 使用 freeze_prototype 防止 Hook 被覆盖
``` ```
[js-reverse] inject_before_load(script=` [camoufox-reverse] freeze_prototype(className="XMLHttpRequest", methodName="send")
// 记录所有全局变量的创建 ```
→ 冻结原型方法(configurable:false + writable:false),防止 JSVMP 等覆盖你的 Hook
### 使用 JSVMP 专项工具
```
[camoufox-reverse] hook_jsvmp_interpreter → 一键插桩 Function.prototype.apply + 30+ 敏感属性
[camoufox-reverse] dump_jsvmp_strings → 提取字符串数组,识别 API 名称
触发操作...
[camoufox-reverse] get_jsvmp_log → 获取 API 调用统计 + 属性读取摘要
[camoufox-reverse] compare_env → 收集完整浏览器环境(补环境基准线)
```
### 使用属性访问追踪
```
[camoufox-reverse] trace_property_access(target_expression="navigator")
触发操作...
[camoufox-reverse] get_property_access_log → 查看 navigator 的哪些属性被读取
```
→ 精确发现 JSVMP 或签名逻辑读取了哪些环境属性
### 使用 search_code_in_script 精确搜索大文件
```
[camoufox-reverse] search_code_in_script(script_id=3, keyword="sign|token|encrypt")
```
→ 在指定脚本中搜索,返回前后 3 行上下文,适合 200KB+ 的 JSVMP 文件
### 使用 add_init_script 进行预分析
```
[camoufox-reverse] add_init_script(script=`
const knownGlobals = new Set(Object.keys(window)); const knownGlobals = new Set(Object.keys(window));
setInterval(() => { setInterval(() => {
const newGlobals = Object.keys(window).filter(k => !knownGlobals.has(k)); const newGlobals = Object.keys(window).filter(k => !knownGlobals.has(k));
@@ -229,14 +290,13 @@
newGlobals.forEach(k => knownGlobals.add(k)); newGlobals.forEach(k => knownGlobals.add(k));
} }
}, 1000); }, 1000);
`) `, persistent=True) → 跨导航持久化
``` ```
### 使用 evaluate_script 提取运行时数据 ### 使用 evaluate_js 提取运行时数据
``` ```
[js-reverse] evaluate_script(function=` [camoufox-reverse] evaluate_js(expression=`
// 提取当前页面的所有 Cookie
JSON.stringify(document.cookie.split('; ').reduce((obj, pair) => { JSON.stringify(document.cookie.split('; ').reduce((obj, pair) => {
const [k, v] = pair.split('='); const [k, v] = pair.split('=');
obj[k] = v; obj[k] = v;
@@ -245,20 +305,29 @@
`) `)
``` ```
## 两个 MCP 的分工原则 ### 使用 intercept_request 拦截/修改请求
| 需求 | 使用 js-reverse | 使用 chrome-devtools | ```
|------|----------------|---------------------| [camoufox-reverse] intercept_request(url_pattern="**/api/data*", action="log") → 仅记录
| 搜索 JS 源码 | ✅ `search_in_sources` | ❌ | [camoufox-reverse] intercept_request(url_pattern="**/ads/*", action="block") → 屏蔽广告
| 设置断点 | ✅ `set_breakpoint_on_text` | ❌ | [camoufox-reverse] intercept_request(url_pattern="**/api/*", action="modify", modify_headers={"X-Custom": "test"})
| 单步调试 | ✅ `step_into/over/out` | ❌ | [camoufox-reverse] intercept_request(url_pattern="**/api/*", action="mock", mock_response={"status": 200, "body": "{\"ok\":1}"})
| 函数追踪 | ✅ `trace_function` | ❌ | ```
| 注入 Hook | ✅ `inject_before_load` | ❌ |
| 页面截图 | ✅ 可以 | ✅ 更灵活(指定元素) | ## camoufox-reverse MCP 工具分类总览(52 个)
| UI 交互(点击/输入) | ❌ | ✅ `click/fill/type_text` |
| 模拟 UA/设备 | ❌ | ✅ `emulate` | | 类别 | 工具 | 说明 |
| DOM 快照 | ❌ | ✅ `take_snapshot` | |------|------|------|
| 保存请求响应到文件 | ❌ | ✅ `get_network_request(responseFilePath)` | | 浏览器生命周期 | `launch_browser` `close_browser` | 启动/关闭 Camoufox |
| WebSocket 分析 | ✅ 完整支持 | ❌ | | 页面导航 | `navigate` `reload` `go_back` | 导航控制 |
| 请求调用栈 | ✅ `get_request_initiator` | ❌ | | 页面交互 | `click` `type_text` `wait_for` | UI 自动化 |
| 执行 JS | ✅ 支持 `mainWorld` 选项 | ✅ 支持 `args` 传 uid | | 页面状态 | `get_page_info` `take_screenshot` `take_snapshot` `get_page_html` | 页面信息 |
| 源码分析 | `list_scripts` `get_script_source` `search_code` `search_code_in_script` `save_script` | JS 逆向核心 |
| 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 级别属性访问监控 |
| 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` | 状态管理 |
| 反检测 | `get_fingerprint_info` `check_detection` `bypass_debugger_trap` | 指纹与反检测 |
+20 -20
View File
@@ -19,9 +19,9 @@
**MCP 辅助**: **MCP 辅助**:
``` ```
[js-reverse] search_in_sources(query="_0x") → 定位混淆入口 [camoufox-reverse] search_code(keyword="_0x") → 定位混淆入口
[js-reverse] evaluate_script → 在浏览器中执行字符串数组还原 [camoufox-reverse] evaluate_js → 在浏览器中执行字符串数组还原
[js-reverse] save_script_source → 保存到本地进一步分析 [camoufox-reverse] save_script → 保存到本地进一步分析
``` ```
### 2. 控制流平坦化 (CFF) ### 2. 控制流平坦化 (CFF)
@@ -47,9 +47,9 @@ while (true) {
**MCP 辅助**: **MCP 辅助**:
``` ```
[js-reverse] set_breakpoint_on_text(text="switch") → 在状态机入口断点 [camoufox-reverse] set_breakpoint_via_hook(target_function="状态机入口函数") → 设置伪断点
[js-reverse] step_over → 逐步追踪状态转移 [camoufox-reverse] trace_function(function_path="状态机函数", log_args=true, log_return=true) → 追踪状态转移
[js-reverse] get_paused_info → 查看当前状态值 [camoufox-reverse] get_trace_data → 查看状态值变化
``` ```
### 3. eval / Function 打包 ### 3. eval / Function 打包
@@ -68,8 +68,8 @@ new Function('return ' + decryptedCode)()
**MCP 辅助**: **MCP 辅助**:
``` ```
[js-reverse] inject_before_load(script="eval/Function Hook") [camoufox-reverse] add_init_script(script="eval/Function Hook")
[js-reverse] list_console_messages → 读取解包后的代码 [camoufox-reverse] get_console_logs → 读取解包后的代码
``` ```
### 4. AAEncode ### 4. AAEncode
@@ -118,9 +118,9 @@ $=~[];$={___:++$,$$$$:(![]+"")[$],...
**MCP 辅助**: **MCP 辅助**:
``` ```
[js-reverse] trace_function(functionName="解释器函数") [camoufox-reverse] trace_function(function_path="解释器函数", log_args=true, log_return=true)
[js-reverse] set_breakpoint_on_text(text="switch") → 在解释器循环断点 [camoufox-reverse] get_trace_data → 观察每步操作的输入输出
[js-reverse] step_over → 观察每步操作 [camoufox-reverse] set_breakpoint_via_hook(target_function="解释器核心函数") → 捕获关键调用
``` ```
### 8. JSVMP(JS 虚拟机保护) ### 8. JSVMP(JS 虚拟机保护)
@@ -139,9 +139,9 @@ $=~[];$={___:++$,$$$$:(![]+"")[$],...
**MCP 辅助**: **MCP 辅助**:
``` ```
[js-reverse] break_on_xhr(url="目标接口") → 在请求发出时暂停 [camoufox-reverse] inject_hook_preset(preset="xhr") → 一键 Hook XHR 请求
[js-reverse] get_paused_info → 获取调用栈 [camoufox-reverse] get_request_initiator(request_id=N) → 获取请求的 JS 调用栈(黄金路径)
[js-reverse] inject_before_load → 注入全局 Hook [camoufox-reverse] add_init_script → 注入全局 Hook
``` ```
## 通用反混淆 Node.js 工具 ## 通用反混淆 Node.js 工具
@@ -189,10 +189,10 @@ function deobfuscate(code) {
## 混淆代码分析的 MCP 工作流 ## 混淆代码分析的 MCP 工作流
``` ```
1. save_script_source → 保存混淆代码到本地 1. save_script → 保存混淆代码到本地
2. search_in_sources → 搜索可能的入口函数 2. search_code → 搜索可能的入口函数
3. set_breakpoint_on_text → 在入口设断点 3. set_breakpoint_via_hook → 在入口设伪断点
4. get_paused_info → 查看运行时变量,理解数据流 4. get_breakpoint_data → 查看捕获的参数和返回值
5. evaluate_script → 在浏览器执行还原操作 5. evaluate_js → 在浏览器执行还原操作
6. step_into → 跟进关键函数调用 6. trace_function → 追踪关键函数调用链
``` ```
+10 -6
View File
@@ -27,11 +27,11 @@
### 解决方案 ### 解决方案
#### 方案 A:使用浏览器自动化(最可靠) #### 方案 A:使用 Camoufox 反检测浏览器(最可靠)
```javascript ```javascript
// 使用 chrome-devtools MCP 在真实浏览器中执行请求 // 使用 camoufox-reverse MCP 在 Camoufox 浏览器环境中执行请求
// [chrome-devtools] evaluate_script // [camoufox-reverse] evaluate_js
const response = await fetch('https://target.com/api/data', { const response = await fetch('https://target.com/api/data', {
method: 'GET', method: 'GET',
headers: { 'Cookie': cookieValue }, headers: { 'Cookie': cookieValue },
@@ -42,7 +42,9 @@ const data = await response.json();
``` ```
MCP 操作: MCP 操作:
[chrome-devtools] evaluate_script(function="fetch(url).then(r=>r.json()).then(d=>JSON.stringify(d))") [camoufox-reverse] launch_browser()
[camoufox-reverse] navigate(url="https://target.com")
[camoufox-reverse] evaluate_js(expression="fetch(url, options).then(r=>r.json()).then(d=>JSON.stringify(d))")
``` ```
#### 方案 B:curl-impersonate #### 方案 B:curl-impersonate
@@ -157,10 +159,12 @@ const client = axios.create({
}); });
``` ```
### MCP 模拟 ### MCP 侧验证
``` ```
[chrome-devtools] emulate(userAgent="自定义UA") [camoufox-reverse] launch_browser(os_type="macos")
[camoufox-reverse] get_fingerprint_info
→ 验证当前浏览器环境中的 UA、平台和指纹是否与目标站点要求一致
``` ```
## 4. Referer 校验 ## 4. Referer 校验
+228
View File
@@ -0,0 +1,228 @@
# 错误排查指南
## 请求失败排查流程
### 排查顺序(必须按序执行)
当协议脚本请求返回非预期结果时,**禁止盲目大改**,按以下 6 步逐项排查:
```
步骤 ① Cookie/Session 失效
│
├─ 排查方法:
│ · 浏览器 F12 → Application → Cookies 获取最新 Cookie
│ · 对比脚本使用的 Cookie 与浏览器当前 Cookie
│ · 检查是否有 HttpOnly Cookie 漏掉(Network 面板看完整 Cookie)
│
├─ MCP 辅助:
│ · get_cookies → 获取 Camoufox 中的 Cookie
│ · evaluate_js(expression="document.cookie") → 获取 JS 可见 Cookie
│
└─ 常见原因:Cookie 过期、登录态失效、缺少 HttpOnly Cookie
步骤 ② 前置请求遗漏
│
├─ 排查方法:
│ · Network 面板查看主请求之前的所有请求
│ · 重点关注:/api/init、/token、/config 等路径
│ · 检查前置请求的响应中是否有 Token、Session ID
│
├─ MCP 辅助:
│ · start_network_capture → 触发操作 → list_network_requests
│ · 按时间排序,找到主请求之前的接口
│
└─ 常见原因:漏掉预热接口、Token 获取接口、验证码初始化接口
步骤 ③ 时间戳绑定
│
├─ 排查方法:
│ · 对比浏览器请求的时间戳与脚本生成的时间戳
│ · 检查精度:秒(10位)vs 毫秒(13位)
│ · 检查是否有时间窗口限制(如签名有效期 30 秒)
│
├─ 常见坑:
│ · Python time.time() 返回浮点数,需要 int()
│ · Node.js Date.now() 返回毫秒
│ · 服务端可能校验请求时间与服务器时间的差值
│
└─ 修复方法:确保签名计算和请求发送使用同一个时间戳值
步骤 ④ Header 缺失或错误
│
├─ 排查方法:
│ · 逐项对比浏览器 Request Headers 与脚本 Headers
│ · 重点检查:Content-Type、Referer、Origin、Accept
│ · 检查自定义 Header(X-开头的、大小写敏感的)
│
├─ 常见坑:
│ · Header 顺序可能影响(极少数站点)
│ · sec-ch-ua 等 Client Hints Header
│ · Accept-Encoding 不支持 br 但发了 br
│
└─ 修复方法:完整复制浏览器 Headers,逐个删减确认必要项
步骤 ⑤ 环境校验
│
├─ 排查方法:
│ · Hook navigator/screen/canvas 等环境 API
│ · 检查签名计算是否包含环境指纹
│ · 对比有无环境值时的请求结果
│
├─ MCP 辅助:
│ · get_fingerprint_info → 查看 Camoufox 指纹
│ · search_code(keyword="navigator|screen|canvas") → 搜索环境检测代码
│
└─ 关键原则:先验证是否真正参与服务端校验,再决定是否补全
步骤 ⑥ 频率限制
│
├─ 排查方法:
│ · 降低请求频率重试(间隔 3-5 秒)
│ · 检查响应中的限流相关字段(rate limit、retry-after)
│ · 检查是否返回了 429 状态码
│
└─ 修复方法:增加请求间隔、使用代理 IP、添加随机延迟
```
---
## 签名值不一致排查
### 逐步对比法
当计算出的签名值与浏览器实际值不匹配时:
```
对比链路(脚本值 vs 浏览器值):
环节 1:原始输入参数
├─ 参数名是否完全一致(大小写、下划线)
├─ 参数值是否完全一致
└─ 是否有隐藏参数(空值但参与签名的参数)
环节 2:参数排序与拼接
├─ 参数排序规则(字典序、自定义顺序、原始顺序)
├─ 拼接分隔符(& / 空 / 其他)
├─ 是否包含 key=value 中的 key 名称
├─ 空值参数是否参与拼接
└─ URL 编码是否在拼接前/后执行
环节 3:时间戳
├─ 精度:秒 (10位) vs 毫秒 (13位)
├─ 类型:字符串 vs 数字
└─ 时区:UTC vs 本地时间
环节 4:随机串
├─ 长度是否匹配
├─ 字符集:hex / alphanumeric / 自定义
└─ 生成方法:Math.random vs crypto.randomBytes
环节 5:密钥/盐值
├─ 密钥值是否正确(注意空格、换行、编码)
├─ 密钥是否是硬编码还是动态获取
└─ 是否有 IV / Salt(AES 加密场景)
环节 6:中间摘要
├─ 多次哈希时逐层对比中间值
└─ 编码格式:hex(小写) vs hex(大写) vs base64
环节 7:最终输出
├─ 编码方式:hex / base64 / 自定义
├─ 大小写:hex 小写 vs 大写
└─ 是否有额外处理(截断、拼接前缀等)
```
### 快速定位技巧
```
技巧 1:二分法
如果有中间值可对比,先对比中间值,
确定偏差发生在前半段还是后半段
技巧 2:固定输入法
在浏览器和脚本中使用完全相同的:
- 时间戳(硬编码一个固定值)
- 随机串(硬编码一个固定值)
- 页码(相同页码)
然后对比签名结果,排除动态因素干扰
技巧 3:打印拼接字符串
在签名函数的哈希/加密调用之前,
打印完整的输入字符串,逐字符对比
技巧 4:MCP 辅助对比
- set_breakpoint_via_hook(target_function="签名函数")
- get_breakpoint_data → 获取浏览器端的真实入参和返回值
- 与脚本端逐项对比
```
---
## 常见错误与解决方案
### HTTP 状态码
| 状态码 | 常见原因 | 排查方向 |
|--------|---------|---------|
| 403 Forbidden | Cookie 失效 / Header 缺失 / IP 被封 / TLS 指纹 | 步骤 ①④⑤⑥ |
| 412 Precondition Failed | 签名校验失败 / 缺少前置请求 | 签名对比 + 步骤 ② |
| 429 Too Many Requests | 频率限制 | 步骤 ⑥ |
| 500 Internal Server Error | 参数格式错误 / 数据类型不匹配 | 检查请求 Body 格式 |
| 200 但数据为空 | 签名正确但参数错误 / 页码越界 / 缺权限 | 检查业务参数 |
### 加密相关
| 问题 | 可能原因 | 解决方法 |
|------|---------|---------|
| MD5 结果不一致 | 编码问题(UTF-8 vs ASCII) | 确认输入字符串的编码方式 |
| AES 解密失败 | 模式错误(CBC vs ECB)/ Padding 错误 / IV 错误 | 逐一确认模式、Padding、IV |
| Base64 结果多余字符 | URL-safe Base64 vs 标准 Base64 | 检查是否需要 `+/` → `-_` 替换 |
| HMAC 结果不一致 | 密钥编码问题 / 算法类型错误 | 确认密钥是字符串还是 hex bytes |
| RSA 加密失败 | 公钥格式错误 / PKCS1 vs OAEP | 检查公钥格式和填充方案 |
### 环境相关
| 问题 | 可能原因 | 解决方法 |
|------|---------|---------|
| Node.js vm 沙箱报错 | 缺少 DOM API(document/window) | 参考 `environment-patch.md` 补环境 |
| Python execjs 报错 | Node.js 未安装 / JS 代码有语法错误 | 检查 Node.js 环境、验证 JS 代码 |
| WASM 加载失败 | 缺少 imports / 内存不足 | 检查 WASM imports 并补全 |
| Cookie 设置不生效 | domain 不匹配 / path 不匹配 | 确认 Cookie 的 domain 和 path |
---
## Python 特有问题
| 问题 | 原因 | 解决方法 |
|------|------|---------|
| `requests` 请求被拒 | TLS 指纹被识别 | 换用 `curl_cffi`(支持浏览器指纹模拟) |
| `hashlib.md5()` 报错 | Python 3.9+ FIPS 模式限制 | 使用 `hashlib.md5(usedforsecurity=False)` |
| `execjs` 执行慢 | 每次创建新的 JS 运行时 | 编译后复用 context:`ctx = execjs.compile(js_code)` |
| `pycryptodome` 与 `pycrypto` 冲突 | 同时安装了两个库 | `pip uninstall pycrypto && pip install pycryptodome` |
| 中文编码问题 | 签名包含中文字符 | 使用 `urllib.parse.quote()` 或确认 UTF-8 编码 |
| `httpx` HTTP/2 连接失败 | 缺少 h2 依赖 | `pip install httpx[http2]` |
---
## Node.js 特有问题
| 问题 | 原因 | 解决方法 |
|------|------|---------|
| `crypto` 模块不可用 | 使用了浏览器打包版本 | 确认运行环境是 Node.js 而非浏览器 |
| `vm` 沙箱超时 | JS 代码有死循环 | 设置 `timeout` 选项 |
| `axios` 被 WAF 拦截 | 默认 User-Agent | 自定义完整浏览器 UA |
| HTTP/2 请求失败 | 证书验证失败 | `rejectUnauthorized: false`(调试用) |
---
## 排查工具速查
### MCP 工具
| 排查场景 | MCP 工具 | 用法 |
|---------|---------|------|
| 对比请求差异 | `start_network_capture` + `get_network_request` | 在 Camoufox 中捕获真实请求,与脚本请求对比 |
| 获取真实签名值 | `set_breakpoint_via_hook` + `get_breakpoint_data` | 在签名函数设伪断点,捕获真实入参和返回值 |
| 验证环境检测 | `get_fingerprint_info` + `check_detection` | 确认哪些环境项参与校验 |
| 追踪调用链 | `get_request_initiator` | 从请求直接定位到签名函数 |
| 实时对比 | `evaluate_js` | 在浏览器中执行还原后的签名函数,与脚本输出对比 |
+9 -9
View File
@@ -93,30 +93,30 @@
- 加密逻辑过于复杂,还原成本远大于自动化 - 加密逻辑过于复杂,还原成本远大于自动化
**技术栈**: **技术栈**:
- `playwright` / `puppeteer` — 浏览器自动化 - `playwright-core` — 浏览器自动化
- chrome-devtools MCP — 直接操作已有 Chrome - `camoufox-reverse` MCP — Camoufox 反检测浏览器、Hook、网络捕获、源码搜索
**注意事项**: **注意事项**:
- 复用本机 Chrome 实例避免指纹差异 - 优先使用 Camoufox 独立浏览器实例,避免污染用户日常浏览环境
- 注入脚本截取中间数据 - 通过 `inject_hook_preset` / `add_init_script` 注入脚本截取中间数据
- 控制请求频率避免触发风控 - 控制请求频率并结合 `check_detection` 验证反检测效果
**模板**:`templates/browser-auto/` **模板**:`templates/browser-auto/`
## 通用调试流程 ## 通用调试流程
``` ```
1. 打开 Network 面板 1. 启动 Camoufox 并开启网络捕获
↓ ↓
2. 触发目标操作(搜索/翻页/登录) 2. 触发目标操作(搜索/翻页/登录)
↓ ↓
3. 找到数据接口,分析参数 3. 找到数据接口,分析参数
↓ ↓
4. 搜索参数名定位 JS 源码 4. 用 `search_code` 搜索参数名定位 JS 源码
↓ ↓
5. 设置 XHR 断点追踪调用栈 5. 用 `get_request_initiator` / `set_breakpoint_via_hook` 追踪调用栈
↓ ↓
6. 在关键函数设置断点 6. 在关键函数设置伪断点或 `trace_function`
↓ ↓
7. 单步调试确认加密逻辑 7. 单步调试确认加密逻辑
↓ ↓
+33 -4
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# 检查 JS 逆向项目所需的依赖环境 # 检查 JS 逆向项目所需的依赖环境(Node.js + Python)
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -7,7 +7,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' NC='\033[0m'
echo "==========================================" echo "=========================================="
echo " JS 逆向工程环境检查" echo " JS 逆向工程环境检查(Node.js + Python)"
echo "==========================================" echo "=========================================="
echo "" echo ""
@@ -43,13 +43,33 @@ check_node_module() {
fi fi
} }
check_python_module() {
local module=$1
local import_name=${2:-$1}
local name=${3:-$1}
if python3 -c "import $import_name" 2>/dev/null; then
local version=$(python3 -c "import $import_name; print(getattr($import_name, '__version__', 'installed'))" 2>/dev/null || echo "installed")
echo -e " ${GREEN}✓${NC} $name: $version"
return 0
else
echo -e " ${YELLOW}○${NC} $name: 未安装(可选)"
return 1
fi
}
errors=0 errors=0
echo "▸ 基础环境" echo "▸ Node.js 环境"
check_command "node" "Node.js" "https://nodejs.org/ 或 brew install node" || ((errors++)) check_command "node" "Node.js" "https://nodejs.org/ 或 brew install node" || ((errors++))
check_command "npm" "npm" "随 Node.js 一起安装" || ((errors++)) check_command "npm" "npm" "随 Node.js 一起安装" || ((errors++))
echo "" echo ""
echo "▸ Python 环境"
check_command "python3" "Python 3" "https://python.org/ 或 brew install python3"
check_command "pip3" "pip3" "随 Python 3 一起安装"
echo ""
echo "▸ 浏览器" echo "▸ 浏览器"
if [ -f "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then if [ -f "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then
chrome_version=$("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version 2>/dev/null) chrome_version=$("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version 2>/dev/null)
@@ -69,6 +89,14 @@ check_node_module "node-forge" "node-forge"
check_node_module "jsdom" "jsdom" check_node_module "jsdom" "jsdom"
echo "" echo ""
echo "▸ Python 包"
check_python_module "requests" "requests" "requests"
check_python_module "pycryptodome" "Crypto" "pycryptodome"
check_python_module "execjs" "execjs" "PyExecJS"
check_python_module "curl_cffi" "curl_cffi" "curl_cffi"
check_python_module "httpx" "httpx" "httpx"
echo ""
echo "▸ 可选工具" echo "▸ 可选工具"
check_command "curl" "curl" "系统自带" check_command "curl" "curl" "系统自带"
check_command "jq" "jq" "brew install jq" check_command "jq" "jq" "brew install jq"
@@ -79,7 +107,8 @@ if [ $errors -gt 0 ]; then
echo -e " ${RED}检查完成: $errors 个必需依赖缺失${NC}" echo -e " ${RED}检查完成: $errors 个必需依赖缺失${NC}"
echo "" echo ""
echo " 快速安装缺失依赖:" echo " 快速安装缺失依赖:"
echo " npm install -g crypto-js axios node-forge" echo " Node.js: npm install -g crypto-js axios node-forge"
echo " Python: pip3 install requests pycryptodome execjs"
exit 1 exit 1
else else
echo -e " ${GREEN}检查完成: 环境就绪${NC}" echo -e " ${GREEN}检查完成: 环境就绪${NC}"
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "js-reverse-browser-auto", "name": "camoufox-reverse-browser-auto",
"version": "1.0.0", "version": "1.0.0",
"description": "JS逆向 - 浏览器自动化模板", "description": "Camoufox逆向 - Camoufox浏览器自动化模板",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "node main.js", "start": "node main.js",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "js-reverse-project", "name": "camoufox-reverse-node-request",
"version": "1.0.0", "version": "1.0.0",
"description": "JS逆向 - 纯Node.js算法还原模板", "description": "Camoufox逆向 - 纯Node.js算法还原模板",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "node main.js", "start": "node main.js",
@@ -0,0 +1,6 @@
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://example.com/"
}
@@ -0,0 +1,5 @@
{
"sign_key": "your_sign_key_here",
"encrypt_key": "1234567890abcdef",
"iv": "abcdef1234567890"
}
+103
View File
@@ -0,0 +1,103 @@
"""
主入口脚本 - 纯 Python 协议请求
使用前请修改 CONFIG 中的配置和 utils/sign.py 中的签名逻辑
"""
import json
import time
from utils.sign import generate_sign, generate_m
from utils.request import RequestClient
CONFIG = {
"base_url": "https://example.com",
"api_path": "/api/data",
"cookies": {
"session_id": "your_session_id_here",
},
"total_pages": 5,
"delay": 1.5,
}
# 从配置文件加载 Headers 模板
with open("config/headers.json", "r", encoding="utf-8") as f:
HEADERS_TEMPLATE = json.load(f)
# 从配置文件加载密钥
with open("config/keys.json", "r", encoding="utf-8") as f:
KEYS = json.load(f)
def fetch_page(client: RequestClient, page: int) -> dict:
"""请求单页数据"""
timestamp = str(int(time.time()))
params = {
"page": str(page),
"ts": timestamp,
}
# 计算签名(根据实际逻辑修改)
sign = generate_sign(params, KEYS.get("sign_key", ""))
params["sign"] = sign
# 如需加密参数 m
# m = generate_m(params, KEYS.get("encrypt_key", ""), KEYS.get("iv", ""))
# params["m"] = m
url = f"{CONFIG['base_url']}{CONFIG['api_path']}"
response = client.get(url, params=params)
return response.json()
def extract_data(response_data: dict) -> list:
"""从响应中提取目标数据(根据实际数据结构修改)"""
return response_data.get("data", {}).get("list", [])
def calculate_result(all_data: list):
"""汇总计算结果(根据实际需求修改)"""
print(f"\n采集完成,共 {len(all_data)} 条数据")
for i, item in enumerate(all_data[:3]):
print(f" [{i+1}] {item}")
if len(all_data) > 3:
print(f" ... 共 {len(all_data)} 条")
def main():
client = RequestClient(
cookies=CONFIG["cookies"],
headers=HEADERS_TEMPLATE,
)
all_data = []
for page in range(1, CONFIG["total_pages"] + 1):
try:
print(f"正在请求第 {page} 页...")
response_data = fetch_page(client, page)
# 打印关键中间值(调试用)
print(f" 状态: {response_data.get('code', 'unknown')}")
page_data = extract_data(response_data)
if not page_data:
print(f" 第 {page} 页无数据,停止翻页")
break
all_data.extend(page_data)
print(f" 获取 {len(page_data)} 条数据")
if page < CONFIG["total_pages"]:
time.sleep(CONFIG["delay"])
except Exception as e:
print(f" 第 {page} 页请求失败: {e}")
if hasattr(e, "response") and e.response is not None:
print(f" 响应状态码: {e.response.status_code}")
print(f" 响应内容: {e.response.text[:200]}")
break
calculate_result(all_data)
if __name__ == "__main__":
main()
@@ -0,0 +1,5 @@
requests>=2.31.0
pycryptodome>=3.20.0
# execjs>=1.5.1
# curl_cffi>=0.6.0
# httpx[http2]>=0.27.0
+121
View File
@@ -0,0 +1,121 @@
"""
HTTP 请求封装
包含 Cookie 管理、重试机制、频率控制
"""
import time
import random
import requests
from typing import Optional
class RequestClient:
"""HTTP 客户端封装"""
def __init__(
self,
cookies: Optional[dict] = None,
headers: Optional[dict] = None,
max_retries: int = 3,
retry_delay: float = 2.0,
):
self.session = requests.Session()
self.max_retries = max_retries
self.retry_delay = retry_delay
default_headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
}
if headers:
default_headers.update(headers)
self.session.headers.update(default_headers)
if cookies:
for name, value in cookies.items():
self.session.cookies.set(name, value)
def request(self, method: str, url: str, **kwargs) -> requests.Response:
"""带重试的请求"""
last_error = None
for attempt in range(1, self.max_retries + 1):
try:
response = self.session.request(method, url, **kwargs)
if response.status_code == 429:
wait = self.retry_delay * (2 ** attempt) + random.uniform(0, 1)
print(f" 触发频率限制,等待 {wait:.1f}s 后重试 ({attempt}/{self.max_retries})")
time.sleep(wait)
continue
if response.status_code in (403, 412):
print(f" 请求被拒绝 ({response.status_code}),可能需要检查 Cookie 或签名")
response.raise_for_status()
if response.status_code >= 500:
wait = self.retry_delay * attempt + random.uniform(0, 1)
print(f" 服务端错误 ({response.status_code}),等待 {wait:.1f}s 后重试 ({attempt}/{self.max_retries})")
time.sleep(wait)
continue
return response
except requests.RequestException as e:
last_error = e
if attempt < self.max_retries:
wait = self.retry_delay * attempt
print(f" 请求异常: {e},等待 {wait:.1f}s 后重试 ({attempt}/{self.max_retries})")
time.sleep(wait)
raise last_error or Exception("请求失败,已达最大重试次数")
def get(self, url: str, **kwargs) -> requests.Response:
return self.request("GET", url, **kwargs)
def post(self, url: str, **kwargs) -> requests.Response:
return self.request("POST", url, **kwargs)
def request_with_delay(
self, method: str, url: str, delay: float = 1.0, **kwargs
) -> requests.Response:
"""带延迟的请求(避免触发频率限制)"""
jitter = random.uniform(0, delay * 0.3)
time.sleep(delay + jitter)
return self.request(method, url, **kwargs)
def set_cookie(self, name: str, value: str, domain: str = ""):
"""设置 Cookie"""
self.session.cookies.set(name, value, domain=domain or None)
def update_cookies_from_response(self, response: requests.Response):
"""从响应中更新 Cookie"""
self.session.cookies.update(response.cookies)
def get_cookie_string(self) -> str:
"""获取 Cookie 字符串"""
return "; ".join(f"{c.name}={c.value}" for c in self.session.cookies)
# ====== curl_cffi 版本(对抗 TLS 指纹检测时使用)======
#
# from curl_cffi import requests as curl_requests
#
# class CurlRequestClient:
# """使用 curl_cffi 的 HTTP 客户端,支持浏览器 TLS 指纹模拟"""
#
# def __init__(self, impersonate: str = "chrome120"):
# self.session = curl_requests.Session(impersonate=impersonate)
#
# def get(self, url: str, **kwargs):
# return self.session.get(url, **kwargs)
#
# def post(self, url: str, **kwargs):
# return self.session.post(url, **kwargs)
+108
View File
@@ -0,0 +1,108 @@
"""
签名函数封装
根据逆向分析结论修改具体的签名逻辑
"""
import hashlib
import hmac
import base64
import json
import time
# 如需执行提取的 JS 代码
# import execjs
def md5(text: str) -> str:
"""MD5 哈希(返回 32 位小写 hex)"""
return hashlib.md5(text.encode("utf-8")).hexdigest()
def sha256(text: str) -> str:
"""SHA-256 哈希(返回 64 位小写 hex)"""
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def hmac_sha256(key: str, message: str) -> str:
"""HMAC-SHA256 签名(返回 hex)"""
return hmac.new(
key.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256
).hexdigest()
def aes_encrypt(plaintext: str, key: str, iv: str) -> str:
"""AES-CBC 加密(返回 Base64)"""
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
cipher = AES.new(
key.encode("utf-8"),
AES.MODE_CBC,
iv.encode("utf-8")
)
padded = pad(plaintext.encode("utf-8"), AES.block_size)
encrypted = cipher.encrypt(padded)
return base64.b64encode(encrypted).decode("utf-8")
def aes_decrypt(ciphertext_b64: str, key: str, iv: str) -> str:
"""AES-CBC 解密"""
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
cipher = AES.new(
key.encode("utf-8"),
AES.MODE_CBC,
iv.encode("utf-8")
)
decrypted = cipher.decrypt(base64.b64decode(ciphertext_b64))
return unpad(decrypted, AES.block_size).decode("utf-8")
def generate_sign(params: dict, secret_key: str) -> str:
"""
生成请求签名(根据实际逻辑修改)
示例签名逻辑:
1. 参数按 key 字典序排序
2. 拼接为 key1=value1&key2=value2 格式
3. 末尾追加密钥
4. 对拼接字符串做 MD5
Args:
params: 请求参数字典
secret_key: 签名密钥
Returns:
签名字符串
"""
sorted_params = sorted(params.items(), key=lambda x: x[0])
param_str = "&".join(f"{k}={v}" for k, v in sorted_params)
sign_str = f"{param_str}&key={secret_key}"
# 打印签名明文(调试用,正式使用时删除)
print(f" 签名明文: {sign_str}")
result = md5(sign_str)
print(f" 签名结果: {result}")
return result
def generate_m(params: dict, encrypt_key: str, iv: str) -> str:
"""
生成加密参数 m(根据实际逻辑修改)
示例:将参数 JSON 序列化后 AES 加密
"""
plaintext = json.dumps(params, separators=(",", ":"), sort_keys=True)
return aes_encrypt(plaintext, encrypt_key, iv)
# ====== execjs 方式(当 JS 逻辑过于复杂时使用)======
#
# def generate_sign_via_js(params: dict) -> str:
# """使用 execjs 执行提取的 JS 代码生成签名"""
# with open("config/sign_logic.js", "r", encoding="utf-8") as f:
# js_code = f.read()
# ctx = execjs.compile(js_code)
# return ctx.call("generateSign", params)
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "js-reverse-vm-sandbox", "name": "camoufox-reverse-vm-sandbox",
"version": "1.0.0", "version": "1.0.0",
"description": "JS逆向 - VM沙箱执行模板", "description": "Camoufox逆向 - VM沙箱执行模板",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "node main.js", "start": "node main.js",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "js-reverse-wasm", "name": "camoufox-reverse-wasm",
"version": "1.0.0", "version": "1.0.0",
"description": "JS逆向 - WASM加载还原模板", "description": "Camoufox逆向 - WASM加载还原模板",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "node main.js", "start": "node main.js",