GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- apk加固 之 一条vmp虚拟指令

我们来把一条简单的原始 ARM 汇编指令(在 Android 的 so 库中)变成 VMP 的虚拟指令的过程拆解清楚。

1. 原始指令和目标

假设我们有一句非常简单的原生 ARM 汇编指令,它的功能是将两个寄存器相加:

armasm
 
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 字节的序列:

python
 
[0x10, 0x00, 0x01, 0x02]  # 含义: ADD VReg0, VReg1, VReg2

注意:这里的 VReg0VReg1VReg2 是 VMP 虚拟机内部的虚拟寄存器,而不是真实的 ARM CPU 寄存器 r0r1r2。虚拟寄存器其实只是内存中的一块区域。

第3步:加密和混淆
VMP 不会傻傻地把 [0x10, 0x00, 0x01, 0x02] 这个字节码明文存放在 so 文件中。它会用各种密码学算法(如 AES、RC4)或简单的异或(XOR)进行加密,变成一堆看起来毫无意义的数据,比如 [0x5A, 0xE7, 0xBC, 0x41]。只有 VMP 虚拟机在运行时才知道如何解密它。

第4步:替换原始代码
原始的二进制的 add r0, r1, r2 指令会被抹掉,替换成一段用于进入虚拟机的代码(称为 Stub 或 Proxy),就像我们上一问中讲的:

armasm
 
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. 在运行时如何执行(虚拟机的执行层面)

当程序运行到被替换的代码时,会发生以下事情:

  1. 进入虚拟机:执行 bl vmp_enter_function,跳转到 VMP 的入口函数。入口函数会分配一块内存作为虚拟寄存器空间(比如 VReg[0] 到 VReg[15])。

  2. 初始化上下文:入口函数会从刚刚保存的真实寄存器中,把 r1 和 r2 的值(5 和 3)加载到虚拟寄存器 VReg1 和 VReg2 中。

    • VReg1 = 5

    • VReg2 = 3

  3. 取指和解码:

    • VMP 虚拟机从指定位置读取加密的字节码 [0x5A, 0xE7, 0xBC, 0x41]

    • 用它内部的密钥进行解密,得到明文的虚拟指令 [0x10, 0x00, 0x01, 0x02]

    • 解码:识别出操作码 0x10 是 “加法” 操作。

  4. 分发和执行(Handler):

    • 虚拟机内部有一个巨大的 “跳转表” ,操作码 0x10 对应着一条真实的、用来执行加法操作的 ARM 汇编代码块(称为 Handler)。

    • CPU 会跳转到这个 “加法 Handler” 去执行。这个 Handler 的伪代码看起来是这样的:

      armasm
       
      ; 加法 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。

  5. 循环与退出:

    • 一条虚拟指令执行完毕,虚拟机回到主循环,准备取下一条虚拟指令。

    • 当所有虚拟指令执行完毕(比如遇到了虚拟的 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 强大的核心原因。

posted on 2025-08-25 07:33  GKLBB  阅读(90)  评论(0)    收藏  举报