看雪学院 前天 08:21
VmProtect 3.x 版本虚拟机分析入门
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入分析了VmProtect 3.x版本的虚拟机保护机制。作者从早期版本开始,逐步深入研究VMProtect的混淆和虚拟机执行流程。新版本采用C++重构,界面更友好,但加密后的文件体积显著增大。文章详细解析了3.0.0 beta版本中,代码入口点后的指令被替换,以及虚拟机handler的执行过程。通过单步调试和代码分析,揭示了新版本在handler隐藏、解密逻辑以及虚拟寄存器管理方面的显著变化,特别强调了handler地址计算方式的革新,以及双重加密的实现。最终,文章总结了虚拟寄存器分布和堆栈的填充过程,并对文件体积增大的原因进行了推测。

🔑 **VmProtect 3.x 版本重构与特性变化**:VmProtect 3.x 版本在代码实现上转向C++,界面采用QT,带来了更好的用户体验。与早期版本相比,加密后的文件体积显著增大,这得益于计算机性能的提升,使得开发者不再过度担忧性能损耗。例如,一个179KB的程序加密后增至590KB,增幅超过400KB。

⚙️ **虚拟机入口与Handler执行机制**:在3.0.0 beta版本中,加密入口点后的指令被替换为跳转指令,如`jmp edi`。虚拟机的主要逻辑通过一系列handler执行,这些handler的地址不再像早期版本那样直接通过基地址加上偏移量计算得出,而是通过复杂的解密和计算过程动态生成。这极大地增加了逆向分析的难度,旨在阻止分析者直接获取所有handler的地址并翻译成正常指令。

🔒 **双重加密与Handler地址计算的演进**:新版本采用了更复杂的双重加密机制,结合了算术指令组合和基于伪代码地址的XOR/ADD/SUB加密。handler的地址计算也发生了根本性变化:每个handler不仅执行其功能,还负责计算并跳转到下一个handler的地址。这种“handler自执行”的模式是文件体积增大的重要原因之一,与早期版本跳回固定地址解密下一handler的方式截然不同。

🗄️ **虚拟寄存器管理与堆栈分配**:文章详细分析了VmProtect 3.x中的虚拟寄存器机制。虚拟寄存器作为真实寄存器的替代,用于在虚拟机内部存储数据。通过分析handler,作者构建了虚拟寄存器分布表,显示了如`vm_ecx`, `vm_ebx`, `vm_edi`等虚拟寄存器在内存中的布局。堆栈也被用于存储和恢复寄存器值,以及作为handler跳转的中间环节。

🔄 **逆向分析的挑战与收获**:尽管VmProtect 3.x版本引入了更强大的混淆和保护机制,使得逆向分析比早期版本更具挑战性,但作者通过细致的单步调试和代码分析,成功揭示了其核心工作原理。文章认为,虽然新版本功能强大,但对于测试版而言,混淆程度相对有限,核心逻辑依然可循,分析过程仍有显著收获。

阿强 2025-10-31 18:06 上海

看雪论坛作者ID:阿强

终于可以开始分析VmProtect3.x,以上的版本了,从刚开始接触vmp,那时是从0.7开始的,算是最早的版本了吧,那时候对虚拟机,一点概念都没有。什么虚拟寄存器,什么伪代码,为什么虚拟机里面没有寄存器的概念等等,慢慢的从了解其基本执行流程,到了解其混淆,一步步的学习,到开始分析3.x版本,还是蛮有成就感的。

又可以接触新的东西了,而且是很多,从3.x版本开始,代码进行了重构,编写语言改用了C++,界面用上了QT变得更好看了,加密后的文件的大小也越来越大,也是因为计算机性能的大幅度提升,不用再考虑太多的性能方面的东西。

上一篇文章,我讲的是2.12.3版本的,加密前是3kb,加密后才12kb。然后这篇的例子,由于这个版本的主程序,还是有点问题的,我在win10运行不了,于是我放xp虚拟机上跑,给程序加壳的时候也是状况百出,但是经过一顿测试,拿了一堆程序,又改来改去加密的代码,终于弄出一个较为满意的例子。这个程序加壳前,大小179kb,加壳后590kb。大小直接增加400多kb,秉承着,柿子专挑乱的捏,选择了VmProtect.3.0.0beta,这个beta是测试版的意思。

这次给大家的附件没有虚拟机主程序,你们去论坛下载就可以了,然后我说下,设置啥的,首先加密的是入口点后面的的下面两行代码,

0040A5A8 >  B8 AAAAAAAA     mov eax,0xAAAAAAAA
0040A5AD    BB BBBBBBBB     mov ebx,0xBBBBBBBB
0040A5B2    B9 CCCCCCCC     mov ecx,0xCCCCCCCC
0040A5B7    BA DDDDDDDD     mov edx,0xDDDDDDDD
0040A5BC    BE 51515151     mov esi,0x51515151
0040A5C1    BF D1D1D1D1     mov edi,0xD1D1D1D1
0040A5C6    B8 78563412     mov eax,0x12345678
0040A5CB    E9 65AA0000     jmp 123.00415035
0040A5D0    C3              retn
0040A5D1    90              nop

也是吸取上一篇文章的教训了,给六个寄存器都赋值,加密的是0040A5C6和0040A5CB这两条代码,然后选项那边,把是都改成否,英文版的,yes全改no,最后Compilation Type(编译类型),这边选择Virtualization(虚拟化)。我们把编译成功的123.vmp.exe拖到OD上

0040A5C6   .- E9 CCC20800   jmp 123_vmp.00496897
0040A5CB   >^ FFE7          jmp edi                      
0040A5CD   .  57            push edi                     
0040A5CE   .  C3            retn
0040A5CF   .  4F            dec edi                      
0040A5D0   .  C3            retn
0040A5D1      90            nop

0040A5C6这边的代码被替换成了jmp指令,0040A5CB这边的代码可能是被替换成某个handler了吧。我们接着往下单步

00496897    68 5AFA2B26     push 0x262BFA5A
0049689C    E8 05BFFBFF     call 123_vmp.004527A6

到这里还是和我们前面分析的vmp1.6一样,push,call组合。我们接着往下单步

004527A6    9C              pushfd
------省略------
004527CD    8B7424 28       mov esi,dword ptr ss:[esp+0x28]
------省略------ 
00452811    8BEC            mov ebp,esp
------省略------ 
0045281C    8DA424 40FFFFFF lea esp,dword ptr ss:[esp-0xC0]
------省略------ 
0045282B   >  8BDE           mov ebx,esi;ebx存放esi的值作为密钥    
------省略------ 
0045283B    8D3D 3B284500   lea edi,dword ptr ds:[0x45283B]
00452841    8B06            mov eax,dword ptr ds:[esi]
00452843    F9              stc
00452844    8DB6 04000000   lea esi,dword ptr ds:[esi+0x4]
0045284A    33C3            xor eax,ebx;解密
0045284C    48              dec eax解密
0045284D    66:38A58      cmp ax,0x588A
00452851    66:85D7         test di,dx
00452854    0FC8            bswap eax解密
00452856    26D7E3B00     sub eax,0x3B7E6D解密
0045285B    35 412CD300     xor eax,0xD32C41解密
00452860    F6C6 C6         test dh,0xC6
00452863    33D8            xor ebx,eax;生成下一次解密的密钥
00452865    F8              clc
00452866    03F8            add edi,eax ;算出handler的地址
00452868    57              push edi
00452869    C3              retn ;执行handler

这边因为虚拟机大小和之前变得大很多,我贴代码不能像以前一样全部贴出来,太占用篇幅了,关键的留住,其他的用省略表示,然后这一大段代码,我们分析的时候可以翻开之前我分析的1.64版本的那个例子,我也放在附件里面了。

这边我们执行到004527CD的时候可以看到,这里和1.64版本的区别是少了那个重复的寄存器。004527CD这边的代码执行完,这时候这个esi是加密了的,00452811这条代码执行完esi,才解密出来,前面的那些对其他寄存器的操作统统都是垃圾指令我们只看esi就行,00452811这条代码我们也可以在1.64版本找到他的位置,0045281C这条代码等价于sub esp,0xC0。0045283B这条代码等价于mov edi,0045283B,这边在我们前面分析过的vmp里面从来没见过了,这个edi不是handler的基地址,只能说用于计算handler的地址。

我们再往下单步,00452841这条指令是从esi读取4字节,以前分析的都是读取1字节,0045284A这条指令和00452863的指令是成对出现的,在我以前分析过1.2版本伪代码解密的时候,有详细分析过,就是用伪代码作为密钥,但是执行一次伪代码就变一次,分析的时候还觉得挺复杂,那时候有三种可能,xor对xor,add对add,sub对sub。

不一样的是这边是4字节,分析到这边总结一下变化,果然重构之后很强大,这里作者已经把handler隐藏了,这里面所有的改动就一个目的隐藏handler,以前那种 eax*4+基地址 = handler地址,类似c++虚函数的时代已经过去了,esp存放虚拟寄存的基地址,之前版本是edi,那么现在edi干什么呢?

之前他作为虚拟寄存器基地址默默无闻,现在可牛逼了,现在成了存放handler地址的寄存器,配合push和retn去执行各个handler,遵循一个公式,edi = 解密(伪代码) + edi,所以说我们要算出所有handler的地址,上一条执行的handler的地址。核心思路还是一样,只不过实现起来和以前发生了很大的改变。

作者的目的就一个,怕我们像以前版本一样知道所有handler的地址,然后把所有伪代码都翻译成正常指令。

我们执行到00452869的代码接着单步,就来到了第一条handler

00447687    0FB606          movzx eax,byte ptr ds:[esi];取出一个字节的伪代码
0044768A    66:D3C9         ror cx,cl
0044768D    81C6 01000000   add esi,0x1;;伪代码指针加1
00447693    03CF            add ecx,edi                  
00447695    81F1 7B41E822   xor ecx,0x22E8417B
0044769B    66:0FBAF1 41    btr cx,0x41
004476A0    32C3            xor al,bl ;解密伪代码
004476A2    0FBFCC          movsx ecx,sp
004476A5    F6D0            not al ;解密伪代码
004476A7    0f94c5          sete ch
004476AA    FEC8            dec al ;解密伪代码
004476AC    D3F9            sar ecx,cl
004476AE    0FB7CB          movzx ecx,bx
004476B1    F6D0            not al ;解密伪代码
004476B3    0FBDCE          bsr ecx,esi                  
004476B6    F6D8            neg al ;解密伪代码
004476B8    0BCF            or ecx,edi                   
004476BA    66:3BC6         cmp ax,si
004476BD    32D8            xor bl,al;生成下次解密的密钥
004476BF    c1d1 f3         rcl ecx,0xf3
004476C2    86ED            xchg ch,ch
004476C4    66:13CE         adc cx,si
004476C7    8B4C25 00       mov ecx,dword ptr ss:[ebp];取出堆栈的值(对应push 0
004476CB    A8 8D           test al,0x8D
004476CD    81C5 04000000   add ebp,0x4;ebp指向下一个堆栈的值(对应push esi)
004476D3    66:F7C7 5744    test di,0x4457
004476D8    890C04          mov dword ptr ss:[esp+eax],ecx ;存入虚拟寄存器(对应Vm_Relocation)
004476DB    66:0FBAF8 99    btc ax,0x99
004476E0    03C7            add eax,edi                  
004476E2    8B06            mov eax,dword ptr ds:[esi];从esi读取4字节
004476E4    81C6 04000000   add esi,0x4;伪代码指针加4
004476EA    F8              clc
004476EB    33C3            xor eax,ebx ;解密伪代码
004476ED    48              dec eax  ;解密伪代码
004476EE    F8              clc
004476EF    F5              cmc
004476F0    0FC8            bswap eax;解密伪代码
004476F2    66:F7C2 FD44    test dx,0x44FD
004476F7    8D80 9381C4FF   lea eax,dword ptr ds:[eax-0x3B7E6D] ;解密伪代码
004476FD    35 412CD300     xor eax,0xD32C41;解密伪代码
00447702    33D8            xor ebx,eax;生成下次解密的密钥
00447704    85DA            test edx,ebx
00447706    03F8            add edi,eax;生成handler的地址
00447708  ^ FFE7            jmp edi;等价于push retn组合              

上面这条handler虽然和以前分析的一样,是填充虚拟寄存器用的,但是也有很大的变化,上一篇我们分析的那个2.12.3版本,虚拟寄存器的偏移直接在伪代码上可以看出来,但是这边采用了双重加密,关于双重加密,我出的课程《VMProtect虚拟机逆向入门》有对主程序的逆向分析,其中对这个双重加密有详细的分析。

这边我简单说一下,这个加密方式早在我分析1.2版的时候就有了,首先双重加密第一次就是用譬如not,inc,bswap等一些算术指令的组合进行加密,第二次就是利用伪代码地址作为密钥使用xor,add,sub其中一个加密,这个伪代码地址也会加密生成新的密钥,所以解密的时候正好反过来,先用伪代码地址作为密钥使用xor,add,sub其中一个解密,接着再用譬如not,inc,bswap等一些算术指令的组合进行解密,这才解密完成,然后就是再算出新的密钥。

大概就是这个意思了,然后这个伪代码地址加密的长度可以是4字节2字节和1字节,如果是2个字节和1个字节那就更新到ebx对应的bx或bl位置,这个解密伪代码地址用ebx存放,第一次加密或者解密的时候就esi的值。其实这个我们大致了解一下就可以了,反正都是加密了,这边还有很多无关紧要的垃圾指令,其实我们了解他的行为之后,那些垃圾指令一眼就看出来了。

前两篇文章我们分析的时候没有考虑到的一个问题,现在还是要考虑一下,就是用vm_+寄存器名,来指代对应的虚拟寄存器,这个虚拟寄存器没什么深奥的,一句话就是用来存放真是寄存器的,仅此而已。

eax对应的虚拟寄存器就是vm_eax,这样子我打字起来不会那么费劲,004476D8这句代码的注释,Vm_Relocation就是指存放重定位的虚拟寄存器了,这个重定位一直都是0,在堆栈上占坑用的,前面我们说过esp存放是虚拟寄存器基地址的,004476D8上的这条指令看到了吧,填充好对应的虚拟寄存器之后,就又去伪代码上读取4字节值,解密后与这条handler的地址相加计算出下一条handler的地址了,然后去执行。

这与我们以前分析的handler区别就是以前执行完,跳回一个固定的地址解密出下一条handler地址去执行,不断的循环,而这里的每一条handler由两部分构成,第一部分实现handler的功能,第二部分,计算下一条handler地址并执行。我感觉为啥,文件会突然间变得那么大,就好比以前电脑是稀罕物的时候大家没电脑的时候去网吧上网,全世界的所有电脑总和就好比文件大小,现在普及了,几乎每个人都买的起电脑,那么全世界电脑总数就突然间变得非常大了,一个道理。

我们接着看下一条handler,同样是填充虚拟寄存的handler,虽然不是同一条handler但是和上一条功能一样,这里把esi的值填充到偏移为0x30的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_esi。上面一个handler忘记记录了,这边记录一下是偏移为0x2c的虚拟寄存器,填充重定位值0,那么偏移为0x2c的虚拟寄存器记为vm_relocation。

我们接着看下一条handler,这里把ebp的值填充到偏移为0x24的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_ebp。

我们接着看下一条handler,这里把ebx的值填充到偏移为0x4的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_ebx。

我们接着看下一条handler,这里把eax的值填充到偏移为0x34的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_eax。

我们接着看下一条handler,这里把edi的值填充到偏移为0x18的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_edi。

我们接着看下一条handler,这里把ecx的值填充到偏移为0x0的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_ecx。

我们接着看下一条handler,这里把edx的值填充到偏移为0x1c的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_edx。

我们接着看下一条handler,这里把eflags的值填充到偏移为0x3c的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_eflags。

我们接着看下一条handler,这里把地址为0x49689c的call的返回值填充到偏移为0x20的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_retaddr。

我们接着看下一条handler,这里把地址为0x496897的push的值填充到偏移为0x28的虚拟寄存器中,那么这个虚拟寄存器就命名为vm_push。

到这里我们制作一个虚拟寄存器分布表,这个虚拟寄存器总的大小是0x40,和我之前分析的文章所讲的版本一样,我分析1.2版本的时候就这样,可以存放16个寄存器,每个大小4字节,但是真正需要的不用这么多,所以多出来的可以用来计算,和用来搞混淆。

0019FE88  vm_ecx      vm_ebx      空闲            空闲
0019FE98  空闲          空闲      vm_edi          vm_edx
0019FEA8  vm_retaddr  vm_ebp     vm_push      vm_relocation
0019FEB8  vm_esi      vm_eax      空闲          vm_eflags

我们接着看下一条handler,这里从伪代码取出4字节数,然后解密出地址00415035,然后存放到[0019FF70]里面,接着又从伪代码中取出4字节数,解密出下一条handler地址。

004840A4    03F8            add edi,eax ;算出下一条handler地址
004840A6  ^ E9 1319FCFF     jmp 123_vmp.004459BE;这条代码不是push edi,或者jmp edi。

004459BE    8D4424 60       lea eax,dword ptr ss:[esp+0x60]
004459C2    3BE8            cmp ebp,eax
004459C4  ^ 0F87 89AFFFFF   ja 123_vmp.00440953 ;混淆,永远都是跳走

00440953  ^\FFE7            jmp edi                             

上面这个代码,在我讲上一篇文章混淆的时候提到过,就是固定跳转的。

我们接着看下一条handler,我们对照上面的虚拟寄存器分布表很容易看出来是,取出vm_eflags的值,然后存放到[0019FF6C]里面。

我们接着看下一条handler,这条handler是取出vm_edx的值,然后存放到[0019FF68]里面。

我们接着看下一条handler,这条handler是从伪代码读取4字节的值,然后解密出来的值是0x12345678,这个值有印象吧,然后存放到[0019FF64]里面。

我们接着看下一条handler,这里把0x12345678填充到偏移为0x14的空闲虚拟寄存器中。

我们接着看下一条handler,这条handler是取出vm_ecx的值,然后存放到[0019FF64]里面。

我们接着看下一条handler,这条handler是取出vm_edi的值,然后存放到[0019FF60]里面。

我们接着看下一条handler,这条handler是取出偏移为0x14的虚拟寄存器的值也就是0x12345678,然后存放到[0019FF5c]里面。

我们接着看下一条handler,这条handler是取出vm_ebx的值,然后存放到[0019FF58]里面。

我们接着看下一条handler,这条handler是取出vm_ebp的值,然后存放到[0019FF54]里面。

我们接着看下一条handler,这条handler是取出vm_esi的值,然后存放到[0019FF50]里面。

上面这几个handler都是将虚拟寄存器弹出到堆栈,这里我们再制作一个表格,标出堆栈上虚拟寄存器的分布,

0019FF50   51515151 ;vm_esi
0019FF54   0019FF80 ;vm_ebp
0019FF58   BBBBBBBB ;vm_ebx
0019FF5C   12345678 ;0x12345678
0019FF60   D1D1D1D1 ;vm_edi
0019FF64   CCCCCCCC ;vm_ecx
0019FF68   DDDDDDDD ;vm_edx
0019FF6C   00000246 ;vm_eflags
0019FF70   00415035 ;00415035

我们上面的这个表,好像没有vm_eax,为啥呢,这里就是因为我们虚拟化了 mov eax,12345678 这条指令的缘故了,其实0019FF5C就是vm_eax,另外0019FF70的值00415035就是我们虚拟化jmp 00415035的结果了。我们看下最后一条handler,

0047F585    8BE5            mov esp,ebp ;修改esp,配合后面的出栈指令
0047F587    0FBFF6          movsx esi,si
0047F58A    5E              pop esi     ;恢复esi                              
0047F58B    9F              lahf
0047F58C    0FBFDA          movsx ebx,dx
0047F58F    F7C5 A23F0045   test ebp,0x45003FA2 
0047F595    5D              pop ebp     ;恢复ebp    
0047F596    5B              pop ebx     ;恢复ebx     
0047F597    98              cwde
0047F598    66:0FBAFA C4    btc dx,0xC4
0047F59D    66:98           cbw
0047F59F    58              pop eax     ;恢复eax   
0047F5A0    c0f9 28         sar cl,0x28
0047F5A3    5F              pop edi     ;恢复edi                                  
0047F5A4    66:0FB6D0       movzx dx,al
0047F5A8    59              pop ecx     ;恢复ecx    
0047F5A9    5A              pop edx     ;恢复edx   
0047F5AA    F6C3 95         test bl,0x95
0047F5AD    9D              popfd       ;恢复eflags  
0047F5AE    C3              retn        ;相当于jmp 00415035

上面这个代码中 执行到0047F59F上的指令时,此时堆栈上的值就是12345678,执行完eax = 12345678,就是模拟了mov eax,12345678,然后执行到0047F59F上的指令时,此时堆栈上的值就是00415035,就是模拟了jmp 00415035。我没有虚拟化其他比较复杂的指令,原理都是差不多的。

到此整个虚拟机执行流程就完毕了,这样看的起来还蛮简单的,文件看上去挺大,可是执行的代码并不多,除了新的东西,感觉还没分析2.12.3版本复杂。

毕竟这是测试版,主要是实现功能而已,混淆的并不多,我还以为会分析的很久,没想到这么快就完成了,不过也看到很多新的东西,还是收获满满。

*本文为看雪论坛优秀文章,由 阿强原创,转载请注明来自看雪社区

球分享

球点赞

球在看

点击阅读原文查看更多

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

VmProtect 虚拟机 软件保护 逆向工程 反汇编 安全 Virtual Machine Software Protection Reverse Engineering Disassembly Security
相关文章