今天手撸一下加载PE文件,并执行加载的PE文件。看完这一节之后相信大家会对PE文件的结构和在内存中的加载顺序有一个比较深刻的理解。
本文中可能对PE文件的基础知识介绍的不是很详细,建议大家先看看PE文件的基础结构,了解了这些基础知识后再看本文会简单许多。废话不多说,下边让我们进入正是环节吧~
主要流程分为这么几步:
1、读取PE文件到内存中;
2、申请用于加载PE文件的内存;
3、复制PE文件的所有节表到内存中;
4、修复IAT表;
5、修复重定位表;
6、转换并执行PE文件的入口函数。
读取PE文件
这块太简单了,就不多说了,直接上代码:
#include <Windows.h>HANDLE hFile = CreateFile(fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);if (hFile){DWORD dwFileSize = GetFileSize(hFile,NULL);LPSTR fileData = new CHAR[dwFileSize];if(fileData ){RtlZeroMemory(fileData, dwFileSize);DWORD dwReadSize = 0;ReadFile(hFile,fileData ,dwFileSize,&dwReadSize,NULL);}CloseHandle(hFile);hFile = NULL;}
申请内存
申请内存之前我们需要先解析PE文件中的DOS头和NT头(PS:不知道这是啥的同学,强烈建议去翻翻PE基础结构的知识,务必!务必!务必!),从中获取到PE文件加载后的大小。
我们使用VirtualAlloc函数申请内存,获取到申请的内存地址后,需要将PE的DOS头和NT头复制到此块内存中,这也是PE文件加载的基础。
#include <Windows.h>
#include <winternl.h>BOOL AllocateMemory()
{//这里的fileData就是第一步加载的PE文件数据//Dos头PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)fileData;//Nt头PIMAGE_NT_HEADERS NtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)fileData + DosHeader->e_lfanew);//申请内存,返回的是申请到内存地址PVOID ImageBase = VirtualAlloc(NULL, NtHeaders->OptionalHeader.SizeOfImage,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);if (!ImageBase)return FALSE;//dos头、nt头和节表头复制到新申请的内存中RtlCopyMemory(ImageBase,DosHeader,NtHeaders->OptionalHeader.SizeOfHeaders);return TRUE;
}
复制节表到内存中
要想复制PE文件中的所有节表,我们需要知道两个数据,一个是节表的大小,另一个则是第一个节表的地址。找第一个节表的地址时,我们可以使用微软提供的宏IMAGE_FIRST_SECTION,传参就是我们的NT头指针。当然,我们也可以手动查找,就在NT头的后边。
找到所有节表之后,我们需要循环将节表的数据复制到我们申请的内存中。
#include <Windows.h>
#include <winternl.h>VOID CopyAllSections()
{//第一个节表PIMAGE_SECTION_HEADER section_header = IMAGE_FIRST_SECTION(NtHeaders);//节表的大小DWORD dwSectionsSize = NtHeaders->FileHeader.NumberOfSections;for (DWORD i = 0; i < dwSectionsSize; i++){RtlCopyMemory((PVOID)((ULONG_PTR)ImageBase+ section_header->VirtualAddress),(PVOID)((ULONG_PTR)DosHeader + section_header->PointerToRawData),section_header->SizeOfRawData);section_header++;}//DOS头PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;//保存申请内存中的NT头PIMAGE_NT_HEADERS MemNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew);//PE文件的入口地址ULONG_PTR EntryPointer = (ULONG_PTR)ImageBase + MemNtHeaders->OptionalHeader.AddressOfEntryPoint;return VOID();
}
修复IAT表
好了,我们正式进入修复IAT表的阶段,这是加载PE文件两个重要过程之一。
IAT表也被叫做导入表,它包含了运行这个PE文件需要用到的库(dll),由于我们的基址已经改变了,所以我们需要将用到的库函数地址进行修复,使其正确的指向要调用的函数,不然会导致PE文件无法正常加载。
导入表从上到下可以分为两层,库名-函数名。
简单解析一下就是,一个PE文件包含多个库,每个库都包含多个函数。
1、我们可以从NT头的目录结构中找到导入表的地址。
2、需要注意的是,PE文件加载导入表的时候有两种情况,一种是根据函数序号获取函数地址,一种是根据函数名获取函数地址。而判断究竟使用了哪种方式。主要是根据OriginalFirstThunk字段的最高位判断。
BOOL RepairIAT()
{//MemNtHeaders 内存中的NT头数据if (MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0)return FALSE;#ifdef _WIN64union{struct{ULONGLONG low : 63;ULONGLONG high : 1;}BitField;ULONGLONG Value;}Temp = { 0 };
#elseunion{struct{ULONG low : 31;ULONG high : 1;}BitField;ULONG Value;}Temp = { 0 };
#endif PIMAGE_IMPORT_DESCRIPTOR pIID = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)ImageBase+ MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);while (pIID->Name){PIMAGE_THUNK_DATA pITD = (PIMAGE_THUNK_DATA)((ULONG_PTR)ImageBase+ pIID->OriginalFirstThunk);//记录真是函数地址的字段PULONG_PTR pFuncAddr = (PULONG_PTR)((ULONG_PTR)ImageBase + pIID->FirstThunk);while (*pFuncAddr != 0){HMODULE hModule = LoadLibraryA((LPSTR)((ULONG_PTR)ImageBase + pIID->Name));if (!hModule)return FALSE;Temp.Value = pITD->u1.AddressOfData;//根据函数序号获取函数地址if (Temp.BitField.high == 1){//修复函数地址*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, (LPCSTR)Temp.BitField.low);}//根据函数名获取函数地址else{//函数名PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)ImageBase+ pITD->u1.AddressOfData);//修复函数地址*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, pFuncName->Name);}pITD++;pFuncAddr++;}pIID++;}return TRUE;
}
修复重定位表
对于可执行文件来说,一般没有重定位表,而对于动态库(dll)文件来说,基本都会有重定位表。重定位表就是对导出函数的地址修正。
要修复重定位表,需要了解它的结构,一般是一个头+一组数据。
BOOL RepairReloc()
{//MemNtHeaders 内存中Nt头数据if (MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0){return FALSE;}//一个重定位的数据是2字节typedef struct BASE_RELOCATION_ENTRY {USHORT offset : 12;USHORT type : 4;}BASE_RELOCATION_ENTRY,*PBASE_RELOCATION_ENTRY;//NtHeaders是PE文件中的Nt头//这里是为了计算我们内存中加载PE文件的基址和PE文件默认基址的一个偏移INT_PTR offset = (INT_PTR)((ULONG_PTR)ImageBase - NtHeaders->OptionalHeader.ImageBase);PIMAGE_BASE_RELOCATION pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)ImageBase+m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);while (pIBR->VirtualAddress != 0){//重定位表的一个数据PBASE_RELOCATION_ENTRY pBlock = (PBASE_RELOCATION_ENTRY)((ULONG_PTR)pIBR+sizeof(IMAGE_BASE_RELOCATION));//一组重定位数据的个数DWORD NumberOfBlocks = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);for (DWORD i = 0; i < NumberOfBlocks; i++, pBlock++){//类型是IMAGE_REL_BASED_ABSOLUTE的不需要进行重定位if (pBlock->type != IMAGE_REL_BASED_ABSOLUTE){//需要重定位的地址PINT_PTR RepairAddr = (PINT_PTR)((ULONG_PTR)ImageBase + pIBR->VirtualAddress + pBlock->offset);if (*RepairAddr <= 0)return FALSE;//重定位*RepairAddr += offset;}}pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pIBR + pIBR->SizeOfBlock);}return TRUE;
}
转换函数入口
从第二步中我们可以得到函数入口地址,直接执行即可。这是对于可执行文件而言的,如果是动态库文件,我们需要转换一下
VOID CallEntryPoint()
{if (IsDllFile()){typedef BOOL(APIENTRY *_DllMain)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);//EntryPointer 是第二步获取到的函数地址((_DllMain)EntryPointer)((HMODULE)ImageBase, DLL_PROCESS_ATTACH,NULL);//如果动态库中有导出函数,则可以使用这个转换一下,获取导出函数地址(MemGetProcAddress)的下边再说typedef int(*_fntestdll)(void);_fntestdll fntestdll = (_fntestdll)MemGetProcAddress("fntestdll");fntestdll();}else{((void(*)())(EntryPointer))();}return VOID();
}
下边我们说一下如何获取动态库中导出函数,简单的原理就是从修复完的PE文件内存中解析导出表,然后从导出表中获取相应的函数。
其中我们需要知道导出表的三个地址:函数名地址、导出函数序号地址、导出函数地址
导出函数序号地址记录的是相应函数名的函数序号,再根据函数序号从导出函数地址中获取函数的地址。
原因嘛,导出函数地址是按函数序号从小到大顺序记录的,但是函数名地址中保存地址和导出函数地址顺序是不一样的。一般函数名地址排序方式是按首字母排序的。
PVOID MemGetProcAddress(LPCSTR funcName)
{if (MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size == 0)return NULL;PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)ImageBase + m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);PDWORD pNameAddress = (PDWORD)((ULONG_PTR)ImageBase + pIED->AddressOfNames);PDWORD pFuncAddress = (PDWORD)((ULONG_PTR)ImageBase + pIED->AddressOfFunctions);PWORD pNameOrdinalsAddress = (PWORD)((ULONG_PTR)ImageBase + pIED->AddressOfNameOrdinals);for (DWORD i = 0; i < pIED->NumberOfNames; i++){if (strcmp(funcName, (LPCSTR)(ULONG_PTR)ImageBase + *pNameAddress) == 0){return (PVOID)((ULONG_PTR)ImageBase + pFuncAddress[pNameOrdinalsAddress[i]]);}pNameAddress++;}return PVOID();
}
好了,手动加载PE文件并执行的全部流程就到此为止了,刚开始的时候大家可能感觉理解起来有点费劲,但是熟悉了之后就感觉还好,为了方便大家,下边给大家附上全部代码(PS:使用C++写的)
DealPEFile.h
#pragma once
#include <Windows.h>
#include <winternl.h>class DealPEFile
{
public:DealPEFile(LPCWSTR fileName);~DealPEFile();VOID LoadMemory();
private://判断是否是PE文件BOOL IsValidPE();//申请内存BOOL AllocateMemory();//拷贝所有节表到内存中VOID CopyAllSections();//修复IAT表BOOL RepairIAT();//修复重定位表BOOL RepairReloc();//获取导出函数PVOID MemGetProcAddress(LPCSTR funcName);//判断是否是Dll文件BOOL IsDllFile();//执行PE文件的入口函数VOID CallEntryPoint(DealPEFile* pDealPEFile);private://文件数据LPSTR m_fileData;//PE基地址PVOID m_ImageBase;//Dos头PIMAGE_DOS_HEADER m_DosHeader;//Nt头PIMAGE_NT_HEADERS m_NtHeaders;//内存中Nt头PIMAGE_NT_HEADERS m_MemNtHeaders;//PE文件入口地址ULONG_PTR m_EntryPointer;
};
DealPEFile.cpp
#include "DealPEFile.h"DealPEFile::DealPEFile(LPCWSTR fileName)
{m_fileData = NULL;m_DosHeader = NULL;m_NtHeaders = NULL;if (fileName != NULL && fileName[0] != L'\0'){HANDLE hFile = CreateFile(fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);if (hFile){DWORD dwFileSize = GetFileSize(hFile,NULL);m_fileData = new CHAR[dwFileSize];RtlZeroMemory(m_fileData, dwFileSize);DWORD dwReadSize = 0;ReadFile(hFile,m_fileData,dwFileSize,&dwReadSize,NULL);CloseHandle(hFile);hFile = NULL;}if (m_fileData){m_DosHeader = (PIMAGE_DOS_HEADER)m_fileData;m_NtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)m_DosHeader + m_DosHeader->e_lfanew);}}
}DealPEFile::~DealPEFile()
{if (m_fileData){delete[] m_fileData;m_fileData = NULL;}m_DosHeader = NULL;m_NtHeaders = NULL;
}VOID DealPEFile::LoadMemory()
{if (m_fileData == NULL)return;//判断是否是ie文件,申请加载PE文件的内存if (!IsValidPE() || !AllocateMemory())return;//复制所有节表到内存中CopyAllSections();//修复IAT表if (!RepairIAT())return;//修复重定位表if (!RepairReloc())return;//执行PE文件的入口函数CallEntryPoint(this);return;
}BOOL DealPEFile::IsValidPE()
{if (m_DosHeader->e_magic != IMAGE_DOS_SIGNATURE || m_NtHeaders->Signature != IMAGE_NT_SIGNATURE)return FALSE;#ifdef _WIN64if (m_NtHeaders->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64){return FALSE;}
#elseif (m_NtHeaders->FileHeader.Machine != IMAGE_FILE_MACHINE_I386){return FALSE;}
#endifreturn TRUE;
}BOOL DealPEFile::AllocateMemory()
{m_ImageBase = VirtualAlloc(NULL, m_NtHeaders->OptionalHeader.SizeOfImage,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);if (!m_ImageBase)return FALSE;RtlCopyMemory(m_ImageBase,m_DosHeader,m_NtHeaders->OptionalHeader.SizeOfHeaders);return TRUE;
}//拷贝所有节表到内存中
VOID DealPEFile::CopyAllSections()
{PIMAGE_SECTION_HEADER section_header = IMAGE_FIRST_SECTION(m_NtHeaders);//节表数量DWORD dwSectionsSize = m_NtHeaders->FileHeader.NumberOfSections;for (DWORD i = 0; i < dwSectionsSize; i++){RtlCopyMemory((PVOID)((ULONG_PTR)m_ImageBase+ section_header->VirtualAddress),(PVOID)((ULONG_PTR)m_DosHeader + section_header->PointerToRawData),section_header->SizeOfRawData);section_header++;}PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)m_ImageBase;m_MemNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew);m_EntryPointer = (ULONG_PTR)m_ImageBase + m_MemNtHeaders->OptionalHeader.AddressOfEntryPoint;return VOID();
}//修复IAT表
BOOL DealPEFile::RepairIAT()
{if (m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0)return FALSE;#ifdef _WIN64union{struct{ULONGLONG low : 63;ULONGLONG high : 1;}BitField;ULONGLONG Value;}Temp = { 0 };
#elseunion{struct{ULONG low : 31;ULONG high : 1;}BitField;ULONG Value;}Temp = { 0 };
#endif //导入表PIMAGE_IMPORT_DESCRIPTOR pIID = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)m_ImageBase+ m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);while (pIID->Name){PIMAGE_THUNK_DATA pITD = (PIMAGE_THUNK_DATA)((ULONG_PTR)m_ImageBase+ pIID->OriginalFirstThunk);//函数地址PULONG_PTR pFuncAddr = (PULONG_PTR)((ULONG_PTR)m_ImageBase + pIID->FirstThunk);while (*pFuncAddr != 0){HMODULE hModule = LoadLibraryA((LPSTR)((ULONG_PTR)m_ImageBase + pIID->Name));if (!hModule)return FALSE;Temp.Value = pITD->u1.AddressOfData;if (Temp.BitField.high == 1){*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, (LPCSTR)Temp.BitField.low);}else{PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)m_ImageBase+ pITD->u1.AddressOfData);*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, pFuncName->Name);}pITD++;pFuncAddr++;}pIID++;}return TRUE;
}//修复重定位表
BOOL DealPEFile::RepairReloc()
{if (m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0){return FALSE;}typedef struct BASE_RELOCATION_ENTRY {USHORT offset : 12;USHORT type : 4;}BASE_RELOCATION_ENTRY,*PBASE_RELOCATION_ENTRY;//偏移INT_PTR offset = (INT_PTR)((ULONG_PTR)m_ImageBase - m_NtHeaders->OptionalHeader.ImageBase);//重定位表PIMAGE_BASE_RELOCATION pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)m_ImageBase+m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);while (pIBR->VirtualAddress != 0){//重定位块数据PBASE_RELOCATION_ENTRY pBlock = (PBASE_RELOCATION_ENTRY)((ULONG_PTR)pIBR+sizeof(IMAGE_BASE_RELOCATION));//一块重定位数据的数量DWORD NumberOfBlocks = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);for (DWORD i = 0; i < NumberOfBlocks; i++, pBlock++){if (pBlock->type != IMAGE_REL_BASED_ABSOLUTE){//修复地址PINT_PTR RepairAddr = (PINT_PTR)((ULONG_PTR)m_ImageBase + pIBR->VirtualAddress + pBlock->offset);if (*RepairAddr <= 0)return FALSE;*RepairAddr += offset;}}pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pIBR + pIBR->SizeOfBlock);}return TRUE;
}//获取导出函数
PVOID DealPEFile::MemGetProcAddress(LPCSTR funcName)
{if (m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size == 0)return NULL;PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)m_ImageBase + m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);//函数名地址PDWORD pNameAddress = (PDWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfNames);//函数地址PDWORD pFuncAddress = (PDWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfFunctions);//函数名序号地址PWORD pNameOrdinalsAddress = (PWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfNameOrdinals);for (DWORD i = 0; i < pIED->NumberOfNames; i++){if (strcmp(funcName, (LPCSTR)(ULONG_PTR)m_ImageBase + *pNameAddress) == 0){return (PVOID)((ULONG_PTR)m_ImageBase + pFuncAddress[pNameOrdinalsAddress[i]]);}pNameAddress++;}return PVOID();
}//判断是否是Dll文件
BOOL DealPEFile::IsDllFile()
{return m_MemNtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL;
}//执行PE文件的入口函数
VOID DealPEFile::CallEntryPoint(DealPEFile* pDealPEFile)
{if (IsDllFile()){typedef BOOL(APIENTRY *_DllMain)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);((_DllMain)pDealPEFile->m_EntryPointer)((HMODULE)m_ImageBase, DLL_PROCESS_ATTACH,NULL);typedef int(*_fntestdll)(void);_fntestdll fntestdll = (_fntestdll)MemGetProcAddress("fntestdll");fntestdll();}else{((void(*)())(pDealPEFile->m_EntryPointer))();}return VOID();
}
此代码加载的PE文件是我测试使用的,测试的代码也给大家附上
Test.exe
#include <Windows.h>#pragma comment(linker,"/entry:Test")int Test()
{MessageBox(NULL,L"Test Exe!!!", L"提示", MB_OK);return 0;
}
Test.dll
dllmain.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <Windows.h>BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:MessageBox(NULL,L"test dll Main",L"提示",MB_OK);break;case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;
}
testdll.h
#ifdef __cplusplus
extern "C" {
#endif__declspec(dllexport) int fntestdll(void);
#ifdef __cplusplus
}
#endif
testdll.cpp
#include "testdll.h"
#include <Windows.h>// 这是导出函数的一个示例。
int fntestdll(void)
{MessageBox(NULL,L"fntestdll",L"提示",MB_OK);return 42;
}