应用加固 --- apk加固 之 vmp进出执行过程
我用一个通俗的比喻和分步解释来帮你理解 VMP 在汇编层面的进入和退出过程。
核心比喻:秘密基地与搬家大队
想象一下,你有一段非常关键的代码(比如验证密码的代码),你不想让任何人看到它原本的样子。
-
原始程序(你的家):你的代码原本安安稳稳地住在内存的某个“地址”(一个固定的房子)里,任何人都可以来看它的真面目(反汇编)。
-
VMP 加密(搬家大队):VMP 就像一支专业的“搬家大队+装修队”。它把你这段关键的代码(家具和贵重物品)从原来的房子里搬走,然后打乱、拆解、加密,藏到一个绝对秘密的新基地里。最后,它还在你原来的房子门口立了一块牌子,上面写着 “搬家了,新地址请按以下神秘步骤查找”。
那么,CPU(执行者)是如何根据这块牌子找到秘密基地并执行代码,最后又返回的呢?
第一部分:进入虚拟化(如何找到秘密基地)
当程序执行到那段被 VMP 保护起来的代码时,它看到的已经不是原来的 CMP
(比较)、JZ
(跳转)等指令了,而是 VMP 设置的一个“入口点”。
在汇编层面,这个过程看起来是这样的:
-
入口点(门口的牌子):
原来的代码被替换成一条或多条非常特殊的指令,通常是push
和jmp
(或call
)的组合。push 12345678h ; 将一个神秘的数值(比如密钥或上下文标识)压入堆栈 jmp VMP_Entry_Point ; 跳转到VMP的总入口函数
或者更常见的,直接一个
call
指令,隐含地压入了返回地址:call VMP_Entry_Point ; 调用VMP入口,同时下一条指令的地址会被压入堆栈
-
VMP 入口函数(搬家大队的调度中心):
CPU 跳转到了VMP_Entry_Point
。这里是一段复杂的、每次运行可能都不同的代码,但它做的工作核心是:-
保存现场:就像你要去秘密基地前,得先把当前的位置记下来。它会把所有CPU寄存器的值(
eax
,ebx
,ecx
,edx
,ebp
,esp
...)全部压入堆栈保存起来。这一大堆数据被称为 “上下文(Context)” 。这是最关键的一步,因为执行完虚拟代码后,必须完全恢复到这个状态,程序才能继续正常运行。 -
初始化虚拟机:VMP 内部有一个模拟的“虚拟 CPU”,它有自己的一套虚拟寄存器(VReg0, VReg1, VReg2...)。入口函数会从刚刚保存的真实寄存器中取出一些值,初始化这些虚拟寄存器。
-
获取“虚拟指令”:VMP 会从一个指针(通常放在
esi
或edx
寄存器中)开始,读取加密的、自定义格式的字节码(Virtual Bytecode)。这些字节码就是你原始代码被加密、打乱后的样子,只有 VMP 自己能看懂。
-
-
分发执行(开始执行秘密基地的指令):
-
VMP 的“调度中心”会根据读到的第一个字节码,在一个巨大的“跳转表”中找到对应的 Handler(处理函数)。
-
每个 Handler 都是一小段真实的 x86 汇编代码,用于模拟一条虚拟指令的功能。比如,有一个
VADD
的虚拟指令,它的 Handler 就是一堆真实的add
,adc
等指令组成的。 -
CPU 会跳转到这个 Handler 去执行。执行完后,Handler 末尾会有一条
jmp
指令,再跳回“调度中心”,去取下一条虚拟指令字节码,如此循环。
-
至此,CPU 已经彻底进入了 VMP 的虚拟世界。它不再执行你原来的代码,而是在不停地执行 VMP 的 Handler,这些 Handler 组合起来,模拟了你原始代码的逻辑。逆向分析者就像陷入了一个巨大的、不停旋转的迷宫(所以叫虚拟机),很难理清原始逻辑。
第二部分:退出虚拟化(如何从秘密基地回家)
虚拟代码不会一直执行下去,它最终需要完成工作,并把控制权交还给原来的正常程序。
-
遇到虚拟返回指令:
当虚拟指令执行到你原始代码中的retn
(返回)或jmp
到外部等指令时,VMP 有一个特殊的 Handler 来处理它。 -
恢复现场:
这个特殊的“退出 Handler”会做与“入口函数”相反的事情:-
从虚拟寄存器中取出结果:它知道你的原始代码最终要把结果放在哪个真实寄存器里(比如
eax
通常用于存放返回值)。它会从虚拟寄存器中计算出最终的值,并准备好放到真实寄存器中。 -
还原现场:它会把第一步中保存的那一大坨“上下文”(所有寄存器的值)从堆栈里全部还原到真实的 CPU 寄存器中。注意:它很可能会修改当时保存的“返回地址”。
-
平衡堆栈:清理掉之前为了操作而压入栈的各种数据,使堆栈指针
esp
恢复到刚进入 VMP 入口点时的状态。
-
-
真正返回:
所有现场恢复完毕后,退出 Handler 会执行一条纯粹的retn
(返回)指令。
这时,CPU 会从堆栈顶端弹出“返回地址”并跳转回去。关键点来了:这个返回地址已经不是当初call VMP_Entry_Point
时压入的那个地址了!VMP 已经把它修改成了原始程序真正应该跳转回去的地址。
; 退出Handler最后的工作就是:
popad ; 从堆栈中弹出所有通用寄存器,恢复现场
retn ; 跳转到被VMP修改过的“返回地址”,真正返回原始程序
总结一下流程:
-
进入:
-
call/push + jmp
进入 VMP 入口。 -
保存所有真实寄存器(压栈)。
-
开始根据自定义字节码循环查找并执行 Handler。
-
-
执行:
-
CPU 在大量破碎的、顺序混乱的 Handler 代码块中跳转,模拟原始程序逻辑。
-
-
退出:
-
遇到虚拟退出指令的 Handler。
-
恢复所有真实寄存器(弹栈),并可能修改返回地址。
-
一条
retn
指令,干净利落地返回原始程序,仿佛什么都没发生过,只有计算结果(如eax
的值)留下了。
-
这种“保存现场 -> 进入虚拟世界 -> 模拟执行 -> 恢复现场 -> 完美退出”的机制,使得 VM