GKLBB

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

导航

应用加固 --- apk加固 之 vmp进出执行过程

我用一个通俗的比喻和分步解释来帮你理解 VMP 在汇编层面的进入和退出过程。

核心比喻:秘密基地与搬家大队

想象一下,你有一段非常关键的代码(比如验证密码的代码),你不想让任何人看到它原本的样子。

  • 原始程序(你的家):你的代码原本安安稳稳地住在内存的某个“地址”(一个固定的房子)里,任何人都可以来看它的真面目(反汇编)。

  • VMP 加密(搬家大队):VMP 就像一支专业的“搬家大队+装修队”。它把你这段关键的代码(家具和贵重物品)从原来的房子里搬走,然后打乱、拆解、加密,藏到一个绝对秘密的新基地里。最后,它还在你原来的房子门口立了一块牌子,上面写着 “搬家了,新地址请按以下神秘步骤查找”。

那么,CPU(执行者)是如何根据这块牌子找到秘密基地并执行代码,最后又返回的呢?


第一部分:进入虚拟化(如何找到秘密基地)

当程序执行到那段被 VMP 保护起来的代码时,它看到的已经不是原来的 CMP(比较)、JZ(跳转)等指令了,而是 VMP 设置的一个“入口点”。

在汇编层面,这个过程看起来是这样的:

  1. 入口点(门口的牌子):
    原来的代码被替换成一条或多条非常特殊的指令,通常是 push 和 jmp(或 call)的组合。

    assembly
     
    push 12345678h        ; 将一个神秘的数值(比如密钥或上下文标识)压入堆栈
    jmp  VMP_Entry_Point  ; 跳转到VMP的总入口函数

    或者更常见的,直接一个 call 指令,隐含地压入了返回地址:

    assembly
     
    call  VMP_Entry_Point  ; 调用VMP入口,同时下一条指令的地址会被压入堆栈
  2. VMP 入口函数(搬家大队的调度中心):
    CPU 跳转到了 VMP_Entry_Point。这里是一段复杂的、每次运行可能都不同的代码,但它做的工作核心是:

    • 保存现场:就像你要去秘密基地前,得先把当前的位置记下来。它会把所有CPU寄存器的值(eaxebxecxedxebpesp...)全部压入堆栈保存起来。这一大堆数据被称为 “上下文(Context)” 。这是最关键的一步,因为执行完虚拟代码后,必须完全恢复到这个状态,程序才能继续正常运行。

    • 初始化虚拟机:VMP 内部有一个模拟的“虚拟 CPU”,它有自己的一套虚拟寄存器(VReg0, VReg1, VReg2...)。入口函数会从刚刚保存的真实寄存器中取出一些值,初始化这些虚拟寄存器。

    • 获取“虚拟指令”:VMP 会从一个指针(通常放在 esi 或 edx 寄存器中)开始,读取加密的、自定义格式的字节码(Virtual Bytecode)。这些字节码就是你原始代码被加密、打乱后的样子,只有 VMP 自己能看懂。

  3. 分发执行(开始执行秘密基地的指令):

    • VMP 的“调度中心”会根据读到的第一个字节码,在一个巨大的“跳转表”中找到对应的 Handler(处理函数)。

    • 每个 Handler 都是一小段真实的 x86 汇编代码,用于模拟一条虚拟指令的功能。比如,有一个 VADD 的虚拟指令,它的 Handler 就是一堆真实的 addadc 等指令组成的。

    • CPU 会跳转到这个 Handler 去执行。执行完后,Handler 末尾会有一条 jmp 指令,再跳回“调度中心”,去取下一条虚拟指令字节码,如此循环。

至此,CPU 已经彻底进入了 VMP 的虚拟世界。它不再执行你原来的代码,而是在不停地执行 VMP 的 Handler,这些 Handler 组合起来,模拟了你原始代码的逻辑。逆向分析者就像陷入了一个巨大的、不停旋转的迷宫(所以叫虚拟机),很难理清原始逻辑。


第二部分:退出虚拟化(如何从秘密基地回家)

虚拟代码不会一直执行下去,它最终需要完成工作,并把控制权交还给原来的正常程序。

  1. 遇到虚拟返回指令:
    当虚拟指令执行到你原始代码中的 retn(返回)或 jmp 到外部等指令时,VMP 有一个特殊的 Handler 来处理它。

  2. 恢复现场:
    这个特殊的“退出 Handler”会做与“入口函数”相反的事情:

    • 从虚拟寄存器中取出结果:它知道你的原始代码最终要把结果放在哪个真实寄存器里(比如 eax 通常用于存放返回值)。它会从虚拟寄存器中计算出最终的值,并准备好放到真实寄存器中。

    • 还原现场:它会把第一步中保存的那一大坨“上下文”(所有寄存器的值)从堆栈里全部还原到真实的 CPU 寄存器中。注意:它很可能会修改当时保存的“返回地址”。

    • 平衡堆栈:清理掉之前为了操作而压入栈的各种数据,使堆栈指针 esp 恢复到刚进入 VMP 入口点时的状态。

  3. 真正返回:
    所有现场恢复完毕后,退出 Handler 会执行一条纯粹的 retn(返回)指令。
    这时,CPU 会从堆栈顶端弹出“返回地址”并跳转回去。关键点来了:这个返回地址已经不是当初 call VMP_Entry_Point 时压入的那个地址了!VMP 已经把它修改成了原始程序真正应该跳转回去的地址。

assembly
 
; 退出Handler最后的工作就是:
popad    ; 从堆栈中弹出所有通用寄存器,恢复现场
retn     ; 跳转到被VMP修改过的“返回地址”,真正返回原始程序

总结一下流程:

  1. 进入:

    • call/push + jmp 进入 VMP 入口。

    • 保存所有真实寄存器(压栈)。

    • 开始根据自定义字节码循环查找并执行 Handler。

  2. 执行:

    • CPU 在大量破碎的、顺序混乱的 Handler 代码块中跳转,模拟原始程序逻辑。

  3. 退出:

    • 遇到虚拟退出指令的 Handler。

    • 恢复所有真实寄存器(弹栈),并可能修改返回地址。

    • 一条 retn 指令,干净利落地返回原始程序,仿佛什么都没发生过,只有计算结果(如 eax 的值)留下了。

这种“保存现场 -> 进入虚拟世界 -> 模拟执行 -> 恢复现场 -> 完美退出”的机制,使得 VM

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