其他
指令壳开源
本文为看雪论坛精华文章
前言
另外,由于我第一次分析VMProtect,有分析不对的地方或者术语用得不恰当的地方,希望老萌萌们能指出来,我立即修改,不能误人子弟。分析的样本有三份,我放到了附件里面。
壳子我是去年写的,当时刚刚看了<<加密与解密>>第四版的第21章,我估摸了一下,自己可以写出来,然后就写出来了。在写之前,我记得当时还在bilibili观看了哈工大姜守旭教授教授的编译原理这门课程的视频,了解了大概就开干,看视频这个操作,对我写指令壳起了一种壮胆的效果。
现在要开源,我又熟悉了下整个项目的流程。代码写得有点乱,我会画一张加壳时的流程图做一个直观的说明,和其他一些要点说明,以减少读代码时遇到的困惑。如果遇到调试或者编译问题,可以留言,我看到会及时回复。指令壳源码我会上传到附件。
分析vmp1.70.4
1. 速度最快
#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:_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
以上操作是把寄存器压到栈里,执行完后如下图:
寄存器压栈完成后,会先计算出调度表的地址。
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。
按F7单步跟,(...省略不重要的部分),接着,程序把edi中所保存的寄存器,再吐出来给ebp所指向的堆栈空间,然后ebp赋值给esp,最后再pop到真实寄存器,退出虚拟机。
退出虚拟机:
2. 开启检测调试器
下拉列表选择最快速度,再把调试器勾选上,其他不选择。
加壳完成后,打开CFF来查看,程序会新增一节.vmp1,程序入口也在这节里面。
此外,还构建了一个TLS表:(但在这儿作用似乎不大)
程序执行到入口后,按F7单步跟,步骤和上面“01速度最快"分析时相差无几,会先构建虚拟机环境。
虚拟环境构建完成后,接着按F7单步跟,我的想法是,很快就能找到一些反调试的线索,但是跟了几个小时,发现不对劲了,和上面“01速度最快"分析时用手工跟踪,完全不在一个数量级的。
天气又大,整个人木在那里。后来,想到在退出虚拟机那个地方下断,方法就是搜索vmp1这节的ret或者ret xx,最终找到两个地方,一个是调度器ret xx,这个不管,另一个就是下图所给出的,在ret 0x40处下断。
按F9程序跑起来后,会断在这里,能看到右边寄存器窗口的字符串。这个位置,可以作为过掉检测调试器的突破口。
当然,最好的办法应该是在一些能检测出调试器的API函数下断。
IsDebuggerPresent
CheckRemoteDebuggerPresent
GetThreadContext
CloseHandle
NtQueryInformationProcess
NtSetInformationThread
//关于这些函数介绍,看雪里有很多大神发了反调试相关的帖子,搜一下就能找到。
注意:API下断时,不要在头部下断,虚拟机会对有些API函数的头部进行0xCC检测,比如在这个程序中,虚拟机执行到GetThreadContext函数之前,会对GetThreadContext函数的头部进行0xCC检测。建议:没有特殊状况,对API下断时要避开在头部下断。 此外,在调用CloseHandle之前,虚拟机会手工构造一个SEH异常处理例程,如果调用成功,没出现异常,那么虚拟机会移除这个SEH。假如调用CloseHandle触发异常,那么将万劫不复,程序进入0x4A99AE后,你会寸步难行,我这儿遇到的是非法写入的异常,程序一直卡在那里。
想过掉CloseHandle检测,可以在CloseHandle头部直接返回(eax=0),然后恢复选区即可。
3. 最大保护
#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. 项目说明以及一些注意事项
(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. 指令分析器
指令分析器的主框架如下:
//解析要保护的指令,翻译为中间表示
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 //操作2
vPushReg VR_eax //操作1
vMOV //普通handler
vPopReg VR_eax //辅助handler
handler操作和数据是分别保存的,仍然以上面那条指令为例:
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()函数用来专门处理内存操作。
vPushReg //eax
vPushImm4 //4
vPushReg4 //ecx
vMUL_MEM //*
vPushReg4 //eax
vAdd4 //+
vPushImm4 //0x401000
vAdd //+
vWriteMemDs4
此外,局部变量的操作,比如这条指令:mov dword ptr[ebp-0x8],eax,仍然可以用MemoryMiddle函数来翻译:
vPushImm4 //0xFFFFFFF8
vPushReg4 //ebp
vAdd4
负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_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
5. 垃圾指令构造器
下面是垃圾指令的构造器核心函数:
//生成垃圾指令
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的设计
先来举一个例子,比如指令:xor eax,eax
翻译为中间表示:
vPushReg4
vPushReg4
vXOR4
vPopReg4
上面每一个中间表示的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中解密这个临时数据结构,解密之后,再加密,并且加上花指令。
花指令构造器具体实现在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块?
添加方式:以inc指令为例子
第一步:
第二步:在vmtest.cpp中实现其函数功能。
第三步:
在VMLoader2.cpp,把55改成56,在g_FunName数组里添加{vINC,"inc ","vINC "},注意"inc "和"vINC ",后面有一个空格,(2) 写在末尾
整个程序,由于在设计上的缺陷,使得虚拟机不能对寄存器进行轮转操作。另外,汇编指令是直接换成handler块的,中间没有先对汇编指令进行任何变形。所以,这只是一个模拟vmprotect的最最简单的指令壳子。
(3) 编译问题
编译时,可能会遇到的编码错误:
Stub项目运行库选择多线程(/MT):
本文附件可点击左下角阅读原文自行下载!
看雪ID:舒默哦
https://bbs.pediy.com/user-home-877885.htm
*本文由看雪论坛 舒默哦 原创,转载请注明来自看雪社区。
《安卓高级研修班》2021年6月班火热招生中!
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!