PE可執行文件加載器


PE文件加載器

模仿操作系統,加載pe文件到內存中
該項目主要是為了檢測pe的學習程度,是否都完全理解了。當然沒有完全理解

實現功能的如下:

  1. 模仿操作系統,加載pe文件到內存中,然后執行待執行的pe文件
  2. 修復IAT,reloc等重要信息

當然,這只是一個雛形,有很多工作都沒有完成,TODO列表

  1. DLL文件加載,這個其實很簡單,只需要解析導出表,然后修正就行了
  2. 綁定IAT的加載,這塊懶得做
  3. 延遲加載,也是懶得做

所以我們的這個小型加載器,只是負責重定位表的解析和重定位表的解析。不過對於一個小型程序來講夠用了。下面說一下思路

  1. 根據pe頭中的optionalheader中的SizeOfImage,申請內存。內存的基地址為ImageBase。SizeOfImage為pe文件在內存對齊的情況下,所需要的空間的大小。基地址這塊的話,建議為ImageBase的地址,當然,如果該pe文件有重定位信息的話,就說明該pe文件可以加載到內存的任意位置。隨后根據重定位表修正就行了
  2. 根據pe頭中的SizeOfHeader,獲取pe頭的大小。該值為文件對齊的值。根據該值,我們調用Rtlmemcopy將pe頭拷貝到內存中
  3. 解析pe頭,獲取numberofSection,根據此值,處理section。將section拷貝到內存中
  4. 處理iat 分別解析iat中的內容,並修正
  5. 處理重定位表。如果加載的基地址為ImageBase的話,則無需處理。否則必須處理
  6. 跳轉到Address of entry,開始執行pe文件

注意事項:

  1. 暫時忽略loadflag等等
  2. 為了方便,申請的內存可讀可寫可執行,並沒有根據section的屬性去設置
  3. 被加載的程序,與主程序使用同一個heap和stack。所以不需要關注sizeofstack等值
  4. 一定要修改主程序的加載基地址,修改非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

  1. 大部分的容錯機制都沒有,畢竟只是一個簡單的程序。
  2. 容易出現無法申請內存的問題

完整的代碼,請去github上看

https://github.com/potats0/PeLoader


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM