内容简介:本文基于Windows平台对PE文件加壳的项目,经过一个月的缓冲,决定复习总结及分享下的我的心得。主要工具实验平台:
本文基于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),区段就增加了一个。
设置整个新增加的区段的数据,主要需要设置的字段如下:
① 区段名(可选)
② 区段数据的实际字节数 Misc.VirtualSize
③ 区段的 VirtualAddress (区段数据在内存中的RVA),此值必须是: 上一个区段的 VirtualAddress + 上一个区段经内存对齐粒度对齐后的大小(内存对齐大小是0x1000的整数倍)
④ 区段以文件对齐粒度对齐后的大小 SizeOfRawData (文件对齐大小是0x200的整数倍)
⑤ 区段的 PointerToRawData (区段数据在文件中的偏移),此值必须是:上一个区段的 PointerToRawData + 上一个区段的 SizeOfRawData
⑥ 区段属性主要设置区段为可读可写可执行如下图
对比上一个区段修改新添加的区段里的字段。
3. 添加区段数据
区段头内容虽然设置好了,但真正重要的区段里的数据还需要插入到文件中,以扩充文件的大小,因为区段头只是一个相当于目录的存在,如果只有目录而没有内容,就会造成这个文件成为一个无效的PE文件。
把010Editor里的数据页滚动到最下面按Ctrl+shift+i添加200h个(16进制)字节
4. 修改PE文件的扩展头的SizeofImage
现在PE文件已经被扩充了大小,扩展头中的映像大小必须更新,否则当PE文件加载到内存后,新区段的数据将无法得到正常加载。
最后一个区段.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中都会用到的一些数据,封装的函数,及共用的结构体!
流程如下:
① 使用加壳器给被加壳程序添加新区段。
② 加密/压缩被加壳程序。
③ 将stub的代码段移植到新区段。
④ 将被加壳程序的OEP记录到share.h中。
⑤ 将被加壳程序的EP设置到新区段。
⑥ 去掉随机基址。
⑦ 保存为新文件。
移植数据到新区段,把整个stub.dll的代码段.text移植到目标文件新添加的区段中,这样就完成了最简单加壳操作。
当然事实上并没有那么简单,stub.dll里的.text段里面的数据需要先进行重定位修复,修复完成后再移植过去,这样壳区段才能正常运行起来。
首先根据stub.dll的重定位表获取出stub.dll中.text段需要重定位的数据,然后把该数据
② 减去原始代码段Rva
③ 加上 新基址(exe目标文件)
④ 加上新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
将工程设置release版本,如果不想代码被优化,可以禁止优化。
大概流程如下:
① 将数据段,只读数据段和代码段进行合并
② 编写代码获取API的地址
③ 加入混淆指令,反调试
④ 解密/解压缩
⑤ 加密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加密,动态解密,反调试等技术吧。
附上源码,源码里没有压缩,之后再发吧。
- End -
看雪ID: 九阳道人
https://bbs.pediy.com/user-847228.htm
本文由看雪论坛 九阳道人 原创
转载请注明来自看雪社区
热门图书推荐
戳 立即购买!
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com
↙ 点击下方“阅读原文”,查看更多干货
以上所述就是小编给大家介绍的《C++写壳详解之基础篇》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- wed服务基础·httpd基础配置详解
- Zookeeper详解-基础(二)
- Java基础-泛型详解
- linux基础-proc详解
- springmvc教程--注解开发基础详解
- Go基础Slice教程详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Black Box Society
Frank Pasquale / Harvard University Press / 2015-1-5 / USD 35.00
Every day, corporations are connecting the dots about our personal behavior—silently scrutinizing clues left behind by our work habits and Internet use. The data compiled and portraits created are inc......一起来看看 《The Black Box Society》 这本书的介绍吧!