在Windows操作系统中,PE(Portable Executable)文件格式是用于可执行文件、DLL文件和其他类型文件的格式。通常,PE文件是通过操作系统的加载器加载到内存中并执行的。加载器会解析PE文件,获取详细的装入参数,并根据这些参数将文件映射到内存中执行。然而,有时我们需要在不使用操作系统加载器的情况下加载和执行PE文件,比如在开发自定义的加载器或进行反病毒与逆向工程研究时。
通过模拟PE文件加载器的工作流程,我们可以手动将可执行文件映射到内存中,并按照规则进行展开和执行。本文将介绍如何在内存中加载和运行PE文件,包括DLL和EXE文件。我们将详细讲解关键步骤和代码实现,并分享一些注意事项和最佳实践。
获取PE文件加载到内存后的镜像大小
PE文件的镜像大小存储在NT头的可选头(Optional Header)中。我们通过获取DOS头,找到NT头的位置,然后读取可选头中的SizeOfImage
字段来获取镜像大小,从而确定在内存中需要分配的空间大小。
DWORD GetImageSize(LPVOID data) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)data; PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew); return ntHeaders->OptionalHeader.SizeOfImage; }
|
将内存PE数据按节对齐映射到进程内存中
PE文件的各个节(Section)在磁盘上是按文件对齐(File Alignment)存储的,但在内存中需要按节对齐(Section Alignment)进行存储。我们需要读取节表头的信息,计算每个节在内存中的位置,然后将节的数据复制到对应位置,以保持内存布局与PE文件的预期一致。
BOOL MapSections(LPVOID data, LPVOID baseAddress) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)data; PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew); DWORD sizeOfHeaders = ntHeaders->OptionalHeader.SizeOfHeaders; WORD numberOfSections = ntHeaders->FileHeader.NumberOfSections; PIMAGE_SECTION_HEADER sectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)ntHeaders + sizeof(IMAGE_NT_HEADERS32)); RtlCopyMemory(baseAddress, data, sizeOfHeaders); for (WORD i = 0; i < numberOfSections; i++) { if ((sectionHeader->VirtualAddress == 0) || (sectionHeader->SizeOfRawData == 0)) { sectionHeader++; continue; } LPVOID srcMem = (LPVOID)((ULONG_PTR)data + sectionHeader->PointerToRawData); LPVOID destMem = (LPVOID)((ULONG_PTR)baseAddress + sectionHeader->VirtualAddress); RtlCopyMemory(destMem, srcMem, sectionHeader->SizeOfRawData); sectionHeader++; } return TRUE; }
|
更新PE文件重定位表
重定位表包含了一系列地址,这些地址需要根据实际加载的基地址进行调整。重定位表的每个条目包含一个块头(Block Header)和若干重定位项(Relocation Entry),每个重定位项描述了需要调整的地址偏移。通过遍历重定位表并计算新的地址,确保PE文件在新的基地址下能正确执行。
BOOL ApplyRelocations(LPVOID baseAddress) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress; PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew); PIMAGE_BASE_RELOCATION relocTable = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)dosHeader + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); if ((PVOID)relocTable == (PVOID)dosHeader) return TRUE;
while (relocTable->VirtualAddress + relocTable->SizeOfBlock != 0) { WORD *relocData = (WORD *)((PBYTE)relocTable + sizeof(IMAGE_BASE_RELOCATION)); int numberOfReloc = (relocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (int i = 0; i < numberOfReloc; i++) { if ((DWORD)(relocData[i] & 0x0000F000) == 0x00003000) { DWORD* address = (DWORD *)((PBYTE)dosHeader + relocTable->VirtualAddress + (relocData[i] & 0x0FFF)); DWORD delta = (DWORD)dosHeader - ntHeaders->OptionalHeader.ImageBase; *address += delta; } } relocTable = (PIMAGE_BASE_RELOCATION)((PBYTE)relocTable + relocTable->SizeOfBlock); } return TRUE; }
|
填写PE文件导入表
PE文件的导入表描述了该文件所依赖的外部DLL和函数。我们需要读取导入表中的DLL名称,加载相应的DLL,并解析导入的函数地址。导入表包含了多个导入描述符(Import Descriptor),每个导入描述符对应一个外部DLL。通过填充导入表,可以确保PE文件在运行时能够正确调用外部函数。
BOOL LoadImports(LPVOID baseAddress) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress; PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew); PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)dosHeader + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); while (importTable->OriginalFirstThunk != 0) { char *dllName = (char *)((ULONG_PTR)dosHeader + importTable->Name); HMODULE hDll = GetModuleHandle(dllName); if (hDll == NULL) { hDll = LoadLibrary(dllName); if (hDll == NULL) { importTable++; continue; } }
PIMAGE_THUNK_DATA32 importNameArray = (PIMAGE_THUNK_DATA32)((ULONG_PTR)dosHeader + importTable->OriginalFirstThunk); PIMAGE_THUNK_DATA32 importFuncAddrArray = (PIMAGE_THUNK_DATA32)((ULONG_PTR)dosHeader + importTable->FirstThunk); for (DWORD i = 0; importNameArray[i].u1.AddressOfData != 0; i++) { PIMAGE_IMPORT_BY_NAME importByName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)dosHeader + importNameArray[i].u1.AddressOfData); FARPROC funcAddress = NULL; if (importNameArray[i].u1.Ordinal & 0x80000000) { funcAddress = GetProcAddress(hDll, (LPCSTR)(importNameArray[i].u1.Ordinal & 0x0000FFFF)); } else { funcAddress = GetProcAddress(hDll, (LPCSTR)importByName->Name); } importFuncAddrArray[i].u1.Function = (ULONG_PTR)funcAddress; } importTable++; } return TRUE; }
|
更新PE文件的加载基地址
PE文件的基地址存储在NT头的可选头中。我们需要将其更新为实际加载的基地址,以确保文件中的所有相对地址都能正确解析。这一步确保了PE文件在内存中能够正确定位其所有数据和代码段。
BOOL UpdateImageBase(LPVOID baseAddress) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress; PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew); ntHeaders->OptionalHeader.ImageBase = (ULONG_PTR)baseAddress; return TRUE; }
|
获取内存DLL的导出函数
PE文件的导出表描述了该文件导出的函数。我们需要读取导出表,查找指定的函数名,并获取函数的地址。导出表包含了多个导出名称(Export Name)、序号(Ordinal)和函数地址(Function Address)。通过遍历导出表,可以模拟GetProcAddress
函数,获取内存中加载的DLL的导出函数地址。
LPVOID MemoryGetProcAddress(LPVOID lpMappedBase, PCHAR lpszFuncName) { PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpMappedBase; PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHdr + pDosHdr->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExpDir = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)pDosHdr + pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD pNames = (PDWORD)((ULONG_PTR)pDosHdr + pExpDir->AddressOfNames); PCHAR pFuncName = NULL; PWORD pOrdinals = (PWORD)((ULONG_PTR)pDosHdr + pExpDir->AddressOfNameOrdinals); PDWORD pFuncs = (PDWORD)((ULONG_PTR)pDosHdr + pExpDir->AddressOfFunctions); LPVOID lpFunc = 0;
for (DWORD i = 0; i < pExpDir->NumberOfNames; i++) { pFuncName = (PCHAR)((ULONG_PTR)pDosHdr + pNames[i]); if (lstrcmpi(pFuncName, lpszFuncName) == 0) { lpFunc = (LPVOID)((ULONG_PTR)pDosHdr + pFuncs[pOrdinals[i]]); break; } } return lpFunc; }
|
释放内存中加载的DLL
通过调用VirtualFree
函数,我们可以释放之前分配的内存,以避免内存泄漏。确保内存资源在使用完毕后能够被正确释放。
BOOL FreeMemoryLibrary(LPVOID lpMappedBase) { return ::VirtualFree(lpMappedBase, 0, MEM_RELEASE); }
|
加载并获取PE文件OEP入口
依次调用上述各个函数,通过分配内存空间、映射节、更新重定位表和导入表、获取OEP位置等,完成PE文件从加载到获取OEP入口地址的全过程,最终将加载好的PE文件入口返回给调用者。
LPVOID LoadPE(LPVOID data, DWORD size) { DWORD imageSize = GetImageSize(data); LPVOID baseAddress = VirtualAlloc(NULL, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (baseAddress == NULL) return NULL; RtlZeroMemory(baseAddress, imageSize); if (!MapSections(data, baseAddress)) return NULL; if (!ApplyRelocations(baseAddress)) return NULL; if (!LoadImports(baseAddress)) return NULL; DWORD oldProtect = 0; if (!VirtualProtect(baseAddress, imageSize, PAGE_EXECUTE_READWRITE, &oldProtect)) return NULL; if (!UpdateImageBase(baseAddress)) return NULL; return baseAddress; }
|
装载并执行PE文件
首先以执行DLL文件为案例,创建一个DLL文件,包含三个导出函数:Message
、AddFunction
和 SubFunction
。这些函数将被导出并在后续步骤中动态加载和使用。
#include <Windows.h> #include <iostream>
extern "C"__declspec(dllexport) BOOL Message(char *lpszText, char *lpszCaption) { printf("lpszText = %s \n", lpszText); printf("lpszCaption = %s \n", lpszCaption); return TRUE; }
extern "C"__declspec(dllexport) INT AddFunction(INT x, INT y) { return x + y; }
extern "C"__declspec(dllexport) INT SubFunction(INT x, INT y) { return x - y; }
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
|
将上述代码保存为hook.cpp
,然后使用Visual Studio
或其他C++编译器编译为DLL文件。例如,在Visual Studio
中,可以创建一个新的DLL
项目,将上述代码文件添加到项目中,并生成项目。这将生成一个hook.dll
文件。
如下代码则是调用DLL
中的导出函数的实例,其中InvokeDllMain
用于获取DLL
的入口并调用该入口函数,以此来实现对DLL
的初始化。在主函数中通过使用ReadFile
将D://hook.dll
下的DLL
文件读入到内存中,当读入后继续使用LoadPE
函数,将文件在内存中进行布局并最终返回一个入口基地址,当有了入口基地址以后,就可以通过调用MemoryGetProcAddress
并传入导出函数名来获取到该函数所在模块中的入口地址。当有了入口地址则可通过函数指针的方式调用这些导出函数。
typedef BOOL(__stdcall *typedef_DllMain)(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved);
BOOL InvokeDllMain(LPVOID lpMappedBase) { PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpMappedBase; PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHdr + pDosHdr->e_lfanew); typedef_DllMain DllMain = (typedef_DllMain)((ULONG_PTR)pDosHdr + pNtHdr->OptionalHeader.AddressOfEntryPoint); BOOL bRet = DllMain((HINSTANCE)lpMappedBase, DLL_PROCESS_ATTACH, NULL); return bRet; }
int main(int argc, char *argv[]) { char dllFileName[] = "D://hook.dll";
HANDLE dllFile = CreateFile(dllFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL ); if (INVALID_HANDLE_VALUE == dllFile) { return 0; }
DWORD dllFileSize = GetFileSize(dllFile, NULL);
BYTE *dllData = new BYTE[dllFileSize]; DWORD bytesRead = 0;
ReadFile(dllFile, dllData, dllFileSize, &bytesRead, NULL);
CloseHandle(dllFile);
LPVOID dllBaseAddress = LoadPE(dllData, dllFileSize); if (dllBaseAddress == NULL) { delete[] dllData; return 0; }
if (!InvokeDllMain(dllBaseAddress)) { FreeMemoryLibrary(dllBaseAddress); delete[] dllData; return 0; }
typedef BOOL(*typedef_Message)(char *lpszText, char *lpszCaption);
typedef_Message Message = (typedef_Message)MemoryGetProcAddress(dllBaseAddress, "Message"); if (Message != NULL) { Message("hello lyshark", "Message"); }
typedef INT(*typedef_AddFunction)(INT x, INT y);
typedef_AddFunction AddFunction = (typedef_AddFunction)MemoryGetProcAddress(dllBaseAddress, "AddFunction");
int add_sum = AddFunction(10, 20); printf("AddFunction = %d \n", add_sum);
typedef INT(*typedef_SubFunction)(INT x, INT y);
typedef_SubFunction SubFunction = (typedef_SubFunction)MemoryGetProcAddress(dllBaseAddress, "SubFunction");
int sub_sum = SubFunction(100, 20); printf("SubFunction = %d \n", sub_sum);
FreeMemoryLibrary(dllBaseAddress); delete[] dllData; system("pause"); return 0; }
|
相对于DLL文件的加载,加载EXE文件的流程与DLL文件保持一致,读者可自行编译一个不带有资源文件的ConsoleApplication
应用程序,并动态调用它执行,代码如下所示;
int main(int argc, char *argv[]) { char exeFileName[] = "D://ConsoleApplication.exe";
HANDLE exeFile = CreateFile(exeFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL ); if (INVALID_HANDLE_VALUE == exeFile) { return 0; }
DWORD exeFileSize = GetFileSize(exeFile, NULL); BYTE *exeData = new BYTE[exeFileSize]; DWORD bytesRead = 0; ReadFile(exeFile, exeData, exeFileSize, &bytesRead, NULL);
CloseHandle(exeFile);
LPVOID exeBaseAddress = LoadPE(exeData, exeFileSize); if (exeBaseAddress == NULL) { delete[] exeData; return 0; }
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)exeBaseAddress; PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);
LPVOID exeEntry = (LPVOID)((ULONG_PTR)dosHeader + ntHeaders->OptionalHeader.AddressOfEntryPoint);
((void(*)())exeEntry)();
delete[] exeData; system("pause"); return 0; }
|