1. 程式人生 > >PE檔案解析-檔案頭與整體介紹

PE檔案解析-檔案頭與整體介紹

一、PE的基本概念

    PE(Portable Execute)檔案是Windows下可執行檔案的總稱,常見的有DLL,EXE,OCX,SYS等,事實上,一個檔案是否是PE檔案與其副檔名無關,PE檔案可以是任何副檔名。
    認識PE檔案不是作為單一記憶體對映檔案被裝入記憶體是很重要的。Windows載入器(又稱PE載入器)遍歷PE檔案並決定檔案的哪一部分被對映,這種對映方式是將檔案較高的偏移位置對映到較高的記憶體地址中。PE檔案的結構在磁碟和記憶體中是基本一樣的,但在裝入記憶體中時又不是完全複製。Windows載入器會決定載入哪些部分,哪些部分不需要載入。而且由於磁碟對齊與記憶體對齊的不一致,載入到記憶體的PE檔案與磁碟上的PE檔案各個部分的分佈都會有差異。

二、PE結構分析

圖1:PE檔案的框架結構
PE檔案至少包含兩個段,即資料段和程式碼段。Windows NT 的應用程式有9個預定義的段,分別為 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,這些段並不是都是必須的,當然,也可以根據需要定義更多的段(比如一些加殼程式)。
在應用程式中最常出現的段有以下6種:
.執行程式碼段,通常  .text (Microsoft)或 CODE(Borland)命名;
.資料段,通常以 .data 、.rdata 或 .bss(Microsoft)、DATA(Borland)命名;
.資源段,通常以 .rsrc命名;
.匯出表,通常以 .edata命名;
.匯入表,通常以 .idata命名;
.除錯資訊段,通常以 .debug命名;

1、DOS頭結構

所有的PE檔案都是以一個64位元組的DOS頭開始。這個DOS頭只是為了相容早期的DOS作業系統。DOS頭的結構如下:

typedef struct IMAGE_DOS_HEADER{
      WORD e_magic;			//DOS頭的標識,為4Dh和5Ah。分別為字母MZ
      WORD e_cblp;
      WORD e_cp;
      WORD e_crlc;
      WORD e_cparhdr;
      WORD e_minalloc;
      WORD e_maxalloc;
      WORD e_ss;
      WORD e_sp;
      WORD e_csum;
      WORD e_ip;
      WORD e_cs;
      WORD e_lfarlc;
      WORD e_ovno;
      WORD e_res[4];
      WORD e_oemid;
      WORD e_oeminfo;
      WORD e_res2[10];
      DWORD e_lfanew;             //指向IMAGE_NT_HEADERS的所在
}IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS頭後跟一個DOS Stub資料,是連結器連結執行檔案的時候加入的部分資料,一般是“This program must be run under Microsoft Windows”。這個可以通過修改連結器的設定來修改成自己定義的資料。

2、PE標頭檔案

   緊跟著DOS stub的時PE標頭檔案(PE Header)。PE Header是PE相關結構NT映像頭(IMAGE_NT_HEADER)的簡稱,其中包含許多PE裝載器用到的重要欄位。執行體在支援PE檔案結構的作業系統中執行時,PE裝載器將從IMAGE_DOS_HEADER結構中的e_lfanew欄位裡找到PE Header的起始偏移量,加上基址得到PE檔案頭的指標。
PNTHeader = ImageBase + dosHeader->e_lfanew
PE頭的資料結構被定義為IMAGE_NT_HEADERS。包含三部分,其結構如下:

typedef struct IMAGE_NT_HEADERS{
      DWORD Signature;
      IMAGE_FILE_HEADER FileHeader;
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS; 

Signature欄位:PE頭的標識。雙字結構。為50h, 45h, 00h, 00h. 即“PE\0\0”。
FileHeader欄位:IMAGE_FILE_HEADER(映像標頭檔案)結構包含了檔案的物理層資訊及檔案屬性。共20位元組的資料,其結構如下:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;					//執行平臺
    WORD    NumberOfSections;			//檔案的區塊數目
    DWORD   TimeDateStamp;				//檔案建立日期和時間
    DWORD   PointerToSymbolTable;		//指向符號表(用於除錯)
    DWORD   NumberOfSymbols;			//符號表中符號個數(用於除錯)
    WORD    SizeOfOptionalHeader;		//IMAGE_OPTIONAL_HEADER32結構大小
    WORD    Characteristics;			//檔案屬性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

OptionalHeader欄位:IMAGE_OPTIONAL_HEADER(可選映像頭)是一個可選的機構,實際上IMAGE_FILE_HEADER結構不足以定義PE檔案屬性,因此可選映像頭中定義了更多的資料。總共224個位元組,最後128個位元組為資料目錄(Data Directory),其結構如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;							//標誌字
    BYTE    MajorLinkerVersion;				//連結器主版本號
    BYTE    MinorLinkerVersion;				//連結器次版本號
    DWORD   SizeOfCode;						//所有含有程式碼表的總大小
    DWORD   SizeOfInitializedData;			//所有初始化資料表總大小
    DWORD   SizeOfUninitializedData;		//所有未初始化資料表總大小
    DWORD   AddressOfEntryPoint;			//程式執行入口RVA
    DWORD   BaseOfCode;						//程式碼表其實RVA
    DWORD   BaseOfData;						//資料表其實RVA
    DWORD   ImageBase;						//程式預設裝入基地址
    DWORD   SectionAlignment;				//記憶體中表的對齊值
    DWORD   FileAlignment;					//檔案中表的對齊值
    WORD    MajorOperatingSystemVersion;	//作業系統主版本號
    WORD    MinorOperatingSystemVersion;	//作業系統次版本號
    WORD    MajorImageVersion;				//使用者自定義主版本號
    WORD    MinorImageVersion;				//使用者自定義次版本號
    WORD    MajorSubsystemVersion;			//所需要子系統主版本號
    WORD    MinorSubsystemVersion;			//所需要子系統次版本號
    DWORD   Win32VersionValue;				//保留,通常設定為0
    DWORD   SizeOfImage;					//映像裝入記憶體後的總大小
    DWORD   SizeOfHeaders;					//DOS頭、PE頭、區塊表總大小
    DWORD   CheckSum;						//映像校驗和
    WORD    Subsystem;						//檔案子系統
    WORD    DllCharacteristics;				//顯示DLL特性的旗標
    DWORD   SizeOfStackReserve;				//初始化堆疊大小
    DWORD   SizeOfStackCommit;				//初始化實際提交堆疊大小
    DWORD   SizeOfHeapReserve;				//初始化保留堆疊大小
    DWORD   SizeOfHeapCommit;				//初始化實際保留堆疊大小
    DWORD   LoaderFlags;					//與除錯相關,預設值為0
    DWORD   NumberOfRvaAndSizes;			//資料目錄表的項數
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

DataDirectory是OptionalHeader的最後128個位元組,也是IMAGE_NT_HEADERS的最後一部分資料。它由16個IMAGE_DATA_DIRECTORY結構組成的陣列構成,指向輸出表、輸入表、資源塊等資料。IMAGE_DATA_DIRECTORY的結構如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;			//資料塊的起始RVA
    DWORD   Size;					//資料塊的長度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

資料表成員結構如下:

#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
#define 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

用LordPE檢視EXE檔案的資料目錄表:

3、區塊表

3.1 區塊結構

在PE檔案頭與原始資料之間存在一個區塊表(Section Table),它是一個IMAGE_SECTION_HEADER結構陣列,區塊表包含每個塊在映像中的資訊(如位置、長度、屬性),分別指向不同的區塊實體。全部有效結構的最後以一個空的IMAGE_SECTION_HEADER結構作為結束,所以節表中總的IMAGE_SECTION_HEADER結構數量等於節的數量加一。另外,節表中 IMAGE_SECTION_HEADER 結構的總數總是由PE檔案頭 IMAGE_NT_HEADERS->FileHeader.NumberOfSections 欄位來指定的。

IMAGE_SECTION_HEADER結構定義如下:

typedef struct _IMAGE_SECTION_HEADER {
    Name						//8個位元組的塊名
    union						
    {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;                     //區塊尺寸</span>
    DWORD VirtualAddress;		//區塊的RVA地址
    DWORD SizeOfRawData;		//在檔案中對齊後的尺寸
    DWORD PointerToRawData;		//在檔案中偏移
    DWORD PointerToRelocations;	//在OBJ檔案中使用,重定位的偏移
    DWORD PointerToLinenumbers;	//行號表的偏移(供除錯使用地)
    WORD NumberOfRelocations;	//在OBJ檔案中使用,重定位項數目
    WORD NumberOfLinenumbers;	//行號表中行號的數目
    DWORD Characteristics;		//區塊屬性如可讀,可寫,可執行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

(1)Name:這是一個8位的ASCII(不是Unicode內碼),用來定義塊名,多數塊名以,開始(如.Text),這個實際上不是必需的,注意如果塊名超過了8個位元組,則沒有最後面的終止標誌NULL位元組,帶有$的區塊的名字會從編譯器裡將帶有$的相同名字的區塊被按字母順序合併。
(2) VirtualSize:指出實際的,被使用的區塊大小,是區塊在沒有對齊處理前的實際大小.如果VirtualSize > SizeOfRawData,那麼SizeOfRawData是可執行檔案初始化資料的大小(SizeOfRawData – VirtualSize)的位元組用0來填充。這個欄位在OBJ檔案中被設為0。
(3)VirtualAddress:該塊時裝載到記憶體中的RVA,注意這個地址是按記憶體頁對齊的,她總是SectionAlignment的整數倍,在工具中第一個塊預設RVA為1000,在OBJ中為0。
(4)SizeofRawData:該塊在磁碟中所佔的大小,在可執行檔案中,該欄位包括經過FileAlignment調整後塊的長度。例如FileAlignment的大小為200h,如果VirtualSize中的塊長度為19Ah個位元組,這一塊儲存的長度為200h個位元組。
(5) PointerToRawData:該塊是在磁碟檔案中的偏移,程式編譯或彙編後生成原始資料,這個欄位用於給出原始資料塊在檔案的偏移,如果程式自裝載PE或COFF檔案(而不是由OS裝載),這種情況,必須完全使用線性映像方法裝入檔案,需要在該塊處找到塊的資料。
(6) PointerToRelocations 在PE中無意義
(7) PointerToLinenumbers 行號表在檔案中的偏移值,檔案除錯的資訊
(8) NumberOfRelocations 在PE中無意義
(9) NumberOfLinenumbers 該塊在行號表中的行號數目
(10) Characteristics 塊屬性,(如程式碼/資料/可讀/可寫)的標誌,這個值可通過連結器的/SECTION選項設定.下面是比較重要的標誌:

通常,區塊中的資料在邏輯上是關聯的。PE 檔案一般至少都會有兩個區塊:一個是程式碼塊,另一個是資料塊。每一個區塊都需要有一個截然不同的名字,這個名字主要是用來表達區塊的用途。例如有一個區塊叫.rdata,表明他是一個只讀區塊。注意:區塊在映像中是按起始地址(RVA)來排列的,而不是按字母表順序。另外,使用區塊名字只是人們為了認識和程式設計的方便,而對作業系統來說這些是無關緊要的。微軟給這些區塊取了個有特色的名字,但這不是必須的。當程式設計從PE 檔案中讀取需要的內容時,如輸入表、輸出表,不能以區塊名字作為參考,正確的方法是按照資料目錄表中的欄位來進行定位。
區塊名稱以及意義:

每個區塊的名稱都是唯一的,不能有同名的兩個區塊。但事實上節的名稱不代表任何含義,他的存在僅僅是為了正規統一程式設計的時候方便程式設計師檢視方便而設定的一個標記而已。所以將包含程式碼的區塊命名為“.Data” 或者說將包含資料的區塊命名為“.Code” 都是合法的。當我們要從PE 檔案中讀取需要的區塊時候,不能以區塊的名稱作為定位的標準和依據,正確的方法是按照 IMAGE_OPTIONAL_HEADER32 結構中的資料目錄欄位結合進行定位。
在Visual C++中,用#pragma來宣告,告訴編譯器插入資料到一個區塊內:
#pragma data_seg("MY_DATA")
連結器的一個有趣特徵就是能夠合併區塊。如果兩個區塊有相似、一致性的屬性,那麼它們在連結的時候能被合併成一個單一的區塊。這取決於是否開啟編譯器的 /merge 開關。下面的連結器選項將.rdata與.text區塊合併為一個.text區塊:
/MERGE : .rdata = .text
注意:當合並區塊時,因為這沒有什麼硬性規定。例如,把.rdata合併到.text裡不會有什麼問題,但是不應該將.rsrc、.reloc或者.pdata合併到其它的區塊裡。

3.2 區塊的對齊

   區塊大小是要對齊的,有兩種對齊值,一種用於磁碟檔案內,另一種用於記憶體中。PE檔案頭指出了這兩個值,他們可以不同。PE 檔案頭裡邊的FileAligment 定義了磁碟區塊的對齊值。每一個區塊從對齊值的倍數的偏移位置開始存放。而區塊的實際程式碼或資料的大小不一定剛好是這麼多,所以在多餘的地方一般以00h 來填充,這就是區塊間的間隙。例如,在PE檔案中,一個典型的對齊值是200h ,這樣,每個區塊都將從200h
 的倍數的檔案偏移位置開始,假設第一個區塊在400h 處,長度為90h,那麼從檔案400h 到490h 為這一區塊的內容,而由於檔案的對齊值是200h,所以為了使這一區塊的長度為FileAlignment 的整數倍,490h 到 600h 這一個區間都會被00h 填充,這段空間稱為區塊間隙,下一個區塊的開始地址為600h 。
    PE 檔案頭裡邊的SectionAligment 定義了記憶體中區塊的對齊值。PE 檔案被對映到記憶體中時,區塊總是至少從一個頁邊界開始。一般在X86 系列的CPU 中,頁是按4KB(1000h)來排列的;在IA-64 上,是按8KB(2000h)來排列的。所以在X86 系統中,PE檔案區塊的記憶體對齊值一般等於 1000h,每個區塊按1000h 的倍數在記憶體中存放。

3.3 檔案偏移與RVA

由於一些PE檔案為減少體積,磁碟對齊值不是一個記憶體頁 1000h,而是 200h,當這類檔案被對映到記憶體後,同一資料相對於檔案頭的偏移量在記憶體中和磁碟檔案中是不同的,這樣就存在著檔案偏移地址與虛擬地址的轉換問題。

由上圖可以看出,檔案被對映到記憶體,DOS檔案頭,PE檔案頭,區塊表的偏移位置和大小都沒有發生改變。而各區塊對映到記憶體後,起偏移位置發生了改變。
轉換需要前面提到的一個公式:設:ΔK為相對虛擬地址RVA與檔案偏移地址File Offset的差值
VA = ImageBase + RVA
File Offset = RVA - ΔK
File Offset = VA - ImageBase - ΔK

 

原文:https://blog.csdn.net/shitdbg/article/details/49734495