Vmp 静态分析: 第二步找到PE文件中的VmExecutor入口
Vmp 编译后的代码,会编码一个或者多个 VmExecutor, 每个 VmExecutor 在程序中会存在 好几个入口,当需要执行 ByteCode 的时候,将ByteCode的地址(加密后的) 放入栈顶,然后然后跳转进入 VmExecutor
先看一个 1.7x 的进入 Vmp的方式
.vmp1:0046B1DE push 0B50EE113h <== 这就是 Byte Code 的地址(加密后)
.vmp1:0046B1E3 call sub_45298E<== 这就是入口地址
Vmprotect 2.x 的进入方式,从 call 变成了 jmp,但是总体情况和这类似
.vmp0:104321F2 push 24BC4F16h <== 这个就是 ByteCode 地址
.vmp0:104321F7 pusha
.vmp0:104321F8 mov [esp+24h+var_8], 7DCA24EDh
.vmp0:10432200 pushf
.vmp0:10432201 pusha
.vmp0:10432202 push [esp+48h+var_48]
.vmp0:10432205 lea esp, [esp+44h]
.vmp0:10432209 jmp loc_1043002A <== 跳入 Vmp 入口
好了,现在就是,怎么找到入口地址了。
首先,入口地址的明显标志就是 jmp 或者 call , 那么,从内存中暴力找 jmp/call指令,对应的机器码为 E8h/E9h, 然后计算跳转的目标地址在不在我们加载的 pe 地址范围…… 如果在,那么就当他是嫌疑地址,注意是嫌疑地址,这样对于一个PE映像来说,可以找到好几万个这样的jmp/call点,然后就是我们下一步确认这些点
第二步,确认 VmExecutor 的入口点。我们需要一个这样的筛子,从成千上完个疑似点中,识别出谁是真正的 VmExecutor入口,谁不是。类似于这样一个函数
bool IsVmExecutor(IntPtr address);
如何实现这个函数? 我们需要借助反汇编引擎,来对程序执行流进行分析。我选用的 beaengine,主要是他能反汇编出一些非法的执行,当然也可以选择其他的引擎,有些指令是反汇编不出非法指令的类似于 sal/shl 这类的指令。借助反汇编指令,我们从疑似入口处一条一条反汇编,碰到 call 就跳转,碰到 jmp/jcc 就跳转,这样就能一直走到 retn xx,或者走到非法地址。如果走到了非法地址,那么这个入口肯定就不是入口了,如果顺利的走到了retn xx,那么我们再看看路中间是碰到过 VmExecutor 的特征指令
1) [ESP], {&ESP:ESP} = pushfd EFLAGS
进入 VmExector 的时候, x86 的所有GPR + EFLAGS都需要入栈,其他的寄存器入展方式各式各样,只有Eflags 入栈方式只有这一种! pushfd.
2) ESI = mov [ESP+xxx]*
对于VmExector 其虚拟的 Instruction Pointer (类似于x86的 eip) 100%使用的是 esi, 为什么呢?后面我们会说。也就是说,最终我们放入栈中的 ByteCode Address 最终都要通过这种方式赋值给 esi。 如果不是这种方式的,我们需要借助数据流分析了。
3) EBX = mov ESI
ebx 在 VmExecutor 中担当解码密钥的作用,每次重新设置 InstructionPointer (ESI)后,都需要更新这个密钥,值就是 esi.
4) ESI, EFLAGS = add ESI, [EBP]*
这句话其实就是地址重定位, EBP 在VmExecutor 中扮演堆栈指针的角色,最后压入堆栈的是当前 Dll的 Relocation 地址,当一个pe加载进入内存的时候,其加载的实际地址,可能和想想的地址不一样,LoadLibrary 会根据编译器的重定位信息修改实际代码, VmExecutor 也需要做类似的事情,所以这条指令少不了
5) EXX = mov [EAX*4+xxxx]*
这是VmExecutor 的指令分发器,al 是取指令后,指令的 opcode, 指令执行就需要根据 opcode 进行分发,分发最简单的方法就是查表,这就是查表了,其中 Exx 可能是 EDX,也可能是 ECX, 右边 MemoryOperand 的 Offset 就是Handler表的绝对地址,
6) RET
最后程序转入 opcode Handler 的方式为 RET 这是 VmExecutor 的御用流程控制指令……
我们看看x86的 8个 GPR 在VmExecutor是转职干什么的……
EAX 指令的 Opcode
ECX 自由
EDX 自由
EBX 机密byte code 的
ESP 这个保存着非byte的的实际堆栈,VmExecutor 不能动,只能在栈顶小打小闹的 push/pop 一堆乱代码,最终还要还原。
EBP VMExecutor 的栈顶。
ESI ByteCode 指令指针
EDI VmExecutor 的内部存贮指针,类似于 x86的 几个 GPR 的地址。
从上面看,VmExecutor 能够自由使用的寄存器只有 ECX,EDX 两个,所以这 VmExecutor 的指令分发器会有两个版本
一个是
EDX = mov [EAX*4+xxx]*
另外一个是
ECX = mov [EAX*4+xxx]*
通过一堆的特征码,就可以基本确定谁不是 VmExecutor 的入口地址了,让然通过了上面的层层筛选,留下来的高度嫌疑犯,还有真不是的,
最后一招!就是校验
ESI = mov [ESP+xxx]* 这条代码处的 esp +xxx 和入口地址处的 esp差别!
如何计算? OMG 自己跟踪 ESP的变化
可能修改 ESP的指令如下
PUSH/POP/CALL/PUSHA/POPA/POPF/POPF/ADD/SUB/LEA 等等等等,每个修改ESP的量是多少,需要查表哦。
反正这样能够跟踪 ESP的值!
1.7 VMp 的堆栈是这样的
+-------------------------+
| 32bit ByteCode Address |
+-------------------------+
| 32bit nomeaning |
| 32bit nomeaning |
+-------------------------+
| 32 bit GPR/E***S 1 |
| 32 bit GPR/E***S 2 |
| 32 bit GPR/E***S 3 |
| 32 bit GPR/E***S 4 |
| 32 bit GPR/E***S 5 |
| 32 bit GPR/E***S 6 |
| 32 bit GPR/E***S 7 |
| 32 bit GPR/E***S 8 |
| 32 bit GPR/E***S 9 |
+-------------------------+
| 32bit relocation |
+-------------------------+
2.x 版本的布局有点变法
+-------------------------+
| 32bit ByteCode Address |
+-------------------------+
| 32bit nomeaning |
| 32bit nomeaning |
+-------------------------+
| 32 bit GPR/E***S 1 |
| 32 bit GPR/E***S 2 |
| 32 bit GPR/E***S 3 |
| 32 bit GPR/E***S 4 |
| 32 bit GPR/E***S 5 |
| 32 bit GPR/E***S 6 |
| 32 bit GPR/E***S 7 |
| 32 bit GPR/E***S 8 |
| 32 bit GPR/E***S 9 |
+-------------------------+
| 32bit unknown |
+-------------------------+
| 32bit relocation |
+-------------------------+
好了,这个布局可以确定esp了吧,在 ebp还未初始化之前,VmExecutor 用的是 ESP 定位堆栈,哈,算一下
1.7 版本中, esp应该比入口 -0xE8
2.x 版本中,esp 应该比入口 -0xEC
通过上面的指令特征,以及最后的 ESP 检验的……应该就是 VmExecutor的入口了,请列个表吧!!!
在找这些入口的时候,我们中间碰到了
EXX = mov [EAX*4+xxxx]*
请把这个立即数也记下,这就是 VmExecutor的 Handle表,每个VmExecutor 有不同的Handle 表,可以做个映射表表明哪个入口对应的是哪个VmExecutor的 Handler表,便于后面的分析
本文介绍的是如何暴力列举VmExecutor的入口清单,同时确认 VmExecutor 的个数,以后将要着重分析每个 VmExecutor 的详细信息。
