首页
社区
课程
招聘
[原创]C++写壳详解之基础篇
2019-4-22 19:27 50569

[原创]C++写壳详解之基础篇

2019-4-22 19:27
50569

C++写壳详解之基础篇

之前在15PB学习,写了一个基于Windows平台对PE文件加壳的项目,经过一个月的缓冲,在这决定复习总结及分享下的我的心得。
主要工具: 010Editor、VS2017、x64dbg、LordPE、OD
实验平台:win10 64位
实现功能:加壳,压缩,对代码段加密。

一、加壳原理

要想弄明白怎么对PE文件加壳,首先需要对PE文件比较熟悉,而最快的熟悉PE文件的方法就是自己写一个PE解析工具和写壳了。
先只用工具010Editor完成一个手工加壳,那么就明白加壳的原理了。

 

首先进行手工加壳
先用VS随便生成一个exe文件,我们使用它进行实验。
可以先使用010Editor、LordPE、OD等工具查看节区个数,我实验程序的原始区段(节区)个数是8个。

 

1.给PE文件添加一个新区段
修改文件头的NumberOfSection
使用010Editor打开测试程序,按alt+4出现一个模板菜单找到NumberOfSection把该数字加1,这里改为了9。

 

2. 设置新的区段头
添加保存之后, 重新运行010Editor的模板(或者重启010),区段就增加了一个。

 

设置整个新增加的区段的数据,主要需要设置的字段如下:

 

1.区段名(可选)
2.区段数据的实际字节数Misc.VirtualSize
3.区段的VirtualAddress(区段数据在内存中的RVA),此值必须是: 上一个区段的VirtualAddress + 上一个区段经内存对齐粒度对齐后的大小(内存对齐大小是0x1000的整数倍)
4.区段以文件对齐粒度对齐后的大小SizeOfRawData(文件对齐大小是0x200的整数倍)
5.区段的PointerToRawData(区段数据在文件中的偏移),此值必须是:上一个区段的PointerToRawData + 上一个区段的SizeOfRawData
6.区段属性主要设置区段为可读可写可执行如下图

 

对比上一个区段修改新添加的区段里的字段。

 

3.添加区段数据
区段头内容虽然设置好了,但真正重要的区段里的数据还需要插入到文件中,以扩充文件的大小,因为区段头只是一个相当于目录的存在,如果只有目录而没有内容,就会造成这个文件成为一个无效的PE文件。
把010Editor里的数据页滚动到最下面按Ctrl+shift+i添加200h个(16进制)字节

 

4. 修改PE文件的扩展头的SizeofImage
现在PE文件已经被扩充了大小,扩展头中的映像大小必须更新,否则当PE文件加载到内存后,新区段的数据将无法得到正常加载。
这个字段的值记录的是一个PE文件在内存中的大小,可以将之设置为: 最后一个区段在内存中的位置 + 最后一个区段在内存中的大小,即:
OptionalHeader.SizeofImage = 最后一个区段.VirtualAddress + 最后一个区段.SizeOfRawData按内存对齐粒度对齐的大小

 

保存之后,运行该程序,就能正常运行(中间某些环节操作错了就会导致该文件无法正常运行)到此添加区段成功了。那么加壳也就成功了90%,这个新区段之后称为壳代码段。

 

5.添加壳代码
先找到扩展头的DLL属性字段,去掉随机基址,把40 81改为 00 81后保存。

 

在这里为了方便,就使用LordPE来操作剩下的步骤了,先记录原始的OEP入口点为11055,把他改为新区段的RVA 1F000然后点击保存。

 

然后再使用OD打开,进入到入口点就是41F000,因为默认加载基址是0x400000, 发现全是00 00 00的字节,没用内容。把第一行代码改为跳转到原来的入口点jmp 0x411055,然后打一个补丁,程序就能正常运行了。

这就是一个完整的壳流程了,虽然这个壳的内容只有一条跳转到原入口点的代码,但万丈高楼平地起。基础的东西弄懂了后面才能少遇见一些坑!

二、为什么用C++写壳?

我的答案是简单、便捷、方便新手入门。
很多常见的壳都用汇编写的,确实,汇编确实可以写出很多短小精悍、骚操作的代码,这是C++所没有的,但是C++支持内联汇编,在一定程度上弥补了它的不足。
使用DLL动态库文件保存壳代码,我们称它为存根部分(stub),直接把这个文件里的内容移植到我们新添加的区段里面,因为PE文件涉及到重定位,而DLL也是一个PE文件,移植后里面的数据就变得很容易修复了。

三、C++加壳流程

1.处理加壳程序
在加壳过程中,有一个加壳器程序和stub.dll两个文件,加壳器程序会把原文件(要加壳的文件)以文件方式读取到堆内存,它还是以文件对齐粒度(200h)对齐的,而stub.dll是以不处理的方式读取到了内存中,它是以内存粒度(1000h)对齐的。
使用LoadLibraryExA加载DLL并且第三个参数使用DONT_RESOLVE_DLL_REFERENCES的时候,他就不会对这个文件进行重定位等操作,是以原始形态加载到内存。

//将DLL以不会执行代码的标志加载到进程中.
    HMODULE hStubDll = LoadLibraryExA("Stub.dll", 0,
        DONT_RESOLVE_DLL_REFERENCES);

再自定义一个共享头文件share.h,这个文件保存一些加壳程序和stub.dll中都会用到的一些数据,封装的函数,及共用的结构体!

 

流程如下:
1.使用加壳器给被加壳程序添加新区段。
2.加密/压缩被加壳程序。
3.将stub的代码段移植到新区段。
4.将被加壳程序的OEP记录到share.h中。
5.将被加壳程序的EP设置到新区段。
6.去掉随机基址
7.保存为新文件。

 

移植数据到新区段,把整个stub.dll的代码段.text移植到目标文件新添加的区段中,这样就完成了最简单加壳操作。

当然事实上并没有那么简单,stub.dll里的.text段里面的数据需要先进行重定位修复,修复完成后再移植过去,这样壳区段才能正常运行起来。
首先根据stub.dll的重定位表获取出stub.dll中.text段需要重定位的数据,然后把该数据
1减去原始基址
2减去原始代码段Rva
3加上新基址(exe目标文件)
4加上新Rva (exe中新添加的区段RVA)

 

用C++写代码,首先封装了很多常用的函数,如获取DOS头和NT头,区段头等。这样会节省后面大量敲代码的时间。

//获取DOS头
PIMAGE_DOS_HEADER GetDosHeader(char* pBase)
{
    return (PIMAGE_DOS_HEADER)pBase;
}

//获取NT头
PIMAGE_NT_HEADERS GetNtHeader(char* pBase)
{
    return (PIMAGE_NT_HEADERS)
        (GetDosHeader(pBase)->e_lfanew + (DWORD)pBase);
}

例如获取NT头:
auto pNt = (PIMAGE_NT_HEADERS)GetNtHeader(pBase);
C++里auto的功能是自动获取后面数据类型,这也体现了C++的强大之处。

 

完整重定位代码:

//修复stub的重定位
void FixStubReloc(char* pTarBuff, char*& hModule,DWORD dwNewBase,DWORD dwNewSecRva)
{
    //获取sutb.dll重定位va
    auto pReloc = (PIMAGE_BASE_RELOCATION)
        (GetOptHeader(hModule)->DataDirectory[5].VirtualAddress
            + hModule);
    //获取stub.dll的.text区段的Rva
    DWORD dwTextRva = (DWORD)GetSecHeader(hModule, ".text")->VirtualAddress;

    //修复重定位
    while (pReloc->SizeOfBlock)
    {
        struct TypeOffset 
        {
            WORD offset : 12;
            WORD type : 4;
        };
        TypeOffset* pTyOf = (TypeOffset*)(pReloc + 1);
        DWORD dwCount = (pReloc->SizeOfBlock - 8) / 2;
        for (size_t i = 0; i < dwCount; i++)
        {
            if(pTyOf[i].type != 3)
                continue;
            //要修复的Rva
            DWORD dwFixRva = pTyOf[i].offset + pReloc->VirtualAddress;                
            //要修复的地址
            DWORD* pFixAddr = (DWORD*)(dwFixRva + (DWORD)hModule);    
            DWORD dwOldProc;
            VirtualProtect(pFixAddr, 4, PAGE_READWRITE, &dwOldProc);
            *pFixAddr -= (DWORD)hModule; //减去原始基址
            *pFixAddr -= dwTextRva;      //减去原始代码段Rva
            *pFixAddr += dwNewBase;      //加上新基址
            *pFixAddr += dwNewSecRva;    //加上新Rva        
            VirtualProtect(pFixAddr, 4, dwOldProc, &dwOldProc);
        }
        //指向下一个重定位块
        pReloc = (PIMAGE_BASE_RELOCATION)
            ((DWORD)pReloc + pReloc->SizeOfBlock);        
    }        
}

现在只是暂时搭建一个壳框架所以先不处理随机基址的问题,所以要去掉随机基址,后期再来解决随机基址的问题。

 

2.处理stub.dll
配置stub工程
将工程设置release版本,如果不想代码被优化,可以禁止优化。
大概流程如下:

  1. 将数据段,只读数据段和代码段进行合并
  2. 编写代码获取API的地址
  3. 加入混淆指令,反调试
  4. 解密/解压缩
  5. 加密IAT等等

之后会把存根文件stub.dll的.data,.rdata这2个区段合并到.text段并设置为可读可写可执行属性,需要前置代码

//把数据段融入代码段
#pragma comment(linker,"/merge:.data=.text")
//把只读数据段融入代码段
#pragma comment(linker,"/merge:.rdata=.text")
//设置代码段为可读可写可执行
#pragma comment(linker,"/section:.text,RWE")

根据之前说的已经知道壳区段就是新添加的区段了,里面将保存移植过来的stub的.text段里的所有内容,称之为壳代码。

 

而使用壳代码的时候要注意,因为加完壳后,在壳代码中无法使用导入表,因此,需要自己动态获取需要使用的API函数的地址。
只要获取到LoadLibraryExA和GetProcAddress两个函数的地址,我们就可以根据LoadLibraryExA来获取任意模块dll的基地址,再使用GetProcAddress函数获取到任意API函数的地址了。
根据kernel32基址可获取到GetProcAddress地址。
下面是我获取kernel32基址的内联汇编代码。

__asm 
    {
        push esi;
        mov esi, fs:[0x30];   //得到PEB地址
        mov esi, [esi + 0xc]; //指向PEB_LDR_DATA结构的首地址
        mov esi, [esi + 0x1c];//一个双向链表的地址
        mov esi, [esi];       //得到第2个条目kernelBase的链表
        mov esi, [esi];       //得到第3个条目kernel32的链表(win10系统)
        mov esi, [esi + 0x8]; //kernel32.dll地址
        mov g_hKernel32, esi;
        pop esi;
    }

然后是获取GetProcAddress函数的汇编代码,可以使用C语言方式获取,但我觉得用汇编写,它就这样赤裸裸呈现,能更加清晰的了解找到一个函数地址的过程。

//获取GetProcAddress函数地址
void MyGetFunAddress()
{    
    __asm 
    {
        pushad;        
        mov ebp, esp;
        sub esp, 0xc;
        mov edx, g_hKernel32;
        mov esi, [edx + 0x3c];     //NT头的RVA
        lea esi, [esi + edx];      //NT头的VA
        mov esi, [esi + 0x78];     //Export的Rva        
        lea edi, [esi + edx];      //Export的Va

        mov esi, [edi + 0x1c];     //Eat的Rva
        lea esi, [esi + edx];      //Eat的Va
        mov[ebp - 0x4], esi;       //保存Eat

        mov esi, [edi + 0x20];     //Ent的Rva
        lea esi, [esi + edx];      //Ent的Va
        mov[ebp - 0x8], esi;       //保存Ent

        mov esi, [edi + 0x24];     //Eot的Rva
        lea esi, [esi + edx];      //Eot的Va
        mov[ebp - 0xc], esi;       //保存Eot

        xor ecx, ecx;
        jmp _First;
    _Zero:
        inc ecx;
    _First:
        mov esi, [ebp - 0x8];     //Ent的Va
        mov esi, [esi + ecx * 4]; //FunName的Rva

        lea esi, [esi + edx];     //FunName的Va
        cmp dword ptr[esi], 050746547h;// 47657450 726F6341 64647265 7373;
        jne _Zero;                     // 上面的16进制是GetProcAddress的ASCII
        cmp dword ptr[esi + 4], 041636f72h;
        jne _Zero;
        cmp dword ptr[esi + 8], 065726464h;
        jne _Zero;
        cmp word  ptr[esi + 0ch], 07373h;
        jne _Zero;

        xor ebx,ebx
        mov esi, [ebp - 0xc];     //Eot的Va
        mov bx, [esi + ecx * 2];  //得到序号

        mov esi, [ebp - 0x4];     //Eat的Va
        mov esi, [esi + ebx * 4]; //FunAddr的Rva
        lea eax, [esi + edx];     //FunAddr
        mov MyGetProcAddress, eax;    
        add esp, 0xc;
        popad;
    }
}

然后再获取下MessageBoxW函数,弹出一个对话框,测试是否成功。

//运行函数
void RunFun()
{
    MyLoadLibraryExA = (FuLoadLibraryExA)MyGetProcAddress(g_hKernel32, "LoadLibraryExA");
    g_hUser32 = MyLoadLibraryExA("user32.dll", 0, 0);
    MyMessageBoxW = (FuMessageBoxW)MyGetProcAddress(g_hUser32, "MessageBoxW");
    MyMessageBoxW(0, L"大家好我是一个壳", L"提示", 0);
}

它在运行原代码之前先运行了壳代码,测试成功。

四、代码段加密

我们在逆向破解的时候通常第一方法是找到关键字符串,关键代码等,他们都是存在于代码段的,那么只要把代码段进行加密,这种方式就不可行了。
先在加壳器中加密,这使用简单的亦或加密。

    //加密代码段
    //1.获取代码段首地址
    char* pTarText = GetSecHeader(pTarBuff, ".text")->PointerToRawData + pTarBuff;
    //2.获取代码段实际大小
    int nSize = GetSecHeader(pTarBuff, ".text")->Misc.VirtualSize;
    for (int i = 0; i < nSize; ++i)
    {
        pTarText[i] ^= 0x15;
    }

再到壳代码里解密,自己写了一个对比字符串的函数。

//自写strcmp
int StrCmpText(const char* pStr, char* pBuff)
{
    int nFlag = 1;
    __asm
    {
        mov esi, pStr;
        mov edi, pBuff;
        mov ecx, 0x6;
        cld;
        repe cmpsb;
        je _end;
        mov nFlag, 0;
    _end:
    }
    return nFlag;
}
//解密
void Decryption()
{    
    //获取.text的区段头
    auto pNt = GetNtHeader((char*)g_hModule);    
    DWORD dwSecNum = pNt->FileHeader.NumberOfSections;    
    auto pSec = IMAGE_FIRST_SECTION(pNt);

    //找到代码区段
    for (size_t i = 0; i < dwSecNum; i++)
    {        
        if (StrCmpText(".text", (char*)pSec[i].Name))
        {
            pSec += i;
            break;
        }            
    }

    //获取代码段首地址
    char* pTarText = pSec->VirtualAddress + (char*)g_hModule;
    int nSize = pSec->Misc.VirtualSize;
    DWORD old = 0;
    //解密代码段
    MyVirtualProtect(pTarText, nSize, PAGE_READWRITE, &old);
    for (int i = 0; i < nSize; ++i) {
        pTarText[i] ^= 0x15;
    }
    MyVirtualProtect(pTarText, nSize, old, &old);
}

五、压缩

压缩是一个比较复杂的过程,对于一个主要功能的加密的壳来说,压缩也有一定的加密效果,如果使用了一些加密库加密,即使你压缩了,会发现加壳后的文件比没加壳之前还要大!

 

这说一下压缩大概思路,首先不能压缩头部,考虑到后面要处理TLS,还有一个程序的图标在资源段,所以不压缩这两个段。
在加壳器中把原文件的中除了.tls和.rsrc段的其他段的数据一个一个的按顺序取出来,然后拼接在一起,然后对这份拼接后数据进行一个整体的压缩,之后需要再添加一个区段专门用于存放压缩后的数据,这个过程中,需要把压缩后的区段的文件偏移和文件大小都清零,如下图所示,把.tsl段和.rsrc段移动到头部的后面。
值得注意的是没有处理TLS时要把TLS表的RVA和大小清零,TLS在数据目录表的第九项。

    auto pData = GetOptHeader(pTarBuff)->DataDirectory;
    pData[9].Size = 0;
    pData[9].VirtualAddress = 0;

运行时,先在壳代码中进行解压缩,再解密,然后程序就能正常运行了。

 

到此一个简单的加密压缩壳就完成了,在这个过程中实际出现了很多bug,因为涉及到DLL文件无法用VS调试, 所以使用OD或者x64dbg进行调试,推荐使用x64dbg(x32dbg),这个软件一直在更新,而且字符串提示更友好,更方便快捷。OD主要用于脱壳破解,逆向还是x64dbg更方便。

 

最后再说一下VS2017使用配置:
有2个工程文件 一个是加壳器,一个是sutb。
加壳器使用x32debug编译
sutb使用x32Release编译
找到工程所在文件夹,新建一个bin目录,把这两个工程属性中的输出目录改为bin,这样操作起来方便一些,不改也行,但是加载stub时路径就要填写正确才行。

 

一个壳的基本框架就搭建完成了,而加壳主要是为了防止被别人破解,所以接下来就可以执行加密操作了,下一次再说说IAT加密,Hash加密,动态解密,反调试等技术吧。

 

附上源码,源码里没有压缩,之后再发吧。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

上传的附件:
收藏
点赞33
打赏
分享
最新回复 (36)
雪    币: 121
活跃值: (1517)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xxRea 2019-4-22 20:46
2
0
优秀的教程·~感谢
雪    币: 69
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
nmgwddj 2019-4-22 20:55
3
0
真心感谢,能自己学会又能帮助别人学会,这样的人不多见。
雪    币: 12083
活跃值: (15459)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2019-4-22 21:50
4
0
mark,楼主辛苦了
雪    币: 4241
活跃值: (8405)
能力值: ( LV9,RANK:181 )
在线值:
发帖
回帖
粉丝
nevinhappy 2 2019-4-23 06:44
5
0
Mark,很详细,学习!!!
雪    币: 310
活跃值: (1917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 2019-4-23 08:07
6
0
mark
雪    币: 19584
活跃值: (60093)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2019-4-23 10:04
7
0
感谢分享~
雪    币: 199
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
bigcat217 2019-4-23 13:18
8
0
真的太适合我们新手学习了,感谢分享。
雪    币: 3021
活跃值: (241)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zlykcaj 2019-4-23 20:34
9
0
感谢分享~~
雪    币: 1705
活跃值: (79)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
pealcock 2019-4-24 08:31
10
0
mark, 感谢楼主的分享精神.
雪    币: 0
活跃值: (130)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
whitelen 2019-4-24 10:22
11
0
mark
雪    币: 545
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_oswximnm 2019-4-25 09:04
12
0
 优秀的教程·~感谢 
雪    币: 5357
活跃值: (11775)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
一半人生 5 2019-4-25 09:26
13
0
看山不是山,看水还是水
雪    币: 355
活跃值: (10)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
Victorgg 2019-4-26 14:40
14
0
 强,点赞收藏一条龙了
雪    币: 54
活跃值: (748)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
wuzhuyecao 2019-4-28 08:19
15
0
不错的教程     
雪    币: 239
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Terrence0605 2019-5-13 16:05
16
0
挺不错的教程 
雪    币: 2428
活跃值: (159)
能力值: ( LV11,RANK:198 )
在线值:
发帖
回帖
粉丝
simpower 2019-5-19 21:48
17
0
非常厉害收藏了
雪    币: 2580
活跃值: (2243)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
. 2019-7-18 08:50
18
0
谢谢大佬
雪    币: 1752
活跃值: (562)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
luluting 2019-7-19 13:56
19
0
感谢分享!~~~~
雪    币: 1263
活跃值: (31)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
火流星 2019-7-19 16:17
20
0
很好的教程,对我帮助很大
雪    币: 2580
活跃值: (2243)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
. 2019-8-2 09:52
21
0
嘤嘤嘤   楼主你添加节的时候为什么是将最后一个节覆盖
雪    币: 2359
活跃值: (278)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
coolsnake 2019-8-11 09:17
22
0
这个太有帮助了
雪    币: 546
活跃值: (582)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
System32 2019-8-15 18:49
23
0
雪    币: 281
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
画骨 2020-2-11 20:03
24
1
为啥最后只运行了壳,无法运行到原代码呢?
找到原因了,g_hModule = (HMODULE)(FuGetModuleHandleW)MyGetModuleHandleW(0);  出错
最后于 2020-2-11 20:31 被画骨编辑 ,原因:
雪    币: 204
活跃值: (346)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
小向i小葵 2020-9-16 21:31
25
0
的确运行到 g_hModule = (HMODULE)(FuGetModuleHandleW)MyGetModuleHandleW(0); 这一句的时候EIP会为0,导致原程序无法执行
游客
登录 | 注册 方可回帖
返回