1709 words
9 minutes
MicroVMP 保护你的 .so 文件

MicroVMP 保护你的 .so 文件#

最近写了一个面向 Android ARM64 的函数级 VMP,名字叫 MicroVMP

这篇文章不聊实现,只看效果:一个正常的 Native 函数经过 MicroVMP 处理以后,在 IDA 里到底会变成什么样

先放结论:原本可以直接阅读的 ARM64 函数会失去原始算法语义。分析者面对的不再是熟悉的循环、位运算、查表和 JNI 调用,而是一层独立的虚拟执行环境,以及一个足以让人冷静几秒钟的解释器 CFG

先看解释器#

这是 MicroVMP 解释器主函数在 IDA 中的控制流图,并且没有加任何混淆,原生编译出来就长这个样子,甚至只是为了运行时效率考虑优化剪枝过的版本

MicroVMP Interpreter CFG

图中的每一个小块都是真实存在的控制流。把视角移到另一片区域,仍然是密集的分支、交叉边和大片连续的执行逻辑

MicroVMP Interpreter CFG detail

源码开发时仍然保持模块化,但最终构建出的热路径会集中在这个大函数中

再按一下 F5:

MicroVMP Interpreter pseudocode

反编译器只能恢复出局部片段,大量控制流最终表现为跨区域跳转。看到这里,即使分析者知道它是一个 VM,接下来仍然要面对状态恢复、语义识别、执行流跟踪和虚拟指令归类等工作

而且,这还只是解释器

JNI 函数:保护前#

下面是一个普通 JNI 导出函数。保护之前,它和绝大多数 Android Native 函数一样,对逆向工具非常友好

Original JNI CFG

CFG 不复杂,调用关系清晰。F5 以后,JNI 参数处理、字符串操作、内部算法调用和返回流程基本都摆在脸上:

Original JNI pseudocode

对于逆向人员来说,这种函数几乎没有阅读门槛。顺着伪代码走一遍,就可以快速找到真正值得关注的算法函数

JNI 函数:MicroVMP 之后#

同一个 JNI 函数经过 MicroVMP 处理以后,再次静态载入 IDA:

Protected JNI CFG

原本清晰的 JNI 业务逻辑已经不在这里了。函数原地址留下的是合法的 ARM64 控制流,但这些代码不再承载原函数的真实语义

再按 F5,入口只剩下一个看似普通的子函数调用:

Protected JNI entry pseudocode

继续跟进去:

Protected JNI child pseudocode

没有原始 JNI 流程,没有字符串处理,也没有可以继续追踪的算法实现

原来那条非常舒服的分析路径:

找到 JNI 导出 → F5 → 跟进内部函数 → 恢复算法

现在变成了:

找到 JNI 导出 → 原始语义消失 → 定位虚拟执行边界
→ 理解解释器 → 识别虚拟语义 → 恢复虚拟控制流 → 重建算法

更何况解释器已经被我们“藏”了起来,一般的逆向人员可能压根找不到它在哪。按钮还是那个 F5,但后面的工作量已经不是一回事了

普通C函数:保护前#

JNI 只是入口,真正值得保护的通常还是算法函数

下面这个函数在保护前拥有完整而连续的 CFG:

Original algorithm CFG

伪代码中可以直接看到循环、查表、异或、移位和数据更新。即使符号全部被抹掉,只要花一点时间整理变量,算法结构仍然非常明显

Original algorithm pseudocode

这也是 Native 算法最常见的尴尬:编译成机器码并不等于获得保护。编译器虽然改变了写法,却没有隐藏算法本身

普通C函数:MicroVMP 之后#

处理后的同一位置变成了下面这样:

Protected algorithm CFG

原来的循环和数据流已经消失,留下的是与原算法无关的控制流。静态分析者不能再从函数原地址逐句还原算法,因为那份原生实现已经不存在了

IDA 尝试恢复出的伪代码如下:

Protected algorithm entry pseudocode

继续跟进其中的子函数:

Protected algorithm child pseudocode

依然得不到任何原始算法信息

保护前,算法是“看得懂,只是需要时间”;保护后,问题变成了“先重新建立一套分析模型,才有资格开始看算法”。这正是 VMP 真正有价值的地方

不只保护 Demo#

目前 MicroVMP 已经在 Android ARM64 真机上覆盖普通 Native 函数、JNI 导出函数和 JNI_OnLoad,也验证了整数运算、访存、分支、调用、浮点、SIMD、原子操作和常见密码学指令场景

我的主要目标不止是算法保护,但 MD5、SHA-256、CRC32、AES、SM4 这一类高频运算函数是重点使用场景。它们的特点是原始伪代码价值很高,同时又非常适合通过虚拟化隐藏真实的数据流和运算过程

性能开销#

当然,保护从来不是免费的。VMP 会带来运行时开销,所以它更适合用在真正敏感的核心函数上,而不是不加区分地覆盖整个 SO。实际使用时,安全收益、函数热度和性能预算需要一起考虑

测试主要是对内存中的 libc.so 和 libart.so 分别计算 crc32 和 MD5 值,对比直接 native call调用经过 vmp 保护的 相等函数 的耗时差距

可以看到计算 crc32 大概相差 10~20 倍,计算 MD5 大概相差 50 倍,但是这是在输入很大的情况。正常计算网络请求相关签名其实是无感的

efficiency

至于 ai 分析#

这是我让 gpt-5.6sol xhigh 分析一个受 MicroVMP 保护的简单 crackme 时的内容截图

ai1

ai2

ai3

跑了我周额度的10%,我就给它停掉了,它结合了unidbg、ida、frida进行了分析,大概跑了40分钟左右,但是还没有找到vm解释器的位置。不过他能意识到 .so 加了壳,我还是很欣慰的

最后#

当前的效果其实很简单:

当别人把 SO 拖进 IDA,满怀期待地点进核心函数时,先看到一片没有原始语义的控制流;继续追踪以后,再看到那个巨大的解释器 CFG

然后停顿一下,开始思考:

这个函数,真的值得继续跟吗?

如果能让分析者产生这几秒钟的犹豫,MicroVMP 的阶段目标就已经达到了

至于后续,我大概会加上 trace 检测以及相关运行时环境的检测,检测到 frida/xposed 就直接狠狠退出,检测到 trace 就跳不同的算法分支

MicroVMP 保护你的 .so 文件
https://yuuki.cool/posts/microvmp/microvmp/
Author
Yuuki
Published at
2026-08-04
License
CC BY-NC-SA 4.0