壳的原理
PE文件到运行时经过的几步重要步骤:
- 将硬盘中的PE文件复制到内存中;
- 按内存对齐值对齐;
- 加载dll等模块;
- 修复IAT,重定位表;
- 进入OEP(Original Entry Point)开始执行
壳的原理则是修改OEP (可选PE头) 指向自身代码的地址,执行完后返回真正的OEP;
壳位于PE文件所处位置需要是可执行区段内,如.text;
计算OEP偏移地址用于jmp指令返回公式:
jmp E9 xxxxxxxx = OEP - 此指令下一指令地址
添加shellcode到PE
使用010editor:
- 添加一个空白区段在PE末尾 (1000h同时满足文件内存对齐);
- 添加一个区段头;
- 修正新加区段的属性(通过最后一个区段头开始以及大小计算新加区段的各属性);
- 修改 numberofsections (PE头);
- 修改 sizeofimage (可选PE头);
- 将shellcode粘贴于新区段处;
- 修改OEP于shellcode处;
如果遇到区段头无空余部分问题,可将PE头和区段头之间内容平移向上覆盖掉DOS存根,并且改掉 lfanew 偏移,之后可添加;
亦或者直接扩大最后一个区段,并修改属性;
或者合并区段:
- 读取PE文件模拟内存对齐对每个区段进行拉伸(防止其他区段合并后偏移错误);
- 只保留第一个区段头信息,其他填充0;
- 修改第一个区段头属性;
- 修改numberofsections;
- 将更改后的PE文件保存为新文件;
这样做会导致原文件扩大,但在内存中的大小不会改变,此时有足够的空间塞入壳代码;
加壳
为壳代码写入准备
如之前插入区段的方式与思路用代码操作PE,此时创建一个叫MyShell的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class MyShell { private: char* fileBuff; DWORD fileSize; PIMAGE_DOS_HEADER pDosHeader; PIMAGE_NT_HEADERS pNtHeader; PIMAGE_FILE_HEADER pFileHeader; PIMAGE_OPTIONAL_HEADER pOptionHeader; PIMAGE_SECTION_HEADER pSectionHeader;
public: MyShell(); ~MyShell(); BOOL LoadFile(const char* path); BOOL SaveFile(const char* path); BOOL InitFileInfo(); BOOL InsertSection(const char* sectionName, DWORD codeSize, char* codeBuff, DWORD dwCharateristics); DWORD GetAlignSize(DWORD realSize, DWORD alignSize); BOOL EncodeSections(); DWORD GetOep(); void SetOep(DWORD OEP); };
|
第一步,读取文件并创建buffer保存PE镜像;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| BOOL MyShell::LoadFile(const char* path) { HANDLE hFile = CreateFileA(path, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); fileSize = GetFileSize(hFile, 0); fileBuff = new char[fileSize] {}; if (ReadFile(hFile, fileBuff, fileSize, 0, 0) == FALSE) { MessageBoxA(0, "文件获取失败!", "异常", 0); return FALSE; } InitFileInfo(); CloseHandle(hFile);
return TRUE; }
|
第二步,解析PE(各个头);
1 2 3 4 5 6 7 8 9 10 11
| BOOL MyShell::InitFileInfo() { pDosHeader = (PIMAGE_DOS_HEADER)fileBuff; pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + fileBuff); pFileHeader = &(pNtHeader->FileHeader); pOptionHeader = &(pNtHeader->OptionalHeader); pSectionHeader = (PIMAGE_SECTION_HEADER)(pFileHeader->SizeOfOptionalHeader + (DWORD)pOptionHeader);
return TRUE; }
|
第三步,插入区段和区段头,设置属性并修改PE某些字段;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| BOOL MyShell::InsertSection(const char* sectionName, DWORD codeSize, char* codeBuff, DWORD dwCharateristics) { DWORD SectionCount = pFileHeader->NumberOfSections; DWORD EndOfSectionHeader = (DWORD)pSectionHeader + SectionCount * IMAGE_SIZEOF_SECTION_HEADER; DWORD BeginOfSection = (DWORD)fileBuff + pOptionHeader->SizeOfHeaders; if (BeginOfSection - EndOfSectionHeader < IMAGE_SIZEOF_SECTION_HEADER * 2) { MessageBoxA(0, "插入失败,节区头大小不足!", "异常", 0); return FALSE; } DWORD newFileSize = GetAlignSize(fileSize + codeSize, pOptionHeader->FileAlignment); char* newFileBuff = new char[newFileSize] {}; memcpy_s(newFileBuff, newFileSize, fileBuff, fileSize); fileSize = newFileSize; delete[] fileBuff; fileBuff = newFileBuff; InitFileInfo(); PIMAGE_SECTION_HEADER lastSectionHeader = pSectionHeader + (SectionCount - 1); PIMAGE_SECTION_HEADER newSectionHeader = lastSectionHeader + 1; strcpy_s((char *)newSectionHeader->Name, 8, sectionName); newSectionHeader->Misc.VirtualSize = GetAlignSize(codeSize, pOptionHeader->SectionAlignment); newSectionHeader->SizeOfRawData = GetAlignSize(codeSize, pOptionHeader->FileAlignment); newSectionHeader->VirtualAddress = lastSectionHeader->VirtualAddress + GetAlignSize(lastSectionHeader->Misc.VirtualSize, pOptionHeader->SectionAlignment); newSectionHeader->PointerToRawData = lastSectionHeader->PointerToRawData + lastSectionHeader->SizeOfRawData; newSectionHeader->Characteristics = dwCharateristics; newSectionHeader->PointerToLinenumbers = 0; newSectionHeader->PointerToRelocations = 0; newSectionHeader->NumberOfLinenumbers = 0; newSectionHeader->NumberOfRelocations = 0; pFileHeader->NumberOfSections++; pOptionHeader->SizeOfImage += GetAlignSize(codeSize, pOptionHeader->SectionAlignment); char* sectionBuff = newSectionHeader->PointerToRawData + fileBuff; memcpy(sectionBuff, codeBuff, codeSize); return TRUE; }
DWORD MyShell::GetAlignSize(DWORD realSize, DWORD alignSize) { if (realSize % alignSize == 0) return realSize; return (realSize / alignSize + 1) * alignSize; }
|
第四步,保存文件;
1 2 3 4 5 6 7 8 9 10 11 12
| BOOL MyShell::SaveFile(const char* path) { HANDLE hFile = CreateFileA(path, GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (WriteFile(hFile, fileBuff, fileSize, 0, 0) == FALSE) { MessageBoxA(0, "保存文件失败!", "异常", 0); return FALSE; } CloseHandle(hFile);
return TRUE; }
|
至此,用代码实现了之前用010手动粘贴shellcode之前的所有步骤;
对原程序编码加密
加壳难度逐级递进,此时先考虑对.text段的加密,因为.data段有IAT表等东西需要处理;
1 2 3 4 5 6 7 8 9
| BOOL MyShell::EncodeSections() { int key = 0xBC; char* pData = pSectionHeader->PointerToRawData + fileBuff; for (int i = 0; i < pSectionHeader->SizeOfRawData; i++) pData[i] ^= key;
return TRUE; }
|
制作壳代码
之前写的壳代码是需要手动从ida里扣的,这里的壳代码写在dll文件里,使用link命令合并区段只剩代码段,此时第一节区便是需要的shellcode;
1 2 3 4 5
| #pragma comment(linker,"/merge:.data=.text") #pragma comment(linker,"/merge:.rdata=.text")
#pragma comment(linker,"/section:.text,RWE")
|
壳代码的特点:
难点:
- 壳代码是后期写入文件里,系统无法修复壳代码的iat,需要动态调用API;
- 壳代码如果有全局变量,会涉及到重定位,需要修复壳的重定位表;
其具体构造如下所示:
1 2 3 4 5 6 7 8 9 10 11 12
| _declspec(naked) void Code() { __asm pushad GetAPI(); DecodeSections(); MyCode(); __asm popad __asm jmp g_OepInfo.oldOEP; }
|
原OEP以及新OEP的传递需要用到dll的结构体导出,以便exe和dll交换OEP信息,其结构体如下所示:
1 2 3 4 5 6 7 8 9
| typedef struct _OEPINFO { DWORD newOEP; DWORD oldOEP; }OEPINFO, * POEPINFO;
extern "C" _declspec(dllexport) OEPINFO g_OepInfo;
OEPINFO g_OepInfo = { (DWORD)Code };
|
在之前的MyShell类里给出了OEP的set和get方法:
1 2 3 4 5 6 7 8 9 10 11 12
| DWORD MyShell::GetOep() { return pOptionHeader->AddressOfEntryPoint + pOptionHeader->ImageBase; }
void MyShell::SetOep(DWORD OEP) { DWORD SectionCount = pFileHeader->NumberOfSections; PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + (SectionCount - 1); pOptionHeader->AddressOfEntryPoint = pLastSectionHeader->VirtualAddress + OEP; }
|
动态调用API
首先是GetAPI的实现,如何动态获取API地址呢,在上一篇ShellCode中说明了需要利用PEB来获取kernel32的基址从而找到LoadLibrary以获取所有可使用的API;
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| DWORD GetEssentialModule() { DWORD dwBase = 0; __asm { mov eax, dword ptr fs : [0x30] mov eax, [eax + 0xc] mov eax, [eax + 0x1c] mov eax, [eax] mov eax, [eax + 0x8] mov dwBase, eax }
return dwBase; }
DWORD MyGetProcAddress(DWORD hModule, LPCSTR funcName) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + (DWORD)hModule); PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((pNtHeader->OptionalHeader.DataDirectory[0].VirtualAddress) + (DWORD)hModule); DWORD* nameTable = (DWORD*)(exportTable->AddressOfNames + (DWORD)hModule); WORD* oridinalTable = (WORD*)(exportTable->AddressOfNameOrdinals + (DWORD)hModule); DWORD* addressTable = (DWORD*)(exportTable->AddressOfFunctions + (DWORD)hModule); for (int i = 0; i < exportTable->NumberOfNames; i++) { char* name = (char*)(nameTable[i] + (DWORD)hModule); if (!strcmp(name, funcName)) return addressTable[oridinalTable[i]] + (DWORD)hModule; }
return 0; }
void GetAPI() { DWORD kernelBase = GetEssentialModule(); g_MyLoadLibraryExA = (MyLoadLibraryExA)MyGetProcAddress(kernelBase, "LoadLibraryExA"); HMODULE kernel32Base = g_MyLoadLibraryExA("kernel32.dll", 0, 0); g_MyGetProcAddress = (MYGetProcAddress)MyGetProcAddress((DWORD)kernel32Base, "GetProcAddress"); g_MyGetModuleHandleA = (MyGetModuleHandleA)g_MyGetProcAddress(kernel32Base, "GetModuleHandleA"); g_MyVirtualProtect = (MyVirtualProtect)g_MyGetProcAddress(kernel32Base, "VirtualProtect"); HMODULE user32Base = g_MyLoadLibraryExA("user32.dll", 0, 0); g_MyMessageBoxA = (MyMessageBoxA)g_MyGetProcAddress(user32Base, "MessageBoxA"); }
|
对以上使用到的全局变量定义为如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| typedef HMODULE (WINAPI * MyLoadLibraryExA)( LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags );
MyLoadLibraryExA g_MyLoadLibraryExA = NULL;
typedef FARPROC (WINAPI * MYGetProcAddress)( HMODULE hModule, LPCSTR lpProcName );
MYGetProcAddress g_MyGetProcAddress = NULL;
typedef HMODULE (WINAPI * MyGetModuleHandleA)( LPCSTR lpModuleName );
MyGetModuleHandleA g_MyGetModuleHandleA = NULL;
typedef BOOL (WINAPI * MyVirtualProtect)( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect );
MyVirtualProtect g_MyVirtualProtect = NULL;
typedef int (WINAPI * MyMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType );
MyMessageBoxA g_MyMessageBoxA = NULL;
|
之后,在解密部分实现里,就可以使用API来操作数据了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| BOOL DecodeSections() { int key = 0xBC; HMODULE hModule = g_MyGetModuleHandleA(0); PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + (DWORD)hModule); PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader); char* sectionBuff = (char *)(pSectionHeader->VirtualAddress + (DWORD)hModule); DWORD oldProtect; g_MyVirtualProtect(sectionBuff, pSectionHeader->SizeOfRawData, PAGE_EXECUTE_READWRITE, &oldProtect); for (int i = 0; i < pSectionHeader->SizeOfRawData; i++) sectionBuff[i] ^= key; g_MyVirtualProtect(sectionBuff, pSectionHeader->SizeOfRawData, oldProtect, &oldProtect);
return TRUE; }
void MyCode() { g_MyMessageBoxA(0, "壳代码执行!", "提示", 0); }
|
此时便有了dll文件,在之前的MyShell类的main函数中做出如下拼装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #define CHARACTERISTICS 0xE00000E0
typedef struct _OEPINFO { DWORD newOEP; DWORD oldOEP; }OEPINFO, * POEPINFO;
int main() { MyShell myShell; if (argc < 2) { printf("\nUsage: %s + ./file_you_want_pack\n\n", argv[0]); return 0; } char* path = argv[1]; myShell.LoadFile(path); myShell.EncodeSections(); HMODULE hModule = LoadLibraryA("ShellCode.dll"); POEPINFO pOepInfo = (POEPINFO)GetProcAddress(hModule, "g_OepInfo"); pOepInfo->oldOEP = myShell.GetOep(); PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + (DWORD)hModule); PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader); char* buff = (char*)(pDllSectionHeader->VirtualAddress + (DWORD)hModule); myShell.InsertSection("BcShell", pDllSectionHeader->Misc.VirtualSize, buff, CHARACTERISTICS); myShell.SetOep(pOepInfo->newOEP - (DWORD)hModule - pDllSectionHeader->VirtualAddress); myShell.SaveFile(path);
return 0; }
|
至此一个加壳项目就 “完成” 了;
但这里任然保留了一个难点还没攻克:重定位表,在shellcode里编写的对全局变量引用的地址和在写入目标exe后所对应的地址是有问题的;
修复重定位表
重定位表结构
重定位表记录编译之前立即数地址所对应内容的位置,用于编译期间修复立即数,防止基址变化引起的立即数定位错误(类似于IAT在编译期间会修复原本指向名称为准确的地址);
其位于datadirectory[5];
重定位表中只有两个字段:VirtualAddress,SizeOfBlock,都为DWORD类型;
一个程序可能有多张重定位表;
其结构如下图所示:
其中sizeofblock为整个结构的大小(DWORD区域和WORD区域);
virtualaddress存放的内容一般为0x1000的整数倍;
word类型区域存放数据加上virtualaddress的数据则是某个立即数的准确rva;
这些rva转换为va之后,存的是立即数,而不是立即数对应的变量值;
举例:
重定位表上的偏移带过去,内存中存的是 “全局变量的地址”a ,因为挪动让a发生变化,所以修复的是a,计算a在fileBuff镜像中的位置,给填入原先的重定位表的每个偏移存放的地址处,就完成了修复;
word类型区域存放数据:0001 0000 0000 0000
,高4位用于标识:0011为有效,其他位才是用来加virtualaddress的数据;
关于0x1000是对于4KB内存页的对齐,节省内存空间才这么设计的上述结构;
修复开始
对于将要修复的重定位表是针对于注入后的壳代码而言的,通过一个共同点:立即数地址对节区的偏移不变;
思路
- 从dll中拿到原本的重定位表里的所有rva,利用这个rva和节区rva计算不变的相对偏移offset;
- 通过offset再拿到镜像中这些立即数的存放PA地址,此时也就得到了立即数;
- 再用同样的方法用立即数获取对应变量在镜像中新的立即数地址,并利用得到的PA地址来替换这些立即数;
对MyShell类新增函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| BOOL MyShell::RepairRelocate(DWORD imageBase) { PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)imageBase; PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + imageBase); PIMAGE_OPTIONAL_HEADER pDllOptionHeader = &(pDllNtHeader->OptionalHeader); PIMAGE_BASE_RELOCATION pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllOptionHeader->DataDirectory[5].VirtualAddress + imageBase); PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader);
while (pDllRelocate->SizeOfBlock) { DWORD reCount = (pDllRelocate->SizeOfBlock - 8) / 2; char* begin = (char *)pDllRelocate + 8;
for (int i = 0; i < reCount; i++) { WORD* pRelocRva = (WORD*)begin; if ((*pRelocRva & 0x3000) == 0x3000) { DWORD relocRva = (*pRelocRva & 0xfff) + pDllRelocate->VirtualAddress; DWORD offset = relocRva - pDllSectionHeader->VirtualAddress; DWORD SectionCount = pFileHeader->NumberOfSections; PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + (SectionCount - 1); DWORD destAddr = offset +(DWORD)(fileBuff + pLastSectionHeader->PointerToRawData); offset = (*(DWORD*)destAddr - imageBase) - pDllSectionHeader->VirtualAddress; DWORD aimVA = offset + (pLastSectionHeader->VirtualAddress + pOptionHeader->ImageBase); *(DWORD*)destAddr = aimVA; } begin += 2; } pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllRelocate->SizeOfBlock + (DWORD)pDllRelocate); }
return TRUE; }
|
对于以上实现的加壳项目只适用于32位且固定基址的程序;
固定基址的原因:全局变量的VA计算用到了dll的imagebase,OEP的提取也用到了imagebase;
动态基址问题
思路是将dll的重定位表也扔到加壳程序里,利用操作系统对壳代码修复重定位表;
此时壳代码可以畅通无阻地运行,则可以在壳代码中动态的获取程序基址计算OEP,修复原程序重定位表;
思路:
- 塞入dll壳代码与重定位表;
- 修复壳代码对应固定基址时立即数;
- 修复重定位表(重定位表的virtualAddress相对加壳程序而言);
- 修改加壳程序datadirectory[5]字段;
- 修复原程序重定位表;
步骤一,更改了main中对insertSection的输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| POEPINFO pOepInfo = (POEPINFO)GetProcAddress(hModule, "g_OepInfo"); pOepInfo->oldOEP = myShell.GetOep(); ------
DWORD oldRelocSize = 0; char * oldReloc = myShell.SaveOldReloc(&oldRelocSize); PPENAME PeName = (PPENAME)GetProcAddress(hModule, "g_PeName"); strcpy_s(PeName->name, path);
PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + (DWORD)hModule); PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader); char* buff = (char*)(pDllSectionHeader->VirtualAddress + (DWORD)hModule); myShell.GetImportTable(); char* pDllRelocate = (char *)(pDllNtHeader->OptionalHeader.DataDirectory[5].VirtualAddress + (DWORD)hModule); DWORD finalSize = oldRelocSize + pDllSectionHeader->Misc.VirtualSize + pDllNtHeader->OptionalHeader.DataDirectory[5].Size + myShell.GetImportTableSize(); char* finalBuff = new char[finalSize]; char* p = finalBuff; memcpy(p, buff, pDllSectionHeader->Misc.VirtualSize); p += pDllSectionHeader->Misc.VirtualSize; if (oldReloc) memcpy(p, oldReloc, oldRelocSize); p += oldRelocSize; memcpy(p, pDllRelocate, pDllNtHeader->OptionalHeader.DataDirectory[5].Size);
------ char* sectionBuff = myShell.InsertSection("BcShell", finalSize, finalBuff, CHARACTERISTICS); delete[] finalBuff;
|
虚线内为更改部分,这使得插入的节区大小可以满足shellcode以及重定位表和原程序自己导入表和原重定位表的大小;
对于新增加的函数:SaveOldReloc()和 GetImportTable(),前者有以下说明,后者放在下一个小标题讲解;
1 2 3 4 5 6 7
| char * MyShell::SaveOldReloc(DWORD * size) { *size = pOptionHeader->DataDirectory[5].Size; char* reloc = Rva2Foa(pOptionHeader->DataDirectory[5].VirtualAddress) + fileBuff;
return reloc; }
|
由于要修改加壳程序的datadirectory[5]字段,所以要先把原来的保存起来,以修复原程序的重定位表;
步骤二三四由之前的 RepairRelocate() 函数修改而来,首先对类定义了一些成员和方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| DWORD inRelocSize; PIMAGE_BASE_RELOCATION inRelocTable; ------ DWORD MyShell::Foa2Rva(DWORD foa) { DWORD rva = 0; for (PIMAGE_SECTION_HEADER p = pSectionHeader; p->Name != NULL; p++) { if (foa >= p->PointerToRawData && foa < p->PointerToRawData + p->SizeOfRawData) { rva = foa + p->VirtualAddress - p->PointerToRawData; break; } }
return rva; }
DWORD MyShell::Rva2Foa(DWORD rva) { DWORD foa = 0; for (PIMAGE_SECTION_HEADER p = pSectionHeader; p->Name != NULL; p++) { if (rva >= p->VirtualAddress && rva < p->VirtualAddress + p->Misc.VirtualSize) { foa = rva - p->VirtualAddress + p->PointerToRawData; break; } }
return foa; }
BOOL MyShell::GetInRelocTable(char* sectionBuff, DWORD offset) { inRelocTable = (PIMAGE_BASE_RELOCATION)(sectionBuff + offset); PIMAGE_BASE_RELOCATION pInR = inRelocTable;
while (pInR->SizeOfBlock) { inRelocSize++; pInR++; }
return TRUE; }
BOOL MyShell::RepairRelocate(DWORD imageBase, char* sectionBuff,DWORD offset) { PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)imageBase; PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + imageBase); PIMAGE_OPTIONAL_HEADER pDllOptionHeader = &(pDllNtHeader->OptionalHeader); PIMAGE_BASE_RELOCATION pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllOptionHeader->DataDirectory[5].VirtualAddress + imageBase); PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader);
DWORD shellCodeRVA = Foa2Rva(sectionBuff - fileBuff); PIMAGE_BASE_RELOCATION pInR = inRelocTable;
while (pDllRelocate->SizeOfBlock) { DWORD reCount = (pDllRelocate->SizeOfBlock - 8) / 2; char* begin = (char *)pDllRelocate + 8;
for (int i = 0; i < reCount; i++) { WORD* pRelocRva = (WORD*)begin; if ((*pRelocRva & 0x3000) == 0x3000) { DWORD relocRva = (*pRelocRva & 0xfff) + pDllRelocate->VirtualAddress; DWORD offset = relocRva - pDllSectionHeader->VirtualAddress; DWORD SectionCount = pFileHeader->NumberOfSections; PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + (SectionCount - 1); DWORD destAddr = offset +(DWORD)(fileBuff + pLastSectionHeader->PointerToRawData); offset = (*(DWORD*)destAddr - imageBase) - pDllSectionHeader->VirtualAddress; DWORD aimVA = offset + (pLastSectionHeader->VirtualAddress + pOptionHeader->ImageBase); *(DWORD*)destAddr = aimVA; } begin += 2; } pInR->VirtualAddress = pDllRelocate->VirtualAddress - pDllSectionHeader->VirtualAddress + shellCodeRVA; pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllRelocate->SizeOfBlock + (DWORD)pDllRelocate); pInR++; } DWORD ss = 0; SaveOldReloc(&ss); DWORD tableNewRva = Foa2Rva((DWORD)sectionBuff + offset - (DWORD)fileBuff - ss); pOptionHeader->DataDirectory[5].VirtualAddress = tableNewRva; pOptionHeader->DataDirectory[5].Size += pDllOptionHeader->DataDirectory[5].Size;
return TRUE; }
|
步骤五,实则已经实现,在步骤一将两张重定位表顺序插入shellcode之后,且在上面的代码中最后几段将导入表size更改为了两个size叠加;
用这个方法可以绕过动态基质,但是这个架构写出的壳有个bug,导致原程序加壳后变成固定基址了…虽然可以正常跑….
加密导入表
此步骤针对于程序安全性质而言;
针对加壳程序的导入表加密,对API进行保护;
步骤:
- 转移导入表进新区段,并抹掉原导入表(填充00,并将datadirectory[1]指向一个假表);
- 对API名称加密;
- 对新导入表加密;
- 于壳代码中解密并手动模拟导入表的修复(使用对应dll的导出表);
此处只给出了转移导入表部分的代码,对于后期加密部分可参考上一篇 Windows_ShellCode;
当此处实现后,因为可以由代码自己修复导入表和重定位表,则原程序的.idata段和.reloc段就随便乱改都没问题了;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| DWORD importTableSize; PIMAGE_IMPORT_DESCRIPTOR pImportTable; ------
char* MyShell::GetImportTable() { pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(Rva2Foa(pOptionHeader->DataDirectory[1].VirtualAddress) + (DWORD)fileBuff); for (PIMAGE_IMPORT_DESCRIPTOR p = pImportTable; p->Name != NULL; p++) importTableSize++; importTableSize++; importTableSize *= sizeof(IMAGE_IMPORT_DESCRIPTOR);
return (char *)pImportTable; }
BOOL MyShell::MoveImportTable(char* sectionBuff, DWORD offset) { PIMAGE_IMPORT_DESCRIPTOR newImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(sectionBuff + offset); memcpy(newImportTable, pImportTable, importTableSize); memset(pImportTable, 0x00, importTableSize); pImportTable = newImportTable; pOptionHeader->DataDirectory[1].VirtualAddress = Foa2Rva((DWORD)newImportTable - (DWORD)fileBuff); return TRUE; }
DWORD MyShell::GetImportTableSize() { return importTableSize; }
|
加密这里有个坑,按dll遍历函数名称填地址的时候,kernel32和kernelbase里有ntdll的函数引用,但是又有同名函数干扰,要想办法将对应dll的API地址填充正确,解决方法是判断导入表名称是否为kernel32或者kernelbase,如果是则多循环一次,多循环的一次dll则加载ntdll;
完成所有内容(包括加密导入表的所有步骤)的加壳项目:
关于加壳dll需要添加一个名字结构数组来传递模块名称,否则GetModuleHandle(0)是进程基址;
( BUG 肯定是有的( 缺陷是支持32位且节区头需要空闲 (
脱壳
脱壳手段于之前基础篇大部分都提及;
此外,esp定律一般是哄骗小学生的,大部分时候都用不到;
但基础篇尚未提及一点,在dump之后的文件虽然是可以查看其源码的,但如果需要动调,是无法实现的;
此时要让dump的程序能运行,则需要修复其导入表,因为此时导入表dump出的是实打实的地址,需要利用地址反找函数符号,重新构建导入表结构;
修复导入表一般用脚本完成,脚本实现思路如上述所示;