VMP1.1 逆向分析

1 引言

几年来,逆向一直停留在CTF坐牢水平,脱壳也就会用工具、最多说说原理。所以决定开始“深入”了解下脱VMP壳。当然,毕竟要循序渐进,我们还是从最简单的开始。这里我们选择了VMP1.1版本,自己编写简单的程序并用其加壳,将原程序与加壳后程序进行对比分析。

2 环境

  • 宿主机:Windows 11 x64
    • VMWare 17
    • Visual Studio 2022
    • IDA Pro
    • VMP 1.1
  • 虚拟机:

3 分析

3.1 示例程序(example1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int __declspec(naked) foo()
{
__asm
{
mov eax, 0x0
ret
}
}

int main()
{
int a = foo();
std::cout << "foo() = " << a << std::endl;
return 0;
}

3.2 虚拟机保护方法分析

在使用VMP进行加壳的时候可以知道,VMP是对指定的函数进行保护。我们可以先通过IDA查看原程序中foo函数的地址,并且要保证该函数的指令要大于等于5个字节。在这里我们将经过VMP保护的foo函数称为foo_vmp。

foo_vmp函数包含一个jmp指令:

1
2
3
4
5
6
.text:004B3F20 ; int __cdecl foo()
.text:004B3F20 ?foo@@YAHXZ proc far ; CODE XREF: foo(void)↑j
.text:004B3F20 jmp near ptr sub_61C127
.text:004B3F20 ; ---------------------------------------------------------------------------
.text:004B3F25 db 7Bh dup(0CCh)
.text:004B3F25 ?foo@@YAHXZ endp

其中的sub_61C13B位于段.vmp0内,也就是说VMP通过修改原函数的指令,让其进入vmp段内执行来实现接管和保护原函数。

sub_61C13B函数实际执行虚拟机化后的foo函数的指令,其中push offset dword_61C0B4将foo_vmp的虚拟机指令起始地址(VmpCode)压入栈中,VmpCode也位于vmp段中,是一个byte数组。

pushf、pusha两个指令将当前的标志寄存器和通用寄存器内容压入栈中,push 0将一个0压入栈,并且它(VmpPC)也用于后续的虚拟机指令寻址。

下面是执行到0061B88D处(ESP=0019FDE0),栈中可能的数据:

1
2
3
4
5
6
7
8
9
10
11
12
Stack[00001CEC]:0019FDE0 dd 0                                    ; VmpPC
Stack[00001CEC]:0019FDE4 dd offset off_19FEE8 ; pusha
Stack[00001CEC]:0019FDE8 dd offset start_1
Stack[00001CEC]:0019FDEC dd offset off_19FEE8
Stack[00001CEC]:0019FDF0 dd offset dword_19FE04 ; pushf
Stack[00001CEC]:0019FDF4 dd offset unk_33F000
Stack[00001CEC]:0019FDF8 dd 1
Stack[00001CEC]:0019FDFC dd offset unk_614066
Stack[00001CEC]:0019FE00 dd offset unk_614066
Stack[00001CEC]:0019FE04 dword_19FE04 dd 246h ; pushf
Stack[00001CEC]:0019FE08 dd offset dword_61C0B4 ; &VmpCode
Stack[00001CEC]:0019FE0C dd 4B581Bh ; ret addr,位于main函数

函数foo_vmp逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.vmp0:0061C127 sub_61C127      proc far                ; CODE XREF: foo(void)↑j
.vmp0:0061C127
.vmp0:0061C127 var_2C = tbyte ptr -2Ch
.vmp0:0061C127 var_4 = dword ptr -4
.vmp0:0061C127
.vmp0:0061C127 ; FUNCTION CHUNK AT .vmp0:0061B400 SIZE 000007EB BYTES
.vmp0:0061C127 ; FUNCTION CHUNK AT .vmp0:0061BFEB SIZE 000000C9 BYTES
.vmp0:0061C127
.vmp0:0061C127 push offset dword_61C0B4
.vmp0:0061C12C jmp loc_61B886
.vmp0:0061C12C sub_61C127 endp
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.vmp0:0061B886 loc_61B886: ; CODE XREF: sub_61C127+5↓j
.vmp0:0061B886 pushf
.vmp0:0061B887 pusha
.vmp0:0061B888 push 0
.vmp0:0061B88D mov esi, [esp+2Ch+var_4] ; 注:esp+2Ch+var_4 == &虚拟机指令
.vmp0:0061B891 cld
.vmp0:0061B892 mov edx, offset dword_617000 ; 存放线程ID(Tid)的数组
.vmp0:0061B897 call ds:__imp__GetCurrentThreadId@0 ; GetCurrentThreadId()
.vmp0:0061B89D mov ebx, eax
.vmp0:0061B89F mov ecx, 100h
.vmp0:0061B8A4 mov edi, edx
.vmp0:0061B8A6 repne scasd ; 在Tid数组中搜索当前Tid
.vmp0:0061B8A8 jz short loc_61B8B7
.vmp0:0061B8AA mov eax, 100h ; 这部分指令逻辑为将Tid放入Tid数组中
.vmp0:0061B8AF xchg eax, ecx
.vmp0:0061B8B0 mov edi, edx
.vmp0:0061B8B2 repne scasd
.vmp0:0061B8B4 mov [edi-4], ebx
.vmp0:0061B8B7
.vmp0:0061B8B7 loc_61B8B7: ; CODE XREF: sub_61C127-87F↑j
.vmp0:0061B8B7 mov ebp, edi ; 若Tid存在于Tid数组中,则直接跳转到此处
.vmp0:0061B8B9 sub edi, edx
.vmp0:0061B8BB shl edi, 1
.vmp0:0061B8BD lea edi, [edx+edi*8+3C0h]
.vmp0:0061B8C4 mov ebx, esi
.vmp0:0061B8C6 add esi, dword ptr [esp+2Ch+var_2C] ; esp+2Ch+var_2C == 之前压入的0的地址
.vmp0:0061B8C9
.vmp0:0061B8C9 loc_61B8C9: ; CODE XREF: sub_61C127-D25↑j
.vmp0:0061B8C9 ; sub_61C127-D1A↑j ...
.vmp0:0061B8C9 lodsb
.vmp0:0061B8CA add al, bl
.vmp0:0061B8CC inc al
.vmp0:0061B8CE ror al, 5
.vmp0:0061B8D1 inc al
.vmp0:0061B8D3 ror al, 2
.vmp0:0061B8D6 inc al
.vmp0:0061B8D8 rol al, 5
.vmp0:0061B8DB not al
.vmp0:0061B8DD add bl, al
.vmp0:0061B8DF movzx eax, al
.vmp0:0061B8E2 jmp ds:jpt_61B8E2[eax*4] ; switch 256 cases
.vmp0:0061B58F ; ---------------------------------------------------------------------------

0061B8E2处的jmp ds:jpt_61B8E2[eax*4]用到一个跳表(jump table),EAX寄存器的值是其索引。EAX的值来源于AL寄存器与BL寄存器进行的一系列计算,而AL寄存器的值是通过访问VmpCode + VmpPC地址的字节得到的。

跳表存储的内容如下:

1
2
3
4
5
6
.vmp0:0061BBEB jpt_61B8E2      dd offset loc_61B769, offset loc_61B5A4, offset loc_61BAA0
.vmp0:0061BBF7 dd offset loc_61B5FE, offset loc_61BA75, offset loc_61C02D
...
...
...
.vmp0:0061BFE7 dd offset loc_61BB1E

3.3 虚拟机指令分析

在最开始,foo_vmp将ESP指向的栈中的数据复制到EDI寄存器指向vmp段中一个固定区域,包括VmpPC、pushf和pusha压入栈中的数据。由于每次只能复制1个DWORD,所以(1)(2)对应的代码会执行多次。

(1)首先将VmpPC复制到地址edi+eax*4处:

1
2
3
4
5
6
7
8
9
10
.vmp0:0061B77D lodsb                                   ; jumptable 0061B8E2 case 213
.vmp0:0061B77E add al, bl
.vmp0:0061B780 xor al, 0E0h
.vmp0:0061B782 dec al
.vmp0:0061B784 xor al, 0FBh
.vmp0:0061B786 rol al, 1
.vmp0:0061B788 dec al
.vmp0:0061B78A add bl, al
.vmp0:0061B78C pop dword ptr [edi+eax*4]
.vmp0:0061B78F jmp loc_61B8C9

存放位置如下:

1
2
3
4
5
.vmp0:0061742C dd 0                                    ; VmpPC
.vmp0:00617430 db 6Fh
...
...
...

(2)随后将pusha指令压入栈中的数据复制到地址edi+eax*4处:

1
2
3
4
5
6
7
8
9
10
11
12
.vmp0:0061B77D loc_61B77D:                             ; CODE XREF: sub_61C127-845↓j
.vmp0:0061B77D ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061B77D lodsb ; jumptable 0061B8E2 case 213
.vmp0:0061B77E add al, bl
.vmp0:0061B780 xor al, 0E0h
.vmp0:0061B782 dec al
.vmp0:0061B784 xor al, 0FBh
.vmp0:0061B786 rol al, 1
.vmp0:0061B788 dec al
.vmp0:0061B78A add bl, al
.vmp0:0061B78C pop dword ptr [edi+eax*4]
.vmp0:0061B78F jmp loc_61B8C9

存放位置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.vmp0:0061742C dd 0                                    ; VmpPC
.vmp0:00617430 dd offset off_19FEE8 ; pusha <----------------------------
.vmp0:00617434 db 12h
...
...
...
;;;;;;;;;;;;;;;;;;;;;;;与栈中内容进行对比;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Stack[00001CEC]:0019FDE0 dd 0 ; VmpPC
Stack[00001CEC]:0019FDE4 dd offset off_19FEE8 ; pusha <----------------------------
Stack[00001CEC]:0019FDE8 dd offset start_1 ; <==== ESP
Stack[00001CEC]:0019FDEC dd offset off_19FEE8
Stack[00001CEC]:0019FDF0 dd offset dword_19FE04 ; pushf
Stack[00001CEC]:0019FDF4 dd offset unk_33F000
Stack[00001CEC]:0019FDF8 dd 1
Stack[00001CEC]:0019FDFC dd offset unk_614066
Stack[00001CEC]:0019FE00 dd offset unk_614066
Stack[00001CEC]:0019FE04 dword_19FE04 dd 246h ; DATA XREF: Stack[00001CEC]:0019FDF0↑o
Stack[00001CEC]:0019FE04 ; pushf
Stack[00001CEC]:0019FE08 dd offset dword_61C0B4 ; &VmpCode
Stack[00001CEC]:0019FE0C dd 4B581Bh ; ret addr

(3)将VmpCode地址pop到EAX寄存器:

1
2
3
4
.vmp0:0061B593 loc_61B593:                             ; CODE XREF: sub_61C127-845↓j
.vmp0:0061B593 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061B593 pop eax ; jumptable 0061B8E2 case 205
.vmp0:0061B594 jmp loc_61B8C9

栈内容如下:

1
2
3
4
5
6
Stack[00001CEC]:0019FE08 dd offset dword_61C0B4                  ; &VmpCode
Stack[00001CEC]:0019FE0C dd 4B581Bh ; ret addr,<==== ESP
Stack[00001CEC]:0019FE10 db 0C7h
Stack[00001CEC]:0019FE11 db 71h ; q
Stack[00001CEC]:0019FE12 db 49h ; I
Stack[00001CEC]:0019FE13 db 0

EAX寄存器值为:0x0061C0B4

(4)压栈(DWORD)操作:

1
2
3
4
5
6
7
8
9
10
11
12
.vmp0:0061B73D loc_61B73D:                             ; CODE XREF: sub_61C127-845↓j
.vmp0:0061B73D ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061B73D lodsd ; jumptable 0061B8E2 case 175
.vmp0:0061B73E add eax, ebx
.vmp0:0061B740 add eax, 19E62AF4h
.vmp0:0061B745 xor eax, 887CD5D9h
.vmp0:0061B74A ror eax, 18h
.vmp0:0061B74D sub eax, 43D91D6h
.vmp0:0061B752 not eax
.vmp0:0061B754 add ebx, eax
.vmp0:0061B756 push eax
.vmp0:0061B757 jmp loc_61B8C9

lodsd指令取出一个DWORD作为操作数,此处为0A110DF68。经计算后,EAX值为0xE01EC2A2,EBX值为0xE080835C。并将EAX值压入栈中。

栈数据如下:

1
2
3
4
0019FE08  E01EC2A2  ; <==== ESP
0019FE0C 004B581B _main+2B
0019FE10 004971C7 start_1
0019FE14 004971C7 start_1

然后,foo_vmp会再执行一次(4)的压栈操作,这次压入了1FE13D5E

(5)压栈(WORD)操作:

1
2
3
4
5
6
7
8
9
.vmp0:0061BA83 lodsb                                   ; jumptable 0061B8E2 case 24
.vmp0:0061BA84 add al, bl
.vmp0:0061BA86 not al
.vmp0:0061BA88 dec al
.vmp0:0061BA8A rol al, 3
.vmp0:0061BA8D inc al
.vmp0:0061BA8F add bl, al
.vmp0:0061BA91 push ax
.vmp0:0061BA93 jmp loc_61B8C9

最开始的lodsb指令与lodsd类似,只不过是取出一个BYTE。最后,指令push ax压入了0x02

然后,foo_vmp压入了一个DWORD值为186E6D

(6)左移操作:

1
2
3
4
5
6
7
.vmp0:0061C055 loc_61C055:                             ; CODE XREF: sub_61C127-845↑j
.vmp0:0061C055 ; DATA XREF: .vmp0:jpt_61B8E2↑o
.vmp0:0061C055 pop edx ; jumptable 0061B8E2 cases 127,160
.vmp0:0061C056 pop cx
.vmp0:0061C058 shl edx, cl
.vmp0:0061C05A push edx
.vmp0:0061C05B jmp loc_61B8C9

从栈中弹出1个DWORD到EDX,弹出1个WORD到CX。然后进行左移(shl)操作,即EDX << CX,然后将结果压入栈中。

此处压入的值为61B9B4,即0x186E6D << 2的结果。

另外提及一下,后面还会有一种将标志寄存器也压入栈中的左移操作。如下所示:

1
2
3
4
5
6
7
8
.vmp0:0061BB99 loc_61BB99:                             ; CODE XREF: sub_61C127-845↑j
.vmp0:0061BB99 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061BB99 pop ax ; jumptable 0061B8E2 case 208
.vmp0:0061BB9B pop cx
.vmp0:0061BB9D shr ax, cl
.vmp0:0061BBA0 push ax
.vmp0:0061BBA2 pushfw
.vmp0:0061BBA4 jmp loc_61B8C9

(7)EDI寻址压栈操作

1
2
3
4
5
6
7
8
9
10
11
12
13
.vmp0:0061B829 loc_61B829:                             ; CODE XREF: sub_61C127-845↓j
.vmp0:0061B829 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061B829 movzx eax, byte ptr [esi] ; jumptable 0061B8E2 case 148
.vmp0:0061B82C add al, bl
.vmp0:0061B82E xor al, 0E0h
.vmp0:0061B830 dec al
.vmp0:0061B832 xor al, 0FBh
.vmp0:0061B834 rol al, 1
.vmp0:0061B836 inc esi
.vmp0:0061B837 dec al
.vmp0:0061B839 add bl, al
.vmp0:0061B83B push dword ptr [edi+eax*4]
.vmp0:0061B83E jmp loc_61B8C9

取出大小为1BYTE的操作数,经过计算后EAX值为0x0000000B,利用该值与EDI寄存器进行寻址,这里edi+eax*4是0,也就是之前push 0指令经过压栈然后复制到EDI所指区域产生的值。

(8)加法操作

1
2
3
4
5
.vmp0:0061B550 loc_61B550:                             ; CODE XREF: sub_61C127-845↓j
.vmp0:0061B550 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061B550 pop ecx ; jumptable 0061B8E2 case 195
.vmp0:0061B551 add dword ptr [esp+28h+var_2C+4], ecx
.vmp0:0061B554 jmp loc_61B8C9

首先从栈上弹出1个DWORD到ECX,然后将ECX加到栈顶所指的[ESP]中。

(9)寻址压栈操作

1
2
3
4
5
.vmp0:0061B719 loc_61B719:                             ; CODE XREF: sub_61C127-845↓j
.vmp0:0061B719 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061B719 pop edx ; jumptable 0061B8E2 cases 140,218
.vmp0:0061B71A push dword ptr [edx]
.vmp0:0061B71C jmp loc_61B8C9

从栈上弹出1个DWORD到EDX,将EDX的值作为内存地址进行寻址,并将访存结果压入栈中。

这里访存结果是E99C6650,是某些指令机器码的一部分。

(10)压入栈顶指针操作:

1
2
3
4
5
.vmp0:0061BA03 loc_61BA03:                             ; CODE XREF: sub_61C127-845↑j
.vmp0:0061BA03 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061BA03 mov ecx, esp ; jumptable 0061B8E2 case 18
.vmp0:0061BA05 push ecx
.vmp0:0061BA06 jmp loc_61B8C9

(11)栈顶访存操作

1
2
3
4
5
.vmp0:0061BB61 loc_61BB61:                             ; CODE XREF: sub_61C127-845↑j
.vmp0:0061BB61 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061BB61 pop edx ; jumptable 0061B8E2 case 8
.vmp0:0061BB62 push dword ptr ss:[edx]
.vmp0:0061BB65 jmp loc_61B8C9

将栈顶DWORD弹出到EDX寄存器,再将[EDX]访存结果压入栈顶。

我们现在还是初步分析VMP,所以接下来我们略过重复的虚拟机指令。

(12)返回(ret)操作

1
2
3
4
5
6
7
.vmp0:0061BAF9 loc_61BAF9:                             ; CODE XREF: .vmp0:0061B8E2↑j
.vmp0:0061BAF9 ; DATA XREF: .vmp0:jpt_61B8E2↓o
.vmp0:0061BAF9 mov dword ptr [ebp-4], 0 ; jumptable 0061B8E2 case 77
.vmp0:0061BB00 pop eax
.vmp0:0061BB01 popa
.vmp0:0061BB02 popf
.vmp0:0061BB03 retn

将栈顶弹出到EAX作为函数的返回值,用popa、popf指令还原寄存器。

4 总结

程序经过VMP保护后,函数的逻辑使用虚拟机指令重写。人工翻译虚拟机指令的工作量太大,所以下一步需要编写工具来实现自动翻译虚拟机指令,供我们阅读。