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:
@@ -1,3 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules/
|
node_modules/
|
||||||
*.log
|
*.log
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# 私有域名映射(不纳入版本控制)
|
||||||
|
cases/_private_mapping.json
|
||||||
|
|||||||
@@ -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 专项分析)
|
||||||
|
|||||||
@@ -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` 格式沉淀到本目录。
|
||||||
@@ -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
@@ -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 纯算法模拟请求
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
→ 在环境检测函数入口捕获入参、返回值和调用栈,确认需要补全的关键分支
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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` 结果
|
||||||
|
|||||||
@@ -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 生成签名 → 真实接口请求 → 确认数据有效
|
||||||
|
```
|
||||||
@@ -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
@@ -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` | 指纹与反检测 |
|
||||||
|
|||||||
@@ -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 → 追踪关键函数调用链
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 校验
|
||||||
|
|||||||
@@ -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` | 在浏览器中执行还原后的签名函数,与脚本输出对比 |
|
||||||
@@ -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
@@ -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}"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user