PE文件加載器
模仿操作系統,加載pe文件到內存中
該項目主要是為了檢測pe的學習程度,是否都完全理解了。當然沒有完全理解
實現功能的如下:
- 模仿操作系統,加載pe文件到內存中,然后執行待執行的pe文件
- 修復IAT,reloc等重要信息
當然,這只是一個雛形,有很多工作都沒有完成,TODO列表
- DLL文件加載,這個其實很簡單,只需要解析導出表,然后修正就行了
- 綁定IAT的加載,這塊懶得做
- 延遲加載,也是懶得做
所以我們的這個小型加載器,只是負責重定位表的解析和重定位表的解析。不過對於一個小型程序來講夠用了。下面說一下思路
- 根據pe頭中的optionalheader中的SizeOfImage,申請內存。內存的基地址為ImageBase。SizeOfImage為pe文件在內存對齊的情況下,所需要的空間的大小。基地址這塊的話,建議為ImageBase的地址,當然,如果該pe文件有重定位信息的話,就說明該pe文件可以加載到內存的任意位置。隨后根據重定位表修正就行了
- 根據pe頭中的SizeOfHeader,獲取pe頭的大小。該值為文件對齊的值。根據該值,我們調用Rtlmemcopy將pe頭拷貝到內存中
- 解析pe頭,獲取numberofSection,根據此值,處理section。將section拷貝到內存中
- 處理iat 分別解析iat中的內容,並修正
- 處理重定位表。如果加載的基地址為ImageBase的話,則無需處理。否則必須處理
- 跳轉到Address of entry,開始執行pe文件
注意事項:
- 暫時忽略loadflag等等
- 為了方便,申請的內存可讀可寫可執行,並沒有根據section的屬性去設置
- 被加載的程序,與主程序使用同一個heap和stack。所以不需要關注sizeofstack等值
- 一定要修改主程序的加載基地址,修改非0x0040000的位置。不然無法申請0x00400000的地址。修改該值的話,在vs的鏈接選項中
下面數一下詳細的操作
判斷是否pe文件
這塊很簡單,沒什么說的,看代碼即可
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)BaseAddr;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((UINT_PTR)BaseAddr + pDos->e_lfanew);
if (pDos->e_magic == IMAGE_DOS_SIGNATURE && pNt->Signature == IMAGE_NT_SIGNATURE) {
return true;
}
申請內存
根據sizeofimage去申請內存即可。當然我這個函數很粗,在imagebase無法使用的情況下,並沒有判斷程序是否可以重定位的情況下,強行修改imagebase。大家在使用的時候最好判斷一下。
DWORD dwSizeOfImage = pnt->OptionalHeader.SizeOfImage;
DWORD dwImageBaseAddr = pnt->OptionalHeader.ImageBase;
//為了安全性,暫時將該申請的內存區域設置成可讀可寫,等一會再根據需要重新設置
//必須要設置MEM_RESERVE,不然不能申請0x00400000地址
LPVOID returnAddr = VirtualAlloc((LPVOID)dwImageBaseAddr, dwSizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (GetLastError() == 0) {
printf("[+] 正在根據pe的加載基地址 申請內存,基地址為 0x%p\n", (LPVOID)dwImageBaseAddr);
return returnAddr;
}
else {
returnAddr = VirtualAlloc(NULL, dwSizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("[+] pe的加載基地址不能用,正在重新申請地址中,基地址為 0x%p\n", (LPVOID)dwImageBaseAddr);
return returnAddr;
}
拷貝pe頭到內存中
其實對於咱們的加載器來講。拷貝不拷貝pe頭,並不會正常影響文件的執行。所以這個是一個可選的步驟。當然,我為了方便,因為在后面我會釋放掉讀取文件的內存。所以必須拷貝pe頭。該函數比較簡單,直接調用rtlcopy函數即可
static void __stdcall CopyNtHeaderToMem(IN LPVOID lpPemem, IN LPVOID Header, SIZE_T size) {
//獲取nt頭的size,文件對齊值,一般是一頁文件對齊
RtlCopyMemory(lpPemem, Header, size);
printf("[+] 正在拷貝pe頭到 0x%p中\n", lpPemem);
}
拷貝section到內存
這塊比較簡單。讀取sectionHeader,header中說明的section的VA和FOA以及size,我們只需要根據這些信息,拷貝到內存的指定位置即可
static void __stdcall CopySectionToMem(IN LPVOID lpPeMem, IN LPVOID lpBaseAddr, IN PIMAGE_NT_HEADERS pNt) {
//暫時不處理內存屬性,全部可讀可寫可執行哈哈哈哈
DWORD dwNumOfSection = pNt->FileHeader.NumberOfSections;
DWORD dwSectionAlignment = pNt->OptionalHeader.SectionAlignment;
PIMAGE_SECTION_HEADER pSecHed = (PIMAGE_SECTION_HEADER)((UINT_PTR)pNt + sizeof(IMAGE_NT_HEADERS));
for (DWORD index = 0; index < dwNumOfSection; index++)
{
DWORD dwRva = pSecHed->VirtualAddress;
DWORD dwFOA = pSecHed->PointerToRawData;
DWORD dwSize = pSecHed->SizeOfRawData;
//拷貝源是文件對齊的foa
LPVOID SecDataSrc = (LPVOID)((UINT_PTR)lpBaseAddr + (UINT_PTR)dwFOA);
//目的地址是RV
LPVOID SecDataDst = (LPVOID)RVA2VA(lpPeMem, dwRva);
//開始拷貝
RtlCopyMemory(SecDataDst, SecDataSrc, dwSize);
printf("[+] 正在拷貝 %s section 到內存的 0x%p, 大小為 %d\n", pSecHed->Name, SecDataDst, dwSize);
pSecHed = (PIMAGE_SECTION_HEADER)((UINT_PTR)pSecHed + sizeof(IMAGE_SECTION_HEADER));
}
return;
}
處理IAT
在PE文件中,IAT(Import address Table)和INT(Import Name Tbable)其實差不了太多。導入表的話一般都在.rdata節中。在pe中,IAT最終會存放相應函數的內存地址。下面以一個例子來說明
某程序會調用KERNEL32.dll!IsProcessorFeaturePresent函數,反匯編代碼如下
004013E3 6A17 push 00000017h
004013E5 E84F090000 call jmp_KERNEL32.dll!IsProcessorFeaturePresent
004013EA 85C0 test eax,eax
0x004013E5中存放的為機器碼,E8代表call執行,后面的值為距離該地址的偏移,偏移值為0x0000094F。
則程序會調轉到 0x004013EA + 0x0000094F,也就是0x0040$D19。下面看一下該地址的反匯編代碼
00401D39 FF251C204000 jmp [KERNEL32.dll!IsProcessorFeaturePresent]
FF代表絕對跳轉, JMP r/m32 絕對跳轉(32位),下一指令地址在r/m32中給出 。也就是取出地址0x0040201c25中的值。跳轉過去。而0x0040201c25,就是rdata節。該處為IAT。
而pe文件中,IAT首先會存放va,指向一個IMAGE_IMPORT_BY_NAME
,里面存放導入函數的名稱和hint。
所以修復IAT很簡單,首先遍歷INT,INT的結構如下
遍歷到INT,拿到加載dll的名字。調用loadlobrary加載。
然后通過FirstTrunk的方式,去遍歷IAT。再根據IAT中的信息,調用GetProcAddress函數,獲取到真正的函數地址。修正IAT即可
代碼如下
PIMAGE_IMPORT_DESCRIPTOR pImportTab = (PIMAGE_IMPORT_DESCRIPTOR)RVA2VA(lpPeMem, dwImportTableRVA);
//根據橋2修復就行了,不用根據橋1
while (pImportTab->OriginalFirstThunk && pImportTab->FirstThunk) {
char* DllName = (char*)(RVA2VA(lpPeMem, pImportTab->Name));
printf("[+] 正在修正導入庫 %s\n", DllName);
PDWORD FirstTunkVA = (PDWORD)RVA2VA(lpPeMem, pImportTab->FirstThunk);
HMODULE hModle = LoadLibraryA(DllName);
while (*FirstTunkVA != 0) {
PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(RVA2VA(lpPeMem, *FirstTunkVA));
//這塊主要是為了處理exitprocess,攔截程序的exitprocess,我們可以從這里獲取程序的返回結果
if (strcmp(pImportName->Name, "ExitProcess") == 0) {
procAddr = (FARPROC)& MyExitProcess;
}
else
{
procAddr = GetProcAddress(hModle, pImportName->Name);
}
*FirstTunkVA = (DWORD)procAddr;
FirstTunkVA = (DWORD*)((DWORD)FirstTunkVA + sizeof(DWORD));
#ifdef _DEBUG
printf("\t[+] 正在修正 %s 的導入地址, 修正后的函數地址為 0x%p\n", pImportName->Name, procAddr);
#endif // _DEBUG
}
printf("\n");
pImportTab = (IMAGE_IMPORT_DESCRIPTOR*)((UINT_PTR)pImportTab + sizeof(IMAGE_IMPORT_DESCRIPTOR));
}
當然,我們也可以在這里hook函數。比如我為了攔截被加載程序的結果。在修復ExitProcess函數的時候,將該函數的調用地址並沒有修正到kernel32.dll中。而是修正到自己的代碼中。
而hook的函數寫法,按照你想hook函數的參數寫就行。例
void MyExitProcess(_In_ UINT uExitCode) {
printf("\n[+] 程序已退出,退出代碼為 %d\n", uExitCode);
ExitProcess(uExitCode);
}
處理重定位表
根據重定位表的定義,里面存放着相對於ImageBase的偏移。我們需要讀取到該偏移后,轉換成virtual address。與當前加載的基地址進行對比。根據偏移去修復即可。重定位表的解釋如圖
代碼如下
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)RVA2VA(lpPeMem, pRelocRVA);
printf("[+] 發現重定位表,開始修正...\n");
while (pReloc->VirtualAddress) {
DWORD dwSizeOfBlock = (pReloc->SizeOfBlock - 8) >> 1;
DWORD dwVa = pReloc->VirtualAddress;
PWORD block = (PWORD)((UINT_PTR)pReloc + sizeof(IMAGE_BASE_RELOCATION));
printf("[+] 發現 %d塊需要重定位的地址信息\n", dwSizeOfBlock);
DWORD dwDelta = (DWORD)lpPeMem - pNt->OptionalHeader.ImageBase;
for (DWORD index = 0; index < dwSizeOfBlock; index++)
{
WORD relocBlock = *block;
if (((relocBlock & 0xF000) >> 12) == IMAGE_REL_BASED_HIGHLOW) {
DWORD wOffset = (relocBlock & 0x0FFF | 0x00000000) + dwVa;
PDWORD pAddress = (PDWORD)(wOffset | (DWORD)lpPeMem);
*pAddress = *pAddress + dwDelta;
#ifdef _DEBUG
printf("[+] 修正后的地址為 0x%08x\t\n", pAddress);
#endif
}
block = (PWORD)((UINT_PTR)block + sizeof(WORD));
}
pReloc = (PIMAGE_BASE_RELOCATION)block;
}
至此,一個pe文件所需要的東西,就已經全部解析完。下面我們需要跳轉到入口點。入口點為optionalheader的entry of address。該值為RVA。需要轉換成VA才可以。轉換完成后,我們在vs中使用內聯匯編。jmp跳轉過去即可。代碼如下
DWORD EntryOfImage = RVA2VA(lpPeMem, pNt->OptionalHeader.AddressOfEntryPoint);
printf("[+] 所有的內容都處理完畢,跳轉到addresss of entry,地址為 0x%p\n\n", (LPVOID)EntryOfImage);
__asm {
jmp EntryOfImage;
}
測試結果
下面來測試一個vs 2019編譯的程序,該程序使用MessageBox彈框,調用printf輸出1111。該程序使用release模式編譯,存在重定位表。加載截圖如下
目前已知的bug
- 大部分的容錯機制都沒有,畢竟只是一個簡單的程序。
- 容易出現無法申請內存的問題
完整的代碼,請去github上看