初識PE檔案結構
前言
目前網路上有關PE檔案結構說明的文章太多了,自己的這篇文章只是單純的記錄自己對PE檔案結構的學習、理解和總結。
基礎概念
PE(Portable Executable:可移植的執行體)是Win32環境自身所帶的可執行檔案格式。它的一些特性繼承自Unix的Coff(Common Object File Format)檔案格式。可移植的執行體意味著此檔案格式是跨win32平臺的,即使Windows執行在非Intel的CPU上,任何win32平臺的PE裝載器都能識別和使用該檔案格式。當然,移植到不同的CPU上PE執行體必然得有一些改變。除VxD和16位的Dll外,所有 win32執行檔案都使用PE檔案格式。因此,研究PE檔案格式是我們洞悉Windows結構的良機。
檔案結構
圖表結構:
DOS頭是用來相容MS-DOS作業系統的
NT頭包含windows PE檔案的主要資訊
節表:是PE檔案後續節的描述
節:每個節實際上是一個容器,可以包含程式碼、資料等等,每個節可以有獨立的記憶體許可權,比如程式碼節預設有讀/執行許可權,節的名字和數量可以自己定義
檔案地址
1、PE檔案在硬碟上和在記憶體裡是不完全一樣的,被載入到記憶體以後其佔用的虛擬地址空間要比在硬碟上佔用的空間大一些,這是因為各個節在硬碟上是連續的,而在記憶體中是按頁對齊的。
2、PE結構內部,表示某個位置的地址採用了兩種方式,針對在硬碟上儲存檔案中的地址,稱為原始儲存地址或實體地址表示距離檔案頭的偏移;另外一種是針對載入到記憶體以後映象中的地址,稱為相對虛擬地址(RVA),表示相對記憶體映象頭的偏移。
3、CPU的某些指令是需要使用絕對地址的,比如取全域性變數的地址,傳遞函式的地址編譯以後的彙編指令中肯定需要用到絕對地址而不是相對映象頭的偏移,因此PE檔案會建議作業系統將其載入到某個記憶體地址(這個叫基地址),這種表示方式叫做虛擬地址(VA)
4、PE檔案無法載入到預期的地址,那麼系統會幫他重新選擇一個合適的基地址將他載入到此處,這時原有的VA就全部失效了,NT頭儲存了PE檔案載入所需的資訊,在不知道PE會載入到哪個基地址之前,VA是無效的,所以在PE檔案頭中大部分是使用RVA來表示地址的
可執行檔案頭
1、PE檔案可以匯出函式讓其他的PE檔案使用,也可以從其他PE檔案匯入函式
2、PE檔案通過匯出表指明自己匯出那些函式,通過匯入表指明需要從哪些模組匯入哪些函式。
3、DOS頭和NT頭就是PE檔案中兩個重要的檔案頭
DOS頭
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
重點關注欄位
e_magic:一個WORD型別,值是一個常數0x4D5A,用文字編輯器檢視該值位‘MZ’,可執行檔案必須都是'MZ'開頭。
e_lfanew:為32位可執行檔案擴充套件的域,用來表示DOS頭之後的NT頭相對檔案起始地址的偏移。
NT頭
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature:類似於DOS頭中的e_magic,其高16位是0,低16是0x4550,用字元表示是'PE‘。
IMAGE_FILE_HEADER是PE檔案頭
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
PE檔案頭
Machine:該檔案的執行平臺,是x86、x64還是I64
NumberOfSections:該PE檔案中有多少個節,也就是節表中的項數。
TimeDateStamp:PE檔案的建立時間,一般有聯結器填寫。
PointerToSymbolTable:COFF檔案符號表在檔案中的偏移。
NumberOfSymbols:符號表的數量。
SizeOfOptionalHeader:緊隨其後的可選頭的大小。
Characteristics:可執行檔案的屬性,可以是下面這些值按位相或。
PE可選頭
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
AddressOfEntryPoint:程式入口的RVA,對於exe這個地址可以理解為WinMain的RVA。對於DLL,這個地址可以理解為DllMain的RVA,如果是驅動程式,可以理解為DriverEntry的RVA。當然,實際上入口點並非是WinMain,DllMain和DriverEntry,在這些函式之前還有一系列初始化要完成,當然,這些不是本文的重點。
BaseOfCode:程式碼段起始地址的RVA。
BaseOfData:資料段起始地址的RVA。
ImageBase:映象(載入到記憶體中的PE檔案)的基地址,這個基地址是建議,對於DLL來說,如果無法載入到這個地址,系統會自動為其選擇地址。
SectionAlignment:節對齊,PE中的節被載入到記憶體時會按照這個域指定的值來對齊,比如這個值是0x1000,那麼每個節的起始地址的低12位都為0。
FileAlignment:節在檔案中按此值對齊,SectionAlignment必須大於或等於FileAlignment。
SizeOfImage:映象的大小,PE檔案載入到記憶體中空間是連續的,這個值指定佔用虛擬空間的大小。
SizeOfHeaders:所有檔案頭(包括節表)的大小,這個值是以FileAlignment對齊的。
CheckSum:映象檔案的校驗和。
SizeOfStackReserve:執行時為每個執行緒棧保留記憶體的大小。
SizeOfStackCommit:執行時每個執行緒棧初始佔用記憶體大小。
SizeOfHeapReserve:執行時為程序堆保留記憶體大小。
SizeOfHeapCommit:執行時程序堆初始佔用記憶體大小。
NumberOfRvaAndSizes:資料目錄的項數,即下面這個陣列的項數
DataDirectory:資料目錄,這是一個數組,陣列的項定義如下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory資料目錄
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
PE匯出表
匯出表是用來描述模組中的匯出函式的結構,如果一個模組匯出了函式,那麼這個函式會被記錄在匯出表中,這樣通過GetProcAddress函式就能動態獲取到函式的地址。函式匯出的方式有兩種,一種是按名字匯出,一種是按序號匯出。這兩種匯出方式在匯出表中的描述方式也不相同。
匯出表定義:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
圖表:
PE匯入表
IMAGE_DIRECTORY_ENTRY_IMPORT就是匯入表,在PE檔案載入時,會根據這個表裡的內容載入依賴的DLL,並填充所需函式的地址
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT叫做繫結匯入表,在第一種匯入表匯入地址的修正是在PE載入時完成,如果一個PE檔案匯入的DLL或者函式多那麼載入起來就會略顯的慢一些,所以出現了繫結匯入,在載入以前就修正了匯入表,這樣就會快一些。
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT叫做延遲匯入表,一個PE檔案也許提供了很多功能,也匯入了很多其他DLL,但是並非每次載入都會用到它提供的所有功能,也不一定會用到它需要匯入的所有DLL,因此延遲匯入就出現了,只有在一個PE檔案真正用到需要的DLL,這個DLL才會被載入,甚至於只有真正使用某個匯入函式,這個函式地址才會被修正。
IMAGE_DIRECTORY_ENTRY_IAT是匯入地址表,前面的三個表其實是匯入函式的描述,真正的函式地址是被填充在匯入地址表中的。
重定位
Windows使用重定位機制保證程式碼無論模組載入到哪個基址都能正確被呼叫。
編譯的時候由編譯器識別出哪些項使用了模組內的直接VA,比如push一個全域性變數、函式地址,這些指令的運算元在模組載入的時候就需要被重定位。
連結器生成PE檔案的時候將編譯器識別的重定位的項紀錄在一張表裡,這張表就是重定位表,儲存在DataDirectory中,序號是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
PE檔案載入時,PE 載入器分析重定位表,將其中每一項按照現在的模組基址進行重定位。
每個重定位項應該是一個DWORD,裡面儲存需要重定位的RVA,這樣只需要簡單操作便能找到需要重定位的項。
然而,Windows並沒有這樣設計,原因是這樣存放太佔用空間了,試想一下,加入一個檔案有n個重定位項,那麼就需要佔用4*n個位元組。
所以Windows採用了分組的方式,按照重定位項所在的頁面分組,每組儲存一個頁面起始地址的RVA,頁內的每項重定位項使用一個WORD儲存重定位項在頁內的偏移,這樣就大大縮小了重定位表的大小。
定義:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress:頁起始地址RVA。
SizeOfBlock:表示該分組儲存了幾項重定位項。
TypeOffset:這個域有兩個含義,頁內偏移用12位就可以表示,剩下的高4位用來表示重定位的型別。而事實上,Windows只用了一種型別IMAGE_REL_BASED_HIGHLOW數值是 3。
哪些專案需要被重定位呢??
1.程式碼中使用全域性變數的指令,因為全域性變數一定是模組內的地址,而且使用全域性變數的語句在編譯後會產生一條引用全域性變數基地址的指令。
2.將模組函式指標賦值給變數或作為引數傳遞,因為賦值或傳遞引數是會產生mov和push指令,這些指令需要直接地址。
3.C++中的建構函式和解構函式賦值虛擬函式表指標,虛擬函式表中的每一項本身就是重定位項
區段名及其含義
.text預設的程式碼區塊,它的內容全是指令程式碼,連結器把所有目標檔案的text塊連線成一個大的.text塊,
.data預設的讀/寫資料塊,全域性變數,靜態變數一般放在這個區段
.rdata預設只讀資料區塊,但程式中很少用到該塊中的資料,一般兩種情況用到,一是MS 的連結器產生EXE檔案中用於存放除錯目錄,二是用於存放說明字串,如果程式的DEF檔案中指定了DESCRIPTION,字串就會出現在rdata中
.idata包含其他外來的DLL的函式及資料資訊,即輸入表,將.idata區塊合併成另一個區塊已成為一種慣例
.edata輸出表,當建立一個輸出API或資料的可執行檔案時,聯結器會建立一個.EXP檔案,這個.EXP檔案包含一個.edata區塊,其會被載入到可執行檔案中,經常被合併到.text或.rdata 區塊中
.rsrc資源,包括模組的全部資源,如圖示,選單,點陣圖等,這個區塊是隻讀的,無論如何不應該把它命名為.rsrc以外的名字,也不能合併到其他的區塊裡
.bss未初始化的資料,很少在用,取而代之的是執行檔案的.data區塊的的VirtualSize被擴充套件大的空間裡用來裝未初始化的資料.
.crt用於C++ 執行時(CRT)所新增的資料
.tlsTLS的意思是執行緒區域性儲存器,用於支援通過_declspec(thread)宣告的執行緒區域性儲存變數的資料,這包括資料的初始化值,也包括執行時所需要的額外變數
.reloc可執行檔案的基址重定位,基址重定位一般僅Dll需要的
.sdata相對於全域性指標的可被定位的 短的讀寫資料
.pdata異常表,包含CPU特定的IAMGE_RUNTIME_FUNTION_ENTRY結構陣列,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它.
.didat延遲裝入輸入資料,在非Release模式下可以找到
裝載PE檔案的主要步驟
第一:當PE檔案被執行,PE裝載器檢查DOS MZ header裡的PE header偏移量。如果找到,則跳轉到PE header。
第二:PE裝載器檢查PE header的有效性。如果有效,就跳轉到PE header的尾部。
第三:緊跟PE header的是節表。PE裝載器讀取其中的節索引資訊,並採用檔案對映方法將這些節對映到記憶體,同時附上節表裡指定的節屬性。
第四:PE檔案對映入記憶體後,PE裝載器將處理PE檔案中類似import table(引入表)邏輯部分。