前言

我一直使用 AI 辅助 JS 逆向分析。从简单的 OB 混淆、sojson,到单 VM 保护,AI 都能很好地完成任务。

但这次遇到了一个新挑战:多态虚拟机 (Polymorphic VM) 保护。

与单 VM 不同,多态 VM 有 30+ 个独立的 dispatcher,同一个 opcode 在不同 VM 中执行完全不同的操作。如果不使用 AI,这种多态 VM 几乎无法静态分析——光是建立 30 套 opcode 映射表就是噩梦。

但 AI 让多 VM 的静态还原成为可能。本文分享三个核心干货:

  1. 多态 VM 的分析方法
  2. AI + 人工的协作模式
  3. 隐蔽的 SHA256 魔改如何发现

一、挑战:多态 VM

1.1 什么是多态 VM?

普通 VM 保护:

1

2

case 2 → 永远是 PUSH 操作

case 5 → 永远是 POP 操作

多态 VM 保护:

1

2

3

VM_A: case 2 → PUSH

VM_B: case 2 → STORE

VM_C: case 2 → CALL

同一个数字,在不同 VM 中含义完全不同。

1.2 难度对比

保护类型 分析复杂度 工具复用性
字符串混淆
控制流平坦化
单 VM 保护 中高
多态 VM 极高 极低

1.3 具体挑战

  1. 30+ 套 opcode 映射:每个 VM 都需要单独分析
  2. 嵌套调用链:签名逻辑分散在多个 VM 中
  3. 隐式参数传递:通过栈操作传递,难以追踪
  4. 魔改标准算法:SHA256、Base64 都经过定制修改

二、方法:AI 辅助分析

2.1 整体流程

1

AST 解混淆 → 提取结构化数据 → AI 分析模式 → 人工验证 → AI 生成代码

2.2 AST 解混淆

工作流程:Gemini 识别混淆类型 → Claude Code 编写 AST 脚本 → 迭代优化

拿到 450KB 的混淆代码后,先让 Gemini 识别混淆类型,然后让 Claude Code 编写 Babel AST 脚本。整个脚本分 9 个 Pass:

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 清理 死代码移除、未使用变量删除

调试技巧:分 Pass 执行,每个 Pass 单独测试,报错信息直接发给 AI 修复。

2.3 AI 分析 VM

解混淆后,用 AST 工具提取:字节码数组、字符串表、VM dispatcher 结构。

然后让 AI 识别:

  • 每个 VM 的 opcode 语义
  • VM 之间的调用关系
  • 关键算法的位置

关键点:用浏览器实际执行结果验证 AI 的分析,发现不一致时针对性重新分析。

2.4 AI + 人工协作模式(重要干货)

这是我在分析过程中摸索出的高效协作模式:

1

2

3

4

5

┌─────────────┐     分析代码,定位关键点     ┌─────────────┐

│             │ ──────────────────────────→ │             │

│     AI      │                             │    人工     │

│             │ ←────────────────────────── │             │

└─────────────┘     执行结果、变量值         └─────────────┘

具体流程

  1. AI 分析代码,找到断点位置

    • AI 阅读解混淆后的代码
    • 识别关键函数入口、可疑的魔改点
    • 告诉人工:「在第 X 行设置断点,观察变量 Y 的值」
  2. 人工在浏览器中操作

    • 打开 DevTools,设置断点
    • 触发签名请求
    • 记录断点处的变量值、调用栈
  3. 把结果反馈给 AI

    • 「断点触发了,变量 X = "abc123",调用栈是 A → B → C」
    • AI 根据实际值分析逻辑
  4. AI 调整分析方向

    • 如果结果符合预期,继续下一个断点
    • 如果不符合,AI 重新分析,找新的断点位置

为什么这种模式有效?

  • AI 擅长:阅读大量代码、识别模式、推理逻辑
  • 人工擅长:操作浏览器、获取运行时数据
  • 互补:AI 无法执行 JS,人工不想读混淆代码

实际案例

发现 _append 魔改就是这样找到的:

1

2

3

4

5

6

AI: 「SHA256 结果不对,在 _append 函数入口设断点,看看传入的参数」

人: 「参数是 "test",但 this.buffer 变成了 "testXXXXXX"

AI: 「说明 _append 内部有额外处理,让我分析这个函数...」

AI: 「找到了,_append 调用了 _eData,在 _eData 设断点」

人: 「_eData 返回值是原字符串 + 固定后缀」

AI: 「这就是第二层魔改!」

这种「AI 指挥 + 人工执行」的模式,比纯静态分析效率高很多。


三、干货:SHA256 魔改还原

在所有算法还原中,魔改 SHA256 是最难的部分。不是因为 SHA256 本身复杂,而是魔改点极其隐蔽。

3.1 发现过程

最初,我以为只需要找到 SHA256 的调用点,直接用标准库就行。但对比浏览器输出后发现结果不一致:

1

2

标准 SHA256("test"= 9f86d08...

京东 SHA256("test"= 完全不同的值

于是开始追踪 SHA256 的实现。

3.2 第一层魔改:_seData1 预处理

输入数据在进入 SHA256 前,会先经过 _seData1 函数处理,追加 10 个字符的动态后缀:

1

"test" → "test" + [10字符动态后缀]

这 10 个字符是根据输入内容计算的,算法是将输入分成 10 块,每块计算字符码累加值,映射到自定义字符表。

3.3 第二层魔改:_eData 预处理(最隐蔽)

找到 _seData1 后,我以为已经完成了。但验证时发现结果还是不对。

问题出在 _append 函数被魔改了

在标准 SHA256 实现中,_append 只是简单地追加数据。但京东的实现中,_append 内部调用了 _eData,会额外追加一个固定后缀:

1

2

3

4

5

6

7

8

9

// 标准实现

_append(data) {

    this.buffer += data;

}

// 京东魔改

_append(data) {

    this.buffer += _eData(data);  // _eData 会追加固定后缀

}

这个魔改非常隐蔽,因为:

  1. 函数名没变,看起来是标准实现
  2. 魔改逻辑在另一个 VM 中(eData VM)
  3. 不仔细追踪调用链根本发现不了

3.4 第三层魔改:字节交换

最后,SHA256 的输出还会进行字节交换——交换第 0 字节和第 2 字节:

1

[a, b, c, d, ...] → [c, b, a, d, ...]

3.5 完整调用链

1

2

3

4

5

6

7

8

9

10

11

jd_sha256(input)

    

_seData1(input)           ← 追加 10 字符动态后缀

    

_eData(result)            ← 追加固定后缀 (隐藏在 _append 中)

    

标准 SHA256

    

swap(byte[0], byte[2])    ← 字节交换

    

输出 64 位 hex

3.6 教训

这个案例说明:魔改可能发生在任何地方,包括看起来无害的辅助函数

如果不是 AI 帮我快速分析了所有相关 VM 的字节码,手动追踪这个调用链可能需要很长时间。


四、VM 架构分析

4.1 整体架构

通过 AI 分析,我梳理出了签名的完整 VM 调用链:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

signSync() 入口

    

┌─────────────────────────────────────────────────────────┐

│  l34 (签名入口 VM)                                       │

│      ↓                                                  │

│  l31 (h5st 组装 VM) ─────────────────────────────────┐  │

│      │                                               │  │

│      ├─→ l24 (密钥生成 VM)                            │  │

│      │       └─→ l25 (辅助函数)                       │  │

│      │                                               │  │

│      ├─→ l28 (签名计算 VM) ──→ jd_sha256             │  │

│      │       └─→ l29 (辅助函数)                       │  │

│      │                                               │  │

│      ├─→ l30 (签名数据 VM)                            │  │

│      │                                               │  │

│      ├─→ wm_encode (自定义 Base64 VM)                 │  │

│      │                                               │  │

│      └─→ l27 (最终组装 VM) ←─────────────────────────┘  │

└─────────────────────────────────────────────────────────┘

4.2 VM 分类

分析了 17 个核心 VM,按功能分类:

签名核心 VM(6 个)

VM 功能 说明
l34 签名入口 signSync() 的实际实现
l31 h5st 组装 协调各子 VM,组装最终结果
l24 密钥生成 根据 token、fingerprint 等生成 key
l28 签名计算 调用 jd_sha256 计算签名
l30 签名数据 生成 signData 字段
l27 最终组装 拼接 10 个字段生成 h5st

加密算法 VM(6 个)

VM 功能 说明
wm_encode 自定义 Base64 5 步变换的编码算法
seData1 SHA256 预处理 追加 10 字符动态后缀
eData 数据编码 追加固定后缀(隐藏在 _append 中)
sha256_seData SHA256 专用预处理 配合 seData1 使用
append 数据追加 被魔改的辅助函数
eKey HMAC 密钥处理 密钥相关处理

辅助 VM(5 个):l25、l29、l32、l33、l23(环境检测)

4.3 多态 opcode 示例

同一个 opcode 数字在不同 VM 中的含义:

Opcode l34 (签名入口) l31 (h5st组装) l23 (环境检测)
2 PUSH_IMM - STORE_wK
5 DUP_GET_PROP PUSH_IMM NE
7 STORE_wR SET_PROP_CHAIN PUSH_STR
12 POP PUSH_OBJ STORE_wF

这就是为什么叫「多态」——同一个数字,在不同 VM 中执行完全不同的操作


五、环境检测分析

5.1 检测目的

签名过程中会收集大量浏览器指纹,用于:

  1. 识别自动化工具(Selenium、Puppeteer 等)
  2. 生成设备唯一标识
  3. 检测运行环境是否正常

5.2 检测项目

通过分析 l23(环境检测 VM),发现以下检测项:

运行时检测

检测项 目标 检测方式
typeof Deno Deno 运行时 类型检查
typeof Bun Bun 运行时 类型检查
typeof process Node.js 类型检查
navigator.webdriver Selenium/Puppeteer 属性检查
window.phantom PhantomJS 属性检查
window._phantom PhantomJS 属性检查
window.callPhantom PhantomJS 属性检查

浏览器指纹

类别 收集项
Navigator userAgent, platform, language, languages, hardwareConcurrency
Screen width, height, availWidth, availHeight, colorDepth, pixelRatio
Canvas toDataURL 指纹
WebGL renderer, vendor
Audio AudioContext 指纹
字体 通过文本宽度测量检测已安装字体

5.3 envData 结构

这些检测结果会被编码到 h5st 的第 8 个字段(envData)中:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

{

    "sua""Mozilla/5.0...",      // userAgent

    "pp": {},                      // 插件信息

    "extend": {

        "wd"false,               // webdriver

        "l": 0,                    // 语言数量

        "ls""zh-CN",             // 语言

        "ml": 8,                   // 最大触摸点

        "pl": 5,                   // 插件数量

        "av""5.0...",            // appVersion

        "ua""Mozilla/5.0...",    // userAgent

        "pf""MacIntel",          // platform

        "canvas""...",           // canvas 指纹

        "webglFp""..."           // webgl 指纹

    },

    "random""xxxxxxxx"           // 随机值

}

5.4 绕过策略

如果要在非浏览器环境运行,需要:

  1. 模拟完整的浏览器环境(navigator、screen、document 等)
  2. 确保 webdriver 等检测项返回正常值
  3. 生成合理的 canvas/webgl 指纹

或者更简单的方式:直接固定 envData,因为这个字段主要用于风控,不影响签名验证。


六、总结

6.1 AI 辅助逆向的优势

  1. 处理重复模式:30+ 个 VM 的 opcode 映射,AI 可以快速建立
  2. 代码生成:还原后的算法,AI 可以直接生成目标语言实现
  3. 调用链追踪:隐蔽的魔改点,AI 可以帮助快速定位

6.2 方法论

1

2

3

1. 先整体后局部:理解架构再深入细节

2. 先静态后动态:AST 分析 + 运行时验证

3. 持续验证:每还原一部分就验证一部分

6.3 AI 的局限性

AI 不是万能的,以下场景仍需人工介入:

  • 动态值提取:盐值、模板字符串等需要从字节码中定位
  • 最终验证:确保还原结果与原始实现一致

结语

多态 VM 是目前 JS 保护的高级形态,传统工具和方法很难应对。但借助 AI 的模式识别和代码生成能力,可以大幅提升分析效率。

核心还是逆向工程的方法论:理解保护机制的原理,找到正确的切入点,持续验证和迭代。

希望本文对研究 VM 保护的朋友有所启发。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐