Files
hello_js_reverse_skill/references/jsvmp-analysis.md
T
白夜 b5cf8b121a 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
2026-04-02 18:10:05 +08:00

487 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 / 浏览器)
- [ ] 实现并验证签名计算结果