应用安全 --- apk加固 之 一条vmp虚拟指令
我们来把一条简单的原始 ARM 汇编指令(在 Android 的 so 库中)变成 VMP 的虚拟指令的过程拆解清楚。
1. 原始指令和目标
假设我们有一句非常简单的原生 ARM 汇编指令,它的功能是将两个寄存器相加:
add r0, r1, r2 ; 含义: r0 = r1 + r2
执行前状态:
-
r1= 5 -
r2= 3
执行后状态: -
r0= 8 -
其他寄存器不变
在没有 VMP 保护的情况下,CPU 会直接执行这条指令,一步完成。
2. VMP 的转换过程(加密和变形)
VMP 保护器(如 Virbox Protector、OLLVM 的 VMP 分支等)会对编译生成的 so 库进行分析,找到需要保护的函数(比如校验函数),然后对其中的指令进行如下操作:
第1步:拆卸 (Disassemble)
VMP 保护器会解析 add r0, r1, r2 这条指令,理解它的语义:”将源寄存器1 (r1)、源寄存器2 (r2) 的值相加,结果存入目标寄存器 (r0)“。
第2步:生成虚拟指令 (Virtual Bytecode)
VMP 不会保留原始的 add 指令,而是会根据其语义设计一套自定义的、加密的字节码来表示这个操作。
假设 VMP 的设计如下:
-
操作码
0x10代表 “加法” 操作。 -
后面跟三个字节代表目标寄存器、源寄存器1、源寄存器2的编号。
那么,这条虚拟指令可能会被编码成一个 4 字节的序列:
[0x10, 0x00, 0x01, 0x02] # 含义: ADD VReg0, VReg1, VReg2
注意:这里的 VReg0、VReg1、VReg2 是 VMP 虚拟机内部的虚拟寄存器,而不是真实的 ARM CPU 寄存器 r0, r1, r2。虚拟寄存器其实只是内存中的一块区域。
第3步:加密和混淆
VMP 不会傻傻地把 [0x10, 0x00, 0x01, 0x02] 这个字节码明文存放在 so 文件中。它会用各种密码学算法(如 AES、RC4)或简单的异或(XOR)进行加密,变成一堆看起来毫无意义的数据,比如 [0x5A, 0xE7, 0xBC, 0x41]。只有 VMP 虚拟机在运行时才知道如何解密它。
第4步:替换原始代码
原始的二进制的 add r0, r1, r2 指令会被抹掉,替换成一段用于进入虚拟机的代码(称为 Stub 或 Proxy),就像我们上一问中讲的:
push {r0-r12, lr} ; 保存所有真实寄存器现场(上下文)
ldr r0, =encrypted_bytecode_address ; 将加密字节码的地址装入寄存器
ldr r1, =vmp_context_addr ; 将虚拟机上下文(虚拟寄存器所在内存块)的地址装入寄存器
bl vmp_enter_function ; 跳转到VMP主入口函数
pop {r0-r12, pc} ; 恢复现场并返回(这只是理想情况,实际流程更复杂)
至此,编译后的 so 文件已经被修改了。原始的 add 指令消失了,取而代之的是一段跳转代码和一堆加密的字节码数据。
3. 在运行时如何执行(虚拟机的执行层面)
当程序运行到被替换的代码时,会发生以下事情:
-
进入虚拟机:执行
bl vmp_enter_function,跳转到 VMP 的入口函数。入口函数会分配一块内存作为虚拟寄存器空间(比如VReg[0]到VReg[15])。 -
初始化上下文:入口函数会从刚刚保存的真实寄存器中,把
r1和r2的值(5 和 3)加载到虚拟寄存器VReg1和VReg2中。-
VReg1= 5 -
VReg2= 3
-
-
取指和解码:
-
VMP 虚拟机从指定位置读取加密的字节码
[0x5A, 0xE7, 0xBC, 0x41]。 -
用它内部的密钥进行解密,得到明文的虚拟指令
[0x10, 0x00, 0x01, 0x02]。 -
解码:识别出操作码
0x10是 “加法” 操作。
-
-
分发和执行(Handler):
-
虚拟机内部有一个巨大的 “跳转表” ,操作码
0x10对应着一条真实的、用来执行加法操作的 ARM 汇编代码块(称为 Handler)。 -
CPU 会跳转到这个 “加法 Handler” 去执行。这个 Handler 的伪代码看起来是这样的:
; 加法 Handler (用于操作码 0x10) load r3, [vmp_context, #offset_VReg1] ; 从内存中把 VReg1 的值加载到真实寄存器 r3 load r4, [vmp_context, #offset_VReg2] ; 从内存中把 VReg2 的值加载到真实寄存器 r4 add r5, r3, r4 ; 执行真实的加法: r5 = r3 + r4 store [vmp_context, #offset_VReg0], r5 ; 把结果 r5 存回虚拟寄存器 VReg0 的内存位置 -
执行完这段 Handler 后,
VReg0的值被设置为了 8。
-
-
循环与退出:
-
一条虚拟指令执行完毕,虚拟机回到主循环,准备取下一条虚拟指令。
-
当所有虚拟指令执行完毕(比如遇到了虚拟的
return指令),虚拟机进入退出流程。 -
退出流程:将虚拟寄存器
VReg0中的结果(8)写回到真实寄存器r0中。然后彻底恢复之前保存的所有真实寄存器现场(但r0现在已经是新值了),最后跳转回原始程序继续执行。
-
总结对比
| 层面 | 原始执行 | VMP 虚拟化执行 |
|---|---|---|
| 指令 | 一条原生 add r0, r1, r2 |
一串加密字节码 [0x5A,0xE7,0xBC,0x41] |
| 寄存器 | 直接操作 CPU 物理寄存器 r0, r1, r2 |
操作内存中的虚拟寄存器 VReg0, VReg1, VReg2 |
| 执行单元 | CPU 硬件直接执行 | VMP 的 “加法 Handler” (一段真实代码)模拟执行 |
| 结果 | r0 = 8 |
VReg0 = 8,最后在退出时被写回 r0 |
| 可见性 | 逆向者直接看到加法指令 | 逆向者看到的是跳转代码、加密数据、以及一大堆分散的Handler,无法直接知道原始逻辑是加法 |
通过这个例子,你可以看到,一句简单的指令被拆解、加密、并由另一段复杂的代码模拟执行。当成千上万条指令都被这样处理,并且 Handler 本身也被混淆后,逆向分析的难度就呈指数级增长了。这就是 VMP 强大的核心原因。
浙公网安备 33010602011771号