(本文的行文思路和前面的原理部分大量抄了“ 穆恩”的3.0.9的分析文章,请大神谅解,有错误也请大家指出。)
刚开始的通用寄存器和标志寄存器:
VMProtect其实已经被前辈们扒得体无完肤了,本来没有什么好写的,但由于最近要把VMP拿出来学习,花了两天时间从1.x -> 2.x -> 3.x,一直到最新的3.3.1顺着分析了一次。本文只是对其虚拟机和代码混淆机制做个笔记,没有太多的技术含量。
写一份最简单的汇编代码:
; Filename: testVM.asm
.386
.model flat, stdcall
option casemap: none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
.data
szMsg db '我是内容', 0
szTitle db '我是标题', 0
.code
start:
push 2019H
invoke MessageBox, NULL, offset szMsg, offset szTitle, MB_OK
invoke ExitProcess, 0
end start
用masm32编译成testVM.exe之后再用OD 1.10打开,是不是跟看源代码似的?
; Filename: testVM.asm
.386
.model flat, stdcall
option casemap: none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
.data
szMsg db '我是内容', 0
szTitle db '我是标题', 0
.code
start:
push 2019H
invoke MessageBox, NULL, offset szMsg, offset szTitle, MB_OK
invoke ExitProcess, 0
end start
用masm32编译成testVM.exe之后再用OD 1.10打开,是不是跟看源代码似的?
用VMP 3.3.1加壳,去掉所有的反调试、保护等等,目的是只保留最简单纯粹的虚拟机部分,避免不必要的干扰,方便我们分析学习:
testVMP.exe原始文件(2560字节)和用VMP不同版本加壳后的文件尺寸如下:
版本
|
文件尺寸
|
原始文件 |
2k (2,560字节) |
1.1 |
7k (7,168字节) |
1.8 |
13k (13,312字节) |
2.13.8 |
16k (16,384字节) |
3.0.9 |
515k (515,072字节) |
3.3.1 |
559k (559,104字节) |
可以看到1.x和2.x都只在原始文件尺寸的基础上增加了一点点,但是从3.x开始其尺寸急剧膨胀,为什么会这样呢?
这里我们要用到OD非常棒的Run trace功能,打开1.8、2.13.8和3.3.1的exe,按Ctrl+F11(或选菜单Debug->Trace into),再选菜单View->Run trace,可以看到运行的指令数:
版本
|
运行指令数 |
1.8 |
3896 |
2.13.8 |
11283 |
3.3.1 |
1500 |
然后在Run trace的窗口点右键选Profile module,按照每条指令的运行次数(Count)排序,各个版本的结果是这样的:
1.8:
Profile of testVMP_
Count Address First command Comment
40. 004042C5 mov byte ptr [esp+8], ch
40. 00404BE5 pushfd
40. 00405106 bt cx, 0A
21. 00404405 lea eax, dword ptr [edi+50]
21. 00404D39 lea esp, dword ptr [esp+C]
21. 00405198 pushfd
21. 0040558B inc ah
21. 004059E6 call 00404405
16. 004056E7 sbb dx, di
16. 0040599E mov dword ptr [edi+eax], edx
13. 0040432A push dword ptr [esp]
13. 00404371 lea edx, dword ptr [esp+C1B1
13. 00405D42 adc dh, 64
5. 004041E6 shld ax, cx, cl
5. 0040539A rol eax, 14
5. 00405B14 pushfd
(...省略)
Profile of testVMP_
Count Address First command Comment
40. 004042C5 mov byte ptr [esp+8], ch
40. 00404BE5 pushfd
40. 00405106 bt cx, 0A
21. 00404405 lea eax, dword ptr [edi+50]
21. 00404D39 lea esp, dword ptr [esp+C]
21. 00405198 pushfd
21. 0040558B inc ah
21. 004059E6 call 00404405
16. 004056E7 sbb dx, di
16. 0040599E mov dword ptr [edi+eax], edx
13. 0040432A push dword ptr [esp]
13. 00404371 lea edx, dword ptr [esp+C1B1
13. 00405D42 adc dh, 64
5. 004041E6 shld ax, cx, cl
5. 0040539A rol eax, 14
5. 00405B14 pushfd
(...省略)
2.13.8:
Profile of testVMP_
Count Address First command Comment
134. 004047FB movsx edx, bl
134. 00404BDC call 004067BB
134. 00405310 push dword ptr [esp]
134. 0040601E shl dx, cl
134. 004067BB jmp 00405310
54. 0040450B mov word ptr [esp], bx
54. 004046A1 pushad
54. 00405C8B pushfd
52. 004042BA pushfd
52. 0040670D cmc
31. 004041C9 dec dh
31. 004043D6 dec esi
31. 0040458A pushad
(...省略)
Profile of testVMP_
Count Address First command Comment
134. 004047FB movsx edx, bl
134. 00404BDC call 004067BB
134. 00405310 push dword ptr [esp]
134. 0040601E shl dx, cl
134. 004067BB jmp 00405310
54. 0040450B mov word ptr [esp], bx
54. 004046A1 pushad
54. 00405C8B pushfd
52. 004042BA pushfd
52. 0040670D cmc
31. 004041C9 dec dh
31. 004043D6 dec esi
31. 0040458A pushad
(...省略)
3.3.1:
Profile of testVMP_
Count Address First command Comment
15. 0040C55B lea edx, dword ptr [esp+60]
15. 0042E47E ja 0043A480
15. 0043A480 push esi
1. 00401000 jmp 0046CC5F
1. 00401026 jmp dword ptr [<&user32.Mess
1. 00407C43 rol eax, 2
1. 00407D78 ror dl, 1
1. 00407E9C sub edi, 4
1. 004082E5 lea edi, dword ptr [edi-1]
1. 004083CD push esi
(...省略)
Profile of testVMP_
Count Address First command Comment
15. 0040C55B lea edx, dword ptr [esp+60]
15. 0042E47E ja 0043A480
15. 0043A480 push esi
1. 00401000 jmp 0046CC5F
1. 00401026 jmp dword ptr [<&user32.Mess
1. 00407C43 rol eax, 2
1. 00407D78 ror dl, 1
1. 00407E9C sub edi, 4
1. 004082E5 lea edi, dword ptr [edi-1]
1. 004083CD push esi
(...省略)
结合上面几点,我们会发现3.x的文件尺寸远超1.x和2.x,但Run trace中的每条指令运行次数反而要远少于1.x和2.x,所以答案就不言而喻了:
- 在1.x和2.x中,有一个统一的VMDispatcher 作为所有字节码(VM ByteCode)的调度者,以寄存器al作为索引进行跳转,所以最大可以有256个指令的Handler。每个Handler执行完后,会跳转回VMDispatcher,通过al取下一条指令的索引并跳转到它的Handler,再周而复始地执行下去;
- 在3.x中,已经没有这个统一的VMDispatcher了,每条指令的Handler几乎都是零散分布的,在上一条指令的Handler执行完后,可能会通过某种类型的跳转跳到下一条指令的Handler去,也就是说每条指令都可能会有一个Handler,哪怕这两条指令是执行相同的功能,因此代码会膨胀得厉害(但不是非常确定,也有可能是Handler-Table变大了);
- 由于没有了这个统一的主循环VMDispatcher,进而不能顺藤摸瓜各个Handler,所以fkvmp、VMP分析插件1.4等上古神器都在3.x中失效了。
再来说说高版本的3.x 与低版本的1.x和2.x相比,寄存器和堆栈的变化:
寄存器:
ebp依然是VM_esp,指向虚拟机的栈顶
再来说说高版本的3.x 与低版本的1.x和2.x相比,寄存器和堆栈的变化:
寄存器:
ebp依然是VM_esp,指向虚拟机的栈顶
edi不再指向VMContext
esi不再指向VM_eip,在跳转Handler的方式上,3.0.9是用jmp edi或者push edi, retn实现,3.3.1是用jmp esi或者push esi, retn实现。
堆栈:
1.x~2.x:栈底 -> ebp -> edi(VMContext)
3.x:栈底 -> ebp -> esp(VMContext),也就是edi已经不再指向VMContext,而是直接由[esp+索引寄存器]来定位到VMContext的某一项,注意这里的“索引寄存器”并不确定,有可能是edx,也有可能是别的通用寄存器,谁有空就用谁。
熟悉1.x和2.x的话,看3.x的虚拟机代码不会有太大的问题,只不过混淆的垃圾指令太多,大片大片跳过即可。
0x0301 初始化
刚开始的通用寄存器和标志寄存器:
EAX CF1028BC
ECX 00401000
EDX 00401000
EBX 002AD000
ESP 0019FF78
EBP 0019FF94
ESI 00401000
EDI 00401000
EFLAGS 00000246
在EntryPoint入口,按几下F7就到保存通用寄存器和标志寄存器的地方了。在早期版本中执行一条pushad和pushfd就完事了,这里用了很多条,还穿插了很多垃圾指令:
EAX CF1028BC
ECX 00401000
EDX 00401000
EBX 002AD000
ESP 0019FF78
EBP 0019FF94
ESI 00401000
EDI 00401000
EFLAGS 00000246
00401000 > $- E9 5ABC0600 jmp 0046CC5F ; 入口第一条指令
0046CC5F 68 A01ABCE0 push E0BC1AA0 ; KEY
0046CC64 E8 99E3FFFF call 0046B002
0046B002 50 push eax ; 保存原始eax
0046B003 ^ E9 1761FBFF jmp 0042111F
0042111F 52 push edx ; 保存原始edx
00421120 B2 2E mov dl, 2E ; // 垃圾指令
00421122 F6D6 not dh ; // 垃圾指令
00421124 87D2 xchg edx, edx ; // 垃圾指令
00421126 57 push edi ; 保存原始edi
00421127 F7D7 not edi ; // 垃圾指令
00421129 51 push ecx ; 保存原始ecx
0042112A 9C pushfd ; 保存eflags
0042112B 87D7 xchg edi, edx ; // 垃圾指令
0042112D 4F dec edi ; // 垃圾指令
0042112E 53 push ebx ; 保存原始ebx
0042112F FECA dec dl ; // 垃圾指令
00421131 0FBFDB movsx ebx, bx ; // 垃圾指令
00421134 C6C6 99 mov dh, 99 ; // 垃圾指令
00421137 56 push esi ; 保存原始esi
00421138 66:0FCB bswap bx ; // 垃圾指令
0042113B F6D6 not dh ; // 垃圾指令
0042113D 55 push ebp ; 保存原始ebp
0042113E 66:8BF5 mov si, bp ; // 垃圾指令
00421141 B9 00000000 mov ecx, 0 ; // 垃圾指令
00421146 E9 C31A0100 jmp 00432C0E
00432C0E 51 push ecx ; ecx=0,跟以前版本的VMP一样,以push 0为寄存器入栈结束的标志
00401000 > $- E9 5ABC0600 jmp 0046CC5F ; 入口第一条指令
0046CC5F 68 A01ABCE0 push E0BC1AA0 ; KEY
0046CC64 E8 99E3FFFF call 0046B002
0046B002 50 push eax ; 保存原始eax
0046B003 ^ E9 1761FBFF jmp 0042111F
0042111F 52 push edx ; 保存原始edx
00421120 B2 2E mov dl, 2E ; // 垃圾指令
00421122 F6D6 not dh ; // 垃圾指令
00421124 87D2 xchg edx, edx ; // 垃圾指令
00421126 57 push edi ; 保存原始edi
00421127 F7D7 not edi ; // 垃圾指令
00421129 51 push ecx ; 保存原始ecx
0042112A 9C pushfd ; 保存eflags
0042112B 87D7 xchg edi, edx ; // 垃圾指令
0042112D 4F dec edi ; // 垃圾指令
0042112E 53 push ebx ; 保存原始ebx
0042112F FECA dec dl ; // 垃圾指令
00421131 0FBFDB movsx ebx, bx ; // 垃圾指令
00421134 C6C6 99 mov dh, 99 ; // 垃圾指令
00421137 56 push esi ; 保存原始esi
00421138 66:0FCB bswap bx ; // 垃圾指令
0042113B F6D6 not dh ; // 垃圾指令
0042113D 55 push ebp ; 保存原始ebp
0042113E 66:8BF5 mov si, bp ; // 垃圾指令
00421141 B9 00000000 mov ecx, 0 ; // 垃圾指令
00421146 E9 C31A0100 jmp 00432C0E
00432C0E 51 push ecx ; ecx=0,跟以前版本的VMP一样,以push 0为寄存器入栈结束的标志
执行完后堆栈是这样的,就是按照上面的各种push顺序,保存了通用寄存器和标志寄存器:
Address Value Comment
0019FF58 00000000 0
0019FF5C 0019FF94 ebp
0019FF60 00401000 esi
0019FF64 002AD000 ebx
0019FF68 00000246 eflags
0019FF6C 00401000 ecx
0019FF70 00401000 edi
0019FF74 00401000 edx
0019FF78 CF1028BC eax
0019FF7C 0046CC69 RETURN to testVMP_.0046CC69 from testVMP_.0046B002
0019FF80 E0BC1AA0 前面压栈的key
Address Value Comment
0019FF58 00000000 0
0019FF5C 0019FF94 ebp
0019FF60 00401000 esi
0019FF64 002AD000 ebx
0019FF68 00000246 eflags
0019FF6C 00401000 ecx
0019FF70 00401000 edi
0019FF74 00401000 edx
0019FF78 CF1028BC eax
0019FF7C 0046CC69 RETURN to testVMP_.0046CC69 from testVMP_.0046B002
0019FF80 E0BC1AA0 前面压栈的key
由于混淆的指令太多,下面我会把垃圾指令删掉,只保留关键指令,所以地址会有点不连续。
分配VMContext的地址空间:
00432C11 8B7C24 28 mov edi, dword ptr [esp+28]
00432C17 47 inc edi
00432C19 C1CF 02 ror edi, 2
00432C1C 81EF A82E2677 sub edi, 77262EA8
00432C2C C1CF 02 ror edi, 2
00432C33 03F9 add edi, ecx ; 解密edi完成,此时edi指向VM_eip,也就是虚拟机的ByteCode的地址
00432C3C 8BEC mov ebp, esp
00432C3E 81EC C0000000 sub esp, 0C0 ; 分配VMContext的空间,大小0xC0个字节,此时esp指向VMContext,虚拟机栈顶仍为ebp
00432C11 8B7C24 28 mov edi, dword ptr [esp+28]
00432C17 47 inc edi
00432C19 C1CF 02 ror edi, 2
00432C1C 81EF A82E2677 sub edi, 77262EA8
00432C2C C1CF 02 ror edi, 2
00432C33 03F9 add edi, ecx ; 解密edi完成,此时edi指向VM_eip,也就是虚拟机的ByteCode的地址
00432C3C 8BEC mov ebp, esp
00432C3E 81EC C0000000 sub esp, 0C0 ; 分配VMContext的空间,大小0xC0个字节,此时esp指向VMContext,虚拟机栈顶仍为ebp
计算第一个Handler的地址:
00432C5A 8D35 5A2C4300 lea esi, dword ptr [432C5A] ; esi是第一个Handler的地址,但此时还没计算出正确的地址
00432C65 81EF 04000000 sub edi, 4 ; 指向下一条ByteCode的地址,可以看出虚拟机是倒着走的
00432C71 8B17 mov edx, dword ptr [edi] ; 取得第一条ByteCode地址的offset
00432C73 33D3 xor edx, ebx ; 下面开始解密该offset
00432C76 D1CA ror edx, 1
00432C79 0FCA bswap edx
00432C7B 81C2 6C42870C add edx, 0C87426C
00432C81 0FCA bswap edx
00432C86 03F2 add esi, edx ; edx解密完成。加上解密完的offset后,esi就指向了第一个Handler的正确的地址
00432C88 E9 FE450000 jmp 0043728B
0043728B FFE6 jmp esi ; 此时esi就是VM_eip,跳到第一个Handler
00432C5A 8D35 5A2C4300 lea esi, dword ptr [432C5A] ; esi是第一个Handler的地址,但此时还没计算出正确的地址
00432C65 81EF 04000000 sub edi, 4 ; 指向下一条ByteCode的地址,可以看出虚拟机是倒着走的
00432C71 8B17 mov edx, dword ptr [edi] ; 取得第一条ByteCode地址的offset
00432C73 33D3 xor edx, ebx ; 下面开始解密该offset
00432C76 D1CA ror edx, 1
00432C79 0FCA bswap edx
00432C7B 81C2 6C42870C add edx, 0C87426C
00432C81 0FCA bswap edx
00432C86 03F2 add esi, edx ; edx解密完成。加上解密完的offset后,esi就指向了第一个Handler的正确的地址
00432C88 E9 FE450000 jmp 0043728B
0043728B FFE6 jmp esi ; 此时esi就是VM_eip,跳到第一个Handler
第一个Handler,实际上就是把虚拟机栈顶的0给POP出来,然后赋值到VMContext[0x38],这里寄存器edx是作为VMContext保存项的索引:
00422619 8B4425 00 mov eax, dword ptr [ebp] ; ebp指向VMP的栈顶,所以这里相当于POP eax,就是把0出栈到eax
00422624 8DAD 04000000 lea ebp, dword ptr [ebp+4] ; 栈顶指针+4,结合00422619处的指令其实就是一条标准的POP
0042262D 81EF 01000000 sub edi, 1 ; edi指向下一个ByteCode的地址
0042263A 0FB617 movzx edx, byte ptr [edi]
0042264F E9 9DB50500 jmp 0047DBF1
; 这里还有一大堆对edx的解密计算,省略...
; 最终edx=0x38
0047DBFC 890414 mov dword ptr [esp+edx], eax ; edx=0x38, esp=VMContext, VMContext[0x38]=0
00422619 8B4425 00 mov eax, dword ptr [ebp] ; ebp指向VMP的栈顶,所以这里相当于POP eax,就是把0出栈到eax
00422624 8DAD 04000000 lea ebp, dword ptr [ebp+4] ; 栈顶指针+4,结合00422619处的指令其实就是一条标准的POP
0042262D 81EF 01000000 sub edi, 1 ; edi指向下一个ByteCode的地址
0042263A 0FB617 movzx edx, byte ptr [edi]
0042264F E9 9DB50500 jmp 0047DBF1
; 这里还有一大堆对edx的解密计算,省略...
; 最终edx=0x38
0047DBFC 890414 mov dword ptr [esp+edx], eax ; edx=0x38, esp=VMContext, VMContext[0x38]=0
当第一个Handler执行完毕后,通过下面的指令序列计算并跳到下一个Handler:
0047DC26 E9 2D10FEFF jmp 0045EC58
0045EC58 8D80 410B104C lea eax, dword ptr [eax+4C100B41]
0045EC66 03F0 add esi, eax ; esi指向下一个Handler的地址
0045EC68 E9 48960000 jmp 004682B5
004682B5 FFE6 jmp esi ; 真正跳转到下一个Handler
在这里可以看出,并没有一个统一的VMDispatcher,而是通过一个又一个的jmp esi,衔接各个Handler,达到混淆的目的。
0047DC26 E9 2D10FEFF jmp 0045EC58
0045EC58 8D80 410B104C lea eax, dword ptr [eax+4C100B41]
0045EC66 03F0 add esi, eax ; esi指向下一个Handler的地址
0045EC68 E9 48960000 jmp 004682B5
004682B5 FFE6 jmp esi ; 真正跳转到下一个Handler
在这里可以看出,并没有一个统一的VMDispatcher,而是通过一个又一个的jmp esi,衔接各个Handler,达到混淆的目的。
接下来的Handler,实际上是把虚拟机栈顶的ebp给POP出来,然后赋值到VMContext[0x1C]:
00472C9B 8B4425 00 mov eax, dword ptr [ebp] ; 这里是把之前压入栈顶的ebp赋值给eax
00472CA2 81C5 04000000 add ebp, 4 ; POP eax
00478281 890414 mov dword ptr [esp+edx], eax ; edx=0x1C, esp=VMContext, VMContext[0x1C]=ebp
00478288 ^ E9 8BC3FAFF jmp 00424618
00472C9B 8B4425 00 mov eax, dword ptr [ebp] ; 这里是把之前压入栈顶的ebp赋值给eax
00472CA2 81C5 04000000 add ebp, 4 ; POP eax
00478281 890414 mov dword ptr [esp+edx], eax ; edx=0x1C, esp=VMContext, VMContext[0x1C]=ebp
00478288 ^ E9 8BC3FAFF jmp 00424618
看到这里,想必聪明的读者已经找到规律了,还记得最前面入口处的指令是在干什么吗?当时是按照以下的顺序保存通用寄存器和标志位寄存器:
PUSH key
PUSH eax
PUSH edx
PUSH edi
PUSH ecx
PUSH eflags
PUSH ebx
PUSH esi
PUSH ebp
PUSH 0
PUSH key
PUSH eax
PUSH edx
PUSH edi
PUSH ecx
PUSH eflags
PUSH ebx
PUSH esi
PUSH ebp
PUSH 0
刚才上面的两条Handler分别是把栈顶的0和ebp给POP出来(存在eax中),然后保存到VMContext的0x38和0x1C偏移处(用edx表示偏移)。
所以这里实际上是执行连续10条POP指令的Handler,把8个通用寄存器和1个标志位寄存器,以及1个0,还有1个key保存到VMContext中。
为了节省篇幅就不把每个Handler都列出来了,全部执行完之后VMContext是这样的:
struct VMContext
{
+0x38 0
+0x1C ebp
+0x28 esi
+0x24 ebx
+0x04 eflags
+0x08 ecx
+0x14 edi
+0x00 edx
+0x10 eax
+0x34 加密key
};
struct VMContext
{
+0x38 0
+0x1C ebp
+0x28 esi
+0x24 ebx
+0x04 eflags
+0x08 ecx
+0x14 edi
+0x00 edx
+0x10 eax
+0x34 加密key
};
跑了几百条指令,这才把VMContext初始化完成了。
这中间充斥着大量的垃圾指令混淆视听,我们分析的时候不必执着于把每条指令都看懂,只要抓关键点,例如 mov dword ptr [esp+edx], eax 这样的就是在写VMContext数组,记下eax表示写入的内容,edx表示写到VMContext的第几项就行了。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-6-6 10:47
被luocong编辑
,原因: