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:
@@ -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() {
|
||||
|
||||
@@ -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}")
|
||||
```
|
||||
|
||||
@@ -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
|
||||
→ 在环境检测函数入口捕获入参、返回值和调用栈,确认需要补全的关键分支
|
||||
```
|
||||
|
||||
@@ -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` 结果
|
||||
|
||||
@@ -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 中搜索关键词 |
|
||||
| **源码获取** | `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` | 指纹与反检测 |
|
||||
|
||||
@@ -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 → 追踪关键函数调用链
|
||||
```
|
||||
|
||||
@@ -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 校验
|
||||
|
||||
@@ -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` — 浏览器自动化
|
||||
- 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. 单步调试确认加密逻辑
|
||||
↓
|
||||
|
||||
Reference in New Issue
Block a user