学逆向论坛

找回密码
立即注册

只需一步,快速开始

发新帖

2万

积分

41

好友

1168

主题
发表于 2019-2-17 18:37:04 | 查看: 12049| 回复: 5
声明:本文出自于学破解论坛的李沉舟,看到大神分析VMP的思路,我感触颇深。我非常愧疚自己当初没认真对待离散数学!

本来想去某处投稿骗点稿费的,结果那地方帖子一直没审查通过,真烦,还是直接发到论坛里来吧……
前言
       VMProtect3很早就出来了,据说代码使用C++重构了,而且虚拟机架构也有很大的变化。网上关于VMP3.X的帖子不是很多,我这个弱鸡也来上篇文章分析一下。
       文章里面用到了一个解混淆的脚本,附录中我会给出这个破脚本的下载链接及大概原理。
三十二变

2019.2.13
准备工作
先对一段VREY EASY的汇编代码进行加密。如下。
802485c693718e263d.png
配置VMProtect V3.3.1对@Main过程进行加密。注意将除代码虚拟化以外的保护全部去除勾选。

附注:那个szText变量请无视,我是写完文章才发现这个坑的,已经懒得改了。
743125c69372617163.png
94105c69372b0e42b.png
编译后,发现文件大小为552KB!意思是说,不包含外壳的代码,只虚拟机部分的代码就膨胀了550KB!不敢想象这是一个怎样的存在,开始我认为是虚拟化的混淆程度又有加强,分析完后才发现原来是虚惊一场……
初探
使用IDA的Trace功能对虚拟机的运行全过程进行记录。发现共执行了1486条语句,这个膨胀率相比与VMP2.X架构可以说是非常小了。加密7条语句,在VMP2.X中光虚拟机指令就可以膨胀到700条左右。

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

则会输出解混淆后的代码。如下。
499445c693748a9240.png
91555c69374d8e8ae.png
注意,栈中的数据被以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次后来到此处。
237935c6937b137a79.png
注意这条shr指令,这表明我们目前处于vShr4 Handle中。
OD中动态调试,在该处下一个条件断点。可以按当时指令流来设置。
中断时,将EAX由0x40修改为0x0即可。
466505c6937bc6ae63.png
211635c6937c104801.png
如果想要深入了解,请去参阅布尔代数,一般讲离散数学的书都有这个内容。

附录
文章中用到的破脚本可以到我的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版本即可

附件已隐藏,回复可下载
游客,如果您要查看本帖隐藏内容请回复

温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046
发表于 2019-2-28 10:00:26

回帖奖励 +2 学币

提醒了我,这学期刚刚学离散,加油加油不能落下!!!
发表于 2019-2-28 12:56:38
发表于 2019-3-28 23:58:06
很厉害有内容的教程,鼓励鼓励!!!!
发表于 2019-3-29 19:41:53
发表于 2021-5-26 18:04:56
Themida / Winlicense (TM / WL)2.4.6-3X以上的 有分析吗?

小黑屋|手机版|站务邮箱|学逆向论坛 ( 粤ICP备2021023307号 )|网站地图

GMT+8, 2024-11-21 23:46 , Processed in 0.162943 second(s), 65 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表