在现代软件保护领域,VMProtect(简称 VMP)因其强大的虚拟化保护能力而广受关注。尤其是其 3.9.4 版本引入了多种高级混淆技术,使得传统的静态分析和特征识别方法难以奏效。本文将围绕如何利用符号执行技术,结合动态 trace 分析,对 VMP 3.9.4 保护的样本进行系统性反混淆,揭示其背后复杂的控制流与数据流混淆机制。
混淆手段分析
通过对加壳样本的 trace 数据进行深入分析,可以归纳出 VMP 3.9.4 主要采用的七类混淆技术:
1. 垃圾代码(Dead Code Insertion)
垃圾代码指那些执行后立即被覆盖且未被后续指令引用的数据写入操作。这类指令虽然不影响程序逻辑,却极大增加了静态分析的复杂度。
mov eax, 0x66
add eax, ebx
mov eax, 0x77
在此例中,第一条 mov 指令的结果被第三条指令覆盖,中间的 add 操作成为无意义的干扰。
2. 代码乱序(Code Reordering)
VMP 将原本连续的基本块通过 jmp imm 或 call imm 拆分并跳跃连接,造成调试器中 RIP 寄存器频繁跳转的现象。例如:
code A
code B
code C
...
==>
jmp labelA
labelB:
code B
jmp labelC
labelC:
code C
jmp next
labelA:
code A
jmp labelB
next:
...
这种结构破坏了代码的空间局部性,使逆向人员难以直观理解执行流程。
3. 永真/永假型不透明谓词(Opaque Predicates)
VMP 使用的不透明谓词属于“永真或永假”类型,即条件跳转(jcc)的实际走向始终固定。其构造方式有两种:
- 常量表达式:如
3 + 3 == 6,结果恒为真。
- 恒成立数学关系:如
x * (x + 1) % 2 == 0,无论 x 取何值,结果均为真。
这类谓词通常用于强化代码乱序效果,形成看似分支实则单一路径的控制流陷阱。
4. 内存操作数常量隐藏
VMP 在内存访问操作中引入冗余寄存器,掩盖真实的偏移量。例如原始指令:
qword ptr ss:[rsp]
被替换为:
qword ptr ss:[rsp + rdx - 0x76B2378A]
其中 rdx 的值恰好等于 0x76B2378A,因此表达式等价。这些寄存器的值可通过前序指令中的立即数逐步计算得出。
5. MBA 混淆(Mixed Boolean Arithmetic)
简单算术运算被转化为复杂的布尔混合表达式,例如:
x + y == (x | y) + y - (~x & y)
x ^ y == (x | y) - y + (~x & y)
此类变换广泛应用于“万用门”计算中,显著增加表达式解析难度。
6. 间接跳转混淆
VMP 利用 call reg 或 jmp reg 实现间接跳转,但目标地址实际是确定的。它通过篡改 call 指令压入栈中的返回地址,再经运算后跳转至唯一目的地。本质上仍属代码乱序的一种变体。
7. 等价语义指令替换
保持语义一致的前提下,使用非常规指令替代常见指令。例如:
push rax 替换为:
lea rsp, qword ptr ss:[rsp - 8]
mov qword ptr ss:[rsp], rax
mov rax, 5 替换为:
or rax, 5 ; 当 rax 已知为 0 时
add rax, 5 ; 同样前提下
反混淆策略设计
为应对上述混淆,构建了一个基于迭代处理器的反混淆框架。核心思想是:只要任一处理器成功修改了代码结构,就继续下一轮处理,直至收敛。
基本块提取与合并
从 trace 中提取所有基本块,并使用集合去重。随后依据以下规则迭代合并:
- 一个基本块有且仅有一个后继;
- 该后继也仅有此一个前驱;
- 控制流为直接无条件跳转或 fallthrough。
例如,基本块 0x14043bf81:6 与 0x140269a1f:5 满足合并条件,合并后形成新的基本块 0x14043bf81:11,依此类推可进一步合并为 0x14043bf81:79。
不透明谓词证明
利用符号执行引擎 triton 对包含条件跳转的基本块进行分析:
- 若路径谓词未被符号化,则为常量表达式,直接判定为不透明谓词;
- 若已被符号化,则使用 SMT 求解器验证两条分支是否有一条不可达。
一旦确认为不透明谓词,即可将其跳转事实固化为直接跳转或 nop,从而恢复原始控制流。
间接跳转去混淆
同样采用符号执行方法,判断间接跳转指令的操作数是否被符号化。若未被符号化,说明目标地址是确定的,可将其还原为直接跳转。
内存操作数还原
识别出参与内存寻址但值为常量的寄存器(如 rdx 在 rsp + rdx - 0x76B2378A 中),并通过 miasm 框架重构指令,将寄存器替换为其实际数值。
实施与验证
实验环境使用 VS2022 编译测试程序,并用 VMP 3.9.4 进行保护。保护配置如下:
- 保护类型:超载(安全+麻烦)
- 重定位:默认
- 检查标志/序列号:否
通过 x64dbg 插件 supertrace 生成完整 trace 文件,共采集到 110,684 条指令记录,提取出 3,577 个基本块。
反混淆流程如下:
- 初始化环境:加载 trace 并建立符号执行上下文;
- 迭代处理:依次应用
merge_basic_block、prove_opaque_predicate、prove_obfuse_indirect 处理器;
- 收敛结果:经过 8 轮迭代,基本块数量从 3,577 降至 763。
最终生成的反混淆脚本对不透明谓词和内存操作数进行了修复。例如,将永假跳转替换为 jmp target,并将 qword ptr ss:[rsp + rdx - 0x76B2378A] 还原为 qword ptr ss:[rsp]。
运行测试表明,反混淆后的程序仍能正常弹出消息框,证明反混淆过程未破坏原始语义。
剩余挑战
目前尚未处理的混淆包括:
- 垃圾代码:需在 IR 层进行活跃变量分析清除;
- MBA 混淆:可通过 SMT 求解或灰盒合成技术简化;
- 等价指令替换:需建立规则库并严格验证语义一致性。
未来计划在更高层次的中间表示上实现这些优化。
参考资料
[1] 刺杀 VMP 3.9.4混淆, 微信公众号:mp.weixin.qq.com/s/V8M4-sWEt2R0LOIdFyx_bg
版权声明:本文由 云栈社区 整理发布,版权归原作者所有。