AI辅助还原自定义VMP保护方案

admin 2026-03-18 19:57:06 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文提出AI辅助还原VMP保护的系统方法,涵盖去混淆、模拟执行Trace、Opcode推断及白盒黑盒分析。文章强调利用AI处理数据与生成验证脚本,通过确定性重放确保准确性,明确了人机协作分工与迭代验证原则,为移动端高级逆向提供实战指南。 综合评分: 91 文章分类: 逆向分析,移动安全,二进制安全,实战经验


cover_image

AI 辅助还原自定义 VMP 保护方案

执着的猫 执着的猫

看雪学苑

2026年3月10日 18:00 上海

VMP(Virtual Machine Protection)是当前移动端 SO 保护的主流方案。核心思路是将关键算法编译为自定义字节码,由嵌入 SO 中的解释器运行时执行。传统静态分析在这种保护下几乎失效——你看到的不是算法本身,而是一个通用解释器在逐条执行你看不懂的字节码。

#

本文分享一套 AI 辅助的 VMP 逆向方法论。不针对任何具体目标,目前已经在某红色书籍app验证可行,只谈为什么这么做怎么做

整体流程:

SO 去混淆 → IDA 导出 → 动态调试环境 + Trace → Opcode 分析 → 字节码提取
→ 黑盒/白盒分析 → 文档记录 → 参数还原 → 端到端验证

#

一、SO 层去混淆

为什么需要去混淆

VMP 保护的 SO 通常叠加控制流平坦化(CFF)等混淆。IDA 反编译后看到的是大量 switch-case dispatcher,真实逻辑被打散。不去混淆的话,即使是 VM 解释器之外的辅助函数(参数编组、内存分配、类型分发)也完全不可读,严重阻碍整体理解。

angr 符号执行

angr 可以自动识别 CFF 的 dispatcher 结构,通过符号执行恢复真实控制流:

  1. 识别 dispatcher 变量和 switch 结构
  2. 对每个基本块做符号执行,求解后继块
  3. patch 掉 dispatcher 跳转,还原直接跳转
  4. 重新让 IDA 分析 patch 后的二进制

AI 可以辅助编写 angr 去混淆脚本,分析失败 case 并调参。

Trace 辅助修补

angr 并非万能。对于失败的函数,可以在动态调试框架中执行目标函数,记录实际走过的基本块序列,然后根据 trace 直接 patch 条件跳转。

两种方法互补:angr 批量处理大部分函数,trace 修补解决剩余。

二、IDA 批量导出-感谢开源插件ida-no-mcp

为什么不直接用 ida-mcp

ida-mcp 允许 AI 直接查询 IDA 数据库,理论上很方便。但 VMP 场景下有个现实问题:函数太大了

混淆后的函数动辄几千到几万字节,反编译后上千行伪代码。通过 ida-mcp 交互式查看,两三个函数就把 AI 的上下文窗口塞满,后续分析无法继续。

解决方案:提前批量导出

使用开源插件ida-no-mcp,一次性把所有函数的伪代码、调用关系、交叉引用导出到本地文件。AI 按需读取单个文件,不浪费上下文,同一份数据可以反复查阅,支持多轮迭代分析。

三、为什么需要动态调试框架

移动端逆向 vs Web 逆向

Web 端逆向相对简单:浏览器自带开发者工具,JavaScript 代码直接可见,可以断点、修改变量、实时调试。

移动端完全不同:

  • 代码编译为 ARM 机器码,运行在手机上
  • 加壳、混淆、反调试层层叠加
  • 真机调试需要 root、过反调试,门槛高且不稳定

模拟执行框架

unidbgUnicornQiling等框架解决的核心问题是:不需要真机,在 PC 上模拟执行 ARM 代码

它们的本质都是 CPU 指令级模拟器,区别在于上层封装:

| 框架 | 定位 | 特点 | | — | — | — | | Unicorn | 纯 CPU 模拟 | 需要自己处理所有系统调用、内存映射 | | unidbg | Android/iOS 模拟 | 内置 JNI 环境、系统调用、文件系统模拟 | | Qiling | 通用系统模拟 | 支持多平台,介于两者之间 | | Frida | 真机注入 | 需要真机/模拟器,但能拿到最真实的运行环境 |

对于 VMP 逆向,我们需要的核心能力是:

1.可控执行:随时暂停、检查寄存器和内存

2.Hook:拦截任意函数,dump 输入输出

3.指令级 Trace:记录每条 ARM 指令的执行

4.注入:修改输入数据、覆写内存,做差分实验

5.确定性重放:固定随机数和时间,让同一输入产生相同输出

unidbg 在 Android SO 逆向场景下最方便——JNI 环境已经搭好,加载 SO 后直接调用 native 函数。以下以 unidbg 为例说明,但同样的思路适用于其他框架。

环境搭建

模拟执行 SO 时需要补全运行环境。这是一个”缺什么补什么”的过程——执行到某个 JNI 调用报错了,就补一个返回值;读某个文件缺失了,就模拟一个。

AI 在这一步可以帮忙查找常见 Android API 的返回值格式,加速补桩。

指令级 Trace

对 VM 解释器区域开启指令 trace,产生百万行级别的执行记录。这是后续所有分析的基础数据

百万行 trace 人工根本无法处理。但对 AI 来说,这正是它的强项:

  • 统计指令热点分布——哪些地址范围执行最多
  • 识别循环结构——重复出现的地址模式
  • 提取特征函数的调用次数和参数
  • 生成各种统计脚本

四、VM 解释器识别与 Opcode 分析

定位 VM Dispatcher

所有字节码 VM 的核心都是一个fetch-decode-execute循环。在 ARM64 层面表现为一组固定的寄存器模式。通过在 dispatcher 处下断点,可以建立 ARM 寄存器到 VM 角色的映射——字节码 PC、求值栈指针、当前 opcode 等。

AI 可以通过分析 dispatcher 附近的指令模式自动推断这些映射。

Opcode 语义推断

核心方法:在 dispatcher 断点处 dump 求值栈,观察每条字节码指令前后的栈变化。

执行前栈: [..., 0x02, 0x05]
执行后栈: [..., 0x14]
→ 5 << 2 = 0x14 → LSL(左移)

需要注意的是,第一直觉经常是错的。比如某个 opcode 看起来像 XOR(因为在地址计算中使用),但实际验证后发现是 ADD——地址加法和 XOR 在某些特定值下结果恰好相同,容易误判。

这就是为什么需要多组数据交叉验证,而不是看一组数据就下结论。AI 可以批量分析大量栈变化数据对,穷举可能的运算并交叉验证。

控制流指令识别

循环是 VM 中最关键的控制流结构。通过统计字节码 PC 的向后跳转(backjump),可以精确识别循环的嵌套结构和迭代次数。

迭代次数往往直接暗示密码学参数——比如某个三层嵌套循环的迭代次数恰好是3 × 9 × 4 = 108,每次迭代做 16 次 GF 乘法,总计 1728 次。这就强烈暗示 AES-128 的 3 个块、9 轮(加上最终轮)、4 列 MixColumns。

五、字节码提取与反汇编

运行时 Dump

通过 hook,在 VM 执行前 dump 两类数据:

  • 字节码段:完整的字节码二进制
  • VM 数据内存:包含查找表、常量等

编写反汇编器

有了 opcode 语义表,编写反汇编器是机械工作——AI 可以直接生成。关键是要处理好变长指令编码和操作数的解析。

结构发现

反汇编后可以看到字节码的宏观结构:函数边界、循环骨架、查表模式、外部调用分布。这是后续白盒分析的起点。

六、白盒密码分析

为什么需要白盒分析

黑盒分析(只看输入输出)能解决一部分问题,但对于复杂的密码学算法,必须深入内部结构才能还原。

白盒分析的核心是:在字节码和 trace 中识别已知的密码学原语

密码原语识别

常见的可识别特征:

  • GF(2^8) 乘法:循环 8 次、归约常量 0x1B(AES 标志)
  • S-box 查表:256 字节排列表(permutation)
  • MixColumns 系数:{2, 3, 1, 1} 的循环模式
  • CRC32 查表:1024 字节(256×4),polynomial 0xEDB88320

AI 熟悉标准密码学原语的特征,能从 trace 中的常量快速定位相关构件。

查表结构还原

白盒密码的核心手法是把密钥融合进查找表。标准 AES 的SBOX[x ^ key]变成一张预计算表,不再有显式的密钥。

还原方法——碰撞验证:如果两张表来自同一个 S-box,那么存在一个常量 delta 使得T1[x] == T2[x ^ delta]对所有 x 成立。delta 就是轮密钥的差值。

def&nbsp;find_key_delta(table1, table2):
for&nbsp;delta&nbsp;in&nbsp;range(256):
if&nbsp;all(table1[x] == table2[x ^ delta]&nbsp;for&nbsp;x&nbsp;in&nbsp;range(256)):
return&nbsp;delta
return&nbsp;None

通过这种方法,可以从大量查表中还原出全部轮密钥,无需逆向密钥调度算法。

假说-验证循环

白盒分析的核心工作模式:

观察数据 → 提出假说 → 编写验证脚本 → 运行验证
&nbsp; &nbsp; ↑ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|
&nbsp; &nbsp; └── 假说失败则修正 ←──────────────────┘

AI 的价值在于加速这个循环——它可以同时生成多个候选假说,并为每个假说编写独立的验证脚本。一轮下来几分钟,人工可能要一整天。

七、黑盒差分分析

为什么需要黑盒分析

白盒分析依赖能读懂字节码。但有些情况下:

  • 字节码混淆严重,静态分析代价太高
  • VM 存在特殊机制(如寄存器内存别名),静态分析容易误判
  • 只是想快速确认某个子模块的行为

这时切换到黑盒视角:只观察输入输出关系,不关心内部实现。

差分探针

核心思想:固定其他输入,只改变一个字节/nibble/dword,观察输出变化。

分层策略:

1.dword 探针(粗粒度):快速定位哪些输入 dword 影响哪些输出区域

2.byte 探针:精确到单字节

3.nibble 探针(细粒度):区分高低 4 位是否独立——这是发现 nibble 级操作的关键

依赖矩阵

把所有探针结果汇总,可以直接推断算法结构:

  • 某个输入字节影响全部输出 → 它参与了密钥/常量的计算
  • 某个输入字节只影响对应位置的输出 → 逐字节变换(XOR/置换)

假说暴力筛选

对于未知的子函数,可以枚举候选算法(如 CRC32、XOR fold、S-box 变换等),用多组探针数据逐一验证。只有全部命中的候选才是正确的。

AI 生成候选假说列表、编写验证脚本、分析结果——这种”批量穷举 + 自动验证”的工作模式,是 AI 最高效的应用场景之一。

八、端到端验证与勘误

确定性重放

VMP 中通常有随机数(时间种子、/dev/urandom 等)参与。要做端到端验证,必须先 hook 这些熵源返回固定值。

固定熵源后,相同输入必须产生完全相同的输出。任何不一致都说明还原有误。

Python 独立验证器

将全部算法用纯 Python 实现,完全独立于动态调试框架。对比 Python 输出和框架输出:

  • 完全一致 → 算法还原正确
  • 不一致 → 定位具体哪个子步骤出错

勘误:为什么一定会犯错

VMP 逆向的复杂度决定了中间过程一定会有错误假设。常见的错误模式:

| 错误类型 | 典型例子 | 发现方法 | | — | — | — | | 混淆因果 | 把动态参数当成固定常量 | 多次运行对比,发现每次不同 | | 过度泛化 | 观察到一种映射关系就认为是全局规则 | 更多数据集验证时不匹配 | | 误判结构 | 把 N 轮独立密码误认为 M×K 轮 CBC | 对中间块做交叉验证失败 | | AI 幻觉 | AI 自信地推断了一个不存在的模式 | 编写验证脚本后立即证伪 |

核心原则:只信事实,不信结论。每个结论都需要独立的实验数据支撑。文档中明确标注”已验证”和”待验证”。

九、文档记录

为什么文档是必需品而非可选项

VMP 逆向通常是一个多周的持续过程。AI 没有跨 session 记忆,人类的记忆同样不可靠。如果不做文档记录:

  • 上次分析到哪里了?忘了
  • 某个假说是已验证还是已证伪?不确定
  • 某组实验数据的含义是什么?想不起来了

文档结构建议

docs/
├──&nbsp;ANALYSIS.md &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 总体进度:什么已完成、什么待做
├── <module-A>.md &nbsp; &nbsp; &nbsp; &nbsp;# 模块级分析文档
├── <module-B>.md
└── archive/ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 过程文档(含已证伪的假说,保留备查)

每份文档中区分三类信息:

已验证事实:有 hook 数据 / trace 数据 / Python 验证 PASS 的结论

待验证假设:有一定证据但尚未充分验证

已证伪假说:保留记录,避免重复踩坑

AI 可以自动维护这些文档——每次新发现自动归类更新。

十、AI 协作经验

AI 擅长什么

  • 海量数据处理:百万行 trace 统计、内存 dump 分析
  • 模式识别:从常量和结构中识别密码学原语
  • 批量代码生成:验证脚本、hook 脚本、反汇编器
  • 假说穷举:一次生成十几种候选假说及其验证代码
  • 文档维护:结构化记录每次发现

AI 不擅长什么

  • 大函数去混淆:需要人工在 IDA 中交互式分析
  • 创造性突破:关键的”灵光一现”仍然依赖人类
  • 自我纠错:会自信地输出错误结论,必须靠验证脚本兜底
  • 上下文超长时的一致性:分析到后期容易遗忘早期结论

关键原则

让 AI 生成验证代码,而非直接要结论——”帮我验证这组数据是否符合 AES-128 CBC” 远好于 “这个算法是什么”

小步迭代——每次只攻克一个子问题,验证通过后再推进。不要让 AI 一次性还原整个算法

人机分工明确——人类决定方向、设计实验、判断关键节点;AI 执行数据处理、编写脚本、维护文档

文档即记忆——所有发现写入文档,AI 每次 session 开始时加载文档继续工作

工具链配置

项目根目录/
├── CLAUDE.md &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 项目级指令(工作流规则、分析原则、禁止事项)
├── docs/ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 分析文档(AI 的"长期记忆")
├── IDA 导出/ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 结构化伪代码(AI 的"眼睛")
├── trace/ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 运行时数据(AI 的"原始素材")
└── scripts/ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 验证脚本(AI 的"实验室")

CLAUDE.md 中写清楚分析原则,比如”不得运行生成的脚本,由用户自行运行”、”遇到大函数立刻停下”等。这些规则会在每次对话中自动加载。

附录:通用分析模板

A. Opcode 语义推断

def&nbsp;infer_opcode(pre_stack, post_stack):
"""从栈变化推断二元运算语义"""
if&nbsp;len(pre_stack) >=&nbsp;2&nbsp;and&nbsp;len(post_stack) ==&nbsp;len(pre_stack) -&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; a, b = pre_stack[-2], pre_stack[-1]
&nbsp; &nbsp; &nbsp; &nbsp; r = post_stack[-1]
&nbsp; &nbsp; &nbsp; &nbsp; ops = {
'ADD': (a + b) &&nbsp;0xFFFFFFFF,
'SUB': (a - b) &&nbsp;0xFFFFFFFF,
'XOR': a ^ b,
'AND': a & b,
'OR': &nbsp;a | b,
'MUL': (a * b) &&nbsp;0xFFFFFFFF,
'LSL': (a << (b &&nbsp;31)) &&nbsp;0xFFFFFFFF,
'LSR': (a >> (b &&nbsp;31)) &&nbsp;0xFFFFFFFF,
&nbsp; &nbsp; &nbsp; &nbsp; }
return&nbsp;[name&nbsp;for&nbsp;name, val&nbsp;in&nbsp;ops.items()&nbsp;if&nbsp;val == r]
return&nbsp;[]

B. 差分探针框架

def&nbsp;build_dependency_matrix(oracle, input_size, output_size):
"""
&nbsp; &nbsp; oracle: 黑盒函数 bytes → bytes
&nbsp; &nbsp; 返回依赖矩阵: dep[i][j] = True 表示 input[i] 影响 output[j]
&nbsp; &nbsp; """
&nbsp; &nbsp; baseline = oracle(bytes(input_size))
&nbsp; &nbsp; dep = [[False] * output_size&nbsp;for&nbsp;_&nbsp;in&nbsp;range(input_size)]
for&nbsp;i&nbsp;in&nbsp;range(input_size):
&nbsp; &nbsp; &nbsp; &nbsp; probe =&nbsp;bytearray(input_size)
&nbsp; &nbsp; &nbsp; &nbsp; probe[i] =&nbsp;0x01
&nbsp; &nbsp; &nbsp; &nbsp; result = oracle(bytes(probe))
for&nbsp;j&nbsp;in&nbsp;range(output_size):
if&nbsp;result[j] != baseline[j]:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dep[i][j] =&nbsp;True
return&nbsp;dep

C. 查表碰撞验证

def&nbsp;find_sbox_key_delta(table1, table2):
"""验证两张 256B 表是否为同一 S-box 的密钥变体"""
for&nbsp;delta&nbsp;in&nbsp;range(256):
if&nbsp;all(table1[x] == table2[x ^ delta]&nbsp;for&nbsp;x&nbsp;in&nbsp;range(256)):
return&nbsp;delta
return&nbsp;None

D. 假说批量验证

def&nbsp;test_hypotheses(hypotheses, probes):
"""
&nbsp; &nbsp; hypotheses: {name: func(input_bytes) → predicted_value}
&nbsp; &nbsp; probes: [(input_bytes, observed_value), ...]
&nbsp; &nbsp; """
for&nbsp;name, func&nbsp;in&nbsp;hypotheses.items():
&nbsp; &nbsp; &nbsp; &nbsp; hits =&nbsp;sum(1&nbsp;for&nbsp;inp, obs&nbsp;in&nbsp;probes&nbsp;if&nbsp;func(inp) == obs)
print(f"{name}:&nbsp;{hits}/{len(probes)}")

#

#

看雪ID:执着的猫

https://bbs.kanxue.com/user-home-1001962.htm

*本文为看雪论坛优秀文章,由 执着的猫 原创,转载请注明来自看雪社区

往期推荐

从ANGR-CTF项目入手ANGR和符号执行技术

AI时代-逆向工作者该如何用好这一利器

EXIF解析缓冲区溢出漏洞分析与利用

从C到Pwn:栈溢出漏洞利用实战入门

Android-ARM64的VMP分析和还原

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 执着的猫 执着的猫《AI 辅助还原自定义 VMP 保护方案》

评论:0   参与:  0