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

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

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

Made-with: Cursor
This commit is contained in:
白夜
2026-04-02 18:10:05 +08:00
parent fd6e51c825
commit b5cf8b121a
30 changed files with 4045 additions and 433 deletions
+10 -6
View File
@@ -27,9 +27,13 @@ setInterval(function() { debugger; }, 100);
### 绕过方案
**方案 A:MCP 注入绕过**
**方案 A:MCP 一键绕过**
```
[js-reverse] inject_before_load(script=反debugger脚本)
[camoufox-reverse] bypass_debugger_trap()
或
[camoufox-reverse] inject_hook_preset(preset="debugger_bypass")
或手动注入:
[camoufox-reverse] add_init_script(script=反debugger脚本)
```
```javascript
@@ -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. 开发者工具检测
@@ -158,7 +162,7 @@ if (someFunction.toString().length !== 1234) {
}
```
**绕过**:不修改原始函数,使用 `inject_before_load` 在执行之前收集所需数据。
**绕过**:不修改原始函数,使用 `add_init_script` 在页面脚本执行之前收集所需数据。
## 4. 时间差检测
@@ -284,7 +288,7 @@ try {
## 通用反反调试注入脚本
以下脚本适合通过 `inject_before_load` 在页面加载前注入:
以下脚本适合通过 `add_init_script` 在页面加载前注入:
```javascript
(function() {
+178 -9
View File
@@ -1,4 +1,4 @@
# 常见加密模式识别与还原
# 常见加密模式识别与还原(Node.js / Python 双语言)
## 快速识别表
@@ -20,12 +20,21 @@
### 标准 MD5
```javascript
// Node.js
const crypto = require('crypto');
function md5(str) {
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 的内部参数(如 chrsz=16),导致输出与标准 MD5 不同。
@@ -51,12 +60,22 @@ sign = md5(md5(password) + timestamp)
## 2. HMAC
```javascript
// Node.js
const crypto = require('crypto');
function hmacSha256(message, secret) {
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`
## 3. AES
@@ -64,6 +83,7 @@ function hmacSha256(message, secret) {
### AES-CBC
```javascript
// Node.js
const crypto = require('crypto');
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
```javascript
// Node.js
function aesEcbEncrypt(plaintext, key) {
const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
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 兼容
```javascript
@@ -118,6 +164,7 @@ const encrypted = CryptoJS.AES.encrypt(plaintext, CryptoJS.enc.Utf8.parse(key),
## 4. DES / 3DES
```javascript
// Node.js
function desEncrypt(plaintext, key) {
const cipher = crypto.createCipheriv('des-ecb', key, null); // 8字节key
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
```javascript
// Node.js
const crypto = require('crypto');
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 公钥格式**:
- PEM 格式(`-----BEGIN PUBLIC KEY-----`)
- 模数(n) + 指数(e) 格式(需要手动构造 PEM)
@@ -169,7 +254,7 @@ function rsaEncrypt(plaintext, publicKeyPem) {
## 6. Base64 变体
```javascript
// 标准 Base64
// Node.js
const encoded = Buffer.from(str).toString('base64');
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)
```javascript
// Node.js
function xorEncrypt(plaintext, key) {
const result = [];
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
```javascript
// Node.js
function rc4(data, key) {
const s = Array.from({ length: 256 }, (_, i) => i);
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. 时间戳处理
```javascript
// 毫秒级(13位)
const tsMs = Date.now(); // 1710000000000
// 秒级(10位)
const tsSec = Math.floor(Date.now() / 1000); // 1710000000
// 注意:某些网站使用服务端时间戳,需要从响应头或接口获取
// Node.js
const tsMs = Date.now(); // 毫秒级(13位)
const tsSec = Math.floor(Date.now() / 1000); // 秒级(10位)
```
```python
# Python
import time
ts_ms = int(time.time() * 1000) # 毫秒级(13位)
ts_sec = int(time.time()) # 秒级(10位)
```
**注意**:某些网站使用服务端时间戳,需要从响应头或接口获取。
## 10. 参数签名常见拼接模式
```javascript
// Node.js
// 模式1:固定格式
sign = md5(`page=${page}&t=${timestamp}&key=${secret}`)
@@ -253,3 +400,25 @@ sign = md5(JSON.stringify(data) + secret)
// 模式4:管道分隔
sign = md5(`${page}|${timestamp}|${secret}`)
```
```python
# Python
import hashlib, json, time
def md5(text: str) -> str:
return hashlib.md5(text.encode('utf-8')).hexdigest()
# 模式1:固定格式
sign = md5(f"page={page}&t={timestamp}&key={secret}")
# 模式2:所有参数排序拼接
params = {"page": 1, "size": 10, "t": int(time.time())}
param_str = "&".join(f"{k}={params[k]}" for k in sorted(params))
sign = md5(param_str + secret)
# 模式3:JSON 字符串(注意 separators 控制格式)
sign = md5(json.dumps(data, separators=(',', ':'), sort_keys=True) + secret)
# 模式4:管道分隔
sign = md5(f"{page}|{timestamp}|{secret}")
```
+9 -6
View File
@@ -387,12 +387,15 @@ function createFullBrowserEnv(options = {}) {
## MCP 辅助确定环境需求
```
1. [js-reverse] search_in_sources(query="window.|document.|navigator.")
→ 识别代码访问了哪些浏览器 API
1. [camoufox-reverse] search_code(keyword="window|document|navigator|location|screen")
→ 先在已加载脚本中识别代码访问了哪些浏览器 API
2. [js-reverse] evaluate_script(function="列出所有被访问的全局变量")
→ 确定需要补全的范围
2. [camoufox-reverse] add_init_script(script="记录被访问的全局变量和属性")
+ reload()
+ get_console_logs
→ 确定运行时真正访问了哪些环境字段
3. [js-reverse] set_breakpoint_on_text(text="typeof window")
→ 在环境检测处断点,确认检测逻辑
3. [camoufox-reverse] set_breakpoint_via_hook(target_function="环境检测函数路径")
+ get_breakpoint_data
→ 在环境检测函数入口捕获入参、返回值和调用栈,确认需要补全的关键分支
```
+13 -12
View File
@@ -10,18 +10,17 @@ Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函
### 通过 MCP 注入(推荐)
```
[js-reverse] inject_before_load(script=HookScript)
[camoufox-reverse] add_init_script(script=HookScript)
→ 在页面脚本加载前注入,确保 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 中使用:
```
[chrome-devtools] evaluate_script(function=HookScript)
[camoufox-reverse] hook_function(function_path="目标函数", hook_code="...", position="before|after|replace")
→ 对指定函数注入自定义 Hook
```
## Hook 模板库
@@ -390,8 +389,10 @@ Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函
## MCP 注入最佳实践
1. **使用 `inject_before_load`**:确保 Hook 在目标代码之前生效
2. **使用 `console.log` 输出**:通过 `list_console_messages` 收集结果
3. **使用 `console.trace`**:在关键点输出调用栈
4. **避免覆写原型链的 toString**:防止被反检测代码发现
5. **使用 Proxy 代替直接覆写**:更隐蔽,不改变 `typeof` 结果
1. **使用 `add_init_script`**:确保 Hook 在目标代码之前生效
2. **优先使用 `inject_hook_preset`**:一键注入 xhr/fetch/crypto/websocket/debugger_bypass 预设 Hook
3. **使用 `hook_function`**:对特定函数注入 before/after/replace Hook
4. **使用 `console.log` 输出**:通过 `get_console_logs` 收集结果
5. **使用 `console.trace`**:在关键点输出调用栈
6. **Camoufox 优势**:Juggler 协议沙箱隔离,Hook 不会被页面 JS 检测到
7. **使用 Proxy 代替直接覆写**:更隐蔽,不改变 `typeof` 结果
+915
View File
@@ -0,0 +1,915 @@
# jsdom 环境补丁知识库
## 概述
当 JSVMP 选择路径 B(环境伪装)在 jsdom 中运行时,jsdom 与真实浏览器存在大量差异。
本文档提供系统化的环境补丁方案,按检测影响分级,所有代码模板可直接复用。
## 使用方法
```javascript
const { JSDOM } = require('jsdom');
function createPatchedJsdom(url, options = {}) {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: url,
referrer: options.referrer || '',
contentType: 'text/html',
pretendToBeVisual: true,
runScripts: 'dangerously',
});
const win = dom.window;
const doc = win.document;
const nav = win.navigator;
patchEnvironment(win, doc, nav, options);
return dom;
}
```
---
## 致命级补丁(缺失即被服务端拒绝)
### 1. Function.prototype.toString 三层防御
**问题**:jsdom 所有 DOM 方法都是 JS 实现,`toString()` 会暴露完整源码(如 `createElement(localName) { const esValue = ...`),
JSVMP 通过此检测判断是否在真实浏览器中。
**难度**:★★★★★ — 这是 jsdom 环境伪装的第一杀手
```javascript
const _origFnToString = win.Function.prototype.toString;
const nativeFnSet = new WeakSet();
// 第一层:精确标记 — WeakSet 注册已知 jsdom 内置函数
function markNative(fn) {
if (typeof fn === 'function') {
nativeFnSet.add(fn);
try {
const name = fn.name || '';
// 第三层:实例级覆写 — 直接在函数对象上定义 toString
Object.defineProperty(fn, 'toString', {
value: function () { return `function ${name}() { [native code] }`; },
writable: true, configurable: true,
});
} catch (e) {}
}
return fn;
}
// 第二层:源码模式正则 — 捕获 WeakSet 遗漏的 jsdom 函数
const jsdomPatterns = [
/^\s*\w+\s*\([^)]*\)\s*\{[\s\S]*?const\s+esValue\s*=/, // jsdom 接口方法模式
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*?this\._globalObject/, // jsdom 全局对象访问模式
/^\s*\w+\s*\([^)]*\)\s*\{\s*const\s+\w+\s*=\s*this\s*!==/, // jsdom this 检查模式
];
// 覆写全局 Function.prototype.toString
win.Function.prototype.toString = function () {
// 第一层:WeakSet 精确匹配
if (nativeFnSet.has(this)) {
return `function ${this.name || ''}() { [native code] }`;
}
let src;
try { src = _origFnToString.call(this); } catch (e) {
return 'function () { [native code] }';
}
// 第二层:源码模式正则检测
for (const pat of jsdomPatterns) {
if (pat.test(src)) return `function ${this.name || ''}() { [native code] }`;
}
return src;
};
// 深度扫描原型链,批量标记所有 jsdom 内置方法
function scanPrototypeChain(obj, maxDepth) {
let proto = obj;
for (let d = 0; d < (maxDepth || 10) && proto; d++) {
for (const name of Object.getOwnPropertyNames(proto)) {
try {
const desc = Object.getOwnPropertyDescriptor(proto, name);
if (desc && typeof desc.value === 'function') markNative(desc.value);
if (desc && typeof desc.get === 'function') markNative(desc.get);
if (desc && typeof desc.set === 'function') markNative(desc.set);
} catch (e) {}
}
proto = Object.getPrototypeOf(proto);
if (proto === Object.prototype || proto === null) break;
}
}
// 扫描 50+ 原型链
const protoTargets = [
win.Document?.prototype, win.HTMLDocument?.prototype,
win.Element?.prototype, win.HTMLElement?.prototype,
win.Node?.prototype, win.EventTarget?.prototype,
win.XMLHttpRequest?.prototype, win.HTMLCanvasElement?.prototype,
win.HTMLInputElement?.prototype, win.HTMLFormElement?.prototype,
win.HTMLAnchorElement?.prototype, win.HTMLImageElement?.prototype,
win.HTMLDivElement?.prototype, win.HTMLSpanElement?.prototype,
win.HTMLBodyElement?.prototype, win.HTMLHeadElement?.prototype,
win.HTMLScriptElement?.prototype, win.HTMLStyleElement?.prototype,
win.HTMLLinkElement?.prototype, win.HTMLMetaElement?.prototype,
win.Window?.prototype, win.Location?.prototype,
win.DOMParser?.prototype, win.URL?.prototype,
win.Event?.prototype, win.CustomEvent?.prototype,
win.MutationObserver?.prototype, win.ResizeObserver?.prototype,
win.DOMTokenList?.prototype, win.NamedNodeMap?.prototype,
win.NodeList?.prototype, win.HTMLCollection?.prototype,
win.CSSStyleDeclaration?.prototype, win.Text?.prototype,
win.Comment?.prototype, win.DocumentFragment?.prototype,
win.Range?.prototype, win.Selection?.prototype,
win.TreeWalker?.prototype, win.NodeIterator?.prototype,
win.Attr?.prototype, win.CharacterData?.prototype,
win.Storage?.prototype, win.FormData?.prototype,
win.Headers?.prototype,
];
for (const proto of protoTargets) {
if (proto) scanPrototypeChain(proto, 3);
}
scanPrototypeChain(doc, 5);
scanPrototypeChain(nav, 5);
```
### 2. navigator.webdriver
**问题**:jsdom 中 `navigator.webdriver` 为 `undefined`,真实浏览器为 `false`(Camoufox 在 C++ 层已处理)。
```javascript
Object.defineProperty(nav, 'webdriver', {
get: () => false, configurable: true, enumerable: true,
});
```
### 3. navigator.plugins 完整结构模拟
**问题**:jsdom `plugins.length = 0`,且无 `item()`/`namedItem()` 方法。JSVMP 不只检查长度,还会遍历结构和检查 `Symbol.toStringTag`。
```javascript
const pluginData = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
];
const mimeData = [
{ type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
{ type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
];
function buildPluginArray(win) {
const mimeTypes = mimeData.map((m, i) => {
const mt = Object.create(null);
Object.defineProperties(mt, {
type: { get: markNative(function type() { return m.type; }), enumerable: true },
description: { get: markNative(function description() { return m.description; }), enumerable: true },
suffixes: { get: markNative(function suffixes() { return m.suffixes; }), enumerable: true },
enabledPlugin: { get: markNative(function enabledPlugin() { return plugins[0]; }), enumerable: true },
});
mt[Symbol.toStringTag] = 'MimeType';
return mt;
});
const plugins = pluginData.map((p, i) => {
const plugin = Object.create(null);
Object.defineProperties(plugin, {
name: { get: markNative(function name() { return p.name; }), enumerable: true },
filename: { get: markNative(function filename() { return p.filename; }), enumerable: true },
description: { get: markNative(function description() { return p.description; }), enumerable: true },
length: { get: markNative(function length() { return mimeTypes.length; }), enumerable: true },
});
mimeTypes.forEach((mt, mi) => { plugin[mi] = mt; });
plugin.item = markNative(function item(idx) { return mimeTypes[idx] || null; });
plugin.namedItem = markNative(function namedItem(name) {
return mimeTypes.find(m => m.type === name) || null;
});
plugin[Symbol.toStringTag] = 'Plugin';
plugin[Symbol.iterator] = markNative(function* () { yield* mimeTypes; });
return plugin;
});
const pluginArray = Object.create(null);
Object.defineProperty(pluginArray, 'length', {
get: markNative(function length() { return plugins.length; }),
enumerable: true,
});
plugins.forEach((p, i) => { pluginArray[i] = p; });
pluginArray.item = markNative(function item(idx) { return plugins[idx] || null; });
pluginArray.namedItem = markNative(function namedItem(name) {
return plugins.find(p => p.name === name) || null;
});
pluginArray.refresh = markNative(function refresh() {});
pluginArray[Symbol.toStringTag] = 'PluginArray';
pluginArray[Symbol.iterator] = markNative(function* () { yield* plugins; });
Object.defineProperty(nav, 'plugins', {
get: markNative(function plugins() { return pluginArray; }),
configurable: true, enumerable: true,
});
// mimeTypes
const mimeTypeArray = Object.create(null);
Object.defineProperty(mimeTypeArray, 'length', {
get: markNative(function length() { return mimeTypes.length; }),
enumerable: true,
});
mimeTypes.forEach((m, i) => { mimeTypeArray[i] = m; });
mimeTypeArray.item = markNative(function item(idx) { return mimeTypes[idx] || null; });
mimeTypeArray.namedItem = markNative(function namedItem(name) {
return mimeTypes.find(m => m.type === name) || null;
});
mimeTypeArray[Symbol.toStringTag] = 'MimeTypeArray';
mimeTypeArray[Symbol.iterator] = markNative(function* () { yield* mimeTypes; });
Object.defineProperty(nav, 'mimeTypes', {
get: markNative(function mimeTypes() { return mimeTypeArray; }),
configurable: true, enumerable: true,
});
}
```
### 4. document.hasFocus()
**问题**:jsdom 的 `document.hasFocus()` 始终返回 `false`,真实浏览器活动标签页返回 `true`。
```javascript
doc.hasFocus = markNative(function hasFocus() { return true; });
```
### 5. DOM 布局属性(offsetHeight/Width/getBoundingClientRect)
**问题**:jsdom 无渲染引擎,所有布局属性返回 0。JSVMP 创建不可见元素测量布局值来检测。
```javascript
function patchDOMLayout(win) {
const HTMLElement = win.HTMLElement;
if (!HTMLElement) return;
function parseStyleDimension(el, prop) {
try {
const style = el.getAttribute && el.getAttribute('style');
if (style) {
const m = style.match(new RegExp(prop + '\\s*:\\s*(\\d+)'));
if (m) return parseInt(m[1], 10);
}
} catch (e) {}
return 0;
}
const layoutProps = ['offsetHeight', 'offsetWidth', 'clientHeight', 'clientWidth',
'scrollHeight', 'scrollWidth', 'offsetTop', 'offsetLeft'];
for (const prop of layoutProps) {
Object.defineProperty(HTMLElement.prototype, prop, {
get: markNative(function () {
const dim = prop.includes('Height') || prop.includes('Top') ? 'height' : 'width';
const fromStyle = parseStyleDimension(this, dim);
if (fromStyle > 0) return fromStyle;
const defaults = { offsetHeight: 150, offsetWidth: 300, clientHeight: 150,
clientWidth: 300, scrollHeight: 150, scrollWidth: 300, offsetTop: 0, offsetLeft: 0 };
return defaults[prop] || 0;
}),
configurable: true, enumerable: true,
});
}
HTMLElement.prototype.getBoundingClientRect = markNative(function getBoundingClientRect() {
const w = parseStyleDimension(this, 'width') || 300;
const h = parseStyleDimension(this, 'height') || 150;
return { x: 0, y: 0, top: 0, left: 0, right: w, bottom: h, width: w, height: h,
toJSON: markNative(function toJSON() { return this; }) };
});
HTMLElement.prototype.getClientRects = markNative(function getClientRects() {
return [this.getBoundingClientRect()];
});
}
```
---
## 高危级补丁(可能参与指纹哈希)
### 6. Object.prototype.toString 标签(Symbol.toStringTag)
**问题**:jsdom 的 document 原型链是 `Document` 而非 `HTMLDocument`,screen/performance 是普通对象。
```javascript
function patchToStringTags(win, doc, nav) {
const tagMap = [
[doc, 'HTMLDocument'],
[nav, 'Navigator'],
[win.screen, 'Screen'],
[win.performance, 'Performance'],
[win.location, 'Location'],
];
for (const [obj, tag] of tagMap) {
if (obj) {
try { obj[Symbol.toStringTag] = tag; } catch (e) {}
}
}
// 修复 document 的原型链构造函数名称
try {
const docProto = Object.getPrototypeOf(doc);
if (docProto && docProto.constructor && docProto.constructor.name !== 'HTMLDocument') {
Object.defineProperty(docProto.constructor, 'name', { value: 'HTMLDocument' });
}
} catch (e) {}
}
```
### 7. window.chrome 对象
**问题**:jsdom 中 `window.chrome` 为 `undefined`,Chrome 浏览器必定存在此对象。
```javascript
function patchChrome(win) {
win.chrome = {
app: {
isInstalled: false,
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
getDetails: markNative(function getDetails() { return null; }),
getIsInstalled: markNative(function getIsInstalled() { return false; }),
},
runtime: {
OnInstalledReason: {
CHROME_UPDATE: 'chrome_update', INSTALL: 'install',
SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update',
},
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
PlatformNaclArch: { ARM: 'arm', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
PlatformOs: { ANDROID: 'android', CROS: 'cros', FUCHSIA: 'fuchsia', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
connect: markNative(function connect() { return { onMessage: { addListener: () => {} }, onDisconnect: { addListener: () => {} } }; }),
sendMessage: markNative(function sendMessage() {}),
id: undefined,
},
csi: markNative(function csi() { return {}; }),
loadTimes: markNative(function loadTimes() {
return { commitLoadTime: Date.now() / 1000, connectionInfo: 'h2', finishDocumentLoadTime: 0,
finishLoadTime: 0, firstPaintAfterLoadTime: 0, firstPaintTime: 0,
navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now() / 1000 - 0.5,
startLoadTime: Date.now() / 1000 - 0.3, wasAlternateProtocolAvailable: false,
wasFetchedViaSpdy: true, wasNpnNegotiated: true };
}),
};
}
```
### 8. navigator.userAgentData
**问题**:jsdom 中不存在 `NavigatorUAData`,Chrome 86+ 浏览器默认提供。
```javascript
function patchUserAgentData(nav, uaBrands) {
const brands = uaBrands || [
{ brand: 'Chromium', version: '120' },
{ brand: 'Google Chrome', version: '120' },
{ brand: 'Not_A Brand', version: '8' },
];
const uaData = {
brands: brands,
mobile: false,
platform: 'macOS',
getHighEntropyValues: markNative(function getHighEntropyValues(hints) {
return Promise.resolve({
brands, mobile: false, platform: 'macOS',
architecture: 'arm', bitness: '64', model: '',
platformVersion: '14.2.0', uaFullVersion: '120.0.0.0',
fullVersionList: brands.map(b => ({ ...b })),
wow64: false,
});
}),
toJSON: markNative(function toJSON() { return { brands, mobile: false, platform: 'macOS' }; }),
};
uaData[Symbol.toStringTag] = 'NavigatorUAData';
Object.defineProperty(nav, 'userAgentData', {
get: markNative(function userAgentData() { return uaData; }),
configurable: true, enumerable: true,
});
}
```
### 9. performance.timing / navigation / memory
**问题**:jsdom 中 `performance.timing` 和 `performance.navigation` 为 `undefined`。
```javascript
function patchPerformance(win) {
const perf = win.performance;
if (!perf) return;
const now = Date.now();
const timing = {
navigationStart: now - 1200, unloadEventStart: now - 1100, unloadEventEnd: now - 1099,
redirectStart: 0, redirectEnd: 0, fetchStart: now - 1050,
domainLookupStart: now - 1000, domainLookupEnd: now - 990,
connectStart: now - 980, connectEnd: now - 900, secureConnectionStart: now - 950,
requestStart: now - 890, responseStart: now - 500, responseEnd: now - 400,
domLoading: now - 380, domInteractive: now - 200, domContentLoadedEventStart: now - 190,
domContentLoadedEventEnd: now - 185, domComplete: now - 50,
loadEventStart: now - 40, loadEventEnd: now - 30,
};
timing[Symbol.toStringTag] = 'PerformanceTiming';
Object.defineProperty(perf, 'timing', {
get: markNative(function timing() { return timing; }),
configurable: true, enumerable: true,
});
const navigation = { type: 0, redirectCount: 0 };
navigation[Symbol.toStringTag] = 'PerformanceNavigation';
Object.defineProperty(perf, 'navigation', {
get: markNative(function navigation() { return navigation; }),
configurable: true, enumerable: true,
});
const memory = { jsHeapSizeLimit: 4294705152, totalJSHeapSize: 35194260, usedJSHeapSize: 24486084 };
Object.defineProperty(perf, 'memory', {
get: markNative(function memory() { return memory; }),
configurable: true, enumerable: true,
});
}
```
### 10. navigator 基础属性覆盖
```javascript
function patchNavigatorBasics(nav, options) {
const overrides = {
platform: options.platform || 'MacIntel',
language: options.language || 'zh-CN',
languages: options.languages || ['zh-CN', 'zh'],
vendor: options.vendor || 'Google Inc.',
productSub: '20030107',
appVersion: options.appVersion || '5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
hardwareConcurrency: options.hardwareConcurrency || 8,
maxTouchPoints: options.maxTouchPoints || 0,
deviceMemory: options.deviceMemory || 8,
pdfViewerEnabled: true,
cookieEnabled: true,
onLine: true,
doNotTrack: null,
};
for (const [key, value] of Object.entries(overrides)) {
try {
Object.defineProperty(nav, key, {
get: markNative(function () { return value; }),
configurable: true, enumerable: true,
});
} catch (e) {}
}
}
```
### 11. document 属性补丁
```javascript
function patchDocument(doc) {
Object.defineProperty(doc, 'hidden', {
get: markNative(function hidden() { return false; }),
configurable: true, enumerable: true,
});
Object.defineProperty(doc, 'visibilityState', {
get: markNative(function visibilityState() { return 'visible'; }),
configurable: true, enumerable: true,
});
// readyState 在 jsdom 中默认为 'loading',JSVMP 运行时应为 'complete'
Object.defineProperty(doc, 'readyState', {
get: markNative(function readyState() { return 'complete'; }),
configurable: true, enumerable: true,
});
Object.defineProperty(doc, 'fullscreenEnabled', {
get: markNative(function fullscreenEnabled() { return true; }),
configurable: true, enumerable: true,
});
}
```
### 12. screen 对象重建
**问题**:jsdom 的 screen 是普通对象,缺少 `orientation`、`availLeft`/`availTop`,且 `colorDepth` 可能不正确。
```javascript
function patchScreen(win, options) {
const screenData = {
width: options.width || 1920, height: options.height || 1080,
availWidth: options.availWidth || 1920, availHeight: options.availHeight || 1055,
availLeft: 0, availTop: 25,
colorDepth: options.colorDepth || 30, pixelDepth: options.pixelDepth || 30,
orientation: {
type: 'landscape-primary', angle: 0,
onchange: null,
[Symbol.toStringTag]: 'ScreenOrientation',
},
};
screenData[Symbol.toStringTag] = 'Screen';
for (const [key, value] of Object.entries(screenData)) {
try {
Object.defineProperty(win.screen, key, {
get: markNative(function () { return value; }),
configurable: true, enumerable: true,
});
} catch (e) {}
}
}
```
---
## 中危级补丁(API 存在性检测)
JSVMP 可能通过 `typeof window.XXX` 检测 API 是否存在。以下是 jsdom 缺失但浏览器存在的 API 存根。
**核心原则**:每个存根函数必须经过 `markNative()` 处理,否则 `toString()` 检测会暴露。
### 13. 批量 API 存根
```javascript
function patchWindowAPIs(win) {
// Notification
if (!win.Notification) {
win.Notification = markNative(function Notification() {});
win.Notification.permission = 'default';
win.Notification.requestPermission = markNative(function requestPermission() {
return Promise.resolve('default');
});
win.Notification.maxActions = 2;
}
// Worker / SharedWorker
if (!win.Worker) {
win.Worker = markNative(function Worker() {
return { postMessage: markNative(function() {}), terminate: markNative(function() {}),
addEventListener: markNative(function() {}), onmessage: null, onerror: null };
});
}
if (!win.SharedWorker) {
win.SharedWorker = markNative(function SharedWorker() {
return { port: { postMessage: markNative(function() {}), start: markNative(function() {}),
addEventListener: markNative(function() {}), onmessage: null } };
});
}
// RTCPeerConnection
if (!win.RTCPeerConnection) {
win.RTCPeerConnection = markNative(function RTCPeerConnection() {
return { createDataChannel: markNative(function() { return {}; }),
createOffer: markNative(function() { return Promise.resolve({}); }),
setLocalDescription: markNative(function() { return Promise.resolve(); }),
close: markNative(function() {}), onicecandidate: null };
});
}
// AudioContext / OfflineAudioContext
if (!win.AudioContext) {
win.AudioContext = markNative(function AudioContext() {
return { sampleRate: 44100, state: 'suspended', currentTime: 0, destination: {},
createOscillator: markNative(function() {
return { type: 'sine', frequency: { value: 440 },
connect: markNative(function() {}), start: markNative(function() {}),
stop: markNative(function() {}) };
}),
createDynamicsCompressor: markNative(function() {
return { threshold: { value: -24 }, knee: { value: 30 },
ratio: { value: 12 }, reduction: 0,
connect: markNative(function() {}) };
}),
createAnalyser: markNative(function() { return { connect: markNative(function() {}) }; }),
createGain: markNative(function() { return { gain: { value: 1 }, connect: markNative(function() {}) }; }),
close: markNative(function() { return Promise.resolve(); }),
resume: markNative(function() { return Promise.resolve(); }) };
});
}
if (!win.OfflineAudioContext) {
win.OfflineAudioContext = markNative(function OfflineAudioContext(channels, length, sampleRate) {
const ctx = new win.AudioContext();
ctx.startRendering = markNative(function startRendering() {
return Promise.resolve({ duration: length / sampleRate, length: length,
numberOfChannels: channels, sampleRate: sampleRate,
getChannelData: markNative(function() { return new Float32Array(length); }) });
});
return ctx;
});
}
// fetch + Request/Response/Headers/AbortController
if (!win.fetch) {
win.fetch = markNative(function fetch() { return Promise.reject(new Error('Network error')); });
win.Request = markNative(function Request() {});
win.Response = markNative(function Response() {});
if (!win.AbortController) {
win.AbortController = markNative(function AbortController() {
return { signal: { aborted: false }, abort: markNative(function() {}) };
});
}
}
// matchMedia
if (!win.matchMedia) {
win.matchMedia = markNative(function matchMedia(query) {
return { matches: false, media: query, onchange: null,
addListener: markNative(function() {}), removeListener: markNative(function() {}),
addEventListener: markNative(function() {}), removeEventListener: markNative(function() {}) };
});
}
// requestIdleCallback / cancelIdleCallback
if (!win.requestIdleCallback) {
win.requestIdleCallback = markNative(function requestIdleCallback(cb) {
return win.setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 1);
});
win.cancelIdleCallback = markNative(function cancelIdleCallback(id) { win.clearTimeout(id); });
}
// requestAnimationFrame(jsdom pretendToBeVisual=true 已提供,但检查一下)
if (!win.requestAnimationFrame) {
win.requestAnimationFrame = markNative(function requestAnimationFrame(cb) {
return win.setTimeout(() => cb(Date.now()), 16);
});
win.cancelAnimationFrame = markNative(function cancelAnimationFrame(id) { win.clearTimeout(id); });
}
// indexedDB
if (!win.indexedDB) {
win.indexedDB = { open: markNative(function open() { return {}; }) };
win.indexedDB[Symbol.toStringTag] = 'IDBFactory';
}
// caches
if (!win.caches) {
win.caches = {
open: markNative(function open() { return Promise.resolve({}); }),
has: markNative(function has() { return Promise.resolve(false); }),
delete: markNative(function _delete() { return Promise.resolve(false); }),
keys: markNative(function keys() { return Promise.resolve([]); }),
match: markNative(function match() { return Promise.resolve(undefined); }),
};
win.caches[Symbol.toStringTag] = 'CacheStorage';
}
// speechSynthesis
if (!win.speechSynthesis) {
win.speechSynthesis = {
speaking: false, pending: false, paused: false,
getVoices: markNative(function getVoices() { return []; }),
speak: markNative(function speak() {}),
cancel: markNative(function cancel() {}),
};
win.speechSynthesis[Symbol.toStringTag] = 'SpeechSynthesis';
}
// customElements
if (!win.customElements) {
win.customElements = {
define: markNative(function define() {}),
get: markNative(function get() { return undefined; }),
whenDefined: markNative(function whenDefined() { return Promise.resolve(); }),
};
win.customElements[Symbol.toStringTag] = 'CustomElementRegistry';
}
// visualViewport
if (!win.visualViewport) {
win.visualViewport = {
width: win.innerWidth || 1920, height: win.innerHeight || 1080,
offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, scale: 1,
onresize: null, onscroll: null,
addEventListener: markNative(function() {}), removeEventListener: markNative(function() {}),
};
win.visualViewport[Symbol.toStringTag] = 'VisualViewport';
}
// isSecureContext
if (win.isSecureContext === undefined) {
Object.defineProperty(win, 'isSecureContext', {
get: markNative(function isSecureContext() { return true; }),
configurable: true, enumerable: true,
});
}
// clientInformation
if (!win.clientInformation) {
Object.defineProperty(win, 'clientInformation', {
get: markNative(function clientInformation() { return nav; }),
configurable: true, enumerable: true,
});
}
}
```
### 14. navigator 扩展 API 存根
```javascript
function patchNavigatorAPIs(nav) {
// connection (NetworkInformation)
if (!nav.connection) {
const conn = {
effectiveType: '4g', downlink: 10, rtt: 50, saveData: false,
onchange: null, type: 'wifi',
addEventListener: markNative(function() {}), removeEventListener: markNative(function() {}),
};
conn[Symbol.toStringTag] = 'NetworkInformation';
Object.defineProperty(nav, 'connection', {
get: markNative(function connection() { return conn; }),
configurable: true, enumerable: true,
});
}
// getBattery
if (!nav.getBattery) {
nav.getBattery = markNative(function getBattery() {
return Promise.resolve({
charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1,
onchargingchange: null, onchargingtimechange: null,
ondischargingtimechange: null, onlevelchange: null,
});
});
}
}
```
### 15. document 扩展 API 存根
```javascript
function patchDocumentAPIs(doc) {
// fonts (FontFaceSet)
if (!doc.fonts) {
const fonts = {
ready: Promise.resolve(),
status: 'loaded',
check: markNative(function check() { return true; }),
load: markNative(function load() { return Promise.resolve([]); }),
forEach: markNative(function forEach() {}),
entries: markNative(function entries() { return [][Symbol.iterator](); }),
keys: markNative(function keys() { return [][Symbol.iterator](); }),
values: markNative(function values() { return [][Symbol.iterator](); }),
[Symbol.iterator]: markNative(function () { return [][Symbol.iterator](); }),
size: 0,
};
fonts[Symbol.toStringTag] = 'FontFaceSet';
Object.defineProperty(doc, 'fonts', {
get: markNative(function fonts() { return fonts; }),
configurable: true, enumerable: true,
});
}
// timeline (DocumentTimeline)
if (!doc.timeline) {
const timeline = { currentTime: win.performance?.now?.() || 0 };
timeline[Symbol.toStringTag] = 'DocumentTimeline';
Object.defineProperty(doc, 'timeline', {
get: markNative(function timeline() { return timeline; }),
configurable: true, enumerable: true,
});
}
}
```
---
## window 尺寸属性
```javascript
function patchWindowDimensions(win, options) {
const dims = {
innerWidth: options.innerWidth || 1920,
innerHeight: options.innerHeight || 969,
outerWidth: options.outerWidth || 1920,
outerHeight: options.outerHeight || 1055,
devicePixelRatio: options.devicePixelRatio || 2,
scrollX: 0, scrollY: 0, pageXOffset: 0, pageYOffset: 0,
};
for (const [key, value] of Object.entries(dims)) {
try {
Object.defineProperty(win, key, {
get: markNative(function () { return value; }),
configurable: true, enumerable: true,
});
} catch (e) {}
}
}
```
---
## Node.js 特征清除
在 jsdom 的 window 上下文中隐藏 Node.js 运行时特征:
```javascript
function hideNodeFeatures(win) {
const nodeGlobals = ['process', 'module', 'exports', 'require',
'global', '__filename', '__dirname', 'Buffer'];
for (const g of nodeGlobals) {
try { delete win[g]; } catch (e) {
try { win[g] = undefined; } catch (e2) {}
}
}
}
```
---
## 完整 patchEnvironment 调用顺序
**顺序很重要**:markNative 必须在所有补丁之前定义,所有补丁函数必须经过 markNative 处理。
```javascript
function patchEnvironment(win, doc, nav, options = {}) {
// ① 定义 markNative(三层防御)— 必须最先
setupMarkNative(win);
// ② 扫描并标记所有 jsdom 内置原型链方法
scanAllPrototypes(win, doc, nav);
// ③ 致命级补丁
patchNavigatorBasics(nav, options); // webdriver / platform / language
buildPluginArray(win); // plugins 完整结构
doc.hasFocus = markNative(function hasFocus() { return true; });
patchDOMLayout(win); // offsetHeight/Width/getBoundingClientRect
// ④ 高危级补丁
patchToStringTags(win, doc, nav); // Symbol.toStringTag
patchChrome(win); // window.chrome
patchUserAgentData(nav); // userAgentData
patchPerformance(win); // timing/navigation/memory
patchDocument(doc); // hasFocus/readyState/visibility
patchScreen(win, options); // colorDepth/orientation
patchWindowDimensions(win, options); // innerWidth/outerWidth/devicePixelRatio
// ⑤ 中危级补丁
patchWindowAPIs(win); // 30+ API 存根
patchNavigatorAPIs(nav); // connection/getBattery
patchDocumentAPIs(doc); // fonts/timeline
// ⑥ 清除 Node.js 特征
hideNodeFeatures(win);
// ⑦ 最后一次全局扫描,标记新增的所有函数
scanAllPrototypes(win, doc, nav);
}
```
---
## 环境采集对比方法
### 浏览器端采集(通过 Camoufox MCP)
分 4 批次通过 `evaluate_js` 在真实浏览器中执行采集脚本,
采集代码模板参考 `cases/` 中的「浏览器指纹采集方法」段。
### jsdom 端采集
在 Node.js 中用 `win.eval()` 执行完全相同的采集代码:
```javascript
const dom = createPatchedJsdom('https://target.com');
const win = dom.window;
const jsdomResult = JSON.parse(win.eval(`JSON.stringify({
nav_userAgent: navigator.userAgent,
nav_plugins_length: navigator.plugins ? navigator.plugins.length : -1,
nav_webdriver: navigator.webdriver,
// ... 与浏览器端完全相同的采集代码
})`));
```
### Diff 生成
```javascript
function diffEnv(browserData, jsdomData) {
const diffs = [];
for (const key of Object.keys(browserData)) {
const bVal = JSON.stringify(browserData[key]);
const jVal = JSON.stringify(jsdomData[key]);
if (bVal !== jVal) {
diffs.push({ key, browser: browserData[key], jsdom: jsdomData[key] });
}
}
return diffs;
}
```
---
## MCP 辅助工作流
```
1. compare_env → 快速采集浏览器基准环境数据(覆盖主流检测项)
→ 适合快速判断差异范围
2. evaluate_js → 分批采集细粒度环境值(4-5 批次)
→ 覆盖 Function.toString / Symbol.toStringTag / DOM 布局等 compare_env 不包含的项
→ 每批代码不要太长,避免执行超时
3. 本地 Node.js 脚本 → 在 jsdom 中运行相同采集代码 + 生成 diff 报告
4. 迭代修复 → 每次修复一批差异 → 重新 diff → 直到差异归零
5. 端到端验证 → jsdom 生成签名 → 真实接口请求 → 确认数据有效
```
+486
View File
@@ -0,0 +1,486 @@
# JSVMP 虚拟机保护分析指南
## 什么是 JSVMP
JSVMP(JavaScript Virtual Machine Protection)是一种高级 JS 代码保护技术,将原始 JS 源码编译为自定义字节码,运行时由内嵌的虚拟机解释器逐条执行。这意味着:
- 原始代码逻辑不再以 JS AST 形式存在,无法通过传统反混淆手段还原
- 代码以字节码数组 + 解释器循环的形式运行
- 常见于瑞数、极验、某数等商业级反爬方案
**核心原则:不反编译字节码,用行为追踪法(Hook / 插桩 / 日志分析)从 I/O 两端夹逼定位签名逻辑。**
---
## 识别 JSVMP
### 文件特征
| 特征 | 描述 |
|------|------|
| 文件大小 | 200KB+ 的单文件,通常 500KB~2MB |
| 变量命名 | 完全无意义:单字母(`a`, `b`, `c`)或 `_0x` 前缀 |
| 超大数组 | 包含数千个数字元素的数组(字节码) |
| 解释器循环 | `while(true) { switch(opcode) { case 0: ... case 1: ... } }` |
| 栈操作 | 频繁出现 `push`、`pop`、`shift` 操作 |
| API 劫持 | 重写 `XMLHttpRequest`、`fetch`、`document.cookie` 等原生 API |
### 代码模式示例
```javascript
// 典型的 JSVMP 解释器结构
var _0x1234 = [3, 15, 7, 22, ...]; // 字节码数组
var _0x5678 = []; // 操作栈
var _0xabcd = 0; // 指令指针
function _0xefgh() {
while (true) {
var _0x9999 = _0x1234[_0xabcd++];
switch (_0x9999) {
case 0: _0x5678.push(_0x1234[_0xabcd++]); break; // PUSH
case 1: var a = _0x5678.pop(), b = _0x5678.pop(); _0x5678.push(a + b); break; // ADD
case 2: /* ... */ break;
// 数十到数百个 case
}
}
}
```
### 如何确认是 JSVMP
```
MCP 操作:
- list_scripts → 找到异常大的 JS 文件
- get_script_source(script_id=N, start_line=1, end_line=50) → 查看文件头部结构
- search_code(keyword="while") → 搜索解释器循环
- search_code_in_script(script_id=N, keyword="switch") → 在指定大文件中精确搜索(前后 3 行上下文)
- search_code(keyword="push|pop|shift") → 搜索栈操作
- dump_jsvmp_strings → 提取字符串数组,识别 API 名称和混淆模式(快速判断)
```
---
## 三板斧方法论
### 快速路径:一键 JSVMP 分析(推荐先试)
对于典型 JSVMP,可以跳过手动 Hook 直接使用专项工具:
```
MCP 操作:
- hook_jsvmp_interpreter → 自动 Hook Function.prototype.apply + 追踪 30+ 敏感属性读取
- dump_jsvmp_strings → 提取字符串数组,识别 API 名称,检测混淆模式
- 触发目标操作(翻页、提交等)
- get_jsvmp_log → 获取结构化分析结果:
· API 调用统计(哪些原生函数被 VM 调用)
· 属性读取摘要(哪些环境属性被访问)
· 调用时序(帮助定位签名生成节点)
- compare_env → 收集完整浏览器环境(navigator/screen/canvas/WebGL/Audio/timing)
· 用于后续 Node.js/Python 补环境时对照
快速路径无法解决时,再进入手动三板斧流程 ↓
```
### 第一板斧:Hook 出入口(确定 I/O 边界)
**目标**:不看 VM 内部实现,只关心"什么进去了"和"什么出来了"。
#### 1.1 Hook 出口 — 签名值去了哪里
```
MCP 操作:
- inject_hook_preset(preset="xhr", persistent=True) → 持久化拦截 XHR(跨导航不丢失)
- inject_hook_preset(preset="fetch", persistent=True) → 持久化拦截 Fetch
- hook_function(
function_path="Document.prototype.cookie",
hook_code="console.log('[COOKIE_SET]', arguments[0], new Error().stack)",
position="before", non_overridable=True
) → 拦截 Cookie 写入(防覆盖)
- freeze_prototype(className="XMLHttpRequest", methodName="send")
→ 冻结 XHR.send 防止 VM 覆盖 Hook
- reload() → 刷新页面触发 Hook
- get_console_logs → 查看捕获的签名值和调用栈
```
**关键信息提取**:
- 签名参数名(如 `sign`、`m`、`_signature`)
- 签名值的格式(长度、字符集、编码方式)
- 签名值出现在请求的哪个位置(URL params / Body / Header / Cookie)
#### 1.2 Hook 入口 — 明文从哪里来
```
MCP 操作:
- inject_hook_preset(preset="crypto", persistent=True)
→ 持久化 Hook btoa/atob/JSON.stringify/JSON.parse
- hook_function(function_path="String.fromCharCode",
hook_code="if(arguments.length > 1) console.log('[fromCharCode]', Array.from(arguments).map(Number), String.fromCharCode(...arguments))",
position="before")
→ JSVMP 高频使用 fromCharCode 构造字符串
- add_init_script(script=`
// Hook 常见加密库入口
const _origMD5 = window.CryptoJS?.MD5;
if (_origMD5) {
window.CryptoJS.MD5 = function() {
console.log('[CryptoJS.MD5] input:', arguments[0]?.toString());
const result = _origMD5.apply(this, arguments);
console.log('[CryptoJS.MD5] output:', result.toString());
return result;
};
}
`)
```
#### 1.3 I/O 关联分析
拿到出口和入口的数据后,进行关联:
```
已知信息:
出口:sign=a1b2c3d4e5f6... (32位hex → 疑似MD5)
入口:CryptoJS.MD5 输入 = "page=1&ts=1680000000&key=secret123"
关联结论:
sign = MD5("page=" + page + "&ts=" + timestamp + "&key=" + secret_key)
```
---
### 第二板斧:插桩解释器(追踪执行链路)
当出入口 Hook 无法直接关联(例如 VM 使用自实现的 MD5 而非 CryptoJS)时,需要插桩解释器。
#### 2.1 定位解释器核心函数
```
MCP 操作:
- search_code(keyword="while.*true") → 找到解释器主循环
- search_code_in_script(script_id=N, keyword="case 0:|case 1:|case 2:")
→ 在指定大文件中搜索 opcode 分发表(前后 3 行上下文,更精确)
- get_script_source → 读取解释器函数体
判断标准:
- 函数体超过 500 行
- 包含 20+ 个 case 分支
- 有数组索引递增操作(指令指针移动)
```
#### 2.2 分层插桩策略
**原则:由粗到细,逐步缩小范围。**
**第一轮 — 分发器级别(粗粒度)**
```
MCP 操作:
- trace_function(
function_path="解释器分发函数名",
log_args=true, log_return=true, max_captures=500,
persistent=True → 持久化追踪,跨导航不丢失
)
- 触发一次包含签名参数的请求
- get_trace_data → 获取 500 条执行记录(自动合并页面数据 + Python 端持久化数据)
分析要点:
- 观察每次调用的参数模式
- 找到签名值首次出现的调用序号
- 缩小时间窗口
```
**第二轮 — 子函数级别(中粒度)**
```
从第一轮日志中识别被频繁调用的子函数(通常 3-5 个核心函数):
MCP 操作:
- trace_function(function_path="子函数A", log_args=true, log_return=true, log_stack=true)
- trace_function(function_path="子函数B", log_args=true, log_return=true, log_stack=true)
- 再次触发请求
- get_trace_data → 分析子函数的输入输出
```
**第三轮 — 字符串操作级别(细粒度)**
```
JSVMP 中字符串操作是签名生成的关键信号:
MCP 操作:
- hook_function(function_path="String.prototype.charAt",
hook_code="console.log('[charAt]', this.substring(0,50), arguments[0])",
position="before")
- hook_function(function_path="String.prototype.charCodeAt", ...)
- hook_function(function_path="String.prototype.substring", ...)
- hook_function(function_path="String.prototype.concat", ...)
- hook_function(function_path="Array.prototype.join", ...)
分析要点:
- 追踪字符串如何被逐步拼接
- 观察 charCodeAt 的输入字符和输出编码
- 关注 join('') 调用 —— 通常是最终拼接点
```
#### 2.3 关键变量监控
**方法 1:使用 MCP 专用工具(推荐)**
```
MCP 操作:
- trace_property_access(target_expression="疑似签名容器对象")
→ Proxy 级别属性访问追踪,自动记录读写操作
- get_property_access_log → 获取属性访问记录
- compare_env → 收集完整浏览器环境信息
→ 用于后续补环境时精确对照 Node.js/Python 差异
```
**方法 2:手动注入监控脚本**
```javascript
// 通过 add_init_script(persistent=True) 注入
(function() {
// 方法1:监控全局变量写入
const watched = ['sign', 'm', '_signature', 'token'];
const origDefineProperty = Object.defineProperty;
watched.forEach(key => {
let val;
try {
origDefineProperty(window, key, {
get() { return val; },
set(v) {
console.log(`[GLOBAL_SET] window.${key} =`, v);
console.trace();
val = v;
},
configurable: true
});
} catch(e) {}
});
// 方法2:监控对象属性赋值
const _origAssign = Object.assign;
Object.assign = function() {
const result = _origAssign.apply(this, arguments);
for (let i = 1; i < arguments.length; i++) {
const src = arguments[i];
if (src && typeof src === 'object') {
Object.keys(src).forEach(k => {
if (watched.includes(k.toLowerCase())) {
console.log(`[ASSIGN] ${k} =`, src[k]);
console.trace();
}
});
}
}
return result;
};
})();
```
---
### 第三板斧:日志分析(从海量数据中提取签名链路)
JSVMP 的 trace 日志通常有数百甚至数千条,需要系统化的分析方法。
#### 3.1 日志采集
```
MCP 操作:
- get_trace_data → 获取函数追踪数据(自动合并页面数据 + Python 端持久化数据)
- get_jsvmp_log → 获取 JSVMP 专项日志(含 API 调用统计 + 属性读取摘要)
- get_console_logs → 获取 Hook 输出
- get_breakpoint_data → 获取伪断点捕获数据
- get_property_access_log → 获取属性访问记录
```
#### 3.2 过滤策略
| 过滤维度 | 方法 | 说明 |
|---------|------|------|
| 关键词过滤 | 搜索签名值片段 | 在日志中搜索已知签名值的前8位 |
| 时间窗口过滤 | 只看请求前 1-2 秒 | 签名通常在请求发送前即时生成 |
| 类型过滤 | 只看字符串参数 | 数值型操作大多是 VM 内部调度 |
| 长度过滤 | 关注长度 > 8 的字符串 | 排除短字符串噪声 |
| 变化过滤 | 对比多次请求日志 | 每次都变的是动态参数,不变的是密钥 |
#### 3.3 反向追踪法(核心技巧)
从已知的签名值反向追踪到原始明文:
```
步骤 1:在日志中搜索签名值
→ 搜索 "a1b2c3d4" (签名值的前几位)
→ 找到:[trace#247] return "a1b2c3d4e5f6..."
步骤 2:查看该次调用的输入
→ [trace#247] args: ["page=1&ts=1680000000&key=secret123"]
→ 这是签名函数的输入明文
步骤 3:继续追踪输入来源
→ 搜索 "page=1&ts=1680000000"
→ [trace#245] return "page=1&ts=1680000000&key=secret123"
→ [trace#245] args: ["page=1", "ts=1680000000", "key=secret123"]
→ 这是参数拼接函数
步骤 4:确认各参数来源
→ "page=1" → 用户输入
→ "ts=1680000000" → Date.now() / 1000
→ "key=secret123" → 硬编码密钥
结论:sign = MD5("page=" + page + "&ts=" + Math.floor(Date.now()/1000) + "&key=secret123")
```
#### 3.4 多次请求对比法
```
请求 1: sign=aaa111, ts=1680000000, page=1
请求 2: sign=bbb222, ts=1680000005, page=2
请求 3: sign=ccc333, ts=1680000010, page=1
对比分析:
- page 相同但 sign 不同(请求1 vs 请求3)→ ts 参与签名
- ts 不同且 sign 不同 → ts 是变化因子之一
- 请求1 和 请求3 page 相同但 sign 不同 → 不仅仅是 page 参与
结论:sign 至少由 page + ts 共同决定
```
#### 3.5 算法指纹识别
根据 I/O 特征反推算法类型:
| 输出特征 | 可能算法 |
|---------|---------|
| 32 位 hex | MD5 |
| 40 位 hex | SHA-1 |
| 64 位 hex | SHA-256 |
| 44 位 Base64(含 `=` 填充) | HMAC-SHA256 + Base64 |
| 24/32/44 位 Base64 | AES 加密(CBC/ECB) |
| 输出长度随输入变化 | 非固定哈希,可能是加密或自定义编码 |
| 输出包含特殊分隔符 | 自定义拼接格式 |
---
## 常见 JSVMP 变体及应对
### 变体 1:VM 劫持 XHR/Fetch
**特征**:VM 重写了 `XMLHttpRequest` 或 `fetch`,请求在 VM 内部完成,外部 Hook 抓不到。
**应对**:
```
- Hook 更底层的接口:
· hook_function(function_path="XMLHttpRequest.prototype.send", ...)
· 在 add_init_script 中保存原始引用,在 VM 重写之前
- 或 Hook 网络层:
· start_network_capture → 不依赖 JS 层 Hook,直接在协议层捕获
· list_network_requests → get_network_request → 获取完整请求详情
```
### 变体 2:VM 动态生成加密函数
**特征**:加密算法不是硬编码在字节码中,而是 VM 运行时通过 `eval` 或 `new Function` 动态构造。
**应对**:
```
- inject_hook_preset(preset="crypto") → 包含 eval/Function Hook
- hook_function(function_path="Function",
hook_code="console.log('[new Function]', arguments[arguments.length-1].substring(0,200))",
position="before")
- 从 Hook 日志中提取动态生成的函数源码
```
### 变体 3:多层 VM 嵌套
**特征**:外层 VM 解密出内层 VM 的字节码,再由内层 VM 执行签名逻辑。
**应对**:
```
- 不要试图理解嵌套关系
- 依然从 I/O 两端入手,Hook 最终的出口和最初的入口
- 增加 trace_function 的 max_captures 到 1000+
- 用时间戳过滤法缩小日志范围
```
### 变体 4:VM + WASM 混合
**特征**:VM 负责流程控制,核心加密算法调用 WASM 导出函数。
**应对**:
```
- search_code(keyword="WebAssembly|wasm|instantiate")
- Hook WASM 导出函数:
evaluate_js(expression=`
// 等 WASM 实例化后 Hook 导出函数
const origInstantiate = WebAssembly.instantiate;
WebAssembly.instantiate = async function() {
const result = await origInstantiate.apply(this, arguments);
const exports = result.instance?.exports || result.exports;
Object.keys(exports).forEach(name => {
if (typeof exports[name] === 'function') {
const orig = exports[name];
exports[name] = function() {
console.log('[WASM]', name, 'args:', Array.from(arguments));
const ret = orig.apply(this, arguments);
console.log('[WASM]', name, 'return:', ret);
return ret;
};
}
});
return result;
};
`)
- 确认 WASM 函数的 I/O 后,下载 .wasm 文件用 Node.js 直接加载调用
```
---
## 还原策略决策
```
JSVMP 签名还原决策树:
├─ Hook 确认使用标准算法(MD5/HMAC/AES)?
│ ├─ YES → 纯算法还原(Node.js crypto / Python hashlib+pycryptodome)
│ │ · 还原拼接顺序和密钥即可
│ │ · 这是最理想的结果
│ └─ NO → 继续分析
│
├─ 能提取出独立的签名函数?
│ ├─ YES → 沙箱执行(Node.js vm / Python execjs)
│ │ · 提取函数及其依赖
│ │ · 构造最小执行环境
│ └─ NO → 继续分析
│
├─ VM 劫持了整个请求链路?
│ ├─ YES → 加载完整 VM(仅调用签名入口)
│ │ · 下载完整 JS 文件
│ │ · 在 Node.js vm 中加载
│ │ · 补最小浏览器环境
│ │ · 调用签名函数获取结果
│ └─ NO → 浏览器自动化兜底
│
└─ 无法脱离浏览器
→ 使用 Camoufox 浏览器自动化(最后手段)
```
---
## 实战检查清单
在 JSVMP 分析过程中,确保以下每项都已完成:
- [ ] 确认文件是 JSVMP(解释器循环 + 字节码数组 + 栈操作)
- [ ] Hook XHR/Fetch 出口,捕获签名值的最终形态
- [ ] Hook Cookie setter,确认是否有动态 Cookie
- [ ] Hook 加密原语入口(btoa/atob/CryptoJS/MD5/SHA)
- [ ] Hook String.fromCharCode(JSVMP 高频信号)
- [ ] 定位解释器核心分发函数
- [ ] 分层插桩(粗 → 中 → 细)
- [ ] 日志反向追踪,找到签名值首次出现的位置
- [ ] 多次请求对比,确认变化因子和固定因子
- [ ] 根据输出特征判断算法类型
- [ ] 选择还原策略(纯算法 / 沙箱 / 完整 VM / 浏览器)
- [ ] 实现并验证签名计算结果
+194 -125
View File
@@ -2,46 +2,51 @@
## 概述
本手册详细说明 `js-reverse` 和 `chrome-devtools` 两个 MCP 服务器的协作使用方式,
本手册详细说明 `camoufox-reverse` MCP 服务器的使用方式,
提供常见逆向场景下的具体操作步骤。
## 工具速查表
### js-reverse MCP(逆向分析核心)
### camoufox-reverse MCP(52 个工具)
| 类别 | 工具 | 核心用途 |
|------|------|---------|
| **源码搜索** | `search_in_sources` | 在所有 JS 中搜索关键词 |
| **源码获取** | `get_script_source` | 获取指定脚本的代码片段 |
| **源码保存** | `save_script_source` | 将脚本保存到本地文件 |
| **浏览器** | `launch_browser` / `close_browser` | 启动/关闭反检测浏览器 |
| **导航** | `navigate` / `reload` / `go_back` | 页面导航 |
| **页面交互** | `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` | 列出页面加载的所有脚本 |
| **断点设置** | `set_breakpoint_on_text` | 按代码文本设置断点 |
| **XHR 断点** | `break_on_xhr` | 在 XHR 请求时暂停 |
| **函数追踪** | `trace_function` | 追踪函数调用,记录参数 |
| **暂停信息** | `get_paused_info` | 获取断点处的栈和变量 |
| **单步调试** | `step_into/over/out` | 单步执行 |
| **脚本注入** | `inject_before_load` | 页面加载前注入 JS |
| **脚本执行** | `evaluate_script` | 在页面执行 JS |
| **网络请求** | `list_network_requests` | 列出网络请求 |
| **请求详情** | `get_network_request` | 获取请求完整信息 |
| **请求来源** | `get_request_initiator` | 获取请求的 JS 调用栈 |
| **WebSocket** | `list_websocket_connections` | 列出 WS 连接 |
| **WS 分析** | `analyze_websocket_messages` | 分析 WS 消息模式 |
| **控制台** | `list_console_messages` | 读取控制台输出 |
### chrome-devtools MCP(浏览器自动化)
| 类别 | 工具 | 核心用途 |
|------|------|---------|
| **页面管理** | `new_page` / `navigate_page` / `close_page` | 页面操作 |
| **UI 交互** | `click` / `fill` / `type_text` / `press_key` | 元素交互 |
| **DOM 快照** | `take_snapshot` | 获取页面文本结构 |
| **截图** | `take_screenshot` | 页面截图 |
| **脚本执行** | `evaluate_script` | 执行 JS |
| **设备模拟** | `emulate` | 模拟 UA/视口/网络 |
| **网络请求** | `list_network_requests` / `get_network_request` | 请求监控 |
| **等待** | `wait_for` | 等待文本出现 |
| **对话框** | `handle_dialog` | 处理弹窗 |
| **执行 JS** | `evaluate_js` / `evaluate_js_handle` | 在页面执行任意 JS |
| **加载前注入** | `add_init_script` | 页面加载前注入脚本(支持 `persistent` 跨导航重注入) |
| **伪断点** | `set_breakpoint_via_hook` | 通过 Hook 设置伪断点(支持 `persistent` 持久化) |
| **断点数据** | `get_breakpoint_data` | 获取伪断点捕获的数据 |
| **函数追踪** | `trace_function` / `get_trace_data` | 追踪函数调用(支持 `persistent` 跨导航持久化) |
| **自定义 Hook** | `hook_function` | 注入自定义 Hook(before/after/replace,支持 `non_overridable` 防覆盖) |
| **预设 Hook** | `inject_hook_preset` | 一键 Hook(xhr/fetch/crypto/websocket/debugger_bypass,默认持久化) |
| **移除 Hook** | `remove_hooks` | 移除所有 Hook(支持 `keep_persistent` 保留持久化 Hook) |
| **冻结原型** | `freeze_prototype` | 冻结任意原型方法,防止页面 JS 覆盖 Hook |
| **属性追踪** | `trace_property_access` / `get_property_access_log` | Proxy 级别属性访问追踪 |
| **JSVMP 插桩** | `hook_jsvmp_interpreter` | 一键插桩 JSVMP(Function.prototype.apply + 30+ 敏感属性) |
| **JSVMP 日志** | `get_jsvmp_log` | 获取 JSVMP 日志(API 调用统计 + 属性读取摘要) |
| **JSVMP 字符串** | `dump_jsvmp_strings` | 提取字符串数组,识别 API 名称,检测混淆模式 |
| **环境采集** | `compare_env` | 全面收集浏览器环境(navigator/screen/canvas/WebGL/Audio/timing) |
| **网络捕获** | `start_network_capture` / `stop_network_capture` | 启停网络捕获(支持 `capture_body` 捕获响应体) |
| **网络请求** | `list_network_requests` | 列出捕获的请求 |
| **请求详情** | `get_network_request` | 获取请求完整信息(含响应体) |
| **请求来源** | `get_request_initiator` | 获取请求的 JS 调用栈(改进 URL 匹配 + 诊断信息) |
| **请求拦截** | `intercept_request` / `stop_intercept` | 拦截/修改/Mock 请求 |
| **Cookie** | `get_cookies` / `set_cookies` / `delete_cookies` | Cookie 管理 |
| **Storage** | `get_storage` / `set_storage` | localStorage / sessionStorage |
| **状态管理** | `export_state` / `import_state` | 保存/恢复浏览器状态 |
| **控制台** | `get_console_logs` | 读取控制台输出 |
| **指纹** | `get_fingerprint_info` | 检查浏览器指纹 |
| **反检测** | `check_detection` | 测试是否被反爬识别 |
| **反调试** | `bypass_debugger_trap` | 绕过 debugger 陷阱 |
## 场景化操作手册
@@ -50,40 +55,46 @@
**目标**:找到接口中加密参数的生成逻辑
```
步骤 1:打开目标页面
[chrome-devtools] new_page(url="https://target.com")
步骤 1:启动反检测浏览器
[camoufox-reverse] launch_browser()
步骤 2:获取页面结构
[chrome-devtools] take_snapshot() → 确认页面正常加载
步骤 2:导航到目标页面
[camoufox-reverse] navigate(url="https://target.com")
步骤 3:触发数据请求(如翻页)
[chrome-devtools] click(uid="翻页按钮的uid")
步骤 3:确认页面加载
[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:查看网络请求
[js-reverse] list_network_requests(resourceTypes=["XHR","Fetch"])
步骤 6:查看网络请求
[camoufox-reverse] list_network_requests
步骤 5:获取目标请求详情
[js-reverse] get_network_request(reqid=目标请求ID)
步骤 7:获取目标请求详情
[camoufox-reverse] get_network_request(request_id=N)
→ 记录 URL、Headers、Params、Response
步骤 6:获取请求调用栈
[js-reverse] get_request_initiator(requestId=目标请求ID)
→ 找到发起请求的 JS 函数
步骤 8:获取请求调用栈(黄金路径)
[camoufox-reverse] get_request_initiator(request_id=N)
→ 直接定位发起请求的 JS 函数
步骤 7:搜索加密参数
[js-reverse] search_in_sources(query="参数名=")
[js-reverse] search_in_sources(query="参数名")
步骤 9:搜索加密参数
[camoufox-reverse] search_code(keyword="参数名=")
[camoufox-reverse] search_code(keyword="参数名")
步骤 8:读取相关源码
[js-reverse] get_script_source(url="包含加密逻辑的脚本URL", startLine=X, endLine=Y)
步骤 10:读取相关源码
[camoufox-reverse] get_script_source(script_url="包含加密逻辑的脚本URL")
步骤 9:设置断点验证
[js-reverse] set_breakpoint_on_text(text="加密函数调用")
步骤 11:设置伪断点验证
[camoufox-reverse] set_breakpoint_via_hook(target_function="加密函数路径")
触发请求 →
[js-reverse] get_paused_info(includeScopes=true)
→ 查看变量值确认加密逻辑
[camoufox-reverse] get_breakpoint_data
→ 查看捕获的参数和返回值
```
### 场景 2:动态 Cookie 逆向
@@ -92,25 +103,31 @@
```
步骤 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 生成
[js-reverse] navigate_page(type="reload")
[camoufox-reverse] reload()
步骤 3:读取 Hook 输出
[js-reverse] list_console_messages(types=["log"])
[camoufox-reverse] get_console_logs
→ 找到 Cookie 设置记录和调用栈
步骤 4:根据调用栈定位生成函数
[js-reverse] search_in_sources(query="目标函数名")
[js-reverse] get_script_source(url="...", startLine=X, endLine=Y)
[camoufox-reverse] search_code(keyword="目标函数名")
[camoufox-reverse] get_script_source(script_url="...")
步骤 5:分析生成逻辑
[js-reverse] set_breakpoint_on_text(text="cookie生成代码片段")
[camoufox-reverse] set_breakpoint_via_hook(target_function="cookie生成函数")
刷新页面 →
[js-reverse] get_paused_info → 查看入参
[js-reverse] step_into → 进入加密函数
[js-reverse] get_paused_info → 查看中间值
[camoufox-reverse] get_breakpoint_data → 查看入参和返回值
[camoufox-reverse] trace_function(function_path="加密函数", log_args=true, log_return=true, log_stack=true)
[camoufox-reverse] get_trace_data → 查看中间值
```
### 场景 3:混淆代码分析
@@ -119,27 +136,27 @@
```
步骤 1:列出所有脚本
[js-reverse] list_scripts()
[camoufox-reverse] list_scripts
→ 找到可疑的混淆脚本(通常文件名无意义或体积很大)
步骤 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:搜索关键特征
[js-reverse] search_in_sources(query="_0x", urlFilter="混淆脚本URL")
[camoufox-reverse] search_code(keyword="_0x")
→ 确认混淆类型(OB混淆)
[js-reverse] search_in_sources(query="switch|while.*true", urlFilter="混淆脚本URL")
[camoufox-reverse] search_code(keyword="switch|while.*true")
→ 检查是否有控制流平坦化
步骤 4:在浏览器中还原字符串
[js-reverse] evaluate_script(function="返回字符串数组的代码")
[camoufox-reverse] evaluate_js(expression="返回字符串数组的代码")
→ 获取解密后的字符串映射
步骤 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 逆向
@@ -148,29 +165,24 @@
```
步骤 1:搜索 WASM 加载代码
[js-reverse] search_in_sources(query="WebAssembly|.wasm|instantiate")
[camoufox-reverse] search_code(keyword="WebAssembly|.wasm|instantiate")
步骤 2:找到 WASM 文件
[js-reverse] list_network_requests(resourceTypes=["Other"])
[camoufox-reverse] list_network_requests
→ 找到 .wasm 文件的请求
步骤 3:分析 WASM 调用
[js-reverse] search_in_sources(query="exports.|instance.exports")
[camoufox-reverse] search_code(keyword="exports.|instance.exports")
→ 找到导出函数的调用代码
步骤 4:追踪输入输出
[js-reverse] set_breakpoint_on_text(text="wasm导出函数调用")
[camoufox-reverse] set_breakpoint_via_hook(target_function="wasm导出函数调用路径")
触发请求 →
[js-reverse] get_paused_info → 查看入参
[js-reverse] step_over → 执行函数
[js-reverse] get_paused_info → 查看返回值
[camoufox-reverse] get_breakpoint_data → 查看入参和返回值
步骤 5:在浏览器中测试
[js-reverse] evaluate_script(function="调用wasm函数并返回结果")
[camoufox-reverse] evaluate_js(expression="调用wasm函数并返回结果")
→ 验证 I/O 关系
步骤 6:下载 WASM 文件
[chrome-devtools] get_network_request(reqid=WASM请求ID, responseFilePath="./project/main.wasm")
```
### 场景 5:WebSocket 通信分析
@@ -178,18 +190,18 @@
**目标**:分析 WebSocket 消息格式和加密
```
步骤 1:列出 WS 连接
[js-reverse] list_websocket_connections()
步骤 1:注入 WebSocket Hook
[camoufox-reverse] inject_hook_preset(preset="websocket")
步骤 2:分析消息模式
[js-reverse] analyze_websocket_messages(wsid=连接ID, direction="sent")
[js-reverse] analyze_websocket_messages(wsid=连接ID, direction="received")
步骤 2:读取 WS 消息日志
[camoufox-reverse] get_console_logs
→ 获取 WS 发送/接收消息
步骤 3:获取具体消息
[js-reverse] get_websocket_messages(wsid=连接ID, show_content=true)
步骤 3:搜索 WS 相关代码
[camoufox-reverse] search_code(keyword="WebSocket|ws.send|onmessage")
步骤 4:搜索 WS 相关代码
[js-reverse] search_in_sources(query="WebSocket|ws.send|onmessage")
步骤 4:手动发送 WS 消息
[camoufox-reverse] evaluate_js(expression="ws.send(JSON.stringify({...}))")
```
## 高级技巧
@@ -197,30 +209,79 @@
### 使用 trace_function 自动记录函数调用
```
[js-reverse] trace_function(
functionName="encrypt",
logArgs=true,
logThis=false,
pause=false
[camoufox-reverse] trace_function(
function_path="window.encrypt",
log_args=true,
log_return=true,
log_stack=true,
max_captures=50,
persistent=True → 跨导航持久化,页面刷新/跳转后自动重注入
)
```
→ 每次 `encrypt` 被调用时自动记录参数,不暂停执行
→ 每次 `encrypt` 被调用时自动记录参数、返回值和调用栈,不暂停执行
→ `get_trace_data` 会自动合并页面数据和 Python 端持久化数据
### 使用条件断点
### 使用 inject_hook_preset 一键 Hook
```
[js-reverse] set_breakpoint_on_text(
text="fetch(",
condition="arguments[0].indexOf('/api/data') !== -1"
默认 persistent=True(持久化,跨导航自动重注入):
[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));
setInterval(() => {
const newGlobals = Object.keys(window).filter(k => !knownGlobals.has(k));
@@ -229,14 +290,13 @@
newGlobals.forEach(k => knownGlobals.add(k));
}
}, 1000);
`)
`, persistent=True) → 跨导航持久化
```
### 使用 evaluate_script 提取运行时数据
### 使用 evaluate_js 提取运行时数据
```
[js-reverse] evaluate_script(function=`
// 提取当前页面的所有 Cookie
[camoufox-reverse] evaluate_js(expression=`
JSON.stringify(document.cookie.split('; ').reduce((obj, pair) => {
const [k, v] = pair.split('=');
obj[k] = v;
@@ -245,20 +305,29 @@
`)
```
## 两个 MCP 的分工原则
### 使用 intercept_request 拦截/修改请求
| 需求 | 使用 js-reverse | 使用 chrome-devtools |
|------|----------------|---------------------|
| 搜索 JS 源码 | ✅ `search_in_sources` | ❌ |
| 设置断点 | ✅ `set_breakpoint_on_text` | ❌ |
| 单步调试 | ✅ `step_into/over/out` | ❌ |
| 函数追踪 | ✅ `trace_function` | ❌ |
| 注入 Hook | ✅ `inject_before_load` | ❌ |
| 页面截图 | ✅ 可以 | ✅ 更灵活(指定元素) |
| UI 交互(点击/输入) | ❌ | ✅ `click/fill/type_text` |
| 模拟 UA/设备 | ❌ | ✅ `emulate` |
| DOM 快照 | ❌ | ✅ `take_snapshot` |
| 保存请求响应到文件 | ❌ | ✅ `get_network_request(responseFilePath)` |
| WebSocket 分析 | ✅ 完整支持 | ❌ |
| 请求调用栈 | ✅ `get_request_initiator` | ❌ |
| 执行 JS | ✅ 支持 `mainWorld` 选项 | ✅ 支持 `args` 传 uid |
```
[camoufox-reverse] intercept_request(url_pattern="**/api/data*", action="log") → 仅记录
[camoufox-reverse] intercept_request(url_pattern="**/ads/*", action="block") → 屏蔽广告
[camoufox-reverse] intercept_request(url_pattern="**/api/*", action="modify", modify_headers={"X-Custom": "test"})
[camoufox-reverse] intercept_request(url_pattern="**/api/*", action="mock", mock_response={"status": 200, "body": "{\"ok\":1}"})
```
## camoufox-reverse MCP 工具分类总览(52 个)
| 类别 | 工具 | 说明 |
|------|------|------|
| 浏览器生命周期 | `launch_browser` `close_browser` | 启动/关闭 Camoufox |
| 页面导航 | `navigate` `reload` `go_back` | 导航控制 |
| 页面交互 | `click` `type_text` `wait_for` | UI 自动化 |
| 页面状态 | `get_page_info` `take_screenshot` `take_snapshot` `get_page_html` | 页面信息 |
| 源码分析 | `list_scripts` `get_script_source` `search_code` `search_code_in_script` `save_script` | JS 逆向核心 |
| JS 执行 | `evaluate_js` `evaluate_js_handle` `add_init_script` | 代码执行(支持持久化) |
| 调试 | `set_breakpoint_via_hook` `get_breakpoint_data` `get_console_logs` | 伪断点调试(支持持久化) |
| Hook | `trace_function` `get_trace_data` `hook_function` `inject_hook_preset` `remove_hooks` `freeze_prototype` | 函数 Hook(支持持久化 + 防覆盖) |
| 属性追踪 | `trace_property_access` `get_property_access_log` | Proxy 级别属性访问监控 |
| JSVMP 专项 | `hook_jsvmp_interpreter` `get_jsvmp_log` `dump_jsvmp_strings` `compare_env` | JSVMP 虚拟机保护分析 |
| 网络 | `start_network_capture` `stop_network_capture` `list_network_requests` `get_network_request` `get_request_initiator` `intercept_request` `stop_intercept` | 网络分析(支持响应体捕获) |
| 存储 | `get_cookies` `set_cookies` `delete_cookies` `get_storage` `set_storage` `export_state` `import_state` | 状态管理 |
| 反检测 | `get_fingerprint_info` `check_detection` `bypass_debugger_trap` | 指纹与反检测 |
+20 -20
View File
@@ -19,9 +19,9 @@
**MCP 辅助**:
```
[js-reverse] search_in_sources(query="_0x") → 定位混淆入口
[js-reverse] evaluate_script → 在浏览器中执行字符串数组还原
[js-reverse] save_script_source → 保存到本地进一步分析
[camoufox-reverse] search_code(keyword="_0x") → 定位混淆入口
[camoufox-reverse] evaluate_js → 在浏览器中执行字符串数组还原
[camoufox-reverse] save_script → 保存到本地进一步分析
```
### 2. 控制流平坦化 (CFF)
@@ -47,9 +47,9 @@ while (true) {
**MCP 辅助**:
```
[js-reverse] set_breakpoint_on_text(text="switch") → 在状态机入口断点
[js-reverse] step_over → 逐步追踪状态转移
[js-reverse] get_paused_info → 查看当前状态值
[camoufox-reverse] set_breakpoint_via_hook(target_function="状态机入口函数") → 设置伪断点
[camoufox-reverse] trace_function(function_path="状态机函数", log_args=true, log_return=true) → 追踪状态转移
[camoufox-reverse] get_trace_data → 查看状态值变化
```
### 3. eval / Function 打包
@@ -68,8 +68,8 @@ new Function('return ' + decryptedCode)()
**MCP 辅助**:
```
[js-reverse] inject_before_load(script="eval/Function Hook")
[js-reverse] list_console_messages → 读取解包后的代码
[camoufox-reverse] add_init_script(script="eval/Function Hook")
[camoufox-reverse] get_console_logs → 读取解包后的代码
```
### 4. AAEncode
@@ -118,9 +118,9 @@ $=~[];$={___:++$,$$$$:(![]+"")[$],...
**MCP 辅助**:
```
[js-reverse] trace_function(functionName="解释器函数")
[js-reverse] set_breakpoint_on_text(text="switch") → 在解释器循环断点
[js-reverse] step_over → 观察每步操作
[camoufox-reverse] trace_function(function_path="解释器函数", log_args=true, log_return=true)
[camoufox-reverse] get_trace_data → 观察每步操作的输入输出
[camoufox-reverse] set_breakpoint_via_hook(target_function="解释器核心函数") → 捕获关键调用
```
### 8. JSVMP(JS 虚拟机保护)
@@ -139,9 +139,9 @@ $=~[];$={___:++$,$$$$:(![]+"")[$],...
**MCP 辅助**:
```
[js-reverse] break_on_xhr(url="目标接口") → 在请求发出时暂停
[js-reverse] get_paused_info → 获取调用栈
[js-reverse] inject_before_load → 注入全局 Hook
[camoufox-reverse] inject_hook_preset(preset="xhr") → 一键 Hook XHR 请求
[camoufox-reverse] get_request_initiator(request_id=N) → 获取请求的 JS 调用栈(黄金路径)
[camoufox-reverse] add_init_script → 注入全局 Hook
```
## 通用反混淆 Node.js 工具
@@ -189,10 +189,10 @@ function deobfuscate(code) {
## 混淆代码分析的 MCP 工作流
```
1. save_script_source → 保存混淆代码到本地
2. search_in_sources → 搜索可能的入口函数
3. set_breakpoint_on_text → 在入口设断点
4. get_paused_info → 查看运行时变量,理解数据流
5. evaluate_script → 在浏览器执行还原操作
6. step_into → 跟进关键函数调用
1. save_script → 保存混淆代码到本地
2. search_code → 搜索可能的入口函数
3. set_breakpoint_via_hook → 在入口设伪断点
4. get_breakpoint_data → 查看捕获的参数和返回值
5. evaluate_js → 在浏览器执行还原操作
6. trace_function → 追踪关键函数调用链
```
+10 -6
View File
@@ -27,11 +27,11 @@
### 解决方案
#### 方案 A:使用浏览器自动化(最可靠)
#### 方案 A:使用 Camoufox 反检测浏览器(最可靠)
```javascript
// 使用 chrome-devtools MCP 在真实浏览器中执行请求
// [chrome-devtools] evaluate_script
// 使用 camoufox-reverse MCP 在 Camoufox 浏览器环境中执行请求
// [camoufox-reverse] evaluate_js
const response = await fetch('https://target.com/api/data', {
method: 'GET',
headers: { 'Cookie': cookieValue },
@@ -42,7 +42,9 @@ const data = await response.json();
```
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
@@ -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 校验
+228
View File
@@ -0,0 +1,228 @@
# 错误排查指南
## 请求失败排查流程
### 排查顺序(必须按序执行)
当协议脚本请求返回非预期结果时,**禁止盲目大改**,按以下 6 步逐项排查:
```
步骤 ① Cookie/Session 失效
│
├─ 排查方法:
│ · 浏览器 F12 → Application → Cookies 获取最新 Cookie
│ · 对比脚本使用的 Cookie 与浏览器当前 Cookie
│ · 检查是否有 HttpOnly Cookie 漏掉(Network 面板看完整 Cookie)
│
├─ MCP 辅助:
│ · get_cookies → 获取 Camoufox 中的 Cookie
│ · evaluate_js(expression="document.cookie") → 获取 JS 可见 Cookie
│
└─ 常见原因:Cookie 过期、登录态失效、缺少 HttpOnly Cookie
步骤 ② 前置请求遗漏
│
├─ 排查方法:
│ · Network 面板查看主请求之前的所有请求
│ · 重点关注:/api/init、/token、/config 等路径
│ · 检查前置请求的响应中是否有 Token、Session ID
│
├─ MCP 辅助:
│ · start_network_capture → 触发操作 → list_network_requests
│ · 按时间排序,找到主请求之前的接口
│
└─ 常见原因:漏掉预热接口、Token 获取接口、验证码初始化接口
步骤 ③ 时间戳绑定
│
├─ 排查方法:
│ · 对比浏览器请求的时间戳与脚本生成的时间戳
│ · 检查精度:秒(10位)vs 毫秒(13位)
│ · 检查是否有时间窗口限制(如签名有效期 30 秒)
│
├─ 常见坑:
│ · Python time.time() 返回浮点数,需要 int()
│ · Node.js Date.now() 返回毫秒
│ · 服务端可能校验请求时间与服务器时间的差值
│
└─ 修复方法:确保签名计算和请求发送使用同一个时间戳值
步骤 ④ Header 缺失或错误
│
├─ 排查方法:
│ · 逐项对比浏览器 Request Headers 与脚本 Headers
│ · 重点检查:Content-Type、Referer、Origin、Accept
│ · 检查自定义 Header(X-开头的、大小写敏感的)
│
├─ 常见坑:
│ · Header 顺序可能影响(极少数站点)
│ · sec-ch-ua 等 Client Hints Header
│ · Accept-Encoding 不支持 br 但发了 br
│
└─ 修复方法:完整复制浏览器 Headers,逐个删减确认必要项
步骤 ⑤ 环境校验
│
├─ 排查方法:
│ · Hook navigator/screen/canvas 等环境 API
│ · 检查签名计算是否包含环境指纹
│ · 对比有无环境值时的请求结果
│
├─ MCP 辅助:
│ · get_fingerprint_info → 查看 Camoufox 指纹
│ · search_code(keyword="navigator|screen|canvas") → 搜索环境检测代码
│
└─ 关键原则:先验证是否真正参与服务端校验,再决定是否补全
步骤 ⑥ 频率限制
│
├─ 排查方法:
│ · 降低请求频率重试(间隔 3-5 秒)
│ · 检查响应中的限流相关字段(rate limit、retry-after)
│ · 检查是否返回了 429 状态码
│
└─ 修复方法:增加请求间隔、使用代理 IP、添加随机延迟
```
---
## 签名值不一致排查
### 逐步对比法
当计算出的签名值与浏览器实际值不匹配时:
```
对比链路(脚本值 vs 浏览器值):
环节 1:原始输入参数
├─ 参数名是否完全一致(大小写、下划线)
├─ 参数值是否完全一致
└─ 是否有隐藏参数(空值但参与签名的参数)
环节 2:参数排序与拼接
├─ 参数排序规则(字典序、自定义顺序、原始顺序)
├─ 拼接分隔符(& / 空 / 其他)
├─ 是否包含 key=value 中的 key 名称
├─ 空值参数是否参与拼接
└─ URL 编码是否在拼接前/后执行
环节 3:时间戳
├─ 精度:秒 (10位) vs 毫秒 (13位)
├─ 类型:字符串 vs 数字
└─ 时区:UTC vs 本地时间
环节 4:随机串
├─ 长度是否匹配
├─ 字符集:hex / alphanumeric / 自定义
└─ 生成方法:Math.random vs crypto.randomBytes
环节 5:密钥/盐值
├─ 密钥值是否正确(注意空格、换行、编码)
├─ 密钥是否是硬编码还是动态获取
└─ 是否有 IV / Salt(AES 加密场景)
环节 6:中间摘要
├─ 多次哈希时逐层对比中间值
└─ 编码格式:hex(小写) vs hex(大写) vs base64
环节 7:最终输出
├─ 编码方式:hex / base64 / 自定义
├─ 大小写:hex 小写 vs 大写
└─ 是否有额外处理(截断、拼接前缀等)
```
### 快速定位技巧
```
技巧 1:二分法
如果有中间值可对比,先对比中间值,
确定偏差发生在前半段还是后半段
技巧 2:固定输入法
在浏览器和脚本中使用完全相同的:
- 时间戳(硬编码一个固定值)
- 随机串(硬编码一个固定值)
- 页码(相同页码)
然后对比签名结果,排除动态因素干扰
技巧 3:打印拼接字符串
在签名函数的哈希/加密调用之前,
打印完整的输入字符串,逐字符对比
技巧 4:MCP 辅助对比
- set_breakpoint_via_hook(target_function="签名函数")
- get_breakpoint_data → 获取浏览器端的真实入参和返回值
- 与脚本端逐项对比
```
---
## 常见错误与解决方案
### HTTP 状态码
| 状态码 | 常见原因 | 排查方向 |
|--------|---------|---------|
| 403 Forbidden | Cookie 失效 / Header 缺失 / IP 被封 / TLS 指纹 | 步骤 ①④⑤⑥ |
| 412 Precondition Failed | 签名校验失败 / 缺少前置请求 | 签名对比 + 步骤 ② |
| 429 Too Many Requests | 频率限制 | 步骤 ⑥ |
| 500 Internal Server Error | 参数格式错误 / 数据类型不匹配 | 检查请求 Body 格式 |
| 200 但数据为空 | 签名正确但参数错误 / 页码越界 / 缺权限 | 检查业务参数 |
### 加密相关
| 问题 | 可能原因 | 解决方法 |
|------|---------|---------|
| MD5 结果不一致 | 编码问题(UTF-8 vs ASCII) | 确认输入字符串的编码方式 |
| AES 解密失败 | 模式错误(CBC vs ECB)/ Padding 错误 / IV 错误 | 逐一确认模式、Padding、IV |
| Base64 结果多余字符 | URL-safe Base64 vs 标准 Base64 | 检查是否需要 `+/` → `-_` 替换 |
| HMAC 结果不一致 | 密钥编码问题 / 算法类型错误 | 确认密钥是字符串还是 hex bytes |
| RSA 加密失败 | 公钥格式错误 / PKCS1 vs OAEP | 检查公钥格式和填充方案 |
### 环境相关
| 问题 | 可能原因 | 解决方法 |
|------|---------|---------|
| Node.js vm 沙箱报错 | 缺少 DOM API(document/window) | 参考 `environment-patch.md` 补环境 |
| Python execjs 报错 | Node.js 未安装 / JS 代码有语法错误 | 检查 Node.js 环境、验证 JS 代码 |
| WASM 加载失败 | 缺少 imports / 内存不足 | 检查 WASM imports 并补全 |
| Cookie 设置不生效 | domain 不匹配 / path 不匹配 | 确认 Cookie 的 domain 和 path |
---
## Python 特有问题
| 问题 | 原因 | 解决方法 |
|------|------|---------|
| `requests` 请求被拒 | TLS 指纹被识别 | 换用 `curl_cffi`(支持浏览器指纹模拟) |
| `hashlib.md5()` 报错 | Python 3.9+ FIPS 模式限制 | 使用 `hashlib.md5(usedforsecurity=False)` |
| `execjs` 执行慢 | 每次创建新的 JS 运行时 | 编译后复用 context:`ctx = execjs.compile(js_code)` |
| `pycryptodome` 与 `pycrypto` 冲突 | 同时安装了两个库 | `pip uninstall pycrypto && pip install pycryptodome` |
| 中文编码问题 | 签名包含中文字符 | 使用 `urllib.parse.quote()` 或确认 UTF-8 编码 |
| `httpx` HTTP/2 连接失败 | 缺少 h2 依赖 | `pip install httpx[http2]` |
---
## Node.js 特有问题
| 问题 | 原因 | 解决方法 |
|------|------|---------|
| `crypto` 模块不可用 | 使用了浏览器打包版本 | 确认运行环境是 Node.js 而非浏览器 |
| `vm` 沙箱超时 | JS 代码有死循环 | 设置 `timeout` 选项 |
| `axios` 被 WAF 拦截 | 默认 User-Agent | 自定义完整浏览器 UA |
| HTTP/2 请求失败 | 证书验证失败 | `rejectUnauthorized: false`(调试用) |
---
## 排查工具速查
### MCP 工具
| 排查场景 | MCP 工具 | 用法 |
|---------|---------|------|
| 对比请求差异 | `start_network_capture` + `get_network_request` | 在 Camoufox 中捕获真实请求,与脚本请求对比 |
| 获取真实签名值 | `set_breakpoint_via_hook` + `get_breakpoint_data` | 在签名函数设伪断点,捕获真实入参和返回值 |
| 验证环境检测 | `get_fingerprint_info` + `check_detection` | 确认哪些环境项参与校验 |
| 追踪调用链 | `get_request_initiator` | 从请求直接定位到签名函数 |
| 实时对比 | `evaluate_js` | 在浏览器中执行还原后的签名函数,与脚本输出对比 |
+9 -9
View File
@@ -93,30 +93,30 @@
- 加密逻辑过于复杂,还原成本远大于自动化
**技术栈**:
- `playwright` / `puppeteer` — 浏览器自动化
- chrome-devtools MCP — 直接操作已有 Chrome
- `playwright-core` — 浏览器自动化
- `camoufox-reverse` MCP — Camoufox 反检测浏览器、Hook、网络捕获、源码搜索
**注意事项**:
- 复用本机 Chrome 实例避免指纹差异
- 注入脚本截取中间数据
- 控制请求频率避免触发风控
- 优先使用 Camoufox 独立浏览器实例,避免污染用户日常浏览环境
- 通过 `inject_hook_preset` / `add_init_script` 注入脚本截取中间数据
- 控制请求频率并结合 `check_detection` 验证反检测效果
**模板**:`templates/browser-auto/`
## 通用调试流程
```
1. 打开 Network 面板
1. 启动 Camoufox 并开启网络捕获
↓
2. 触发目标操作(搜索/翻页/登录)
↓
3. 找到数据接口,分析参数
↓
4. 搜索参数名定位 JS 源码
4. 用 `search_code` 搜索参数名定位 JS 源码
↓
5. 设置 XHR 断点追踪调用栈
5. 用 `get_request_initiator` / `set_breakpoint_via_hook` 追踪调用栈
↓
6. 在关键函数设置断点
6. 在关键函数设置伪断点或 `trace_function`
↓
7. 单步调试确认加密逻辑
↓