本文仅供安全研究和学习交流,请勿用于非法用途。
我一直使用 AI 辅助 JS 逆向分析。从简单的 OB 混淆、sojson,到单 VM 保护,AI 都能很好地完成任务。
但这次遇到了一个新挑战:多态虚拟机 (Polymorphic VM) 保护。
与单 VM 不同,多态 VM 有 30+ 个独立的 dispatcher,同一个 opcode 在不同 VM 中执行完全不同的操作。如果不使用 AI,这种多态 VM 几乎无法静态分析——光是建立 30 套 opcode 映射表就是噩梦。
但 AI 让多 VM 的静态还原成为可能。本文分享三个核心干货:
普通 VM 保护:
多态 VM 保护:
同一个数字,在不同 VM 中含义完全不同。
工作流程:Gemini 识别混淆类型 → Claude Code 编写 AST 脚本 → 迭代优化
拿到 450KB 的混淆代码后,先让 Gemini 识别混淆类型,然后让 Claude Code 编写 Babel AST 脚本。整个脚本分 9 个 Pass:
调试技巧:分 Pass 执行,每个 Pass 单独测试,报错信息直接发给 AI 修复。
解混淆后,用 AST 工具提取:字节码数组、字符串表、VM dispatcher 结构。
然后让 AI 识别:
关键点:用浏览器实际执行结果验证 AI 的分析,发现不一致时针对性重新分析。
这是我在分析过程中摸索出的高效协作模式:
具体流程:
AI 分析代码,找到断点位置
人工在浏览器中操作
把结果反馈给 AI
AI 调整分析方向
为什么这种模式有效?
实际案例:
发现 _append 魔改就是这样找到的:
这种「AI 指挥 + 人工执行」的模式,比纯静态分析效率高很多。
在所有算法还原中,魔改 SHA256 是最难的部分。不是因为 SHA256 本身复杂,而是魔改点极其隐蔽。
最初,我以为只需要找到 SHA256 的调用点,直接用标准库就行。但对比浏览器输出后发现结果不一致:
于是开始追踪 SHA256 的实现。
输入数据在进入 SHA256 前,会先经过 _seData1 函数处理,追加 10 个字符的动态后缀:
这 10 个字符是根据输入内容计算的,算法是将输入分成 10 块,每块计算字符码累加值,映射到自定义字符表。
找到 _seData1 后,我以为已经完成了。但验证时发现结果还是不对。
问题出在 _append 函数被魔改了。
在标准 SHA256 实现中,_append 只是简单地追加数据。但京东的实现中,_append 内部调用了 _eData,会额外追加一个固定后缀:
这个魔改非常隐蔽,因为:
最后,SHA256 的输出还会进行字节交换——交换第 0 字节和第 2 字节:
这个案例说明:魔改可能发生在任何地方,包括看起来无害的辅助函数。
如果不是 AI 帮我快速分析了所有相关 VM 的字节码,手动追踪这个调用链可能需要很长时间。
通过 AI 分析,我梳理出了签名的完整 VM 调用链:
分析了 17 个核心 VM,按功能分类:
签名核心 VM(6 个):
加密算法 VM(6 个):
辅助 VM(5 个):l25、l29、l32、l33、l23(环境检测)
同一个 opcode 数字在不同 VM 中的含义:
这就是为什么叫「多态」——同一个数字,在不同 VM 中执行完全不同的操作。
签名过程中会收集大量浏览器指纹,用于:
通过分析 l23(环境检测 VM),发现以下检测项:
运行时检测:
浏览器指纹:
这些检测结果会被编码到 h5st 的第 8 个字段(envData)中:
如果要在非浏览器环境运行,需要:
或者更简单的方式:直接固定 envData,因为这个字段主要用于风控,不影响签名验证。
AI 不是万能的,以下场景仍需人工介入:
多态 VM 是目前 JS 保护的高级形态,传统工具和方法很难应对。但借助 AI 的模式识别和代码生成能力,可以大幅提升分析效率。
核心还是逆向工程的方法论:理解保护机制的原理,找到正确的切入点,持续验证和迭代。
希望本文对研究 VM 保护的朋友有所启发。
免责声明:本文仅用于安全研究和技术交流,请遵守相关法律法规。
case 2 → 永远是 PUSH 操作
case 5 → 永远是 POP 操作
case 2 → 永远是 PUSH 操作
case 5 → 永远是 POP 操作
VM_A: case 2 → PUSH
VM_B: case 2 → STORE
VM_C: case 2 → CALL
VM_A: case 2 → PUSH
VM_B: case 2 → STORE
VM_C: case 2 → CALL
| 保护类型 |
分析复杂度 |
工具复用性 |
| 字符串混淆 |
低 |
高 |
| 控制流平坦化 |
中 |
中 |
| 单 VM 保护 |
中高 |
高 |
| 多态 VM |
极高 |
极低 |
AST 解混淆 → 提取结构化数据 → AI 分析模式 → 人工验证 → AI 生成代码
AST 解混淆 → 提取结构化数据 → AI 分析模式 → 人工验证 → AI 生成代码
| Pass |
功能 |
说明 |
| 1 |
字符串解密函数还原 |
_4o5l4("xvvw") → "test" |
| 2 |
字符串数组还原 |
_1w8l4[42] → "appid" |
| 3 |
常量折叠 |
0x1a2b + 0x3c4d → 数值 |
| 4 |
逗号表达式展开 |
(a=1, b=2, c()) → 多行 |
| 5 |
变量语义重命名 |
_2n8l4 → Bytecode |
| 6 |
对象字典展开 |
_$wR.add(x,y) → x + y |
| 7-9 |
清理 |
死代码移除、未使用变量删除 |
┌─────────────┐ 分析代码,定位关键点 ┌─────────────┐
│ │ ──────────────────────────→ │ │
│ AI │ │ 人工 │
│ │ ←────────────────────────── │ │
└─────────────┘ 执行结果、变量值 └─────────────┘
┌─────────────┐ 分析代码,定位关键点 ┌─────────────┐
│ │ ──────────────────────────→ │ │
│ AI │ │ 人工 │
│ │ ←────────────────────────── │ │
└─────────────┘ 执行结果、变量值 └─────────────┘
AI: 「SHA256 结果不对,在 _append 函数入口设断点,看看传入的参数」
人: 「参数是 "test",但 this.buffer 变成了 "testXXXXXX"」
AI: 「说明 _append 内部有额外处理,让我分析这个函数...」
AI: 「找到了,_append 调用了 _eData,在 _eData 设断点」
人: 「_eData 返回值是原字符串 + 固定后缀」
AI: 「这就是第二层魔改!」
AI: 「SHA256 结果不对,在 _append 函数入口设断点,看看传入的参数」
人: 「参数是 "test",但 this.buffer 变成了 "testXXXXXX"」
AI: 「说明 _append 内部有额外处理,让我分析这个函数...」
AI: 「找到了,_append 调用了 _eData,在 _eData 设断点」
人: 「_eData 返回值是原字符串 + 固定后缀」
AI: 「这就是第二层魔改!」
标准 SHA256("test") = 9f86d08...
京东 SHA256("test") = 完全不同的值
标准 SHA256("test") = 9f86d08...
京东 SHA256("test") = 完全不同的值
"test" → "test" + [10字符动态后缀]
"test" → "test" + [10字符动态后缀]
_append(data) {
this.buffer += data;
}
_append(data) {
this.buffer += _eData(data);
}
_append(data) {
this.buffer += data;
}
_append(data) {
this.buffer += _eData(data);
}
[a, b, c, d, ...] → [c, b, a, d, ...]
[a, b, c, d, ...] → [c, b, a, d, ...]
jd_sha256(input)
↓
_seData1(input) ← 追加 10 字符动态后缀
↓
_eData(result) ← 追加固定后缀 (隐藏在 _append 中)
↓
标准 SHA256
↓
swap(byte[0], byte[2]) ← 字节交换
↓
输出 64 位 hex
jd_sha256(input)
↓
_seData1(input) ← 追加 10 字符动态后缀
↓
_eData(result) ← 追加固定后缀 (隐藏在 _append 中)
↓
标准 SHA256
↓
swap(byte[0], byte[2]) ← 字节交换
↓
输出 64 位 hex
signSync() 入口
↓
┌─────────────────────────────────────────────────────────┐
│ l34 (签名入口 VM) │
│ ↓ │
│ l31 (h5st 组装 VM) ─────────────────────────────────┐ │
│ │ │ │
│ ├─→ l24 (密钥生成 VM) │ │
│ │ └─→ l25 (辅助函数) │ │
│ │ │ │
│ ├─→ l28 (签名计算 VM) ──→ jd_sha256 │ │
│ │ └─→ l29 (辅助函数) │ │
│ │ │ │
│ ├─→ l30 (签名数据 VM) │ │
│ │ │ │
│ ├─→ wm_encode (自定义 Base64 VM) │ │
│ │ │ │
│ └─→ l27 (最终组装 VM) ←─────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
signSync() 入口
↓
┌─────────────────────────────────────────────────────────┐
│ l34 (签名入口 VM) │
│ ↓ │
│ l31 (h5st 组装 VM) ─────────────────────────────────┐ │
│ │ │ │
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!