查看原文
其他

指令壳开源

舒默哦 看雪学苑 2022-07-01

本文为看雪论坛精华文章

看雪论坛作者ID:舒默哦




前言




在把壳子开源之前,我会先对VMProtect1.70.4这个版本做一个简单的分析。在这几天分析过程中,我感受到了VMProtect的威力,并得出一个结论:分析VMProtect非常耗时间,如果没有做好与之长期斗争的准备,很难有实用的成果。
另外,由于我第一次分析VMProtect,有分析不对的地方或者术语用得不恰当的地方,希望老萌萌们能指出来,我立即修改,不能误人子弟。分析的样本有三份,我放到了附件里面。
壳子我是去年写的,当时刚刚看了<<加密与解密>>第四版的第21章,我估摸了一下,自己可以写出来,然后就写出来了。在写之前,我记得当时还在bilibili观看了哈工大姜守旭教授教授的编译原理这门课程的视频,了解了大概就开干,看视频这个操作,对我写指令壳起了一种壮胆的效果。

现在要开源,我又熟悉了下整个项目的流程。代码写得有点乱,我会画一张加壳时的流程图做一个直观的说明,和其他一些要点说明,以减少读代码时遇到的困惑。如果遇到调试或者编译问题,可以留言,我看到会及时回复。指令壳源码我会上传到附件。

分析vmp1.70.4




1. 速度最快


写一个简单程序来测试:(ps:这个程序在vmp样本一文件夹)
#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这节里面:

//入口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:_mainCRTStartup004A703B| 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+4A78004A52F8| jmp debug_test2.vmp.4A64C1 | 004A64C1| call debug_test2.vmp.4A5FE9 | 004A5FE9| pushad |004A5FEA| mov dword ptr ss:[esp+24],ebp | [esp+24]:_mainCRTStartup004A5FEE| push esp |004A5FEF| pushfd |004A5FF0| push 72B57CE7 |004A5FF5| pushfd |004A5FF6| mov dword ptr ss:[esp+30],eax | eax:___use_sse2_mathfcns+2D1004A5FFA| 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+2D1004A6BC9| call debug_test2.vmp.4A5C3D | 004A5C3D| call debug_test2.vmp.4A6681 | 004A6681| mov dword ptr ss:[esp+38],edx | [esp+38]:___use_sse2_mathfcns+2D1004A6685| push dword ptr ss:[esp+8] | [esp+8]:___use_sse2_mathfcns+42C0004A6689| mov dword ptr ss:[esp+38],ecx | [esp+38]:___use_sse2_mathfcns+2D1, ecx:___use_sse2_mathfcns+2D1004A668D| 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+2D1004A633B| call debug_test2.vmp.4A6129 | 004A6129| mov dword ptr ss:[esp+4],ebx |004A612D| movsx esi,cl | esi:___use_sse2_mathfcns+2D1004A6130| 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寄存器里。
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块:
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 块,整个流程可以记录为如下形式:

通过写程序来验证,与虚拟机算出来的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到真实寄存器,退出虚拟机。

退出虚拟机:

2. 开启检测调试器


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

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


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


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

程序执行到入口后,按F7单步跟,步骤和上面“01速度最快"分析时相差无几,会先构建虚拟机环境。
虚拟环境构建完成后,接着按F7单步跟,我的想法是,很快就能找到一些反调试的线索,但是跟了几个小时,发现不对劲了,和上面“01速度最快"分析时用手工跟踪,完全不在一个数量级的。

天气又大,整个人木在那里。后来,想到在退出虚拟机那个地方下断,方法就是搜索vmp1这节的ret或者ret xx,最终找到两个地方,一个是调度器ret xx,这个不管,另一个就是下图所给出的,在ret 0x40处下断。
按F9程序跑起来后,会断在这里,能看到右边寄存器窗口的字符串。这个位置,可以作为过掉检测调试器的突破口。

当然,最好的办法应该是在一些能检测出调试器的API函数下断。


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

想过掉CloseHandle检测,可以在CloseHandle头部直接返回(eax=0),然后恢复选区即可。

3. 最大保护


测试程序:(ps:这个程序在vmp样本三文件夹)
#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块:
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 指令,上面有分析过,除了垃圾指令,其他没变:
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块:
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

4. 小结


关于隐藏常量和内存保护的解密过程,只是跟了几遍,了解了大概流程,没有具体分析,退出虚拟机时,加密寄存器,这个解密模式和隐藏常量和内存保护的解密过程似乎差不多。
总的来说,这次分析过程是失败的,因为隐藏常量、内存保护以及离开虚拟机时加密寄存器的解密过程没有分析出来,只是把汇编指令在虚拟机中的handler块找出来了。
我觉得,这些解密操作,正是vmprotect虚拟机最精华的部分之一,在跟踪这些解密操作的时候,我脑袋都大了,暂时先搁在这儿,做一些更有意义的事情(^_^)。这节可以省略不看。如果有像我一样的初等选手,跟起来又有点费劲,又想了解这个解密过程的,可以在看雪搜搜,有很多大神都应该分析过。

指令壳项目开源说明




1. 纲要


项目名称:指令壳框架
功能:可以对32位可执行程序加壳(*.exe)
编译器:vs2019(编译模式采用的是Debug模式,也就是调试模式)

开发语言:C、C++、内联汇编

解决方案:一个解决方案,两个项目(VMProtect、Stub,VMProtect是现实核心功能,Stub是外壳部分)

2. 项目说明以及一些注意事项


程序用win32编写的,没用MFC或者QT。
(1) 在整个项目中,会用到汇编引擎和反汇编引擎,汇编引擎用的是XEDparse,反汇编引擎用的是BeaEngine。
(2) 此外,我定义了几个主要模块:指令分析器,垃圾模块构造指令器,IAT加密(解密)模块,反调试模块。
(3) 没有处理异常,也就是说,加了异常处理的函数,不要加壳。
Common文件夹里封装了一些类:
CString类即是字符串操作的类,支持字符串和整型混合相加(字符串+(DWORD)16进制/10进),生成一个字符串。(注意:加16进制时,前面要加DWORD表示这是16进制。)

PE类封装了处理PE文件格式一些函数,比如文件拉伸、修复重定位表、添加新节等等。

FileOpenration类是文件操作类,封装了打开文件、删除文件、保存文件、创建子进程等等一些函数。



在加壳过程中,要频繁用到内存申请、内存释放的操作,为了防止内存泄漏,我封装了一个类(AllocMemory)用来申请内存,


这个类的作用就是只管申请内存,不用管释放,这个类会自动释放内存:
#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; }};

3. 流程图



程序外观:


实验:对test_vmp函数加壳
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项目。

4. 指令分析器


指令分析器的作用:把要保护的指令,翻译为中间表示。我用的是BeaEngine引擎,所以在解析指令的时候,需要遵循BeaEngine反汇编引擎的规则。
指令分析器的主框架如下:
//解析要保护的指令,翻译为中间表示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。


翻译为中间表示就是:
vPushReg VR_ecx //操作2vPushReg VR_eax //操作1vMOV //普通handlervPopReg VR_eax //辅助handler
handler操作和数据是分别保存的,仍然以上面那条指令为例:
vPushReg VR_ecx //操作2vPushReg VR_eax //操作1vMOV //普通handlervPopReg VR_eax //辅助handler 把VR_ecx、VR_eax、VR_eax分离出来保存在一个数据表的结构体中。翻译就可以这样表示了:vPushRegvPushRegvMOV vPopReg
内存操作处理起来比较麻烦,至少对我来说是如此,MemoryMiddle()函数用来专门处理内存操作。


例如这条指令mov dword ptr[eax+ecx*4+0x401000],eax,可以译成如下的中间表示:
vPushReg //eaxvPushImm4 //4vPushReg4 //ecxvMUL_MEM //*vPushReg4 //eaxvAdd4 //+vPushImm4 //0x401000vAdd //+vWriteMemDs4
此外,局部变量的操作,比如这条指令:mov dword ptr[ebp-0x8],eax,仍然可以用MemoryMiddle函数来翻译:
vPushImm4 //0xFFFFFFF8vPushReg4 //ebpvAdd4负8会被BeaEngine引擎解析为0xFFFFFFF8,ebp-0x8与0xFFFFFFF8+ebp是等价的
下面举个完整的例子:
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 }}
上面这个函数,翻译为中间表示如下:
VMStartVM_2vPushImm4vPushReg4vSUB4vPopReg4VCheckESPvPushReg4vPUSHvPushReg4vPUSHvPushReg4vPUSHvPushImm4vReadMemDs4vPushReg4vPopReg4vPushImm4vPushReg4vAdd4vPopReg4vPushReg4vPUSHvRetnNOT_vNotSimulatevResumeStart_vPushImm4vJMPvPushImm4vPushImm4vPushReg4vSUB4vPopReg4VCheckESPvPushImm4vPushReg4vMOV4vPopReg4vPushReg4vPushReg4vXOR4vPopReg4vPushImm4vPushReg1_abovevMOV4vPopReg1_abovevPushImm4vPushReg1_lowvMOV4vPopReg1_lowvPushImm4vPushReg4vAdd4vPopReg4vPushReg1_lowvPushReg1_abovevAdd4vPopReg1_abovevPushImm4vPushReg4vCMPvPushImm4vJLEvPushImm4vPushImm4vPushReg4vAdd4vPopReg4vPushImm4vPushReg4vMOV4vPopReg4vPushImm4vPushReg4vMOV4vPopReg4vPushReg1_abovevPushImm4vPushReg4vMUL_MEMvPushReg4vAdd4vPushImm4vAdd4vWriteMemDs1vPushImm4vJMPvPushImm4vPushReg4vPushImm4vWriteMemDs4vPushImm4vRetnNOT_vCALLvResumeStart_vPushImm4vPushReg4vMOV4vPopReg4vPushReg4vPushReg4vXOR4vPopReg4vRetnNOT_vNotSimulatevResumeStart_vPushReg4vPushImm4vWriteMemDs4vPushImm4vReadMemDs1vPushReg1_lowvPopReg1_lowvPushReg4vPushImm4vWriteMemDs4vPushReg4vPopReg4vPOP4vPushReg4vPopReg4vPOP4vPushReg4vPopReg4vPOP4vPushImm4vPushReg4vAdd4vPopReg4VCheckESPvPushImm4vRETN

5. 垃圾指令构造器


垃圾指令构造器的设计很简单,对我来说,难点在于垃圾指令的选择,有些指令是不能作为垃圾指令,改变普通寄存器的指令我没有用,比如AAA指令,会改变eax寄存器的值。
下面是垃圾指令的构造器核心函数:
//生成垃圾指令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函数,每被调用一次就可以构造一条垃圾指令。

6. handler的设计


把要用到的handler全部放到一个表格中归类整理,如下图:

先来举一个例子,比如指令:xor eax,eax
翻译为中间表示:vPushReg4vPushReg4vXOR4vPopReg4
上面每一个中间表示的handler都有对应一个函数:
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项目。

7. IAT加密


IAT解密模块、反调试模块以及花指令构造器,都在Stub项目中,Stub.dll动态库是整个程序的外壳部分。


IAT加密过程:

第一步把IAT表转存到一个临时数据结构中,然后清除IAT和INT表,最后把临时数据结构中的函数名称加密。这步是在VMProtect项目中完成的。


第二步在Stub中解密这个临时数据结构,解密之后,再加密,并且加上花指令。


花指令构造器具体实现在JunkCode.cpp文件中。以下列出花指令构造器的核心函数:

//这是一个多跳、往回跳的花指令构造器,之后跳到真实指令。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; }}

8. 补充


(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):


本文附件可点击左下角阅读原文自行下载!



- End -




看雪ID:舒默哦

https://bbs.pediy.com/user-home-877885.htm

  *本文由看雪论坛 舒默哦  原创,转载请注明来自看雪社区。



《安卓高级研修班》2021年6月班火热招生中!



# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存