写壳的步骤
编写加壳器,加载被加壳程序和壳dll程序
将 dll 程序中 .text 拷贝到被加壳程序
将被加壳程序的 eip 指向stub 代码
需要让 stub 提供一个入口点
1. 加载 PE 文件5. 加载 Stub 文件 8. 加载共享数据,写入了原始OE篇2. 添加了一个区段4. 实现了一个 stub 提供了 start 7. 提供了一个共享数据结构 9. 重新跳转到 oep3. 将区段的内容进行了拷贝6. 重新设置了 oep10. 因为没有进行壳代码重定位,所以跳转失败 11. 对壳代码进行了重定位12. 加密代码段,保存了 key rva size13. 壳代码根据提供的内容进行解密 14. 解密时分页没有访问属性 15. 提供了获取函数的功能,添加了一个 VirtualProtect 函数 16. 设置属性,修复了加密的后的代码,最终跳转oep
首先,加载要加壳的PE文件
// 加载一个 PE 文件void CMyPack::LoadFile(LPCSTR FileName){ // 打开一个文件,理论上应该对文件进行判断,是否是一个 PE 文件,位数是多少 HANDLE FileHandle = CreateFileA(FileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // 获取到文件的大小,并申请相应的堆空间 FileSize = GetFileSize(FileHandle, NULL); FileBase = (DWORD)malloc(sizeof(BYTE) * FileSize); // 读取 PE 文件的内容 DWORD BytesRead = 0; ReadFile(FileHandle, (LPVOID)FileBase, FileSize, &BytesRead, NULL); // 关闭句柄,防止句柄泄露 CloseHandle(FileHandle);}
提供一个 DLL 作为 Stub 区域使用
【切记,否则程序跑不起来】将 DLL 设置为 Release 版本进行编译、小、内联了一些函数
C\C++ -> 代码生成 -> GS安全检查 -> 禁用 (取消一些库函数的调用)
C\C++ -> 所有选项 -> 运行库 -> MT (取消一些库函数的调用)
合并区段,并设置属性为可读可写可执行(0xE00000E0)
main.cpp
#include <windows.h>#include "header.h"// 将 .data 和 .rdata 合并到 .text 区域#pragma comment(linker, "/merge:.data=.text") #pragma comment(linker, "/merge:.rdata=.text")// 并且设置 .text 区属性为壳读壳写壳执行#pragma comment(linker, "/section:.text,RWE")DWORD MyGetProcAddress(DWORD Module, LPCSTR FunName){ // 获取 Dos 头和 Nt 头 auto DosHeader = (PIMAGE_DOS_HEADER)Module; auto NtHeader = (PIMAGE_NT_HEADERS)(Module + DosHeader->e_lfanew); // 获取导出表结构 DWORD ExportRva = NtHeader->OptionalHeader.DataDirectory[0].VirtualAddress; auto ExportTable = (PIMAGE_EXPORT_DIRECTORY)(Module + ExportRva); // 找到导出名称表、序号表、地址表 auto NameTable = (DWORD*)(ExportTable->AddressOfNames + Module); auto FuncTable = (DWORD*)(ExportTable->AddressOfFunctions + Module); // [易错]序号表示WORD*不是DWORD* auto OrdinalTable = (WORD*)(ExportTable->AddressOfNameOrdinals + Module); // 遍历找名字 for (DWORD i = 0; i < ExportTable->NumberOfNames; ++i) { // 获取名字 char* Name = (char*)(NameTable[i] + Module); if (!strcmp(Name, FunName)) return FuncTable[OrdinalTable[i]] + Module; } return -1;}// 跳转到 OEP_declspec(naked) void JmpOep(){ __asm { mov eax, StubData.OldOEP; 原始 OEP 的RVA mov ebx, dword ptr FS : [0x30]; PEB mov ebx, dword ptr[ebx + 0x08]; 当前加载基址 add eax, ebx; 计算出 OEP jmp eax; 跳转 OEP }}_declspec(naked) DWORD GetKernelBase(){ __asm { // 按照加载顺序 mov eax, dword ptr fs : [0x30] mov eax, dword ptr[eax + 0x0C] mov eax, dword ptr[eax + 0x0C] mov eax, dword ptr[eax] mov eax, dword ptr[eax] mov eax, dword ptr[eax + 0x18] ret }}// 解密数据void XorData(){ DWORD ImageBase = 0, OldProtect = 0; __asm { mov ebx, dword ptr FS : [0x30]; PEB mov ebx, dword ptr[ebx + 0x08]; 当前加载基址 mov ImageBase, ebx } // 获取到被加密区段的起始位置 BYTE* Data = (BYTE*)(ImageBase + StubData.XorRVA); pVirtualProtect(Data, StubData.XorSize, PAGE_READWRITE, &OldProtect); // 循环进行解密 for (DWORD i = 0; i < StubData.XorSize; ++i) Data[i] ^= StubData.XorKey; pVirtualProtect(Data, StubData.XorSize, OldProtect, &OldProtect);}// 获取所有想要使用的函数void GetApis(){ pVirtualProtect = (fnVirtualProtect)MyGetProcAddress(GetKernelBase(), "VirtualProtect");}// 导出且是一个裸函数,不会自动生成代码_declspec(dllexport) _declspec(naked) void start(){ GetApis(); XorData(); JmpOep();}
header.h
#include <windows.h>// 提供一个结构体,这里的数据应该由加壳器填充struct ShareData{ DWORD OldOEP = 0; BYTE XorKey = 0; DWORD XorRVA = 0; DWORD XorSize = 0;};extern "C"{ // 导出这个结构体的一个变量,提供给加壳器 _declspec(dllexport) ShareData StubData; // 导出且是一个裸函数,不会自动生成代码 _declspec(dllexport) void start();}typedef BOOL(WINAPI* fnVirtualProtect)( _In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect );fnVirtualProtect pVirtualProtect;
加载Stub
// 加载一个 PE 文件void CMyPack::LoadStub(LPCSTR FileName){ // 以不调用 dllmain 的方式加载 dll 到当前的内存,会展开 StubBase = (DWORD)LoadLibraryExA(FileName, NULL, DONT_RESOLVE_DLL_REFERENCES); // 获取 start 函数在 .text 的段内偏移 DWORD Start = (DWORD)GetProcAddress((HMODULE)StubBase, "start"); StartOffset = Start - StubBase - GetSection(StubBase, ".text")->VirtualAddress; // 加载stub数据对象,向其中填充内容 StubData = (ShareData*)GetProcAddress((HMODULE)StubBase, "StubData");}
为了方便调试,进行如下设置
从stub拷贝到exe
// 拷贝区段,从 stub 拷贝 SrcName 区段到 exe 中并命名为 DestNamevoid CMyPack::CopySection(LPCSTR DestName, LPCSTR SrcName){ // 从 dll 中获取到需要拷贝的区段对应的区段头结构 auto SrcSection = GetSection(StubBase, ".text"); // 获取到最后一个区段的位置,下标从0开始,区段数量-1 auto LastSection = &IMAGE_FIRST_SECTION(NTHeader(FileBase)) [FileHeader(FileBase)->NumberOfSections - 1]; // 将文件头中的区段数量+1 FileHeader(FileBase)->NumberOfSections += 1; // 获取新添加的区段头表的首地址 auto DestSection = LastSection + 1; // 将源区段的属性直接拷贝到新的区段 memcpy(DestSection, SrcSection, sizeof(IMAGE_SECTION_HEADER)); // 设置区段的名称,可以是用 memcpy strcpy memcpy(DestSection->Name, DestName, 7); // 设置区段起始位置的 RVA = 上一个区段 RVA + 对齐的内存大小 DestSection->VirtualAddress = LastSection->VirtualAddress + Aligment(LastSection->Misc.VirtualSize, OptHeader(FileBase)->SectionAlignment); // 设置区段起始位置的 FOA = 上一个区段 FOA + 对齐的文件大小 DestSection->PointerToRawData = LastSection->PointerToRawData + Aligment(LastSection->SizeOfRawData, OptHeader(FileBase)->FileAlignment); // 重新的分配空间,大小是 最后一个区段的FOA + 最后一个区段的文件大小 FileSize = DestSection->PointerToRawData + DestSection->SizeOfRawData; // 使用 realloc 的时候,它新的地址会作为返回值进行返回 FileBase = (DWORD)realloc((LPVOID)FileBase, FileSize); // 重新设置映像大小 = 最后一个区段的 RVA + 最后一个区段的内存大小 OptHeader(FileBase)->SizeOfImage = DestSection->VirtualAddress + DestSection->Misc.VirtualSize;}
添加区段的步骤
将文件头中的区段数量进行 +1
在区段头表中增加一项,直接覆盖后面的40字节数据
要设置区段表的名称,理论不能超过 7 字节
设置文件大小和内存大小,可以直接设置成一样
设置 RVA : RVA = 上一个区段的RVA + 上一个区段内存对齐后的内存大小
设置 FOA:FOA = 上一个区段的FOA + 上一个区段文件对齐后的文件大小
修改区段的属性,通常需要设置成 0xE00000E0,读写执行
填充 PE 文件,使其大小 = 最后一个区段的 FOA + 最后一个区段的文件大小
重新设置 SizeOfImage, 使其 = 最后一个区段的 RVA + 最后一个区段的内存大小
重新设置OEP
// 设置入口点为新区段中的 start 的位置(RVA)void CMyPack::SetOep(){ // 保存原始的 OEP StubData->OldOEP = OptHeader(FileBase)->AddressOfEntryPoint; // 因为获取到的 start 函数的地址是在dll中的地址,现在这个区段被拷贝到了 // 被加壳程序中,所以需要重新计算 start 的 RVA 并设置为 OEP OptHeader(FileBase)->AddressOfEntryPoint = GetSection(FileBase, ".mypack")->VirtualAddress + StartOffset;}
修复重定位
STUB 区域代码的重定位问题
起初代码是放在了 dll 中,需要重定位的内容,是以 dll 实际加载基址设置的
这里的重定位实际上是为了让 stub 区的代码跑起来,是 必然 要执行的操作
// 修复壳代码的重定位void CMyPack::FixRealoc(){ ULONG Size = 0, OldProtect = 0; // 获取到程序的重定位表 auto RealocTable = (PIMAGE_BASE_RELOCATION) ImageDirectoryEntryToData((PVOID)StubBase, TRUE, 5, &Size); // 遍历重定位表,重定位表以一个空表结尾 while (RealocTable->SizeOfBlock) { // 获取重定位项,并依次修复重定位项 auto Item = (TypeOffset*)(RealocTable + 1); // 获取到重定位项的个数 DWORD Count = (RealocTable->SizeOfBlock - 8) / 2; // 遍历重定位项并修复 for (DWORD i = 0; i < Count; ++i) { // 修改整个分页的属性,使它能够被写入 VirtualProtect((LPVOID)(RealocTable->VirtualAddress + StubBase), 0x1000, PAGE_READWRITE, &OldProtect); // 只需要关注 type == 3 的类型 if (Item[i].Type == 3) { // 需要重定位的数据所在的地址 Base + RVA + Offset DWORD* ItemAddr = (DWORD*)(StubBase + RealocTable->VirtualAddress + Item[i].Offset); // 计算出不会改变(会改变的有基址和段的RVA)的偏移 DWORD Offset = *ItemAddr - StubBase - GetSection(StubBase, ".text")->VirtualAddress; // 计算出新的 VA *ItemAddr = Offset + GetSection(FileBase, ".mypack")->VirtualAddress + OptHeader(FileBase)->ImageBase; } // 还原属性 VirtualProtect((LPVOID)(RealocTable->VirtualAddress + StubBase), 0x1000, OldProtect, &OldProtect); } // 找到下一个重定位块 RealocTable = (PIMAGE_BASE_RELOCATION)((DWORD)RealocTable + RealocTable->SizeOfBlock); } // 手动的关闭动态基址,否则程序无法运行 OptHeader(FileBase)->DllCharacteristics = 0;}
加密
// 异或指定区段的数据void CMyPack::XorFileSection(LPCSTR SectionName){ // 获取指定的区段 auto Section = GetSection(FileBase, SectionName); // 保存区段的 RVA 以及 Size StubData->XorRVA = Section->VirtualAddress; StubData->XorSize = Section->SizeOfRawData; // 计算出区段的位置 BYTE* Buffer = (BYTE*)(FileBase + Section->PointerToRawData); // 随机生成一个加密的 key srand((unsigned int)time(0)); StubData->XorKey = rand() % 0x100; // 根据文件大小对它进行异或 for (DWORD i = 0; i < Section->SizeOfRawData; ++i) Buffer[i] ^= StubData->XorKey;}
拷贝数据
// 拷贝区段的内容,需要指定区段的名称void CMyPack::CopySectionData(LPCSTR DestName, LPCSTR SrcName){ // 获取到源区段的区段头表结构并计算出偏移(DLL加载到了内存,使用的是内存偏移) auto SrcSection = GetSection(StubBase, SrcName); BYTE* SrcData = (BYTE*)(SrcSection->VirtualAddress + StubBase); // 获取到目标区段的区段头表结构并计算出偏移(PE文件没有对齐,使用的是文件偏移) auto DestSection = GetSection(FileBase, DestName); BYTE* DestData = (BYTE*)(DestSection->PointerToRawData + FileBase); // 以文件大小进行拷贝 memcpy(DestData, SrcData, SrcSection->SizeOfRawData);}
保存文件
// 保存修改后的 PE 文件void CMyPack::SaveFile(LPCSTR NewName){ // 打开一个文件,理论上应该对文件进行判断,是否是一个 PE 文件,位数是多少 HANDLE FileHandle = CreateFileA(NewName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // 写入 PE 文件的内容 DWORD BytesWrite = 0; WriteFile(FileHandle, (LPVOID)FileBase, FileSize, &BytesWrite, NULL); // 关闭句柄,防止句柄泄露 CloseHandle(FileHandle);}