分析内存动态加载PE文件
在Windows操作系统中,PE(Portable Executable)文件格式是用于可执行文件、DLL文件和其他类型文件的格式。通常,PE文件是通过操作系统的加载器加载到内存中并执行的。加载器会解析PE文件,获取详细的装入参数,并根据这些参数将文件映射到内存中执行。然而,有时我们需要在不使用操作系统加载器的情况下加载和执行PE文件,比如在开发自定义的加载器或进行反病毒与逆向工程研究时。
通过模拟PE文件加载器的工作流程,我们可以手动将可执行文件映射到内存中,并按照规则进行展开和执行。本文将介绍如何在内存中加载和运行PE文件,包括DLL和EXE文件。我们将详细讲解关键步骤和代码实现,并分享一些注意事项和最佳实践。
获取PE文件加载到内存后的镜像大小
PE文件的镜像大小存储在NT头的可选头(Optional Header)中。我们通过获取DOS头,找到NT头的位置,然后读取可选头中的SizeOfImage
字段来获取镜像大小,从而确定在内存中需要分配的空间大小。
// 获取PE文件加载到内存后的镜像大小
DWORD GetImageSize(LPVOID data)
{
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)data;
// 获取NT头
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文件的预期一致。
// 将内存PE数据按节大小对齐映射到进程内存中
BOOL MapSections(LPVOID data, LPVOID baseAddress)
{
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)data;
// 获取NT头
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文件在新的基地址下能正确执行。
// 更新PE文件重定位表
BOOL ApplyRelocations(LPVOID baseAddress)
{
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
// 获取NT头
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);
// 如果没有重定位表,直接返回TRUE
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文件在运行时能够正确调用外部函数。
// 填写PE文件导入表
BOOL LoadImports(LPVOID baseAddress)
{
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
// 获取NT头
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)
{
// 获取导入的DLL名称
char *dllName = (char *)((ULONG_PTR)dosHeader + importTable->Name);
// 尝试加载DLL
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文件在内存中能够正确定位其所有数据和代码段。
// 更新PE文件的加载基地址
BOOL UpdateImageBase(LPVOID baseAddress)
{
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
// 获取NT头
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的导出函数地址。
// 模拟GetProcAddress获取内存DLL的导出函数
LPVOID MemoryGetProcAddress(LPVOID lpMappedBase, PCHAR lpszFuncName)
{
// 获取DOS头
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpMappedBase;
// 获取NT头
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
函数,我们可以释放之前分配的内存,以避免内存泄漏。确保内存资源在使用完毕后能够被正确释放。
// 释放内存中加载的DLL
BOOL FreeMemoryLibrary(LPVOID lpMappedBase)
{
// 释放内存
return ::VirtualFree(lpMappedBase, 0, MEM_RELEASE);
}
加载并获取PE文件OEP入口
依次调用上述各个函数,通过分配内存空间、映射节、更新重定位表和导入表、获取OEP位置等,完成PE文件从加载到获取OEP入口地址的全过程,最终将加载好的PE文件入口返回给调用者。
// 加载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>
// 导出 Message 函数
extern "C"__declspec(dllexport) BOOL Message(char *lpszText, char *lpszCaption)
{
printf("lpszText = %s \n", lpszText);
printf("lpszCaption = %s \n", lpszCaption);
return TRUE;
}
// 导出 AddFunction 函数
extern "C"__declspec(dllexport) INT AddFunction(INT x, INT y)
{
return x + y;
}
// 导出 SubFunction 函数
extern "C"__declspec(dllexport) INT SubFunction(INT x, INT y)
{
return x - y;
}
// DLL入口点
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
并传入导出函数名来获取到该函数所在模块中的入口地址。当有了入口地址则可通过函数指针的方式调用这些导出函数。
// 定义DllMain函数指针类型
typedef BOOL(__stdcall *typedef_DllMain)(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved);
// 调用DLL的入口函数DllMain
BOOL InvokeDllMain(LPVOID lpMappedBase)
{
// 获取DOS头
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpMappedBase;
// 获取NT头
PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHdr + pDosHdr->e_lfanew);
// 获取DllMain入口点
typedef_DllMain DllMain = (typedef_DllMain)((ULONG_PTR)pDosHdr + pNtHdr->OptionalHeader.AddressOfEntryPoint);
// 调用DllMain入口点
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;
}
// ---------------------------------------------------------
// 打开PE文件并将其加载到内存中
// ---------------------------------------------------------
// 获取DLL文件大小
DWORD dllFileSize = GetFileSize(dllFile, NULL);
// 分配内存以存储DLL数据
BYTE *dllData = new BYTE[dllFileSize];
DWORD bytesRead = 0;
// 读取DLL文件数据
ReadFile(dllFile, dllData, dllFileSize, &bytesRead, NULL);
// 关闭文件句柄
CloseHandle(dllFile);
// ---------------------------------------------------------
// 加载PE文件并获得入口地址
// ---------------------------------------------------------
// 调用加载功能
LPVOID dllBaseAddress = LoadPE(dllData, dllFileSize);
if (dllBaseAddress == NULL)
{
delete[] dllData;
return 0;
}
// 调用DllMain函数
if (!InvokeDllMain(dllBaseAddress))
{
// 失败了则释放内存
FreeMemoryLibrary(dllBaseAddress);
delete[] dllData;
return 0;
}
// ---------------------------------------------------------
// 调用DLL功能
// ---------------------------------------------------------
// 调用第一个Message函数
typedef BOOL(*typedef_Message)(char *lpszText, char *lpszCaption);
typedef_Message Message = (typedef_Message)MemoryGetProcAddress(dllBaseAddress, "Message");
if (Message != NULL)
{
Message("hello lyshark", "Message");
}
// 调用第二个AddFunction函数
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);
// 调用第三个SubFunction函数
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;
}
// ---------------------------------------------------------
// 打开PE文件并将其加载到内存中
// ---------------------------------------------------------
// 获取EXE文件大小
DWORD exeFileSize = GetFileSize(exeFile, NULL);
// 分配内存以存储EXE数据
BYTE *exeData = new BYTE[exeFileSize];
DWORD bytesRead = 0;
// 读取EXE文件数据
ReadFile(exeFile, exeData, exeFileSize, &bytesRead, NULL);
// 关闭文件句柄
CloseHandle(exeFile);
// 加载并运行EXE
LPVOID exeBaseAddress = LoadPE(exeData, exeFileSize);
if (exeBaseAddress == NULL)
{
delete[] exeData;
return 0;
}
// ---------------------------------------------------------
// 执行EXE入口点
// ---------------------------------------------------------
// 读入EXE基地址
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;
}