文章总结: 文档分析了VMProtect3.8.1版本在源码泄露后实施的重大混淆策略升级,包括全指令变形、内存地址常数加密、Handler变形与融合等核心技术。这些改进显著提升了代码抗逆向分析能力,使传统基于指令特征匹配的分析方法失效,体现了软件保护技术的前沿发展。 综合评分: 85 文章分类: 逆向分析,二进制安全,安全开发,恶意软件,其他
VMProtect 3.8.1 混淆策略大揭秘
阿强 阿强
看雪学苑
2026年5月6日 17:59 上海
在小说阅读器读本章
去阅读
自从VMProtect 3.5.1 被爆出源码泄露开始直到最后的那一两个最重要的源文件也被公之于众之后,VMProtect 3.5.1算是在所有的逆向开发人员面前裸奔了。
有意思的是VMProtect并没有就此一蹶不振,反而绝地反击,它的实力和之前相比反而变得更强了,这是怎么回事呢?
这里面我们八卦一下,扒一扒他的版本,源码完全流出直到论坛上有大神分享编译的文章这时候是 2023年12月份左右,我们记住这个时间点,当我们全民都拥有源码的时候,其实VMProtect早已脱胎换骨了,我们来到他的官网https://vmpsoft.com/news/page/3/可以找到他的更新日志。
我们可以看到这里增加好的选项,这些选项可不是简简单单的混淆,每一个拿出来都是王炸,毫不夸张的说如果3.8版本之后的VMProtect是一位浑身都是的肌肉的猛男。
那么3.8版本之前的VMProtect只能算是一个娇羞的小萝莉,我们可以看到3.8更新的时间点是2023年初,这里我们大家可以讨论一下VMProtect3.8的大更新是不是因为源码泄露而做出的紧急补救手段。因为VMProtect3.8发布的时间恰巧也是源码泄露的那段时间,当时我第一次得知VMProtect源码泄露的时候,还以为VMProtect就此跌落神坛,从此一蹶不振,没想到他居然放出大招,这次的大更新可以将其称之为新版VMProtect。
它不是像牙膏厂一样挤药膏似的更新,而是夸张到等同于重构了整个源码,增加了非常多的变态混淆策略,在很多地方都进行了非常大的改动。那么接下来我们将逐一分析他的逆天改动。
第1点:全指令变形,但凡能够使用替代指令几乎都使用了,目前只有异或解密指令没有进行混淆。
其他所有指令都或多或少有一两种变体,大家也可以联想到他们变种的样子,这里我举几个例子。
伪代码解密:
00422864 | xoral,byteptrss:[ebp+edx-0x7]//伪代码读取指令有四种变体
00422868 | xoral,bl//无法变形,维持原样
0042286A | jmp123.vmp模版+部分.42BE65
0042BE65 | adddwordptrss:[esp+edx*2+0x2],0xC8169CA3 |
0042BE6D | incal
0042BE6F | xchgwordptrss:[esp+edx+0x1],dx |
0042BE74 | movdwordptrss:[esp+edx*8-0x729D0],edx |
0042BE7B | shldwordptrss:[esp+edx-0xE53B],0x57 |
0042BE83 | xoral,0xA6
0042BE85 | leaedx,dwordptrds:[edx*2+0x2FBCE804] |
0042BE8C | incdl |
0042BE8E | setedl |
0042BE91 | decal
0042BE93 | jmp123.vmp模版+部分.5088D5 |
005088D5 | call123.vmp模版+部分.5074A0 |
005074A0 | negal
005074A2 | popedx |
005074A3 | addedx,0xFFF7AC3A |
005074A9 | jmpedx |
00483514 | movedx,0x119DB08A
00483519 | xchgbyteptrss:[esp+edx*4-0x4676C225],dl |
00483520 | addal,0x9E
00483522 | leaedx,dwordptrds:[edx+edx*4+0x11B81E34]
00483529 | xaddwordptrss:[esp+edx-0x69CC901C],dx |
00483532 | xorbl,al//无法变形,维持原样
伪代码读取指令的五种可能形式:
moveax,dwordptrss:[esi]//原始版本
xoreax,eax//新增前置eax清零指令,也可以是其他清零指令
addeax,dwordptrss:[esi]//变体1
xoreax,dwordptrss:[esi]//变体2
oreax,dwordptrss:[esi]//变体3
adceax,dwordptrss:[esi]//变体4
计算Handler地址:
004C9172 | moveax,dwordptrss:[ebp-0x4]//读取伪代码
004C9176 | jno123.vmp模版+部分.46E996
0046E996 | movedx,0x581983A |
0046E99B | xoreax,ebx
0046E99D | roreax,0x1
0046E99F | addedx,edx |
0046E9A1 | bswapeax
0046E9A3 | pushedx |
0046E9A4 | shlbyteptrss:[esp+edx*8-0x5819839E],0x84 |
0046E9AC | inceax
0046E9AD | jmp123.vmp模版+部分.490ABF |
00490ABF | orwordptrss:[esp+edx*8-0x581983A0],dx |
00490AC7 | movsxecx,dx |
00490ACA | roleax,0x2
00490ACD | shldwordptrss:[esp+edx-0xB033074],cl |
00490AD4 | xorebx,eax//解密加数完成
00490AD6 | orcl,byteptrss:[esp+edx-0xB033071]//增加前置cf标志清零指令,执行完cf = 0
00490ADD | adcedi,eax//add指令的变体
第2点:内存地址常数加密,一条包含内存操作数的指令,那么他的内存操作数通常有几种情况。
只有内存地址常数 例如 mov eax,dword ptr ss:[0x123] 只有基址寄存器 mov dword ptr ss:[esp], 0x123 基址寄存器+索引寄存器+内存地址常数 mov dword ptr ss:[esp+edi+0x123], 0x123
如果内存地址常数0也算进去,那么一条包含内存操作数的指令,他的内存操作数就都含有内存地址常数,那么这个内存地址常数就可以被用来加密了。
//这条指令此时伪代码寄存器是ebp,这时候的ecx是07400000,那么这里面的内存地址常数
//其实是-5,这个-5加上ebp就是当前伪代码的指针
00433C60 | movzx eax,byte ptr ds:[ecx+ebp-0x7400005]
00433C68 | ror ecx,0x77 |
00433C6B | not cx |
00433C6E | and cl,cl |
00433C70 | xor al,bl
写到这里有些人可能觉得这样子有什么意义呢?除了让代码变得更加复杂难看点,其他还有什么作用呢?这个问题问的好,作者可不是这么想的,他的每一个地方的改动都是根据编写分析插件的思路来的,我猜测原作者团队里面应该有专门开发分析插件的部门,他的每一次更新都是会收集所有能够找到的分析资料进行修复,那么这些改动对于我们编写分析插件来说影响是相当大的,3.8之前还能使用指令文本特征进行匹配从而来识别Handler的方法这里已经完全失效了。
第3点:Handler的变形,这里面的Handler变形只存在于包含计算虚拟寄存器地址的Handler。
handler PushVR32:
004BF2B6 | movzxeax,byteptrss:[ebp-0xB]//读取伪代码
004BF2BB | xoral,bl
004BF2BD | movecx,dwordptrss:[esp+0xC]
004BF2C1 | addecx,0xFFFEE8B1 |
004BF2C7 | jmpecx |
0044D3C2 | negal
0044D3C4 | incal
0044D3C6 | negal
0044D3C8 | movedx,dwordptrss:[esp+0x2D] |
0044D3CC | notedx |
0044D3CE | incal
0044D3D0 | movzxecx,dx
0044D3D3 | xorbl,al//解密出偏移
0044D3D5 | addecx,0xD3A68C98
0044D3DB | leaeax,dwordptrss:[esp+eax+0x3C]//算出虚拟寄存器地址
0044D3DF | oredx,edx |
0044D3E1 | xchgdwordptrss:[esp+0x1C],edx
0044D3E5 | adcedx,0xFFFD3506 |
0044D3EB | jmpedx |
0042075C | movedx,dwordptrds:[eax]//取出虚拟寄存器的值
0042075E | movdwordptrds:[esi-0x1C],edx//值写入虚拟栈
变形后:
00420766 | leaecx,dwordptrss:[esp+ecx-0x2C596EE3]//算出虚拟寄存器地址
0042076D | moveax,dwordptrds:[ecx]//取出虚拟寄存器的值
0042076F | movdwordptrds:[esi-0x20],eax//值写入虚拟栈
第4点:Handler的一级融合,这里面Handler与Handler之间本来是上一条Handler执行完在结尾跳到下一条handelr去执行,那么每条Handler中都有一个专门计算下一条handelr地址的代码块,这个代码块都有读取伪代码的操作,经过融合后,下一条用来计算handelr地址的伪代码被丢弃了,同时计算下一条handelr地址的代码块也没有了。
那么两条Handler使用跳转指令或者直接连接变成一条Handler,那么后面的Handler同样可以这样子接上去,有点像人体蜈蚣。
//第一条handler PushVR32的变形
00479089 | movdwordptrss:[esp+edx*2-0x2],edx
0047908D | subdwordptrss:[esp+edx*2+0x16],0x8B38041C |
00479095 | movzxeax,byteptrss:[esp+edx+0xA] |
0047909A | leaecx,dwordptrss:[esp+edx+0x55]//算出虚拟寄存器地址
0047909E | moveax,dwordptrds:[ecx+edx-0x3]//取出虚拟寄存器的值
004790A2 | shldx,0x68 |
004790A6 | movdwordptrds:[edi+edx*8-0x1808],eax//值写入虚拟栈
//第二条handler PushVR32的变形
004790AD | moveax,edx
004790AF | xaddbyteptrss:[esp+edx-0x2F9],dl |
004790B7 | call123.vmp模版+部分.4B01CD |
004B01CD | leaecx,dwordptrss:[esp+edx*4-0xB80]//算出虚拟寄存器地址
004B01D4 | jo123.vmp模版+部分.520FD5 |
004B01DA | moveax,dwordptrds:[ecx+edx*8-0x1800]//取出虚拟寄存器的值
004B01E1 | leaecx,dwordptrds:[edx+edx*2+0x6BAEF303] |
004B01E8 | movdwordptrds:[edi+edx*4-0xC0C],eax//值写入虚拟栈
//第三条handler PushVR32的变形
004B01EF | leaecx,dwordptrds:[edx+edx-0x50DA8F6D]
004B01F6 | ordl,byteptrss:[esp+edx-0x2FB] |
004B01FD | negedx |
004B01FF | leaeax,dwordptrss:[esp+0x44]//算出虚拟寄存器地址
004B0203 | rordwordptrss:[esp+0xF],cl |
004B0207 | popecx |
004B0208 | addecx,0xFFFDC76A |
004B020E | jmpecx |
00455826 | popedx |
00455827 | movedx,dwordptrds:[eax]//取出虚拟寄存器的值
00455829 | call123.vmp模版+部分.44DAE9 |
0044DAE9 | movdwordptrds:[edi-0x10],edx//值写入虚拟栈
第5点:Handler的二级融合,在一级融合的基础上还有二级融合,比如执行模拟加法运算的Handler组合。首先第一条Handler是从伪代码读取常数作为加数,第二条Handler是从伪代码读取常数作为被加数,第三条Handler是执行加法运算,以前的handler逻辑是第一条和第二条handler,从伪代码读取到加数和被加数都是放到虚拟栈中去。然后第三条加法handler从虚拟栈中获取两个加数和被加数再执行ADD指令,在这里他把值放到虚拟栈中的指令给丢弃了,直接绑定了对应的寄存器。
例如第一条Handler读取的值存放到eax,第二条Handler读取的值存放到ecx,第三条handelr直接执行add eax,ecx,算出的结果在eax,同样可以不放到虚拟栈中,参与第四条handler的运算。
//PushVR8点变形
0043B239 | leaedx,dwordptrss:[esp+0x50]//算出虚拟寄存器地址
0043B23D | movecx,0x489D36B2 |
0043B242 | sarecx,0x45 |
0043B245 | rorcl,0x67 |
0043B248 | movzxcx,byteptrds:[ecx+edx-0x244E96B]//取出虚拟寄存器的值存入cx
0043B251 | jb123.vmp模版+部分.48BAC5 |
0043B257 | moveax,0xFF9BE60E |
0043B25C | movdx,cx//将cx的值转移到dx,这个其实也可以算作一个混淆大点
0043B25F | shleax,0xB1//到这来并没有将读取到的值存入到虚拟栈中去
//PopVR8
0043B262 | movzxecx,byteptrss:[ebp-0x6]//读取伪代码
0043B267 | popeax |
0043B268 | addeax,0xD15B |
0043B26D | jmpeax |
00499668 | xorcl,bl
0049966A | moveax,0x822BE31F
0049966F | shlal,0xC6 |
00499672 | subax,0x923B |
00499676 | xorcl,0x3E
00499679 | notcl
0049967B | jmp123.vmp模版+部分.4570F0 |
004570F0 | pusheax |
004570F1 | addcl,0xA3
004570F4 | xorax,ax |
004570F7 | rorcl,0x1
004570F9 | shleax,0x4D |
004570FC | sareax,0xB7 |
004570FF | xorbl,cl//解密出偏移
00457101 | negal |
00457103 | subax,0xEC15 |
00457107 | sbbeax,eax |
00457109 | leaecx,dwordptrss:[esp+ecx+0x4]//算出虚拟寄存器地址
0045710D | roral,0xC1 |
00457110 | movbyteptrds:[ecx],dl//将dl值写入虚拟寄存器中
第6点:寄存器的释放,我们知道 ebp,esi,edi这三个轮转寄存器都有各自的功能,在一个流程块快结束的时候,也就是快要执行虚拟机退出Handler的时候,当某一条Handler之后的handler不再读取伪代码指令或者计算下一条handler的地址的时候,其相应的寄存器会被释放。
比如说伪代码寄存器或者调度寄存器的值进行随意赋值或者将其用作加密内存地址常数的寄存器,这么做表面上看起来毫无意义,但是在我们开发插件的代码中就需要进行非常大的逻辑改动了。
果然原作者还是自己会先把自家VMProtect给狂扁一顿:
//执行跳转到下一条Handler
0042E08C | jmpebp
//PushVR32
004DF21B | jmp123.vmp简单+最大保护.4E7299
004E7299 | movecx,0xEC3505BA |
004E729E | movzxeax,byteptrds:[esi-0x1]//读取伪代码
004E72A2 | movsxebp,cl//调度寄存器ebp的值被释放
004E72A5 | xoral,bl
004E72A7 | roral,0x1
004E72A9 | jmp123.vmp简单+最大保护.4F5E38 |
004F5E38 | movzxedx,cl |
004F5E3B | addal,0x98
004F5E3D | movsxesi,dl//调度寄存器ebp的值被释放
004F5E40 | pushedx |
004F5E41 | notwordptrss:[esp+edx*4-0x2E8] |
004F5E49 | notal
004F5E4B | rolal,0x1
004F5E4D | decdl |
004F5E4F | andecx,dwordptrss:[esp+edx*8-0x5C8] |
004F5E56 | jge123.vmp简单+最大保护.4AD5DC |
004AD5DC | decal
004AD5DE | xorbl,al//伪代码解密完成
004AD5E0 | notcx |
004AD5E3 | rolwordptrss:[esp+ecx-0xFAFE],0xA7 |
004AD5EC | leaeax,dwordptrss:[esp+eax+0x4]// 算出虚拟寄存器
004AD5F0 | shldx,0xEE |
004AD5F4 | sbbecx,0x39B0CEB2 |
004AD5FA | movedx,dwordptrds:[eax+edx*4-0x10000]//读取虚拟寄存器的值
004AD601 | movdwordptrds:[edi-0x4],edx//值存入虚拟栈
//PushVR32的变形
004AD604 | leaedx,dwordptrss:[esp+0x4C]// 算出虚拟寄存器
004AD608 | movecx,dwordptrds:[edx]//读取虚拟寄存器的值
004AD60A | subesi,ebp |
004AD60C | movsxeax,byteptrss:[esp+esi*8] |
004AD610 | pusheax |
004AD611 | movdwordptrds:[edi+esi-0x8],ecx//值存入虚拟栈
// VMExit 离开虚拟机
004AD615 | movedx,ebp
004AD617 | leaesp,dwordptrss:[esp+0x8] |
004AD61B | leaesp,dwordptrds:[edi+esi-0x8]//虚拟栈指针还给esp
004AD61F | leaebx,dwordptrds:[eax-0x3B72475A] |
004AD625 | popebp//恢复到真实ebp
004AD626 | shrbl,0x41 |
004AD629 | popecx//恢复到真实ecx
004AD62A | setnpbl |
004AD62D | shldl,0x47 |
004AD630 | notsi |
004AD633 | popfd//恢复到真实eflag
004AD634 | bswapesi |
004AD636 | popeax//恢复到真实eax
004AD637 | popedi//恢复到真实edi
004AD638 | popesi//恢复到真实esi
004AD639 | popedx//恢复到真实edx
004AD63A | popebx//恢复到真实ebx
004AD63B | call123.vmp简单+最大保护.495550 |
00495550 | leaesp,dwordptrss:[esp+0x4] |
00495554 | ret//离开虚拟机返回到真实指令
第7点:轮转寄存器和解密寄存器的偷换,这是我在分析万用门handler的时候发现的,万用门handler算是虚拟机中比较敏感的handler了,这类handler通常都是找jcc爆破的关键handler,所以说作者在这方面应该是加固加固再加固的。
怎么换呢?这里面先将虚拟栈寄存器ebp的值给eax,那么eax这时候就是ebp的分身了。这样子,我们在判断某一条指令是不是读取虚拟栈内存的时候是不是就变得困难了,这里面可能还包括伪代码寄存器也会这样做,目前我还没发现。
//PushVStack 这是虚拟栈寄存器存入虚拟栈的Handler
004F61DF | movecx,esi//当前虚拟栈寄存器是esi值给ecx
004F61E1 | rordl,0xC5 |
004F61E4 | jae123.vmp模版+部分.556582 |
004F61EA | movdwordptrds:[esi+eax*4-0x8AA4860],ecx//ecx值存入虚拟栈
//Dispatcher 计算下一条Handler地址
004F61F1 | oral,ah
004F61F3 | moveax,dwordptrss:[ebp+eax*2-0x4552532]//读取伪代码
004F61FA | pushedx |
004F61FB | leaedx,dwordptrds:[edx+edx+0x4A284788] |
004F6202 | leaebp,dwordptrss:[ebp+edx-0x4A2848FC]
004F6209 | movsxecx,dl |
004F620C | jne123.vmp模版+部分.431ABA |
00431ABA | xoreax,ebx
00431ABC | subeax,0x91B19C05
00431AC1 | call123.vmp模版+部分.4A467A |
004A467A | adddx,cx |
004A467D | noteax
004A467F | movdwordptrss:[esp+edx-0x4A2848F0],ecx |
004A4686 | xoreax,0x4122C6BE
004A468B | xorcl,0x27 |
004A468E | subedx,0xF40FEF8C |
004A4694 | deceax
004A4695 | call123.vmp模版+部分.43DAFA |
0043DAFA | shlecx,0x96 |
0043DAFD | oredx,edx |
0043DAFF | xorebx,eax//伪代码解密完成
0043DB01 | addedi,eax//edi是调度寄存器,计算下一条Handler地址
//Nor32 这是一个万用门Handler
0043DB03 | pushecx
0043DB04 | subcx,wordptrss:[esp+edx-0x56185964] |
0043DB0C | incdx |
0043DB0F | moveax,dwordptrds:[esi+edx-0x56185969]//从虚拟栈取出前面压入的虚拟栈指针
0043DB16 | popecx |
0043DB17 | movedx,dwordptrss:[eax+edx-0x56185965]//这条指令相当于从虚拟栈中读取参数1
0043DB1F | jo123.vmp模版+部分.449C0D |
0043DB25 | inccl |
0043DB27 | moveax,edx//将参数1从eax转移到edx
0043DB29 | popecx |
0043DB2A | adcecx,0x2C648 |
0043DB30 | jmpecx |
004D0CE2 | jmp123.vmp模版+部分.4F19A1 |
004F19A1 | movedx,dwordptrds:[esi]//从虚拟栈中读取参数2
004F19A4 | movecx,0xD886303E
004F19A9 | noteax//取反参数1
004F19AB | deccx |
004F19AE | rorcl,0x27 |
004F19B1 | notedx//取反参数2
004F19B3 | shlbyteptrss:[esp+0x2],0x42 |
004F19B8 | orecx,ecx//执行或运算
004F19BA | andeax,edx |
004F19BC | movdwordptrds:[esi],eax//结果存放到虚拟栈
以上就是 VMProtect 3.8.1 核心混淆策略的全拆解,高版本 VMP 的全指令变形、Handler 多级融合、内存常数加密等机制,看似彻底阻断逆向分析,实则有完整的破解思路与落地方法。
如果想把文章里的理论变成可实战的逆向能力,彻底搞定高版本 VMP 逆向、Handler 抗变形识别、x32/x64 调试器插件开发,欢迎关注课程:《VMProtect 分析与调试器插件开发》从 VMP 底层架构到插件实战开发,全程带你啃下高版本 VMP 逆向硬骨头。
VMProtect分析与调试器插件开发-999元
#
看雪ID:阿强
https://bbs.kanxue.com/user-home-1004848.htm
*本文为看雪论坛优秀文章,由 阿强 原创,转载请注明来自看雪社区
往期推荐
Ptrace注入代码在不同平台的区别(ARM64、x86-64、MIPS64)
浅谈梯度分析与样本对抗:以vlm和ddddocr为例
ANDROID 黑科技 : 保活机制深度逆向
更好理解:CVE-2021-1732漏洞分析报告与利用
LLVM Pass编写及去除 —— 控制流平坦化
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 阿强 阿强《VMProtect 3.8.1 混淆策略大揭秘》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论