大家好,欢迎来到IT知识分享网。

0x01 前言
作为一名安全菜鸟,单纯的了解某一个方面是并不合格的,安全并不仅限于某一门语言、某一个OS,现如今安全研究的技术栈要求的更深、更广。虽说 PE 文件内存加载已经是多年前的技术,但是招不在新、有用就行,内存加载技术仍然有非常广泛的应用(隐藏自身,至于为什么要隐藏自身,dddd),由于笔者之前认知的偏差导致对PE相关的知识仅停留在知道的地步,并没有静下心来去认真分析学习,借此机会补足一下技术点,同时顺便为自己的恶意代码分析的学习之旅开个头。
0x02 关键步骤
0x1 Section 对齐
因为exe以文件形式存储的时候区段间的对齐方式与在内存中的对齐方式不尽相同,因此在手动加载exe时不能单纯的将文件格式的 exe 直接拷贝到内存中,而是应当根据内存区段(page size)的对齐方式做对齐处理。

为了验证一下,随便找一个 exe 文件作为学习资料。


FileAlignment 为 0x200,实际的Section也均是以 0x200 为单位进行对齐的,实际调试时就会发现section的对齐变为了SectionAlignment的大小:0x1000

0x2 导入表修复

要理解导入表首先要了解以下这几个结构:
IMAGE_DATA_DIRECTORY
IMAGE_DATA_DIRECTORY 位于 IMAGE_Optional_header 中的最后一个字段,是一个由16个_IMAGE_DATA_DIRECTORY 结构体构成的结构体数组,每个结构体由两个字段构成,分别为VirtualAddress和Size字段:
- VirtualAddress字段记录了对应数据结构的RVA。
- Size字段记录了该数据结构的大小。

Offset (PE/PE32+) Description 96/112 Export table address and size 104/120 Import table address and size 112/128 Resource table address and size 120/136 Exception table address and size 128/144 Certificate table address and size 136/152 Base relocation table address and size 144/160 Debugging information starting address and size 152/168 Architecture-specific data address and size 160/176 Global pointer register relative virtual address 168/184 Thread local storage (TLS) table address and size 176/192 Load configuration table address and size 184/200 Bound import table address and size 192/208 Import address table address and size 200/216 Delay import descriptor address and size 208/224 The CLR header address and size 216/232 Reserved
根据微软提供的信息,IMAGE_DATA_DIRECTORY 的第二项指向的就是导入表了。
IMAGE_IMPORT_DESCRIPTOR
既然已经找到了导入表,就需要根据导入表内的元素来加载对应的dll,获取不同的函数地址了,此时就需要用到 IMAGE_IMPORT_DESCRIPTOR 结构了,该结构的详细内容如下:
Offset Size Field 0 4 Import Lookup Table RVA 4 4 Time/Date Stamp 8 4 Forwarder Chain 12 4 Name RVA 16 4 Import Address Table RVA typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk;//(1) 指向导入名称表(INT)的RAV* }; DWORD TimeDateStamp; // (2) 时间标识 DWORD ForwarderChain; // (3) 转发链,如果不转发则此值为0 DWORD Name; // (4) 指向导入映像文件的名字* DWORD FirstThunk; // (5) 指向导入地址表(IAT)的RAV* } IMAGE_IMPORT_DESCRIPTOR;
0x03 具体分析
文件读取
文件读取步骤基本上可以说条条大路通罗马,只要将 PE 文件完整的读取到内存中可供后续处理即可,除了把一个文件放在目录中进行读取以外还有很多种方式,比如将要加载的 exe 转换成shellcode进行加载、将shellcode进行简单xor后在内存xor回来再加载。。。
ifstream inFile("nc.exe", ios::in | ios::binary); stringstream tmp; tmp << inFile.rdbuf(); unsigned char* content = (unsigned char*)tmp.str().c_str();
初始内存分配
因为 exe 文件默认有加载基址,一般情况下在运行 exe 的时候会首先尝试加载到默认地址上去,因此就要根据 exe 的默认加载基址和映像大小(exe加载到内存后的大小:SizeOfImage)来分配内存
SIZE_T SizeOfImage = pFileNtHeader->OptionalHeader.SizeOfImage;// 获取加载基址 DWORD base = pFileNtHeader->OptionalHeader.ImageBase; // 分配内存 unsigned char* memExeBase = (unsigned char*)VirtualAlloc((LPVOID)base, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(memExeBase, content, pFileNtHeader->OptionalHeader.SizeOfHeaders);
分配内存时的 VirtualAlloc指定的页类型为 MEM_COMMIT|MEM_RESERVE这里是一个小小的延迟分配的知识点,如果是 MEM_RESERVE的话只有当对该段内存进行内存操作时才会被真正Load进入物理内存中。页权限使用的是PAGE_EXECUTE_READWRITE,这在实际编码过程中是一个很不好的习惯,为了更清晰的理解 exe 内存加载的核心流程,就省略了根据section来确定内存权限的步骤。
拷贝Header
分配内存完毕后首先要将 PE header 拷贝到相应的地址空间去,因为后续的操作均需要用到。
memcpy(memExeBase, content, pFileNtHeader->OptionalHeader.SizeOfHeaders); PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)memExeBase; PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(memExeBase + pDosHeader->e_lfanew);
之后要根据新分配的内存地址计算新的 DOS头 和 NT头。
修复ImageBase
因为已经根据ImageBase分配了内存,所以需要将拷贝后的OptionalHeader中的ImageBase字段根据实际内存地址进行更新,如果开启了aslr的话需要根据实际的内存地址更新ImageBase,分配到默认基址上的话没有必要。
pNtHeader->OptionalHeader.ImageBase = (DWORD)memExeBase;
拷贝区段
拷贝区段这部分是内存加载的第一个关键点,要根据内存页的大小来将原本的文件区段进行处理。在文件中Section通常以 0x200 进行对齐,内存中页大小单位为 0x1000, 因此内存对齐单位为 0x1000,所以当PE文件加载到内存中后需要对Section进行变换。
// 拷贝区段 PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(pNtHeader); int sectionSize; for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++, section++) { if (section->SizeOfRawData == 0) { sectionSize = pNtHeader->OptionalHeader.SectionAlignment; // 最小内存Seciton单位为 SectionAlignment的大小 } else { sectionSize = section->SizeOfRawData; } if (sectionSize > 0) { void* dest = memExeBase + section->VirtualAddress; memcpy(dest, content + section->PointerToRawData, sectionSize); } }
修复导入表
OriginalFirstChunk指向的INT表表项以4字节为单位,全0结尾,如果最高位为1则代表是函数序号,反之则是一个RVA,指向IMAGE_IMPORT_BY_NAME结构,INT表实际上的功能是获取要导入的函数名。目前没有遇到最高位为1的情况。
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1]; //函数名称,0结尾. } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
IAT表项则直接是一个指向真实函数地址的指针。
PIMAGE_IMPORT_DESCRIPTOR pImportDesc; bool result = true; PIMAGE_DATA_DIRECTORY pDataDir = (PIMAGE_DATA_DIRECTORY)(&pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]); // 获取IMAGE_DATA_DIRECTORY 位置 pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(memExeBase + pDataDir->VirtualAddress);// 获取第一个IMAGE_IMPORT_DESCRIPTOR for (;!IsBadReadPtr(pImportDesc,sizeof(IMAGE_IMPORT_DESCRIPTOR)) && pImportDesc->Name;pImportDesc++) { uintptr_t* thunkRef; FARPROC* funcRef; HMODULE handle = LoadLibraryA((LPCSTR)(memExeBase + pImportDesc->Name)); // 加载dll(此处也是可以手工加载的) if (pImportDesc->OriginalFirstThunk) { thunkRef = (uintptr_t*)(memExeBase + pImportDesc->OriginalFirstThunk); funcRef = (FARPROC*)(memExeBase + pImportDesc->FirstThunk); } else { thunkRef = (uintptr_t*)(memExeBase + pImportDesc->FirstThunk); funcRef = (FARPROC*)(memExeBase + pImportDesc->FirstThunk); } for (; *thunkRef; thunkRef++, funcRef++) { if (IMAGE_SNAP_BY_ORDINAL(*thunkRef)) { // 判断OriginalFirstThunk表项最高位为1的情况 *funcRef = GetProcAddress(handle, (LPCSTR)(IMAGE_ORDINAL(*thunkRef)));//修复导入表 } else { PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(memExeBase + (*thunkRef)); *funcRef = GetProcAddress(handle, (LPCSTR)&thunkData->Name); //修复导入表 } if (*funcRef == 0) { cout << " error import " << endl; exit(0); } } }
上述修复导入表的代码实际完成的工作就是遍历导入表中的 IMAGE_IMPORT_DESCRIPTOR结构,根据dll名称将对应的dll加载到内存中,并根据OriginalFirstThunk字段来获取所需的函数名进而使用GetProcAddress来获取该函数在内存中的实际地址填充到FirstThunk字段指向的空间中。
获取入口点启动程序
if (pNtHeader->OptionalHeader.AddressOfEntryPoint != 0) { ExeEntryProc exeEntry = (ExeEntryProc)(LPVOID)(memExeBase + pNtHeader->OptionalHeader.AddressOfEntryPoint); exeEntry(); }
结语
至此exe的内存加载就已经结束了,诱发我写下这篇文章的一个主要原因是回忆起之前看过的几篇APT相关的分析文章,涉及到主机的远控目前内存加载已经是标配,杀软动态检测的对抗方式种类繁多,静态对抗的方法以内存加载为王。
参考链接
本文由D4ck原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/
安全客 – 有思想的安全新媒体
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/187328.html