指令壳开源
2021-05-20 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:238 收藏

本文为看雪论坛精华文章

看雪论坛作者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 //ebpvAdd48会被BeaEngine引擎解析为0xFFFFFFF8,ebp-0x80xFFFFFFF8+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
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

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


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458385050&idx=1&sn=044dd50ba0fb8673012035b91f817823&chksm=b180ca1086f74306f37ba9b47ce6859280db2d227ae1531d1dbb01f40cdd511139a6401a862f#rd
如有侵权请联系:admin#unsafe.sh