前言
目前網絡上有關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(引入表)邏輯部分。