初識PE文件結構

  • 2019 年 10 月 3 日
  • 筆記

前言

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