写壳笔记
完整的项目代码:
https://wws.lanzous.com/iiLa7nlxqle
密码:9ygk
前置知识:
- PE结构
- C++编程
- TEB结构
写壳的几个步骤
-
编写加壳器,实现在目标程序添加一个区段
-
在DLL项目中,编写壳代码,导出壳代码start函数,该函数执行我们对exe的加载及修复重定位,IAT等操作
-
将DLL区段合并到text区段,拷贝到目标程序新建的区段
-
设置OEP到壳代码起始位置
1.创建两个项目
加壳器程序(exe)
用于加载原程序后进行加壳工作
- 编译环境 Debug x86 (方便调试)
- 设置项目属性
- C/C++ -代码生成-运行库 : 多线程调试DLL(MDd)
- C/C++ -常规-支持仅我的代码调式 :否
- 右键项目 -生成依赖项-项目依赖项 : 勾选stub (重新编译的时候会同时重新编译stub)
stub程序 (DLL)
用于执行壳代码
- 编译环境 Release x86 (优化一些系统API)
- 设置项目属性
- C/C++ -常规-支持仅我的代码调式 :否
- C/C++ -代码生成-安全检查(GS):禁用
- C/C++ -代码生成-运行库 : 多线程L(MT)
- 前置代码
- 合并区段 --因为会用到全局变量,常量,而这些都在.data段,所以为了避免代码复杂度过高,合并到.text段,在stub.cpp文件加以下代码
#pragma comment(linker, "/merge:.data=.text") //合并区段
#pragma comment(linker, "/merge:.rdata=.text") //合并区段
#pragma comment(linker, "/section:.text,RWE") //设置区段属性
2.编写加壳器
加壳器需要做的事情:
- 将宿主程序以文件形式读取到内存
- 将stub以LoadLibrary方式动态加载到内存,不执行DLLmian (设置标志DONT_RESOLVE_DLL_REFERENCES)
- 在宿主程序新建区段.pack区段, 拷贝壳代码.text区段到宿主程序.pack区段
- 修复壳代码全局变量基址
- 处理TLS,加密区段,修复重定位,压缩等
- 设置新的OEP
- 另存为新的EXE
3.编写stub程序( 壳代码)
stub程序需要做的事情:
- 合并区段.rdata 、.data 到.text区段
- 导出一个起始函数,这里取名为start
- 动态获取API
- 解密IAT,修复宿主程序重定位,解压代码段等
- 可进行反调试,反虚拟机等操作
- 跳转到宿主程序OEP
拷贝区段到宿主程序如何运行
-
在这里会遇到一个问题,如果壳代码中有定义全局变量或staic变量,直接运行后会出现崩溃
原因是所有涉及全局变量的代码,都是以 dll 和.text区段 为基址进行计算的
例如: 模块的地址为 0x60000000,那么所有的地址都是以它开头的
但是现在区段被拷贝到了 exe 文件中,exe 文件的默认基址是 0x400000
如果使用前面 0x60000000 为基址的代码或数据继续运行,几乎都会出现错误
所以需要进行重定位的修复,修复的公式为 :需要重定位的地址 - dll基址 - dll.text基址 + exe基址 + exe.pack的基址
-
注意: 以LoadLibrary方式进行动态加载DLL ,并设置标志DONT_RESOLVE_DLL_REFERENCES ,使其不会执行DLLmain
/* 加壳器程序代码 */
VOID CPack::FixReloc()
{
/*
1. 找到 dll 的重定位表,遍历其中的重定位块,以全 0 结构结尾
2. 解析重定位块,找到所有 Type 为 3 的项,使用上面的公式重定位
3. 是否支持随机基址,不支持需要关闭随机基址
*/
typedef struct _TYPEOFFSET
{
WORD offset : 12;
WORD type : 4;
} TYPEOFFSET, * PTYPEOFFSET;
//1. 找到 dll 的重定位表,遍历其中的重定位块,以全 0 结构结尾
auto reloc_table = (PIMAGE_BASE_RELOCATION)(OptionalHeader(m_StubBase)->DataDirectory[5].VirtualAddress + m_StubBase);
ULONG dllbase = m_StubBase;
ULONG dlltext = GetSection(m_StubBase, ".text")->VirtualAddress;
ULONG exebase = OptionalHeader(m_FileBase)->ImageBase;
ULONG exepack = GetSection(m_FileBase,newSectionName)->VirtualAddress;
while (reloc_table->SizeOfBlock)
{
auto item = (PTYPEOFFSET)(reloc_table + 1); //获得重定位项
ULONG count = (reloc_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2; //重定位数量
for (ULONG i = 0; i < count; i++)
{
//2. 解析重定位块,找到所有 Type 为 3 的项,使用上面的公式重定位
if (item[i].type == 3)
{
//需要重定位的数据地址,因为需要修改存储此数据所在区域的内存属性,所以要设置一个指针指向该地址
ULONG* address = (ULONG*)(item[i].offset + reloc_table->VirtualAddress + m_StubBase);
DWORD old_protect = 0; //重定位所在区域不可写,需要修改内存分页属性
VirtualProtect(address, 4, PAGE_READWRITE, &old_protect);
*address = *address - dllbase - dlltext + exebase + exepack;
VirtualProtect(address, 4, old_protect,&old_protect);
}
}
reloc_table = (PIMAGE_BASE_RELOCATION) ((ULONG)reloc_table + reloc_table->SizeOfBlock);
}
//3. 目前的壳代码不支持重定位,需要关闭
//OptionalHeader(m_FileBase)->DllCharacteristics &= ~IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE;
//判断是否有重定位,没有不进行修复
if (OptionalHeader(m_FileBase)->DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
{
CopySutbReloc(); //函数实现代码可查看 关于重定位的修复
}
return;
}
关于加密代码段
有加密就要有解密,所以加密信息一定要告知壳代码,让壳代码解密,那么如何做到加壳器与壳代码的通信呢?
在两个项目中都添加一个共享结构体,并在壳代码中导出一个该结构体变量,加壳器通过GetProcAddress动态获取该结构体变量的地址,向里面写入数据即可实现通信
/*加壳器代码*/
VOID CPack::EncryptCode(LPCSTR SectionName) //加密代码段
{
//1.找到.text区段
auto section = GetSection(m_FileBase, SectionName);
auto text = (BYTE*)(section->PointerToRawData + m_FileBase);
//2.保存加密信息到共享结构体
m_share->xor_key = rand() % 0x100; //密钥
m_share->xor_SecVirAddr = section->VirtualAddress;
m_share->xor_Size = section->SizeOfRawData;
//3.加密
for (ULONG i = 0; i < m_share->xor_Size; i++)
text[i] ^= m_share->xor_key;
return VOID();
}
/*stub程序代码*/
void DecryptCode() //解密代码段
{
auto SectionVA = (BYTE*)(share.xor_SecVirAddr + g_imageBase); //1. 计算出被加密区段的 va
ULONG oldProtect = 0;
pVirtualProtect(SectionVA, share.xor_Size, PAGE_READWRITE, &oldProtect);
for (ULONG i = 0; i < share.xor_Size; i++)
SectionVA[i] ^= share.xor_key; //2. 使用加壳器提供的大小和 key 解密区段
pVirtualProtect(SectionVA, share.xor_Size, oldProtect, &oldProtect); // 3. 由于代码段不可读写,所以需要修改属性
}
关于加密IAT
IAT: 集合了导入API函数地址的一张表
如果我们的dll地址发生改变,要对每一处调用修改其调用地址是很麻烦的,如果有这样的表,我们将所有的函数调用的入口地址集中在一个集中的地址中,然后通过解析这个表方便我们进行修改,这样我们只要修改这个表所指的函数入口地址。
为什么要加密IAT?
目的: 增加分析难度,IAT错乱后,在OD或x64Dbg调试器中,无法直观的看到API名称
如何进行加密IAT?
- 在加壳器中其实不算加密,只是简单的清空掉宿主程序导入表IAT的VirtualAddress及Size字段为0,当然,数据目录表下标12的IAT表首地址和size也是需要清空的
- 在壳代码中,用之前保存在共享结构体中的导入表RVA,解析宿主程序导入表,动态加载所有导入模块,解析INT或者IAT(因为这时这两个表存放的内容还是一样的,别忘了,现在我们是PE加载器的身份),通过序号或名称获得函数地址.
- 精心构造一段shellcode,将shellcode首地址填入到IAT表中,只需要注意,最终在shellcode中能够跳转到正确的函数地址即可
/*加壳器代码*/
VOID CPack::EncryptIAT()
{
// 1.保存IAT的RVA到共享结构体
m_share->import_RVA = OptionalHeader(m_FileBase)->DataDirectory[1].VirtualAddress;
// 2.将IAT表清空 (注意有两个地方需要清空)
OptionalHeader(m_FileBase)->DataDirectory[1].VirtualAddress = 0;
OptionalHeader(m_FileBase)->DataDirectory[1].Size = 0;
OptionalHeader(m_FileBase)->DataDirectory[12].VirtualAddress = 0;
OptionalHeader(m_FileBase)->DataDirectory[12].Size = 0;
}
/*stub程序代码*/
void DecryptIAT()
{
// 1.通过共享找到原始程序的IAT
auto import_table = (PIMAGE_IMPORT_DESCRIPTOR)(share.import_RVA + g_imageBase);
BYTE shellcode[] = "\xB8\x00\x00\x00\x00\x35\x15\x15\x15\x15\xFF\xE0"; //中间4个0x00替换成加密后的函数地址
//2.遍历导入表
while (import_table->Name)
{
auto dllName =(char*)(import_table->Name + g_imageBase); //DLL名称
HMODULE hModule = pLoadLibraryA(dllName);
auto iat_table =(ULONG*)(import_table->FirstThunk+ g_imageBase);
auto int_table =(ULONG*)(import_table->OriginalFirstThunk + g_imageBase);
for (int i = 0; int_table[i]; i++)
{
ULONG address = 0;
ULONG old_protect = 0;
pVirtualProtect(&iat_table[i], 4, PAGE_READWRITE, &old_protect);
if (IMAGE_SNAP_BY_ORDINAL(int_table[i])) //如果最高位是1,那就是序号导入
{
address = (ULONG) pGetProcAddress(hModule, (LPCSTR)(LOWORD(int_table[i]))); //通过序号找到函数地址
}
else
{
auto funcName = (PIMAGE_IMPORT_BY_NAME)(int_table[i]+ g_imageBase);
address = (ULONG)pGetProcAddress(hModule, funcName->Name);//通过名称找到函数地址
}
BYTE* buffer = (BYTE*)pVirtualAlloc(NULL,0x1000,MEM_COMMIT, PAGE_EXECUTE_READWRITE);//开辟空间
memcpy(buffer, shellcode, 13);
address ^= 0x15151515;
*((ULONG*)&buffer[1]) = address; //替换成加密后的函数地址
iat_table[i] = (ULONG)buffer; //缓冲区首地址填充到IAT表
pVirtualProtect(&iat_table[i], 4, old_protect, &old_protect);
}
import_table++;
}
}
关于重定位的修复
-
什么是重定位?
- 当可执行程序不是以默认加载基址加载的时候,PE加载器会进行修复重定位操作
-
如何模拟PE加载器修复重定位项?
- 修复公式非常简单: 修复后的地址 = 修复前的地址 - 默认加载基址 + 当前加载基址
-
但仅仅如此操作是不够的,虽然目标程序重定位是由壳代码修复的,但是壳代码运行前也是需要PE加载器进行修复重定位的.
所以,我们还需要将壳代码中的重定位信息拷贝到目标程序,并改变原来重定位表指向壳代码的重定位表,让PE加载器可以修复壳代码的重定位元素
-
改变指向后,还需要将每一个重定位项的RVA进行修正
- 原因是: 之前所有重定位项的RVA是根据DLL基址为准的,现在将区段拷贝到了exe程序的.pack区段中,还用原来的RVA寻址就会出现错误
- 修复公式: 修复后的RVA = 修复前的RVA - 旧区段RVA + 新区段RVA
/*加壳器代码*/ /*这里修复的是壳代码的重定位*/
VOID CPack::CopySutbReloc()
{
/*
1.拷贝sutb重定位表到exe
1.1 由于重定位元素的位置之前是在sutb的.text区段,我们将其拷贝到了exe的.pack区段
所以需要把所有重定位项的RVA加上exe.pack区段RVA -sutb.text区段RVA才行
2 自定义添加一个区段 .reloc2
2.1 区段大小和stub重定位大小一致或更多
2.2 因为需要以全0结构结尾,所以注意初始化内存
3.改变exe重定位表的指向
*/
typedef struct _TYPEOFFSET
{
WORD offset : 12;
WORD type : 4;
} TYPEOFFSET, * PTYPEOFFSET;
//备份exe重定位表RVA地址 和 默认加载基址
m_share->reloc_RVA = OptionalHeader(m_FileBase)->DataDirectory[5].VirtualAddress;
m_share->old_imageBase = OptionalHeader(m_FileBase)->ImageBase;
//DLL重定位表
auto reloc_table = (PIMAGE_BASE_RELOCATION)(OptionalHeader(m_StubBase)->DataDirectory[5].VirtualAddress + m_StubBase);
auto reloc_size = OptionalHeader(m_StubBase)->DataDirectory[5].Size;
auto reloc_base = (ULONG*)reloc_table;//保存重定位表起始位置,用于拷贝
auto exe_pack_RVA = GetSection(m_FileBase, newSectionName)->VirtualAddress; //获得exe .pack区段RVA
auto sutb_text_RVA = GetSection(m_StubBase, ".text")->VirtualAddress; //获得sutb .text区段RVA
//修复重定位项的RVA
while (reloc_table->SizeOfBlock)
{
DWORD old_protect = 0;
ULONG* address = &reloc_table->VirtualAddress;
VirtualProtect(address, 4, PAGE_READWRITE, &old_protect);
*address = *address + exe_pack_RVA - sutb_text_RVA;
VirtualProtect(address, 4, old_protect, &old_protect);
reloc_table = (PIMAGE_BASE_RELOCATION)((ULONG)reloc_table + reloc_table->SizeOfBlock);
}
//自定义添加一个区段 .reloc2 区段大小和stub重定位大小一致
AddSection(".reloc2", reloc_size);
//拷贝sutb重定位表到宿主程序.reloc2区段
auto destSection = GetSection(m_FileBase, ".reloc2");
auto destAddr = destSection->PointerToRawData + m_FileBase;
memcpy((LPVOID)destAddr, reloc_base, reloc_size);
//修改重定位表指向 和大小 : 注意destAddr在这里是FOA 需要转换成RVA
OptionalHeader(m_FileBase)->DataDirectory[5].VirtualAddress = FOAtoRVA(destAddr - m_FileBase , m_FileBase);
OptionalHeader(m_FileBase)->DataDirectory[5].Size = reloc_size;
}
/*stub代码*/ /*这里修复的是宿主程序的重定位*/
void FixReloc()
{
typedef struct _TYPEOFFSET
{
WORD offset : 12;
WORD type : 4;
} TYPEOFFSET, * PTYPEOFFSET;
if (!share.reloc_RVA) //没有重定位元素不进行修复
return;
//1.找到exe重定位表
auto reloc_table = (PIMAGE_BASE_RELOCATION)(share.reloc_RVA + g_imageBase);
while (reloc_table->SizeOfBlock)
{
auto item = (PTYPEOFFSET)(reloc_table + 1); //获得重定位项
ULONG count = (reloc_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;
for (ULONG i = 0; i < count; i++)
{
//2. 解析重定位块,找到所有 Type 为 3 的项,使用上面的公式重定位
if (item[i].type == 3)
{
//需要重定位的数据地址,因为需要修改存储此数据所在区域的内存属性,所以要设置一个指针指向该地址
ULONG* address = (ULONG*)(item[i].offset + reloc_table->VirtualAddress + g_imageBase);
DWORD old_protect = 0; //重定位所在区域不可写,需要修改内存分页属性
pVirtualProtect(address, 4, PAGE_READWRITE, &old_protect);
*address = *address - share.old_imageBase + g_imageBase;
pVirtualProtect(address, 4, old_protect, &old_protect);
}
}
reloc_table = (PIMAGE_BASE_RELOCATION)((ULONG)reloc_table + reloc_table->SizeOfBlock);
}
}