前言
VMProtect3很早就出来了,据说代码使用C++重构了,而且虚拟机架构也有很大的变化。网上关于VMP3.X的帖子不是很多,我这个弱鸡也来上篇文章分析一下。
文章里面用到了一个解混淆的脚本,附录中我会给出这个破脚本的下载链接及大概原理。
三十二变
2019.2.13
准备工作
先对一段VREY EASY的汇编代码进行加密。如下。

配置VMProtect V3.3.1对@Main过程进行加密。注意将除代码虚拟化以外的保护全部去除勾选。
附注:那个szText变量请无视,我是写完文章才发现这个坑的,已经懒得改了。


编译后,发现文件大小为552KB!意思是说,不包含外壳的代码,只虚拟机部分的代码就膨胀了550KB!不敢想象这是一个怎样的存在,开始我认为是虚拟化的混淆程度又有加强,分析完后才发现原来是虚惊一场……
初探
使用IDA的Trace功能对虚拟机的运行全过程进行记录。发现共执行了1486条语句,这个膨胀率相比与VMP2.X架构可以说是非常小了。加密7条语句,在VMP2.X中光虚拟机指令就可以膨胀到700条左右。
仍然在Trace文件中从头部开始搜索RET指令。如下。

有一个比较令人惊讶的发现,VMP3.X没有采用栈混淆。纵览进入虚拟机的环境备份代码,发现只有简单的针对寄存器的插入死代码。
注意,不完全是以push esi/retn指令对的形式进行转移,还有以jmp esi实现流程转移的情况,事实上前者完全可以看成后者的一个变形。
运行脚本,按要求输入进程ID,起始地址(可以输入Dispatch或Handle的起始地址)。
则会输出解混淆后的代码。如下。


注意,栈中的数据被以DWORD为单位从0开始编号,read/write列表中包含每条指令读取/写入的栈变量序号。
[Asm] 纯文本查看 复制代码
|
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
45E756 push0x6dae122f read = [] write = [0]
//此处压入一个加密后的常数,经解码后可以取出指令流地址
4748B1 push esiread = [] write = [2]
4748B5 push ebxread = [] write = [3]
4748B8 push ediread = [] write = [4]
4748BF push edxread = [] write = [5]
4748C7pushfd read = [] write = [6]
4748C8 push ecxread = [] write = [7]
4748C9 push ebpread = [] write = [8]
4748CA push eaxread = [] write = [9]
456DA6 mov eax,0 read = [] write = []
456DB2 push eaxread = [] write = [10]
//上面这个代码块是在备份虚拟机执行环境,以及重定位信息
456DBA mov ebp,dword ptr [esp + 0x28] read = [0] write = []
456DC2 neg ebpread = [] write = []
456DC4 dec ebpread = [] write = []
456DC9 rol ebp,3 read = [] write = []
456DCC xor ebp,0x3684751d read = [] write = []
456DDC lea ebp,[ebp + 0x5bae11e7] read = [] write = []
456DEA neg ebpread = [] write = []
456DEC add ebp,eax read = [] write = []
//上面这个代码块取出了压入的加密常数,进行一系列解密操作后得到指令流起始地址,并赋值给了ebp,故ebp作为新的指令流寄存器,add ebp,eax是修正重定位操作
456DF1 mov edi,esp read = [] write = []
//edi是虚拟机堆栈
456E0B mov ebx,ebp read = [] write = []
//初始化执行密钥,ebx仍然是密钥寄存器
456E26 lea esi,[0x456e26] read = [] write = []
//esi = 0x456e26
456E33 mov ecx,dword ptr [ebp] read = [] write = []
456E37 add ebp,4 read = [] write = []
456E40 xor ecx,ebx read = [] write = []
456E42 dec ecxread = [] write = []
456E44 xor ecx,0x25873dcc read = [] write = []
4691EB inc ecxread = [] write = []
4691EF neg ecxread = [] write = []
4691F1 xor ebx,ecx read = [] write = []
4691F3 add esi,ecx read = [] write = []
41F273 push esiread = [] write = [59]
41F274 ret read = [59] write = []
//上面这个代码块从指令流中取出了一个DWORD,赋予ecx,并递增了指令流指针。将取出的指令流用密钥进行解密,而后与esi相加,得到下一条Handle的地址
总结:在本样本的虚拟机结构中,edi作为虚拟机堆栈,ebp作为指令流指针,ebx仍然作为执行密钥,esi作为中转基址,esp作为上下文指针。传统的分发器结构消失了,取而代之的是一种新的链式结构的虚拟机。不同样本可能会有不同的架构,不像VMP2.X架构,3.X中寄存器是随机选用的。新架构又需要新的分析工具,不知道何年何月才会有牛人共享出来……
再探
vPopImm32
使用脚本对其解混淆后的代码如下。注意,如果后文中没有特殊说明,贴出的代码均为解混淆后的代码。
46C27Fmov edx, dword ptr [edi] read = [] write = []
46C28Cadd edi, 4 read = [] write = []
//从虚拟机堆栈中弹出操作数
46C292movzx ecx, byte ptr [ebp] read = [] write = []
46C297lea ebp, [ebp + 1] read = [] write = []
46C2A3xor cl, bl read = [] write = []
46C2A5neg cl read = [] write = []
442B1Ainc cl read = [] write = []
442B1Dror cl, 1 read = [] write = []
412AB8neg cl read = [] write = []
412AC0xor bl, cl read = [] write = []
//从指令流中取出操作数(在上下文结构中的偏移),并解密,更新密钥,递增指令流
412AC3mov dword ptr [esp + ecx], edx read = [] write = []
412AC8mov ecx, dword ptr [ebp] read = [] write = []
//写入上下文结构
412AD0add ebp, 4 read = [] write = []
412AD7xor ecx, ebx read = [] write = []
412ADFsub ecx, 0x2d2f25e5 read = [] write = []
412AE5rol ecx, 2 read = [] write = []
412AE8sub ecx, 0x1a4c24fd read = [] write = []
43CA2Aror ecx, 3 read = [] write = []
43CA2Dxor ebx, ecx read = [] write = []
43CA34add esi, ecx read = [] write = []
43CA36jmp esi read = [] write = []
该Handle从虚拟机栈中弹出一个DWORD,并写入上下文结构中指定字段。
vPushImm32
48D637mov eax, dword ptr [ebp] read = [] write = []
48D63Blea ebp, [ebp + 4] read = [] write = []
48D646xor eax, ebx read = [] write = []
48D64Cadd eax, 0xb4c16be read = [] write = []
48D651not eax read = [] write = []
48D656lea eax, [eax - 0x51cc037d] read = [] write = []
48D65Cmov cl, 0x42 read = [] write = []
48D65Eneg eax read = [] write = []
48D66Cror eax, 1 read = [] write = []
48D676lea eax, [eax - 0x61b03f11] read = [] write = []
//从指令流中取出操作数,并进行解密操作
48D67Cxor ebx, eax read = [] write = []
48D67Flea edi, [edi - 4] read = [] write = []
48D688mov dword ptr [edi], eax read = [] write = []
//将解密后的操作数压入虚拟机堆栈
48D691mov ecx, dword ptr [ebp] read = [] write = []
48D695lea ebp, [ebp + 4] read = [] write = []
40E56Dxor ecx, ebx read = [] write = []
40E570rol ecx, 3 read = [] write = []
40E576sub ecx, 0x1865595b read = [] write = []
40E583bswap ecx read = [] write = []
40E586lea ecx, [ecx - 0x7c371840] read = [] write = []
40E58Dxor ebx, ecx read = [] write = []
422848add esi, ecx read = [] write = []
4520F4lea eax, [esp + 0x60] read = [] write = []
48294Cpush esi read = [] write = [0]
48294Dret read = [0] write = []
每次执行入栈操作后,都会检查边界,判断虚拟机栈指针与上下文指针是否接近,如果是,则会将上下文结构向下移动,如下。
lea eax, [esp+60h]
cmp edi, eax
但是我写的那个破脚本脚本没考虑到这点,会将这段代码舍去,所以需要特别注意一下。
该Handle向虚拟机堆栈中压入一个DWORD大小的常数。
vPushRx32
413B2Amovzx edx, byte ptr [ebp] read = [] write = []
413B2Fsetge al read = [] write = []
413B32shl ah, 0x30 read = [] write = []
413B35add ebp, 1 read = [] write = []
413B46xor dl, bl read = [] write = []
413B51ror dl, 1 read = [] write = []
413B53sub dl, 0x3e read = [] write = []
413B5Aneg dl read = [] write = []
413B5Frol dl, 1 read = [] write = []
413B61inc dl read = [] write = []
413B6Arol dl, 1 read = [] write = []
413B78xor bl, dl read = [] write = []
//取出操作数(在上下文结构中的偏移,并解密)
413B7Dmov eax, dword ptr [esp + edx] read = [] write = []
//取出指定字段
413B89sub edi, 4 read = [] write = []
413B91mov dword ptr [edi], eax read = [] write = []
//压栈
413B93mov edx, dword ptr [ebp] read = [] write = []
413B9Dadd ebp, 4 read = [] write = []
413BAAxor edx, ebx read = [] write = []
413BADror edx, 1 read = [] write = []
413BB3neg edx read = [] write = []
413BB9lea edx, [edx - 0x796d16c6] read = [] write = []
413BC2not edx read = [] write = []
413BC9xor ebx, edx read = [] write = []
413BCBadd esi, edx read = [] write = []
4520F4lea eax, [esp + 0x60] read = [] write = []
48294Cpush esi read = [] write = [0]
48294Dret read = [0] write = []
该Handle从上下文结构中取出指定字段,并压入堆栈。
vRET
注意,那个破脚本对这条Handle完全不适用了,等我有空再看看BUG。
mov esp, edi
pop eax
pop ebp
pop ecx
popf
pop edx
pop edi
pop ebx
pop esi
retn
//还原堆栈,同时还原环境
|
总结:结合上述介绍的Handle,读者可自行完成对本文附带的例子的分析。该例并不复杂,与原汇编代码基本可以说是一一对应的关系。稍微注意一下,对于函数调用,VMP3.X的处理方式是先退出虚拟机,同时将返回地址设为进入虚拟机的代码地址。
举个例子。如下。
0012FFA8 00401032 <jmp.&user32.MessageBoxA>
0012FFAC 004207C7 1_vmp.004207C7
0012FFB0 00000000
0012FFB4 00403000 ASCII "VMProtect V2.12.3"
0012FFB8 00403012 ASCII "三十二变"
0012FFBC 00000000
这是在执行vRET的最后一条retn指令时的堆栈环境。
调用完成后,返回到0x004207C7,又重新进入虚拟机。
004207C7 68 B31D7ABA push 0xBA7A1DB3
004207CC E8 FD8FFEFF call 1_vmp.004097CE
奇技淫巧之简单爆破
因为业务需要不同,对VM的研究程度也不同,所以对应的也会产生一些奇技淫巧,比如,无脑爆破……不是我BS这种方法,是真的无脑,但很多分析虚拟机的文章都会介绍这个,作为一篇自重自爱的虚拟机介绍文章,本文当然也不会省略这个环节。
举一个例子,如下。
cmp eax,2010
je label1
我们可以修改cmp指令实现爆破,同样可以修改je指令实现爆破。无脑就无脑到底好了,本文介绍修改跳转指令,因为它不需要了解复杂的逻辑门运算知识。
以爆破je指令为例。
以下为未加密前的源文件。
@Main proc
mov eax,2018h
.if eax == 2019h
invoke MessageBox,0,offset szOK,offset szTitle,MB_OK
.else
invoke MessageBox,0,offset szNO,offset szTitle,MB_OK
.endif
ret
@Main endp
因为VMP有执行密钥,用来动态解密指令流。跳转指令有两个分支,两条分支下去,不可能还能再用同一个密钥继续编码下去。所以遇到流程转移指令一定会重新设置密钥,我们直接搜索mov ebx语句。
第二次对ebx(密钥)直接赋值的地方与第一次(进入虚拟机)相差很远,我们再以此为基准向上搜索00000040(对应的是ZF标志位为1的EFLAGS)。
搜索3次后来到此处。
举一个例子,如下。
cmp eax,2010
je label1
我们可以修改cmp指令实现爆破,同样可以修改je指令实现爆破。无脑就无脑到底好了,本文介绍修改跳转指令,因为它不需要了解复杂的逻辑门运算知识。
以爆破je指令为例。
以下为未加密前的源文件。
@Main proc
mov eax,2018h
.if eax == 2019h
invoke MessageBox,0,offset szOK,offset szTitle,MB_OK
.else
invoke MessageBox,0,offset szNO,offset szTitle,MB_OK
.endif
ret
@Main endp
因为VMP有执行密钥,用来动态解密指令流。跳转指令有两个分支,两条分支下去,不可能还能再用同一个密钥继续编码下去。所以遇到流程转移指令一定会重新设置密钥,我们直接搜索mov ebx语句。
第二次对ebx(密钥)直接赋值的地方与第一次(进入虚拟机)相差很远,我们再以此为基准向上搜索00000040(对应的是ZF标志位为1的EFLAGS)。
搜索3次后来到此处。

注意这条shr指令,这表明我们目前处于vShr4 Handle中。
在OD中动态调试,在该处下一个条件断点。可以按当时指令流来设置。
中断时,将EAX由0x40修改为0x0即可。
在OD中动态调试,在该处下一个条件断点。可以按当时指令流来设置。
中断时,将EAX由0x40修改为0x0即可。


如果想要深入了解,请去参阅布尔代数,一般讲离散数学的书都有这个内容。
附录
2.出口代码处的所有栈变量都是活跃的
3.栈变量的活跃性持续向上传递,但遇到对该栈变量的读写操作时,会变更活跃性
4.当某一栈变量向上传递活跃性遇到写操作时,则活跃性变为死状态,若遇到读操作时,则活跃性变为活状态
5.当出现对同一栈变量进行读写操作时,我们默认读操作先于写操作
具体实现请看脚本……
附录
文章中用到的破脚本可以到我的GITEE上面下载。
https://gitee.com/sanxcr/VMPFuck
大概原理就是消除死代码。比如。
int x;
x = 10; ///1
x = 5; //2
x += 20;
printf(“%d”, x);
其中1是死代码,因为1仅对x进行赋值操作而x在引用前就被重新定值了。
因为VMP3.X中没有栈混淆,所以可以直接对寄存器进行消除死代码。
而VMP2.X因为有栈混淆,所以可能复杂一些,思路就是将栈中的数据以DWORD为单位开始编号,仍然将它们视为变量,进行消除死代码。
以栈变量活跃分析为例(寄存器与标志位的活跃分析是一样的),需要遵循以下原则。
1.活跃分析应从底部向顶部分析2.出口代码处的所有栈变量都是活跃的
3.栈变量的活跃性持续向上传递,但遇到对该栈变量的读写操作时,会变更活跃性
4.当某一栈变量向上传递活跃性遇到写操作时,则活跃性变为死状态,若遇到读操作时,则活跃性变为活状态
5.当出现对同一栈变量进行读写操作时,我们默认读操作先于写操作
具体实现请看脚本……
使用需基于capstone、pythonwin
安装capstone:
1. pip install capstone
安装pythonwin:
1. 百度搜索pythonwin,找到对应你的python版本即可
512

被折叠的 条评论
为什么被折叠?



