About VMProtect

书接上文

上篇中说到,会去写个下载器。

由于下载过程中需要大量人工交互,所以没有自动化的必要。

但下载所用到的软件需要注册,大几千块,虽然公司购买了,但只有一台电脑能用,原因可能是注册过程中有标识机器唯一性的码的参与,具体没细看。

想着crack,查壳,VMProtect 3.x的壳,哇,极有兴趣!!!

但到我写这篇blog的时候,我就丧失了大半兴致,这里只是留个桩,如果以后遇到VMProtect,方便继续研究。

以前就想着研究虚拟机壳,这次趁这个机会好好了解下,半个多月都耗在这上面了,权当写个总结。

VMProtect 入门

google了一堆相关资料,然后慢慢读…

筛选了以下适合入门的资料,认真读完能对VMProtect有一个大致的了解。建议按以下顺序阅读。

http://static.usenix.org/event/woot09/tech/full_papers/rolles.pdf

这篇前面在说废话,精华在II-C Method of Circumvention 以及 III-B推荐的几个链接。因为当时是08年,感觉已经把VM机制研究的挺透彻了

RolfRolles系列:

http://www.openrce.org/articles/full_view/28

这篇读的是最细致的,是当时VM的一种代表,属于简易版VMProtect,但机制什么的跟VMProtect差不多,把这个壳理解了,感觉读相关文章完全不是问题。

这里专门给HyperUnpackMe2做个总结


以往的壳都是在前面的桩代码中加入反汇编和反调试的技术,抹去导入信息等,但最终会还原到原始的样子,也就是存在OEP,在此时dump即可脱壳

这个壳是在理解代码及dump方面设置障碍,比如将部分代码转换成只有内嵌解释器可以执行的字节码(称为VM),还有将部分代码copy到其他地方(称为 stolen bytes, stolen functions)

idb文件已经将一些地方注释过了,所以看不到本来的面目,找不到原加壳程序 ,idb文件应该是已经被作者写的ida的process module处理过了

这个壳采用了上面两种机制,他用了VM并且反调试很严重

作者快速浏览了下,提出以下几个疑点:

  1. 模块内部的直接调用被替换掉了,替换成了int 3 / 5x NOP,此时无法确定是直接修复,还是通过SEH修复

  2. 原来的IIDs和IATs都直接置0

  3. 并没有直接调用导入表

  4. 函数调用那一行被置0,即stolen functions

  5. call和jmp后面的地址都被置为0

首先是VM机制

标准的动态分析虽然理论上是可行的,但是你动态跟VM 解析只有该VM能认识的字节码也没什么意思,不懂VM的机制的话,你是不能理解这条VM指令是在干吗的,只是看见一些值在改变,但你不知道这些改变能够最终产生什么效果,有时候可能最终效果就是一个简单的加运算,但中间过程可能会很复杂,涉及到内存移动,加,栈操作等等一系列操作,也就是动态分析容易混乱且找不到重点

静态分析也比较难,因为不同的VM采用的指令编码方式不一样,并且同一个功能可能指令也不同,即polymorphic。

VM机制解析(文中扎心的几句话):

  1. Before the first instruction is executed, the VM context structure is allocated, and the registers and pointers are initialized, which usually involves allocating memory (perhaps on the host stack) for the VM stack.

  2. After initialization, the archetypal VM enters into a loop which:Decodes instructions at VM_context.EIP,Performs the commands specified by the instruction, and then Calculates the next EIP.

  3. The process of execution usually involves examining the first byte of the instruction and determining which function/switch statement case to execute.

  4. Eventually, the VM reaches some stop condition, and either exits or transfers control back to the native processor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct TH_registers
{
unsigned long rESP; unsigned long r1; unsigned long r2;
unsigned long r3; unsigned long r4; unsigned long r5;
unsigned long r6; unsigned long r7; unsigned long r8;
unsigned long r9; unsigned long rA; unsigned long rB;
unsigned long rC; unsigned long rD; unsigned long rE;
unsigned long rF;
};
struct TH_context
{
unsigned char *vm_data;
unsigned long vm_data_len;
unsigned char *EIP;
unsigned long EFLAGS;
TH_registers registers;
TH_keyed_mem keyed_mem_array[502];
unsigned long stack[0x9000/4];
};

可以看出,总体由一个TH_context结构所描述,该结构中包含了一个TH_registers结构

将原来的代码转换为内嵌VM识别的字节码,该VM对该字节码进行解析,从而理解该字节码所对应的指令功能,从而分发到相应的功能handler,但是指令功能的实现,也就是功能handler的实现,完全是基于原有X86汇编指令功能架构的,从两个例子就可以看出来,只不过是加了一些堆栈转换或者说是为了把原有的x86生硬的改成了VM,所以这个混淆做的不好,作者也举了两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
TheHyper:0104A159 VM_set_flags_dword:
TheHyper:0104A159 cmp [edi], esi
TheHyper:0104A15B pushf
TheHyper:0104A15C pop [eax+VM_context_structure.EFLAGS]

TheHyper:0104A316 VM_jz:
TheHyper:0104A316 push [eax+VM_context_structure.EFLAGS]
TheHyper:0104A319 popf
TheHyper:0104A31A jnz short loc_104A31F
TheHyper:0104A31C mov [eax+VM_context_structure.EIP], edi
TheHyper:0104A31F loc_104A31F:
TheHyper:0104A31F jmp short VM_dispatcher_13h_locret

也就是说instructions are implemented very thinly on top of existing x86 instructions, reflecting the fundamental similarity of this virtual processor to it.

该程序进入VM的流程比较有意思,来看下,从start开始:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
TheHyper:0104A87C
TheHyper:0104A87C public start
TheHyper:0104A87C start proc near
TheHyper:0104A87C 55 push ebp
TheHyper:0104A87D 8B EC mov ebp, esp
TheHyper:0104A87F 83 EC 14 sub esp, 14h
TheHyper:0104A882 8B FC mov edi, esp
TheHyper:0104A884 E8 14 00 00 00 call loc_104A89D
TheHyper:0104A884
TheHyper:0104A884 ; ---------------------------------------------------------------------------
TheHyper:0104A889 1A 37 01 01 dd offset GetProcAddress
TheHyper:0104A88D 16 37 01 01 dd offset LoadLibraryA
TheHyper:0104A891 0E 6F 03 00 dd 36F0Eh
TheHyper:0104A895 97 37 01 01 dd offset unk_1013797
TheHyper:0104A899 2F A8 04 01 dd offset VM__BeginExecution
TheHyper:0104A89D ; ---------------------------------------------------------------------------
TheHyper:0104A89D
TheHyper:0104A89D loc_104A89D: ; CODE XREF: start+8↑p
TheHyper:0104A89D 5E pop esi
TheHyper:0104A89E E8 0D 00 00 00 call loc_104A8B0
TheHyper:0104A89E
TheHyper:0104A89E ; ---------------------------------------------------------------------------
TheHyper:0104A8A3 6B 65 72 6E 65 6C+str__Kernel32_dll_0 db 'kernel32.dll',0
TheHyper:0104A8B0 ; ---------------------------------------------------------------------------
TheHyper:0104A8B0
TheHyper:0104A8B0 loc_104A8B0: ; CODE XREF: start+22↑p
TheHyper:0104A8B0 8B 46 04 mov eax, [esi+4]
TheHyper:0104A8B3 FF 10 call dword ptr [eax]
TheHyper:0104A8B3
TheHyper:0104A8B5 8B D8 mov ebx, eax
TheHyper:0104A8B7 E8 0D 00 00 00 call loc_104A8C9
TheHyper:0104A8B7
TheHyper:0104A8B7 ; ---------------------------------------------------------------------------
TheHyper:0104A8BC 56 69 72 74 75 61+str__Virtualalloc db 'VirtualAlloc',0
TheHyper:0104A8C9 ; ---------------------------------------------------------------------------
TheHyper:0104A8C9
TheHyper:0104A8C9 loc_104A8C9: ; CODE XREF: start+3B↑p
TheHyper:0104A8C9 53 push ebx
TheHyper:0104A8CA 8B 06 mov eax, [esi]
TheHyper:0104A8CC FF 10 call dword ptr [eax]
TheHyper:0104A8CC
TheHyper:0104A8CE 89 07 mov [edi+VM_API_calls.VirtualAlloc], eax
TheHyper:0104A8D0 E8 0C 00 00 00 call loc_104A8E1
TheHyper:0104A8D0
TheHyper:0104A8D0 ; ---------------------------------------------------------------------------
TheHyper:0104A8D5 56 69 72 74 75 61+str__Virtualfree db 'VirtualFree',0
TheHyper:0104A8E1 ; ---------------------------------------------------------------------------
TheHyper:0104A8E1
TheHyper:0104A8E1 loc_104A8E1: ; CODE XREF: start+54↑p
TheHyper:0104A8E1 53 push ebx
TheHyper:0104A8E2 8B 06 mov eax, [esi]
TheHyper:0104A8E4 FF 10 call dword ptr [eax]
TheHyper:0104A8E4
TheHyper:0104A8E6 89 47 04 mov [edi+VM_API_calls.VirtualFree], eax
TheHyper:0104A8E9 E8 0F 00 00 00 call loc_104A8FD
TheHyper:0104A8E9
TheHyper:0104A8E9 ; ---------------------------------------------------------------------------
TheHyper:0104A8EE 47 65 74 50 72 6F+str__Getprocessheap db 'GetProcessHeap'
TheHyper:0104A8FC 00 db 0
TheHyper:0104A8FD ; ---------------------------------------------------------------------------
TheHyper:0104A8FD
TheHyper:0104A8FD loc_104A8FD: ; CODE XREF: start+6D↑p
TheHyper:0104A8FD 53 push ebx
TheHyper:0104A8FE 8B 06 mov eax, [esi]
TheHyper:0104A900 FF 10 call dword ptr [eax]
TheHyper:0104A900
TheHyper:0104A902 89 47 08 mov [edi+VM_API_calls.GetProcessHeap], eax
TheHyper:0104A905 E8 0A 00 00 00 call loc_104A914
TheHyper:0104A905
TheHyper:0104A905 ; ---------------------------------------------------------------------------
TheHyper:0104A90A 48 65 61 70 41 6C+str__Heapalloc db 'HeapAlloc'
TheHyper:0104A913 00 align 4
TheHyper:0104A914
TheHyper:0104A914 loc_104A914: ; CODE XREF: start+89↑p
TheHyper:0104A914 53 push ebx
TheHyper:0104A915 8B 06 mov eax, [esi]
TheHyper:0104A917 FF 10 call dword ptr [eax]
TheHyper:0104A917
TheHyper:0104A919 89 47 0C mov [edi+VM_API_calls.HeapAlloc], eax
TheHyper:0104A91C E8 09 00 00 00 call loc_104A92A
TheHyper:0104A91C
TheHyper:0104A91C ; ---------------------------------------------------------------------------
TheHyper:0104A921 48 65 61 70 46 72+str__Heapfree db 'HeapFree',0
TheHyper:0104A92A ; ---------------------------------------------------------------------------
TheHyper:0104A92A
TheHyper:0104A92A loc_104A92A: ; CODE XREF: start+A0↑p
TheHyper:0104A92A 53 push ebx
TheHyper:0104A92B 8B 06 mov eax, [esi]
TheHyper:0104A92D FF 10 call dword ptr [eax]
TheHyper:0104A92D
TheHyper:0104A92F 89 47 10 mov [edi+VM_API_calls.HeapFree], eax
TheHyper:0104A932 57 push edi
TheHyper:0104A933 FF 76 08 push dword ptr [esi+8]
TheHyper:0104A936 FF 76 0C push dword ptr [esi+0Ch]
TheHyper:0104A939 FF 56 10 call dword ptr [esi+10h]
TheHyper:0104A939
TheHyper:0104A93C 8B E5 mov esp, ebp
TheHyper:0104A93E 5D pop ebp
TheHyper:0104A93F C3 retn
TheHyper:0104A93F
TheHyper:0104A93F start endp

TheHyper:0104A884 E8 14 00 00 00 call loc_104A89D
TheHyper:0104A884
TheHyper:0104A884 ; ---------------------------------------------------------------------------
TheHyper:0104A889 1A 37 01 01 dd offset GetProcAddress
TheHyper:0104A88D 16 37 01 01 dd offset LoadLibraryA
TheHyper:0104A891 0E 6F 03 00 dd 36F0Eh
TheHyper:0104A895 97 37 01 01 dd offset unk_1013797
TheHyper:0104A899 2F A8 04 01 dd offset VM__BeginExecution
TheHyper:0104A89D ; ----------------------------------------------------

注意第一个call,call指令会将下一条指令的地址压栈,即栈顶为0104A889,然后在loc_104A89D中第一条指令就是pop esi,所以esi
的值就是0x0104A889,而且在loc_104a89d中并没有ret指令。

1
2
3
4
5
6
7
8
9
10
pop     esi
TheHyper:0104A89E E8 0D 00 00 00 call loc_104A8B0
TheHyper:0104A89E
TheHyper:0104A89E ; ---------------------------------------------------------------------------
TheHyper:0104A8A3 6B 65 72 6E 65 6C+str__Kernel32_dll_0 db 'kernel32.dll',0
TheHyper:0104A8B0 ; ---------------------------------------------------------------------------
TheHyper:0104A8B0
TheHyper:0104A8B0 loc_104A8B0: ; CODE XREF: start+22↑p
TheHyper:0104A8B0 8B 46 04 mov eax, [esi+4]
TheHyper:0104A8B3 FF 10 call dword ptr [eax]

首先是0104A89E处的call,这个call会将0104A8A3压入栈中,该地址其实为kernel32.dll字符串的指针esi+4即为LoadLibraryA,则下面的call就是call LoadLibraryA,而参数就是通过0104A89E处的call压入栈中的kernel32.dll字符串的指针,下面就是同样的套路,最后,

1
2
3
4
TheHyper:0104A932 57                                push    edi
TheHyper:0104A933 FF 76 08 push dword ptr [esi+8]
TheHyper:0104A936 FF 76 0C push dword ptr [esi+0Ch]
TheHyper:0104A939 FF 56 10 call dword ptr [esi+10h]

通过上面的几句,进入 VM__BeginExecution(unk_1013797,36F0Eh,原栈指针),unk_1013797就是VM字节码的开始地址,即VM_EIP的初始值,通过以下可以验证:

1
2
3
TheHyper:0104A849 8B 5D 08                          mov     ebx, [ebp+arg_0_VM_data_ptr]
TheHyper:0104A84C 89 1A mov [edx+VM_context_structure.VM_data], ebx
TheHyper:0104A84E 89 5A 08 mov [edx+VM_context_structure.EIP], ebx

进入VM__BeginExecution后,就进入了正常的VM循环,即取值,译码,执行的loop

重点来分析下该VM的反dump措施,就是重定向程序流,只不过方式略有不同,

The stolen functions are copied into VirtualAlloc'ed memory.
The API calls and API-referencing instructions point to obfuscated stubs which eventually redirect to their intended targets, which are actually in copies of the referenced DLLs, not the originals. There are 73 kilobytes' worth of obfuscated stubs in the packer section.
Relative jumps and calls travel through tiny stub functions in VirtualAlloc'ed memory onto their destinations. 

可以总结为,原来的改变执行流的语句,如call,jmp等,后面跟的地址被置为0,然后通过一系列的桩代码对该地址进行修复,修复过后,该地址会修复为动态分配的地址,接下来会跳转到动态分配的地址处去执行,也就是说所有的功能函数代码,都是出现在动态分配的内存中

有一个问题一直困扰我,就是上面的措施是如何进行anti-dump的呢,可以这样理解:

以 call 0x1234为例,其中0x1234是功能函数func在.text段的地址,在加过这个壳后,根据上面的分析,call 0x1234会变成call 0x0000(具体几个0看情况而定),而原来处于.text段的0x1234处的具体的功能代码会被硬编码到TheHyper段的某个位置,而且这个位置的地址会出现在VM修复call 0x0000的桩代码中,修复的原则是,将TheHyper段的原0x1234函数体代码拷贝到一块新分配的内存区然后将call 0x0000修改为call 新分配内存区的修改,当然会对函数体本身的一些操作数地址进行修复,以确保函数体的正常运行.而dump是将进程此时在内存中的状态给转储下来,生成一个文件,我们从dump的结果出发,常规的dump的文件经过修复iat的操作即可变为可执行文件而可执行文件中只包含.text,.data段等区段,并不会将栈,堆等内存空间dump下来,也就是说,如果你在修复成功后进行dump的话,dump下来的为call 新分配的内存地址(是一个固定的数值,如0x5678),而0x5678一般是处于堆中的,是不会dump下来的,所以你dump下来的文件在执行到该语句后就会出现访问错误,我们dump的目的就是为了将具体的功能代码给dump下来,而上面的措施通过将控制流重定向到动态分配的内存空间地址来anti-dump

修复错误语句的桩代码是VM才能识别的字节码


http://www.openrce.org/blog/view/913/T2_2006_VM_Analysis

http://www.openrce.org/blog/view/1110/Compiler_1,_X86_Virtualizer_0

https://www.openrce.org/blog/view/1239/Part_1:__Bytecode_and_IR

http://www.msreverseengineering.com/blog/2014/6/23/vmprotect-part-0-basics

http://www.msreverseengineering.com/blog/2014/6/23/1v20av0uhf5kygyyaprvj2i6u5ze2a

http://www.msreverseengineering.com/blog/2014/6/23/vmprotect-part-2-primer-on-optimization

http://www.msreverseengineering.com/blog/2014/6/23/vmprotect-part-3-optimization-and-code-generation

VMProtect相关分析

https://forum.tuts4you.com/topic/30733-vmprotect-ultra-unpacker-10/

这个比较成熟,而且好像VMProtect3.0以下的可以做到通吃。。。

https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0ahUKEwj_yKqyrL7YAhWIFJQKHZA-D5AQFgglMAA&url=http%3A%2F%2Flille1tv.univ-lille1.fr%2Ftelecharge.aspx%3Fid%3Dd5b2487e-cacc-4596-ab37-dab2b362cb9e&usg=AOvVaw2WDrd43-or5xXFy6sV_65u

http://shell-storm.org/talks/SSTIC2017_Deobfuscation_of_VM_based_software_protection.pdf

https://zhuanlan.kanxue.com/article-354.htm

https://lifeinhex.com/use-of-syscall-and-sysenter-in-vmprotect-3-1/

https://zhuanlan.kanxue.com/article-354.htm

https://www.52pojie.cn/forum.php?mod=viewthread&tid=586130


在52 VMProtect3分析的文章中,VM的初始化其实分为两个阶段:

  1. 寄存器入栈(该栈为x86下的原始栈) + 初始化各寄存器的值(esi,ebp等),也可以理解为将各寄存器赋予VM意义

  2. 通过10个pop handler将刚才压入x86栈中的真实寄存器的值复制一份到虚拟寄存器数组vm_context中

1
2
3
4
5
6
7
8
9
10
11
00481F31 Main     call console_.00436946                    ; ESP=0019FF78

;寄存器入栈
00436946 Main push eax ; ESP=0019FF74
00436965 Main push eax ; ESP=0019FF54
004369A7 Main mov ebp,esp ; EBP=0019FF54
004369B0 Main sub esp,0xC0 ; ESP=0019FE94
第一个pop handler:
0043EB6B Main mov ecx,dword ptr ss:[ebp] ; ECX=00000000
0042B819 Main add ebp,0x4 ; FL=0, EBP=0019FF58
004539B2 Main mov dword ptr ss:[esp+eax],ecx

在第一个pop handler中,结合EBP依然是vm_esp,指向虚拟栈顶,ESP成为vm_context指针来理解,可以看出是将寄存器入栈过程中的最后一个入栈的值,即0给复制到VM_context + 0x38的地方,即复制到了VM_context的第(0x38/4)=14个寄存器中,用VMP分析插件的命名风格,即为vPopReg4 R14 (0x38),注意上述指令中esp及ebp寄存器的值,不难验证,10个pop handler正好把压入x86栈中的10个值给复制了一份到VM_context中这10个值,由为了恢复x86环境的8个寄存器的值 + VM中要用到的2个值组成,一个是解密的初始key,一个是参数0

初始化可以理解为x86 -> VM的crossover

执行完上面的初始化后,就开始了与原指令相同功能的VM handler的执行,考虑到VM是栈机,所以一条CISC指令会变为几条RISC指令来通过
栈操作来实现,比如mov eax, 0deadbeefh就会变为push + pop实现,即vPushImm4 DEADBEEF vPopReg4 R4 (0x10)

执行完功能代码对应的VM字节码,就需要完成VM -> x86的crossover了:

  1. 将VM_context保存的用于恢复x86环境的寄存器的值复制一份到x86栈上,

  2. 栈桢切换 + pop恢复x86环境,也可以理解为将各寄存器恢复x86意义


https://www.52pojie.cn/thread-607830-1-1.html

上面很多文章中都提到了解决VMProtect的思路:

  1. 分析VM机,得到整个VM机的handler机器对应语义,据此来解释VM字节码的含义,得到程序原始功能,在VM字节码翻译为x86汇编码的过程中,可以使用IR、编译优化等来达到化简的效果
  2. 通过动态执行以及污点分析等技术,分析VM机解析VM字节码功能的变化,将该功能的变化用其他语言来进行描述,使之产生相同的影响,即只在功能上恢复原程序(这种方法前景较大)

VMProtect 辅助工具

https://www.52pojie.cn/thread-381423-1-1.html

https://github.com/jjyg/metasm

https://github.com/0xbadc0de1/VmP_DBG

https://github.com/0xbadc0de1/VMP_Tools/tree/master/Documents/Visual%20Studio%202013/Projects/vmp_coprocessor/vmp_coprocessor

https://github.com/JonathanSalwan/Tigress_protection

https://github.com/anatolikalysch/VMAttack

https://github.com/jnraber/VirtualDeobfuscator

小结 & 计划

  1. 搞不动VMProtect3.x
  2. 本想早点分析VMProtect的工具的源码来读下,但读不下去,关键是兴致没了
  3. 在此过程中,看到了动态执行、污点分析、数据流分析之类的,接下来应该会去搞搞这方面,看能不能把我在angr理解方面的问题解决了,这里会以JonathanSalwan的Triton为切入点来进行研究,JonathanSalwan牛逼,orz!!!