用 LLVM 编译器协助破解 VMProtect
文章目录
偶然看到两篇有意思的文章[1,2],说利用了 LLVM 编译器协助破解号称最强大的软件保护工具之一 VMProtect,本文简单介绍下,不过涉及 LLVM 编译器的篇幅很少。
1、VMProtect
这里先跑个题,介绍一下 VMProtect,看了一下也挺有趣的。
VMProtect 是强大的软件保护工具,广泛应用于游戏反外挂、商业软件防破解等领域。
VMProtect 支持 x86/x86_64/ARM64 二进制文件以及使用 C/C++、C#/VB .NET、Rust 和 Golang 编译的 .NET 程序集,适用于所有主流操作系统:Windows、Linux、macOS 和 Android。
1.1、原理
VMProtect 的核心原理是代码虚拟化,执行流程如下:
- 编译转换:VMProtect 会把原本的指令转换成一套只有它自己懂的私有指令集(Bytecode/字节码)。
- 嵌入引擎:在程序里塞入一个解释器(Virtual Machine Interpreter)。
- 运行时:当程序运行到被保护的代码时,物理 CPU 不再直接执行原来的逻辑,而是跳转到执行这个“解释器”。解释器读取那些私有的字节码,模拟出原本的指令操作。
除了虚拟化,VMProtect 还组合了多种技术来恶心破解者:
- 指令变形(Mutation): 把简单的指令变得极其复杂。
- 控制流平坦化(Control Flow Flattening): 打乱代码的执行顺序。
- 多态性(Polymorphism): 这是 VMP 最强的地方。每次编译保护同一个程序,生成的虚拟 CPU 架构、指令编码、解释器逻辑全都不一样。
1.2、对比
和传统加壳方式对比如下:
- 普通压缩壳/加密壳。
- 程序开始运行时,壳的代码先运行,把原本的代码在内存中解压/解密还原来运行。
- 破解者可以在运行时直接分析软件。
- VMProtect。
- 编译时把指令集机器码换成全新的、私有的中间语言,运行时由自定义虚拟机执行。
- 破解者需要花功夫逆向虚拟机中的解释逻辑。
1.3、使用方式
可以在源码开发时进行标记。
|
|
如果不想改代码,或者没有源码。开发者可以在 VMProtect 的软件界面里打开编译好的 EXE 文件。 VMP 会分析出所有的函数列表。开发者像点菜一样,手动勾选。
1.4、优缺点
优势:
- 极高的逆向难度。虚拟化的保护效果很强。
- 防篡改能力强。VMP 具有完整性校验功能,如果检测到内存校验失败,就会崩溃。
- 跨平台兼容性。 虽然生成的字节码是私有的,但解释器是跨平台的。
劣势:
- 性能损耗。解释器指令影响执行效率。
- 文件体积膨胀。引入了额外指令和虚拟机引擎。
- 杀毒软件误报。病毒和木马也喜欢用 VMProtect 来隐藏自己的恶意代码。
- 调试困难。如果程序在被保护的代码段里崩溃了,开发者自己都没法很好地调试。
2、破解 VMProtect
好了,终于到正题了。破解的思路可以总结为:既然 VMP 把代码变成了复杂的“垃圾指令”,那就想办法把这些指令翻译成 LLVM 能看懂的 IR 语言,然后让 LLVM 的优化器帮忙把垃圾清理掉!
该作者的研究目标是纯函数 。纯函数定义为:对于相同的输入,始终产生相同的输出,且不依赖于随机性、全局变量或 I/O 操作。由于这些特性,纯函数特别适合去虚拟化。
具体步骤如下:
- Trace。用工具(Intel Pin)全程录下程序运行时的每一个动作。
- Symbolic Execution。分析找出输入和输出的关系。
- Lifting。把分析出来的逻辑写成 LLVM IR。
- Optimization。交给 LLVM 优化器优化,只留下最核心的逻辑。
- Recompile。最后把清理干净的代码重新编译成普通的机器码。
2.1、Trace
Pin 是英特尔提供的动态二进制插桩 (DBI) 框架,支持 Windows 和 Linux 环境下的 x86 和 x64 二进制文件。Pin 不直接修改目标程序,而是通过基于 JIT(即时编译)的代码转换来干预执行流程。
但是记录全部会比较多,所以首先要找到跳转 VMP 入口的地方,通常是跳转到奇怪的地方的指令。

2.2、Symbolic Execution
通过记录程序运行轨迹,作者抓到了大量的指令流。但不需要看懂每一行,只需要关注状态的变化。

Triton(一个动态二进制分析框架) 能在程序运行时,“看穿”每一条 CPU 指令背后的逻辑,并用数学公式来表达这些逻辑。作者利用 Triton 来模拟执行这些指令,把复杂的数学运算(比如 VMP 喜欢用的 MBA 表达式)简化成原本的样子。

跑题一下,Triton 的工作流程可以想象成一个 “翻译 + 求解” 的过程。核心机制由以下三个引擎驱动:
- 指令语义提升(Lifting to AST)。
- 当 CPU 执行一条机器码(例如
add eax, ebx)时,Triton 不仅仅是执行它,还会把它翻译成一种抽象语法树(AST)。这样就把冰冷的机器码变成了可以被程序理解和操作的数学结构。
- 当 CPU 执行一条机器码(例如
- 符号执行引擎(Dynamic Symbolic Execution, DSE)。
- Triton 内置了 SMT 求解器接口(通常连接 Z3 求解器),并且维护着一个“符号状态”。假设程序要求输入一个数字
x,然后执行if (x + 5 == 10)。它把x标记为一个符号变量(SymVar),把这个公式扔给 Z3,Z3 会瞬间算出SymVar_0 = 5,从而告诉你:“只要输入 5,程序就会走进 True 分支”。
- Triton 内置了 SMT 求解器接口(通常连接 Z3 求解器),并且维护着一个“符号状态”。假设程序要求输入一个数字
- 污点分析引擎(Taint Analysis)。
- 它可以给特定的数据(比如从网络接收的数据或用户输入)打上“标签”(Taint)。这能让你看到攻击者控制的数据在程序里传播了多远,是否流向了
system()或strcpy()等敏感函数。
- 它可以给特定的数据(比如从网络接收的数据或用户输入)打上“标签”(Taint)。这能让你看到攻击者控制的数据在程序里传播了多远,是否流向了
2.3、Lifting & Optimization & Recompile
看透了本质后(即 Triton 简化后),作者把这些逻辑翻译成 LLVM IR(中间表示)。因为 LLVM 自带极强的优化器,然后就把那些翻译好的、看起来还是很冗长的 LLVM IR 扔给优化器。
于是,LLVM 大手一挥,把 VMP 精心设计的混淆代码全部删掉,只留下了最核心的那几行逻辑。
最后转成普通的机器码,就可以愉快地分析软件了。
参考
文章作者 calssion
上次更新 2026-01-14