声明:本文出自于学破解论坛的李沉舟,看到大神分析VMP的思路,我感触颇深。我非常愧疚自己当初没认真对待离散数学!
本来想去某处投稿骗点稿费的,结果那地方帖子一直没审查通过,真烦,还是直接发到论坛里来吧……
前言 VMProtect3很早就出来了,据说代码使用C++重构了,而且虚拟机架构也有很大的变化。网上关于VMP3.X的帖子不是很多,我这个弱鸡也来上篇文章分析一下。 文章里面用到了一个解混淆的脚本,附录中我会给出这个破脚本的下载链接及大概原理。 三十二变
2019.2.13 准备工作 先对一段VREY EASY的汇编代码进行加密。如下。
附注:那个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列表中包含每条指令读取/写入的栈变量序号。 [pre]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的地址
[/pre]
总结:在本样本的虚拟机结构中,edi作为虚拟机堆栈,ebp作为指令流指针,ebx仍然作为执行密钥,esi作为中转基址,esp作为上下文指针。传统的分发器结构消失了,取而代之的是一种新的链式结构的虚拟机。不同样本可能会有不同的架构,不像VMP2.X架构,3.X中寄存器是随机选用的。新架构又需要新的分析工具,不知道何年何月才会有牛人共享出来……
再探
vPopImm32
使用脚本对其解混淆后的代码如下。注意,如果后文中没有特殊说明,贴出的代码均为解混淆后的代码。
[pre]
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 = []
[/pre]
该Handle从虚拟机栈中弹出一个DWORD,并写入上下文结构中指定字段。
vPushImm32
[pre]
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 = []
[/pre]
每次执行入栈操作后,都会检查边界,判断虚拟机栈指针与上下文指针是否接近,如果是,则会将上下文结构向下移动,如下。
[pre]
lea eax, [esp+60h]
cmp edi, eax
[/pre]
但是我写的那个破脚本脚本没考虑到这点,会将这段代码舍去,所以需要特别注意一下。
该Handle向虚拟机堆栈中压入一个DWORD大小的常数。
vPushRx32
[pre]
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 = []
[/pre]
该Handle从上下文结构中取出指定字段,并压入堆栈。
vRET
注意,那个破脚本对这条Handle完全不适用了,等我有空再看看BUG。
[pre]
mov esp, edi
pop eax
pop ebp
pop ecx
popf
pop edx
pop edi
pop ebx
pop esi
retn
//还原堆栈,同时还原环境[/pre]
总结:结合上述介绍的Handle,读者可自行完成对本文附带的例子的分析。该例并不复杂,与原汇编代码基本可以说是一一对应的关系。稍微注意一下,对于函数调用,VMP3.X的处理方式是先退出虚拟机,同时将返回地址设为进入虚拟机的代码地址。 举个例子。如下。 [pre]0012FFA8 00401032 <jmp.&user32.MessageBoxA> 0012FFAC 004207C7 1_vmp.004207C7 0012FFB0 00000000 0012FFB4 00403000 ASCII "VMProtect V2.12.3" 0012FFB8 00403012 ASCII "三十二变" 0012FFBC 00000000[/pre] 这是在执行vRET的最后一条retn指令时的堆栈环境。 调用完成后,返回到0x004207C7,又重新进入虚拟机。 [pre]004207C7 68 B31D7ABA push 0xBA7A1DB3004207CC E8 FD8FFEFF call 1_vmp.004097CE[pre]
奇技淫巧之简单爆破
因为业务需要不同,对VM的研究程度也不同,所以对应的也会产生一些奇技淫巧,比如,无脑爆破……不是我BS这种方法,是真的无脑,但很多分析虚拟机的文章都会介绍这个,作为一篇自重自爱的虚拟机介绍文章,本文当然也不会省略这个环节。
举一个例子,如下。
[pre]
cmp eax,2010
je label1[/pre]
我们可以修改cmp指令实现爆破,同样可以修改je指令实现爆破。无脑就无脑到底好了,本文介绍修改跳转指令,因为它不需要了解复杂的逻辑门运算知识。
以爆破je指令为例。
以下为未加密前的源文件。 [pre]
@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
[/pre] 因为VMP有执行密钥,用来动态解密指令流。跳转指令有两个分支,两条分支下去,不可能还能再用同一个密钥继续编码下去。所以遇到流程转移指令一定会重新设置密钥,我们直接搜索mov ebx语句。
第二次对ebx(密钥)直接赋值的地方与第一次(进入虚拟机)相差很远,我们再以此为基准向上搜索00000040(对应的是ZF标志位为1的EFLAGS)。
搜索3次后来到此处。 注意这条shr指令,这表明我们目前处于vShr4 Handle中。
在OD中动态调试,在该处下一个条件断点。可以按当时指令流来设置。
中断时,将EAX由0x40修改为0x0即可。 如果想要深入了解,请去参阅布尔代数,一般讲离散数学的书都有这个内容。
附录 文章中用到的破脚本可以到我的GITEE上面下载。 https://gitee.com/sanxcr/VMPFuck 大概原理就是消除死代码。比如。 [pre] int x; x = 10; ///1 x = 5; //2 x += 20; printf(“%d”, x); [/pre] 其中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版本即可
附件已隐藏,回复可下载
|