在把壳子开源之前,我会先对VMProtect1.70.4这个版本做一个简单的分析。在这几天分析过程中,我感受到了VMProtect的威力,并得出一个结论:分析VMProtect非常耗时间,如果没有做好与之长期斗争的准备,很难有实用的成果。另外,由于我第一次分析VMProtect,有分析不对的地方或者术语用得不恰当的地方,希望老萌萌们能指出来,我立即修改,不能误人子弟。分析的样本有三份,我放到了附件里面。
壳子我是去年写的,当时刚刚看了<<加密与解密>>第四版的第21章,我估摸了一下,自己可以写出来,然后就写出来了。在写之前,我记得当时还在bilibili观看了哈工大姜守旭教授教授的编译原理这门课程的视频,了解了大概就开干,看视频这个操作,对我写指令壳起了一种壮胆的效果。现在要开源,我又熟悉了下整个项目的流程。代码写得有点乱,我会画一张加壳时的流程图做一个直观的说明,和其他一些要点说明,以减少读代码时遇到的困惑。如果遇到调试或者编译问题,可以留言,我看到会及时回复。指令壳源码我会上传到附件。
写一个简单程序来测试:(ps:这个程序在vmp样本一文件夹)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <Windows.h> #include <iostream> void __declspec(naked) test_vmp( int a, int b) { / * __asm { mov eax,dword ptr[esp + 4 ] mov ecx,dword ptr[esp + 8 ] add eax,ecx ret } * / __asm { xor eax,ebx xor eax, ebx ret } } int main() { test_vmp( 1 , 2 ); printf( "%d\n" , x); system( "pause" ); return 0 ; } |
在OD中打开,函数在0x401080这个位置。

打开vmp,开始加壳:

下拉列表选择最快速度,其他不选择。

程序加好壳子后,用OD打开,开始分析:

0x401080这儿已经变为jmp了,跳到.vmp0这节里面:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | / / 入口 00401080 | jmp debug_test2.vmp. 4A77D2 | Debug_test2.cpp: 5 004A77D2 | push 4A781400 | 004A77D7 | call debug_test2.vmp. 4A6B9F | 004A6B9F | jmp debug_test2.vmp. 4A703A | 004A703A | push esi | esi:_mainCRTStartup 004A703B | jmp debug_test2.vmp. 4A52E8 | 004A52E8 | pushfd | 004A52E9 | push D3AC6D6A | 004A52EE | mov byte ptr ss:[esp + 4 ], 8F | 004A52F3 | pushfd | 004A52F4 | pop dword ptr ss:[esp + 4 ] | [esp + 4 ]:___use_sse2_mathfcns + 4A78 004A52F8 | jmp debug_test2.vmp. 4A64C1 | 004A64C1 | call debug_test2.vmp. 4A5FE9 | 004A5FE9 | pushad | 004A5FEA | mov dword ptr ss:[esp + 24 ],ebp | [esp + 24 ]:_mainCRTStartup 004A5FEE | push esp | 004A5FEF | pushfd | 004A5FF0 | push 72B57CE7 | 004A5FF5 | pushfd | 004A5FF6 | mov dword ptr ss:[esp + 30 ],eax | eax:___use_sse2_mathfcns + 2D1 004A5FFA | mov byte ptr ss:[esp],cl | 004A5FFD | call debug_test2.vmp. 4A701F | 004A701F | call debug_test2.vmp. 4A5A25 | 004A5A25 | jmp debug_test2.vmp. 4A6BC5 | 004A6BC5 | mov dword ptr ss:[esp + 34 ],edx | [esp + 34 ]:___use_sse2_mathfcns + 2D1 004A6BC9 | call debug_test2.vmp. 4A5C3D | 004A5C3D | call debug_test2.vmp. 4A6681 | 004A6681 | mov dword ptr ss:[esp + 38 ],edx | [esp + 38 ]:___use_sse2_mathfcns + 2D1 004A6685 | push dword ptr ss:[esp + 8 ] | [esp + 8 ]:___use_sse2_mathfcns + 42C0 004A6689 | mov dword ptr ss:[esp + 38 ],ecx | [esp + 38 ]:___use_sse2_mathfcns + 2D1 , ecx:___use_sse2_mathfcns + 2D1 004A668D | push E7FE4EC3 | 004A6692 | mov byte ptr ss:[esp],dh | 004A6695 | lea esp,dword ptr ss:[esp + 3C ] | 004A6699 | jmp debug_test2.vmp. 4A632E | 004A632E | btr si, 6 | 004A6333 | push edi | 004A6334 | push 20AD5139 | 004A6339 | xchg esi,ecx | esi:_mainCRTStartup, ecx:___use_sse2_mathfcns + 2D1 004A633B | call debug_test2.vmp. 4A6129 | 004A6129 | mov dword ptr ss:[esp + 4 ],ebx | 004A612D | movsx esi,cl | esi:___use_sse2_mathfcns + 2D1 004A6130 | pop ecx | ecx: "U嬱Q梓\x0E" 004A6131 | btc di,cx | 004A6135 | pushad | 004A6136 | mov dword ptr ss:[esp + 1C ], 0 | 004A613E | stc | 004A613F | jmp debug_test2.vmp. 4A70CF |
以上操作是把寄存器压到栈里,执行完后如下图:

寄存器压栈完成后,会先计算出调度表的地址。
[esp+48]这个值就是入口push 4A781400 这个值,经过计算后得到调度表的地址。调度表地址存放在esi寄存器里。
1 2 3 4 5 6 7 8 9 10 | 004A70CF | mov esi,dword ptr ss:[esp + 48 ] | 004A70D3 | btr bp,sp | 004A70D7 | jmp debug_test2.vmp. 4A53AF | 004A53AF | movzx bp,bl | 004A53B3 | btc bp,si | 004A53B7 | rol esi, 18 | 004A53BA | push 124E6496 | 004A53BF | inc esi | 004A53C0 | jmp debug_test2.vmp. 4A6C85 | |
如下,内存5里显示的就是经过加密的调度表:

然后按F7,到下面那个位置,箭头指向的三步,ebp指向的是真实堆栈,edi就是虚拟机的上下文环境(VMContext)。edi指向的堆栈空间,最大的作用就是存放寄存器。


在接下来的步骤中,通过在esi指向的调度表中取值,然后把ebp所指向的寄存器的值,挨个放到edi的虚拟环境中。

至此,虚拟机的环境构建完成,准备工作已经做好。
接下来进入正题,程序会通过edi寄存器取ebx的值,取两次,第一次的值压入[ebp],第二次压入[ebp+4],然后进入一个handler块进行运算。
这就是那个handler块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 004A53F7 | rol ah, 6 | 004A53FA | sbb edx,esp | 004A53FC | mov eax,dword ptr ss:[ebp] | 004A53FF | jmp debug_test2.vmp. 4A5BC4 004A5BC4 | cmc | 004A5BC5 | mov edx,dword ptr ss:[ebp + 4 ] | 004A5BC8 | push 25584F9E | 004A5BCD | pushad | 004A5BCE | clc | 004A5BCF | bt cx,C | 004A5BD4 | not eax | 004A5BD6 | pushad | 004A5BD7 | bt cx,bx | 004A5BDB | call debug_test2.vmp. 4A5D63 | 004A5D63 | cmc | 004A5D64 | not edx | 004A5D66 | cmp edi,F7A6A772 | 004A5D6C | stc | 004A5D6D | stc | 004A5D6E | stc | 004A5D6F | and eax,edx | 004A5D71 | jmp debug_test2.vmp. 4A5A3F | 004A5A3F | jmp debug_test2.vmp. 4A6E72 | 004A6E73 | pushfd | 004A6E74 | mov dword ptr ss:[ebp + 4 ],eax | 004A6E77 | jmp debug_test2.vmp. 4A564D | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - / / 化简之后 004A53FC | mov eax,dword ptr ss:[ebp] | 004A5BC5 | mov edx,dword ptr ss:[ebp + 4 ] | 004A5BD4 | not eax | 004A5D64 | not edx | 004A5D6F | and eax,edx | 004A6E74 | mov dword ptr ss:[ebp + 4 ],eax | 004A6E77 | jmp debug_test2.vmp. 4A564D | |
可以把上面的handler块命名为Handler_NOT_AND
那么,可以把以上的运算过程可以表示成这样:not(ebx) and not(ebx)
执行xor eax,ebx这条指令的时候,虚拟机会多次调用Handler_NOT_AND 块,整个流程可以记录为如下形式:
T = not(not(ebx) and not(ebx)) and not(not(eax) and not(eax));即为:T = ebx and eax;
S= not(eax) and not(ebx);
最终结果:ret= not(T) and not(S)。
通过写程序来验证,与虚拟机算出来的0x690035一致,说明流程记录没有问题。那么,xor eax,ebx可以用这个表达式来表示:eax = not(ebx and eax) and not(not(eax) and not(ebx))。

执行完xor eax,ebx之后,eax寄存器的位置在edi中的会变:

下一个xor eax,ebx 和上面的操作是一样的,这个操作完了,eax=0x004A3035。
eax寄存器的位置在edi中又变了:

按F7单步跟,(...省略不重要的部分),接着,程序把edi中所保存的寄存器,再吐出来给ebp所指向的堆栈空间,然后ebp赋值给esp,最后再pop到真实寄存器,退出虚拟机。

退出虚拟机:

测试程序和上面一样。(ps:这个程序在vmp样本二文件夹)

下拉列表选择最快速度,再把调试器勾选上,其他不选择。

加壳完成后,打开CFF来查看,程序会新增一节.vmp1,程序入口也在这节里面。


此外,还构建了一个TLS表:(但在这儿作用似乎不大)

程序执行到入口后,按F7单步跟,步骤和上面“01速度最快"分析时相差无几,会先构建虚拟机环境。
虚拟环境构建完成后,接着按F7单步跟,我的想法是,很快就能找到一些反调试的线索,但是跟了几个小时,发现不对劲了,和上面“01速度最快"分析时用手工跟踪,完全不在一个数量级的。天气又大,整个人木在那里。后来,想到在退出虚拟机那个地方下断,方法就是搜索vmp1这节的ret或者ret xx,最终找到两个地方,一个是调度器ret xx,这个不管,另一个就是下图所给出的,在ret 0x40处下断。按F9程序跑起来后,会断在这里,能看到右边寄存器窗口的字符串。这个位置,可以作为过掉检测调试器的突破口。

当然,最好的办法应该是在一些能检测出调试器的API函数下断。
vmp1.70.4这个版本,开启调试器检测后,程序会依次调用以下的API来检测是否有调试器存在:
1 2 3 4 5 6 7 | IsDebuggerPresent CheckRemoteDebuggerPresent GetThreadContext CloseHandle NtQueryInformationProcess NtSetInformationThread / / 关于这些函数介绍,看雪里有很多大神发了反调试相关的帖子,搜一下就能找到。 |
注意:API下断时,不要在头部下断,虚拟机会对有些API函数的头部进行0xCC检测,比如在这个程序中,虚拟机执行到GetThreadContext函数之前,会对GetThreadContext函数的头部进行0xCC检测。建议:没有特殊状况,对API下断时要避开在头部下断。
此外,在调用CloseHandle之前,虚拟机会手工构造一个SEH异常处理例程,如果调用成功,没出现异常,那么虚拟机会移除这个SEH。假如调用CloseHandle触发异常,那么将万劫不复,程序进入0x4A99AE后,你会寸步难行,我这儿遇到的是非法写入的异常,程序一直卡在那里。

想过掉CloseHandle检测,可以在CloseHandle头部直接返回(eax=0),然后恢复选区即可。
测试程序:(ps:这个程序在vmp样本三文件夹)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #include <Windows.h> #include <iostream> int g_num = 0 ; void __declspec(naked) test_vmp( int a, int b) { __asm { mov eax,[esp + 4 ] / / [esp + 4 ] = = a mov ebx,[esp + 8 ] / / [esp + 8 ] = = b xor eax, ebx mov g_num,eax ret } } void test2() { test_vmp( 0x10 , 0x21 ); printf( "%X\n" , g_num); } int main() { test2(); system( "pause" ); return 0 ; } |
用OD打开,找到test_vmp函数的位置:0x4010E0。

打开vmp,开始加壳:

选择最大保护。

把加壳后的程序,拖入OD,程序断在了入口处:

找到0x4010E0,下一个硬件断点,然后按F9让程序跑起来:

程序断在了这里,按F7单步跟踪:

程序会先构建虚拟机环境,上面已经分析了,这里省略。

开始进入正题,因为程序在加壳的时候勾选了隐藏常量和内存保护,对我这种初等选手,所以刚开始的时候就遇到了困难。
在第一条指令(mov eax,[esp+4])中,虚拟机会先对[esp+4]解码,又因为4是常量,所以虚拟机刚开始的时候,会对这个常量解密操作。
大概步骤:程序会在esi指向的调度表读取四个字节,并且在解密过程,还会读取多次,来对常量解密。esp寄存器也是加密了的,解码操作和解码常量差不多。
mov eax,[esp+4] 模型是:mov 寄存器,内存
这种模式的指令会走如下的handler块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 004A6AFF | 66 : 0FB6C3 | movzx ax,bl | 004A6B03 | F6D0 | not al | 004A6B05 | 66 : 0FB6C3 | movzx ax,bl | 004A6B09 | 66 : 0FBEC2 | movsx ax,dl | 004A6B0D | 8B45 00 | mov eax,dword ptr ss:[ebp] | 004A6B10 | 60 | pushad | 004A6B11 | E9 41140000 | jmp debug_test2.vmp. 4A7F57 | 004A6A55 | 36 : 8B00 | mov eax,dword ptr ss:[eax] | 004A6A58 | 55 | push ebp | 004A6A59 | E9 8A000000 | jmp debug_test2.vmp. 4A6AE8 | 004A6AE8 | 882C24 | mov byte ptr ss:[esp],ch | 004A6AEB | FF3424 | push dword ptr ss:[esp] | 004A6AEE | 8945 00 | mov dword ptr ss:[ebp],eax | 004A6AF1 | FF3424 | push dword ptr ss:[esp] | 004A6AF4 | 9C | pushfd | 004A6AF5 | 9C | pushfd | 004A6AF6 | 8D6424 38 | lea esp,dword ptr ss:[esp + 38 ] | 004A6AFA | E9 83150000 | jmp debug_test2.vmp. 4A8082 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 可以化简为: 004A6B0D | 8B45 00 | mov eax,dword ptr ss:[ebp] | 004A6A55 | 36 : 8B00 | mov eax,dword ptr ss:[eax] | 004A6AEE | 8945 00 | mov dword ptr ss:[ebp],eax | 可以把上面的handler块命名为Handler_Reg_Mem |
关于xor eax, ebx 指令,上面有分析过,除了垃圾指令,其他没变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 004A6113 | push ebp | 004A6114 | lahf | 004A6115 | pushad | 004A6116 | mov eax,dword ptr ss:[ebp] | 004A6119 | rcr dh, 6 | 004A611C | bts dx, 7 | 004A6121 | bts dx, 1 | 004A6126 | mov edx,dword ptr ss:[ebp + 4 ] | 004A6129 | stc | 004A612A | not eax | 004A612C | pushfd | 004A612D | push dword ptr ss:[esp] | 004A6130 | not edx | 004A6132 | jmp debug_test2.vmp. 4A66E3 | 004A6137 | not esi | 004A6139 | mov byte ptr ss:[esp],dh | 004A613C | pushfd | 004A613D | push C19B900A | 004A6142 | pushfd | 004A6143 | lea esp,dword ptr ss:[esp + 4C ] | 004A6147 | jmp debug_test2.vmp. 4A60CA | 004A66E3 | clc | 004A66E4 | and eax,edx | 004A66E6 | push edi | 004A66E7 | push esi | 004A66E8 | jmp debug_test2.vmp. 4A61EF | 004A61EF | mov dword ptr ss:[ebp + 4 ],eax | 004A61F2 | mov byte ptr ss:[esp + C], 31 | 31 : '1' 004A61F7 | mov byte ptr ss:[esp + C], 65 | 65 : 'e' 004A61FC | push A8B985C4 | 004A6201 | mov word ptr ss:[esp + C],sp | 004A6206 | pushfd | 004A6207 | pop dword ptr ss:[esp + 34 ] | 004A620B | mov byte ptr ss:[esp + 8 ],ah | 004A620F | call debug_test2.vmp. 4A78E2 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - / / 可以化简为: 004A6116 | mov eax,dword ptr ss:[ebp] | 004A6126 | mov edx,dword ptr ss:[ebp + 4 ] | 004A612A | not eax | 004A6130 | not edx | 004A66E4 | and eax,edx | 004A61EF | mov dword ptr ss:[ebp + 4 ],eax | |
在mov g_num,eax这条指令中,虚拟机对g_num内存地址也是加密了的,解密时候,程序会对esi指向的调度表读取四个字节,并且会读取多次,经过计算最终得到g_num的内存地址。
mov g_num,eax 模型是:mov 内存地址,寄存器
这种模式的指令会走如下handler块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 004A69C7 | 04 08 | add al, 8 | 004A69C9 | 60 | pushad | 004A69CA | 66 : 05 7B36 | add ax, 367B | 004A69CE | 8B45 00 | mov eax,dword ptr ss:[ebp] | 004A69D1 | 20D6 | and dh,dl | 004A69D3 | 66 :F7D2 | not dx | 004A69D6 | 08C2 | or dl,al | 004A69D8 | 8B55 04 | mov edx,dword ptr ss:[ebp + 4 ] | 004A69DB | 68 37EDD2A5 | push A5D2ED37 | 004A69E0 | 66 : 81FF 7052 | cmp di, 5270 | 004A69E5 | 84CB | test bl,cl | 004A69E7 | F8 | clc | 004A69E8 | 83C5 08 | add ebp, 8 | 004A69EB | FF7424 04 | push dword ptr ss:[esp + 4 ] | 004A69EF | 66 : 896424 14 | mov word ptr ss:[esp + 14 ],sp | 004A69F4 | E9 94F4FFFF | jmp debug_test2.vmp. 4A5E8D | 004A5E8D | 8910 | mov dword ptr ds:[eax],edx | 004A5E8F | 9C | pushfd | 004A5E90 | 66 : 895424 04 | mov word ptr ss:[esp + 4 ],dx | 004A5E95 | 8D6424 2C | lea esp,dword ptr ss:[esp + 2C ] | 004A5E99 | E9 E4210000 | jmp debug_test2.vmp. 4A8082 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 可以化简为: 004A69CE | 8B45 00 | mov eax,dword ptr ss:[ebp] | 004A69D8 | 8B55 04 | mov edx,dword ptr ss:[ebp + 4 ] | 004A69E8 | 83C5 08 | add ebp, 8 | 004A5E8D | 8910 | mov dword ptr ds:[eax],edx | 可以把上面的handler块命名为Handler_Mem_Reg |
小结
关于隐藏常量和内存保护的解密过程,只是跟了几遍,了解了大概流程,没有具体分析,退出虚拟机时,加密寄存器,这个解密模式和隐藏常量和内存保护的解密过程似乎差不多。总的来说,这次分析过程是失败的,因为隐藏常量、内存保护以及离开虚拟机时加密寄存器的解密过程没有分析出来,只是把汇编指令在虚拟机中的handler块找出来了。我觉得,这些解密操作,正是vmprotect虚拟机最精华的部分之一,在跟踪这些解密操作的时候,我脑袋都大了,暂时先搁在这儿,做一些更有意义的事情(^_^)。这节可以省略不看。如果有像我一样的初等选手,跟起来又有点费劲,又想了解这个解密过程的,可以在看雪搜搜,有很多大神都应该分析过。
项目名称:指令壳框架
功能:可以对32位可执行程序加壳(*.exe)
编译器:vs2019(编译模式采用的是Debug模式,也就是调试模式)
开发语言:C、C++、内联汇编
解决方案:一个解决方案,两个项目(VMProtect、Stub,VMProtect是现实核心功能,Stub是外壳部分)
- 程序用win32编写的,没用MFC或者QT。
- 在整个项目中,会用到汇编引擎和反汇编引擎,汇编引擎用的是XEDparse,反汇编引擎用的是BeaEngine。
- 此外,我定义了几个主要模块:指令分析器,垃圾模块构造指令器,IAT加密(解密)模块,反调试模块。
- 没有处理异常,也就是说,加了异常处理的函数,不要加壳。
Common文件夹里封装了一些类:
CString类即是字符串操作的类,支持字符串和整型混合相加(字符串+(DWORD)16进制/10进),生成一个字符串。(注意:加16进制时,前面要加DWORD表示这是16进制。)
PE类封装了处理PE文件格式一些函数,比如文件拉伸、修复重定位表、添加新节等等。
FileOpenration类是文件操作类,封装了打开文件、删除文件、保存文件、创建子进程等等一些函数。


在加壳过程中,要频繁用到内存申请、内存释放的操作,为了防止内存泄漏,我封装了一个类(AllocMemory)用来申请内存,
这个类的作用就是只管申请内存,不用管释放,这个类会自动释放内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #pragma once #include <vector> #include <basetsd.h> using namespace std; class AllocMemory { vector<char * >p; public: virtual ~AllocMemory() { for ( int i = 0 ; i < p.size(); i + + ) { if (p[i] = = 0 ) { continue ; } free(p[i]); p[i] = 0 ; } p.clear(); } public: template<typename T> T auto_malloc( ULONG_PTR MAXSIZE) { T tmp = (T)malloc(MAXSIZE); memset((char * )tmp, 0 , MAXSIZE); p.push_back((char * )tmp); return tmp; } }; |

程序外观:


实验:对test_vmp函数加壳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void __declspec(naked) test_vmp( int a, int b) { __asm { mov eax, [esp + 4 ] mov eax, [esp + 4 ] mov eax, [esp + 4 ] mov eax,[esp + 4 ] / / [esp + 4 ] = = a mov ebx,[esp + 8 ] / / [esp + 8 ] = = b xor eax, ebx mov g_num,eax ret } } int main() { test_vmp( 1 , 2 ); system( "pause" ); return ; } |
拖入OD,在0x401010这个位置:

打开vmp_1.0,开始加壳:


回到项目,点击编译:

用OD打开,经过加了花指令的从IAT表拷贝过来的API的跳转地址,每次执行时,样式都不一样。
第一次打开:

用OD第二次打开:

此外,对加了该指令壳的函数,每次进入该函数后,指令也会变,这些操作都是在外壳中完成的,具体请参考Stub项目。
指令分析器的作用:把要保护的指令,翻译为中间表示。我用的是BeaEngine引擎,所以在解析指令的时候,需要遵循BeaEngine反汇编引擎的规则。
指令分析器的主框架如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | / / 解析要保护的指令,翻译为中间表示 void MiddleRepresent(DISASM disAsm) { / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / / * 1 、是否有操作 3 * / / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / if (NO_ARGUMENT ! = disAsm.Argument3.ArgType) { switch (disAsm.Argument3.ArgType & 0xF0000000 ) { case REGISTER_TYPE: / / 寄存器 break ; case MEMORY_TYPE: / / 内存 break ; case CONSTANT_TYPE: / / 常数 break ; default: break ; } } / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / / * 2 、是否有操作 2 * / / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / if (NO_ARGUMENT ! = disAsm.Argument2.ArgType) { switch (disAsm.Argument2.ArgType & 0xF0000000 ) { case REGISTER_TYPE: / / 寄存器 break ; case MEMORY_TYPE: / / 内存 break ; case CONSTANT_TYPE: / / 常数 break ; default: break ; } } / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / / * 3 、是否有操作 1 * / / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / if (NO_ARGUMENT ! = disAsm.Argument1.ArgType) { switch (disAsm.Argument1.ArgType & 0xF0000000 ) { case REGISTER_TYPE: / / 寄存器 break ; case MEMORY_TYPE: / / 内存 break ; case CONSTANT_TYPE: / / 常数 break ; default: break ; } } / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / / * 4 、处理普通handler * / / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / / / 省略... / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / / * 5 、判断是否有辅助handler * / / * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / if ( 0x10000000 ! = disAsm.Argument1.ArgType || 0x10000000 ! = disAsm.Argument2.ArgType || 0x10000000 ! = disAsm.Argument3.ArgType ) { if (NO_ARGUMENT ! = disAsm.Argument1.ArgType) { switch (disAsm.Argument1.ArgType & 0xF0000000 ) { case REGISTER_TYPE: / / 寄存器 break ; case MEMORY_TYPE: / / 内存 break ; case CONSTANT_TYPE: / / 常数 break ; default: break ; } } } } |
上面这个解析器,对一条指令是从右往左解析的,比如这条指令:mov eax,eax
翻译为中间表示就是:
1 2 3 4 | vPushReg VR_ecx / / 操作 2 vPushReg VR_eax / / 操作 1 vMOV / / 普通handler vPopReg VR_eax / / 辅助handler |
handler操作和数据是分别保存的,仍然以上面那条指令为例:
1 2 3 4 5 6 7 8 9 10 11 | vPushReg VR_ecx / / 操作 2 vPushReg VR_eax / / 操作 1 vMOV / / 普通handler vPopReg VR_eax / / 辅助handler 把VR_ecx、VR_eax、VR_eax分离出来保存在一个数据表的结构体中。 翻译就可以这样表示了: vPushReg vPushReg vMOV vPopReg |
内存操作处理起来比较麻烦,至少对我来说是如此,MemoryMiddle()函数用来专门处理内存操作。
例如这条指令 mov dword ptr[eax+ecx*4+0x401000],eax,可以译成如下的中间表示:
1 2 3 4 5 6 7 8 9 | vPushReg / / eax vPushImm4 / / 4 vPushReg4 / / ecx vMUL_MEM / / * vPushReg4 / / eax vAdd4 / / + vPushImm4 / / 0x401000 vAdd / / + vWriteMemDs4 |
此外,局部变量的操作,比如这条指令:mov dword ptr[ebp-0x8],eax,仍然可以用MemoryMiddle函数来翻译:
1 2 3 4 | vPushImm4 / / 0xFFFFFFF8 vPushReg4 / / ebp vAdd4 负 8 会被BeaEngine引擎解析为 0xFFFFFFF8 ,ebp - 0x8 与 0xFFFFFFF8 + ebp是等价的 |
下面举个完整的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | void _declspec(naked) _stdcall code_vm_test( int x) { / / MessageBoxA(NULL, 0 , 0 , 0 ); _asm { sub esp, 0x150 push eax push ecx push edx lea ecx, code_vm_test add ecx, 10h push ecx pop dword ptr[g_num + 4 ] jmp L14 sub esp, 0x150 L14: mov ecx, 1 xor eax,eax mov ah, 10h mov bl, 30h L13: add ecx, 1 add ah,bl cmp ecx, 0x10 jle L13 / / je L11 add eax, 0x432 mov ebx, 4 mov ecx, 1 mov byte ptr[g_num + ebx + ecx * 4 ],ah / / mov word ptr[g_num + ebx + ecx * 4 ],ax / / mov dword ptr[g_num + ebx + ecx * 4 ],eax jmp L12 / / L11: mov g_num,eax call test2 L12: mov eax, 01h / / eax = 1 :取CPU序列号 xor edx, edx cpuid mov acpuid, eax mov dl,byte ptr[acpuid] mov lcpuid, edx pop edx pop ecx pop eax add esp, 0x150 retn 4 } } |
上面这个函数,翻译为中间表示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 | VMStartVM_2 vPushImm4 vPushReg4 vSUB4 vPopReg4 VCheckESP vPushReg4 vPUSH vPushReg4 vPUSH vPushReg4 vPUSH vPushImm4 vReadMemDs4 vPushReg4 vPopReg4 vPushImm4 vPushReg4 vAdd4 vPopReg4 vPushReg4 vPUSH vRetnNOT_ vNotSimulate vResumeStart_ vPushImm4 vJMP vPushImm4 vPushImm4 vPushReg4 vSUB4 vPopReg4 VCheckESP vPushImm4 vPushReg4 vMOV4 vPopReg4 vPushReg4 vPushReg4 vXOR4 vPopReg4 vPushImm4 vPushReg1_above vMOV4 vPopReg1_above vPushImm4 vPushReg1_low vMOV4 vPopReg1_low vPushImm4 vPushReg4 vAdd4 vPopReg4 vPushReg1_low vPushReg1_above vAdd4 vPopReg1_above vPushImm4 vPushReg4 vCMP vPushImm4 vJLE vPushImm4 vPushImm4 vPushReg4 vAdd4 vPopReg4 vPushImm4 vPushReg4 vMOV4 vPopReg4 vPushImm4 vPushReg4 vMOV4 vPopReg4 vPushReg1_above vPushImm4 vPushReg4 vMUL_MEM vPushReg4 vAdd4 vPushImm4 vAdd4 vWriteMemDs1 vPushImm4 vJMP vPushImm4 vPushReg4 vPushImm4 vWriteMemDs4 vPushImm4 vRetnNOT_ vCALL vResumeStart_ vPushImm4 vPushReg4 vMOV4 vPopReg4 vPushReg4 vPushReg4 vXOR4 vPopReg4 vRetnNOT_ vNotSimulate vResumeStart_ vPushReg4 vPushImm4 vWriteMemDs4 vPushImm4 vReadMemDs1 vPushReg1_low vPopReg1_low vPushReg4 vPushImm4 vWriteMemDs4 vPushReg4 vPopReg4 vPOP4 vPushReg4 vPopReg4 vPOP4 vPushReg4 vPopReg4 vPOP4 vPushImm4 vPushReg4 vAdd4 vPopReg4 VCheckESP vPushImm4 vRETN |
垃圾指令构造器的设计很简单,对我来说,难点在于垃圾指令的选择,有些指令是不能作为垃圾指令,改变普通寄存器的指令我没有用,比如AAA指令,会改变eax寄存器的值。
下面是垃圾指令的构造器核心函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | / / 生成垃圾指令 CString VMLoader2::ProduceRubbishOpecode(char * reg04, char * reg05) { VMTable vmtbl = vmtable32[SrandNum( 0 , m_vmlength)]; CString str = vmtbl.strInstruction; / / 1 、目的操作 switch (vmtbl.optype[ 0 ]) { case NONETYPE: / / 没有操作数 break ; case IMMTYPE: / / 立即数 { if ( 8 = = vmtbl.bitnum[ 0 ]) { str = str + " " + 4 ; } else if ( 16 = = vmtbl.bitnum[ 0 ]) { str = str + " " + 4 ; } else { str = str + " " + 8 ; } } break ; case REGTYPE: / / 寄存器 { if ( 8 = = vmtbl.bitnum[ 0 ]) { for ( int i = 0 ; i < 14 ; i + + ) { if (stricmp(reg04, regname_[ 2 ][i]) = = 0 ) { str = str + " " + regname_[ 0 ][i]; break ; } } } else if ( 16 = = vmtbl.bitnum[ 0 ]) { for ( int i = 0 ; i < 14 ; i + + ) { if (stricmp(reg05, regname_[ 2 ][i]) = = 0 ) { str = str + " " + regname_[ 1 ][i]; break ; } } } else { str = str + " " + reg05; } } break ; case MEMTYPE: / / 内存 { / / 随机选择vmp1节中没有用到的内存 DWORD dnum = SrandNum(m_vmps.vmp1_startaddr + 0x4000 , m_vmps.vmp1_startaddr + 0x5000 ); CString memstr = dnum; if ( 8 = = vmtbl.bitnum[ 0 ]) { str = str + " byte ptr[" + memstr.GetString() + "]" ; } else if ( 16 = = vmtbl.bitnum[ 0 ]) { str = str + " word ptr[" + memstr.GetString() + "]" ; } else { str = str + " dword ptr[" + memstr.GetString() + "]" ; } } break ; default: break ; } / / 2 、源操作数 switch (vmtbl.optype[ 1 ]) { case NONETYPE: / / 没有操作数 break ; case IMMTYPE: / / 立即数 { if ( 8 = = vmtbl.bitnum[ 1 ]) { str = str + "," + 4 ; } else if ( 16 = = vmtbl.bitnum[ 1 ]) { str = str + "," + 8 ; } else { str = str + "," + 4 ; } } break ; case REGTYPE: / / 寄存器(操作数 2 的寄存器可以在 8 个寄存器中任意选择) { if ( 0 = = stricmp(vmtbl.strInstruction, "xchg" )) { / / 如果是xchg,寄存器则选择reg04,或者reg05 if ( 8 = = vmtbl.bitnum[ 1 ]) { for ( int i = 0 ; i < 14 ; i + + ) { if (stricmp(reg05, regname_[ 2 ][i]) = = 0 ) { str = str + "," + regname_[ 0 ][i]; break ; } } } else if ( 16 = = vmtbl.bitnum[ 1 ]) { for ( int i = 0 ; i < 14 ; i + + ) { if (stricmp(reg04, regname_[ 2 ][i]) = = 0 ) { str = str + "," + regname_[ 1 ][i]; break ; } } } else { str = str + "," + reg04; } break ; } if ( 8 = = vmtbl.bitnum[ 1 ]) { str = str + "," + regname_[ 0 ][SrandNum( 0 , 8 )]; } else if ( 16 = = vmtbl.bitnum[ 1 ]) { str = str + "," + regname_[ 1 ][SrandNum( 0 , 8 )]; } else { str = str + "," + regname_[ 2 ][SrandNum( 0 , 8 )]; } } break ; case MEMTYPE: / / 内存 { / / 随机选择vmp1节内的地址,或者选esp寄存器 DWORD dnum = SrandNum(m_vmps.vmp1_startaddr, m_vmps.vmstartaddr); CString memstr = dnum; const char * memchr[ 5 ] = { memstr.GetString(), "esp+20" , "esp+28" , "esp+0x30" , "esp+0x14" }; const char * srandstr = memchr[SrandNum( 0 , 5 )]; if ( 8 = = vmtbl.bitnum[ 1 ]) { str = str + ",byte ptr[" + srandstr + "]" ; } else if ( 16 = = vmtbl.bitnum[ 1 ]) { str = str + ",word ptr[" + srandstr + "]" ; } else { str = str + ",dword ptr[" + srandstr + "]" ; } } break ; default: break ; } return str ; } |
ProduceRubbishOpecode函数,每被调用一次就可以构造一条垃圾指令。
把要用到的handler全部放到一个表格中归类整理,如下图:

先来举一个例子,比如指令:xor eax,eax
1 2 3 4 5 | 翻译为中间表示: vPushReg4 vPushReg4 vXOR4 vPopReg4 |
上面每一个中间表示的handler都有对应一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | CString vPushReg4(char * VR0, char * VR1) { CString str = "mov " ; str = str + VR0 + ",dword ptr[ebp]\n" ; str = str + "add ebp,4\n" ; str = str + "xor " + VR0 + "," + dataencrypt + "\n" ; str = str + "mov " + VR0 + ",dword ptr [edi+" + VR0 + "*4]\n" ; str = str + "push " + VR0 + "\n" ; return str ; } CString vXOR4(char * VR0, char * VR1) { CString str = "mov " ; str = str + VR0 + ",dword ptr[esp]\n" ; str = str + "mov " + VR1 + ",dword ptr[esp+4]\n" ; str = str + "xor " + VR0 + "," + VR1 + "\n" ; str = str + "add esp,8\n" ; str = str + "push " + VR0 + "\n" ; return str ; } CString vPopReg4(char * VR0, char * VR1) { CString str = "mov " ; str = str + VR0 + ",dword ptr[ebp]\n" ; str = str + "xor " + VR0 + "," + dataencrypt + "\n" ; str = str + "add ebp,4\n" ; str = str + "pop dword ptr[edi+" + VR0 + "*4]\n" ; return str ; } |
vmtest.h和vmtest.cpp分别存放了所有handler块的声明和具体实现。请参考VMProtect项目。
IAT解密模块、反调试模块以及花指令构造器,都在Stub项目中,Stub.dll动态库是整个程序的外壳部分。
IAT加密过程:
第一步把IAT表转存到一个临时数据结构中,然后清除IAT和INT表,最后把临时数据结构中的函数名称加密。这步是在VMProtect项目中完成的。
第二步在Stub中解密这个临时数据结构,解密之后,再加密,并且加上花指令。
花指令构造器具体实现在JunkCode.cpp文件中。以下列出花指令构造器的核心函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 | / / 这是一个多跳、往回跳的花指令构造器,之后跳到真实指令。 void JunkCode_::SrandJunkCode() { BUFFERSTRUCT_ buffer ; buffer .value = jncode_one; buffer .match = 1 ; g_buffer.push_back( buffer ); buffer .value = buffer .match = 0 ; g_buffer.push_back( buffer ); char x = jncode[rand_v() % 4 ]; buffer .value = x; g_buffer.push_back( buffer ); if (x = = 0xFF ) { buffer .value = second[rand_v() % 2 ]; g_buffer.push_back( buffer ); } int y = rand_v() % 3 ; for ( int i = 0 ; i < y; i + + ) { buffer .value = randsss[rand_v() % RANDSSS]; g_buffer.push_back( buffer ); } buffer .value = jncode_one; buffer .match = 0x3 ; buffer .jmpmatch = 0x2 ; g_buffer.push_back( buffer ); buffer .value = buffer .match = buffer .jmpmatch = 0 ; g_buffer.push_back( buffer ); x = jncode[rand_v() % 4 ]; buffer .value = x; g_buffer.push_back( buffer ); if (x = = 0xFF ) { buffer .value = second[rand_v() % 2 ]; g_buffer.push_back( buffer ); } y = rand_v() % 3 ; for ( int i = 0 ; i < y; i + + ) { buffer .value = randsss[rand_v() % RANDSSS]; g_buffer.push_back( buffer ); } for ( int i = 0 ; i < 5 ; i + + ) { if (i = = 0 ) { buffer .jmpmatch = 1 ; buffer .recodemodify = 1 ; buffer .value = moveax[i]; g_buffer.push_back( buffer ); buffer .jmpmatch = buffer .recodemodify = 0 ; continue ; } buffer .value = moveax[i]; g_buffer.push_back( buffer ); } buffer .value = jncode_one; buffer .match = 0x2 ; g_buffer.push_back( buffer ); buffer .value = buffer .match = buffer .jmpmatch = 0 ; g_buffer.push_back( buffer ); x = jncode[rand_v() % 4 ]; buffer .value = x; g_buffer.push_back( buffer ); if (x = = 0xFF ) { buffer .value = second[rand_v() % 2 ]; g_buffer.push_back( buffer ); } y = rand_v() % 2 ; for ( int i = 0 ; i < y; i + + ) { buffer .value = randsss[rand_v() % RANDSSS]; g_buffer.push_back( buffer ); } for ( int i = 0 ; i < 7 ; i + + ) { if (i = = 0 ) { buffer .jmpmatch = 3 ; / / 3 buffer .value = jmpoep[i]; g_buffer.push_back( buffer ); buffer .match = buffer .jmpmatch = 0 ; continue ; } buffer .value = jmpoep[i]; g_buffer.push_back( buffer ); } / / 修复数据 vector_< BUFFERSTRUCT_>::iterator iter_buff = g_buffer.begin(); vector_< BUFFERSTRUCT_>::iterator iter_buff_1 = g_buffer.begin(); for ( int i = 0 ; i < g_buffer.size(); i + + ) { if (( * iter_buff).match ! = 0 ) { int temp = ( * iter_buff).match; for ( int j = 0 ; j < g_buffer.size(); j + + ) { if (temp = = ( * iter_buff_1).jmpmatch) { ( * (iter_buff + 1 )).value = j - i - 2 ; iter_buff_1 = g_buffer.begin(); break ; } + + iter_buff_1; } } + + iter_buff; } } |
1)怎么添加handler块?
测试的时候,我只是对常用的指令添加了handler块,还有很多指令是没有处理的,那么,程序在加壳过程中,如果有jmp或者jxx跳转到未知指令(未知指令是指没有添加handler的指令,找不到匹配),就会出错,此时,则应该先检查是否有未知指令,并添加相应的handler块。
添加方式:以inc指令为例子
第一步:
在vmtest.h中添加声明CString vINC(char VR0, char VR1);

第二步:在vmtest.cpp中实现其函数功能。

第三步:
在VMLoader2.cpp,把55改成56,在g_FunName数组里添加{vINC,"inc ","vINC "},注意"inc "和"vINC ",后面有一个空格,
不然程序在匹配inc指令的时候匹配不上,就会把inc当成不可模拟指令来处理。

2)写在末尾
这个加壳程序,设计上有先天缺陷,这可以归咎于我正向开发的基础不扎实,还有就是只掌握了编译原理的一些皮毛知识,好些地方都有点乱,像是硬怼的,很多地方现在还能看到打斗的痕迹。整个程序,由于在设计上的缺陷,使得虚拟机不能对寄存器进行轮转操作。另外,汇编指令是直接换成handler块的,中间没有先对汇编指令进行任何变形。所以,这只是一个模拟vmprotect的最最简单的指令壳子。
3)编译问题
编译时,请采用Debug和x86模式:

编译时,可能会遇到的编码错误:

Stub项目运行库选择多线程(/MT)
