1. 程式人生 > 其它 >PE檔案整體結構解析

PE檔案整體結構解析

DOS頭

在之前,我們已經瞭解過PE檔案的整體結構了,並且我們進行了靜動態差異的檔案分析,其開頭部分就是DOS
部分,包含了DOS MZ檔案頭和DOS塊,那麼我們來了解一些DOS部分的結構和其相關意義。

DOS MZ檔案頭

DOS MZ檔案頭就是一個結構體IMAGE_DOS_HEADER,其定義如下所示:

 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;

它有很多成員,但我們並不需要去深入的理解每個成員的含義和作用,這是因為這個結構體是給16位平臺看
的,而我們現在的環境大部分都是32位和64位的,所以現在的平臺不再需要這個完整的結構體了,只需要其中
的兩個成員e_magice_lfanew.
你可以嘗試在16進位制的編輯器中去編輯某個EXE檔案保留兩個成員e_magic和e_lfanew,其他的以0x00填充,然
後儲存檔案,你會發現修改後的檔案還是可以正常執行的:

保留這兩個成員的原因是因為它們代表著我們之前所說的PE指紋,作業系統也是根據這個來識別是否是PE檔案
的,所以不能夠更改、刪除(e_magic是一種標識,e_lfanew則表示PE檔案頭的位置

)。

DOS塊

DOS塊就是夾在DOS MZ檔案頭和PE檔案頭之間的內容,這裡面的內容可以根據自己的需要隨意的修改和新增,
並不會影響檔案的正常執行。

PE頭

PE頭整體就是如下這個結構體:

 typedef struct _IMAGE_NT_HEADERS {
 DWORD Signature; // PE標識
 IMAGE_FILE_HEADER FileHeader; // 標準PE頭
 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 擴充套件PE頭
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

第一個成員就是PE標識,該標識不能破壞,因為作業系統在啟動一個程式的時候會檢測這個標識。

標準PE頭

標準PE頭是PE頭的第二個成員,它是如下所示的結構體:

typedef struct _IMAGE_FILE_HEADER {
 WORD Machine; // 可以執行在什麼樣的CPU上
 WORD NumberOfSections; // 表示節的數量
 DWORD TimeDateStamp; // 編譯器填寫的時間戳
 DWORD PointerToSymbolTable; // 除錯相關
 DWORD NumberOfSymbols; // 除錯相關
 WORD SizeOfOptionalHeader; // 擴充套件PE頭的大小
 WORD Characteristics; // 檔案屬性
 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

其第一個成員Machine表示可以執行在什麼樣的CPU上,如果它的值為0x0則表示可以執行在任意的CPU上,支
持在Intel 386以及後續的型號CPU執行則值為0x14c,支援64位的CPU型號則值為0x8664。
我們可以分別在32位、64位系統上提取notepad.exe進行對比來看看這個成員(010 Editor → Tools → Compare
Files...):


第二個成員NumberOfSections表示當前PE檔案中節的數量,也就是節表中有幾個結構體;第三個成員
TimeDateStamp表示編譯器編譯的時候插入的時間戳,與檔案屬性裡面的建立時間和修改時間是無關的。
第四、第五個成員是除錯相關的,我們暫時不用去了解;第六個成員SizeOfOptionalHeader表示擴充套件PE頭的大
小,預設情況下32位PE檔案對應值位0xE0,64位PE檔案對應值為0xF0。
第七個成員Characteristics用來記錄當前PE檔案的一些屬性,該成員是16位(2位元組)大小,其每一資料位對
應的屬性如下所示:

擴充套件PE頭

擴充套件PE頭在32位和64位環境下是不一樣的,在本章節中只介紹32位擴充套件PE頭。如下結構體就是32位的擴充套件PE
頭:

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD Magic; // PE32:10B PE32+:20B
  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; // PE檔案自身的版本號
 WORD MinorImageVersion; // PE檔案自身的版本號
 WORD MajorSubsystemVersion; // 執行所需子系統版本號
 WORD MinorSubsystemVersion; // 執行所需子系統版本號
 DWORD Win32VersionValue; // 子系統版本的值,必須為0
 DWORD SizeOfImage; // 記憶體中整個PE檔案的對映的尺寸
 DWORD SizeOfHeaders; // 所有頭加節表按照檔案對齊後的大小,否則載入會出錯
 DWORD CheckSum; // 校驗和
 WORD Subsystem; // 子系統,驅動程式(1)、圖形介面(2) 、控制檯/DLL(3)
 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;

擴充套件PE頭的成員有很多,但我們不需要每個都記住,大概的瞭解一下即可,重點關注如下這幾個成員:
成員Magic表示當前PE檔案是32位還是64位,32位時該值對應0x10B,64位時該值對應0x20B。
成員AddressOfEntryPoint表示當前程式入口的地址,這個成員要與成員ImageBase相加才能得出真正的入口地
址,成員ImageBase用來表示記憶體映象基址,也就是PE檔案在記憶體中按記憶體對齊展開後的首地址,我們可以在
實際PE檔案中看下,如下圖所示就是PE檔案靜態狀態下的兩個成員值,AddressOfEntryPoint為0x739D,
ImageBase為0x1000000,那麼最終的程式在記憶體中的入口地址就是0x100739D:
那麼如何證實推斷的結果是正確的呢,我們可以直接使用DTDebug之類的偵錯程式開啟這個PE檔案,偵錯程式會自
動在程式入口斷點,如下圖所示則表示我們的推測是正確的:

成員FileAlignment、SectionAlignment和SizeOfHeader在之前的章節中已經瞭解過了,這裡不再贅述。
成員SizeOfImage表示在記憶體中整個PE檔案對映的大小,可比實際的值大(記憶體對齊之後的大小,也就表示必須是SectionAlignment的整數倍)。
擴充套件PE頭的成員有很多,但我們不需要每個都記住,大概的瞭解一下即可,重點關注如下這幾個成員:
成員Magic表示當前PE檔案是32位還是64位,32位時該值對應0x10B,64位時該值對應0x20B。
成員AddressOfEntryPoint表示當前程式入口的地址,這個成員要與成員ImageBase相加才能得出真正的入口地
址,成員ImageBase用來表示記憶體映象基址,也就是PE檔案在記憶體中按記憶體對齊展開後的首地址,我們可以在
實際PE檔案中看下,如下圖所示就是PE檔案靜態狀態下的兩個成員值,AddressOfEntryPoint為0x739D,
ImageBase為0x1000000,那麼最終的程式在記憶體中的入口地址就是0x100739D:
那麼如何證實推斷的結果是正確的呢,我們可以直接使用DTDebug之類的偵錯程式開啟這個PE檔案,偵錯程式會自
動在程式入口斷點,如下圖所示則表示我們的推測是正確的:

成員FileAlignment、SectionAlignment和SizeOfHeader在之前的章節中已經瞭解過了,這裡不再贅述。
成員SizeOfImage表示在記憶體中整個PE檔案對映的大小,可比實際的值大(記憶體對齊之後的大小,也就表示必須是SectionAlignment的整數倍)。

成員CheckSum表示校驗和,是用來判斷檔案是否被修改的,它的計算方法就是檔案的兩個位元組與兩個位元組相加,最終的值(不考慮溢位情況)就是校驗和。

最後一個需要我們瞭解的成員是DllCharacteristics,它用來表示PE檔案的特性,但不要被名字所迷惑,它不是針對DLL檔案的;它的資料寬度是16位(4位元組),其每一資料位對應的屬性如下所示:

PE節表

在PE中,節資料有幾個,分別對應著什麼型別以及其他相關的屬性都是由PE節表來決定的,PE節表是一個結構體陣列,結構體的定義如下所示:

#define IMAGE_SIZEOF_SHORT_NAME 8
 typedef struct _IMAGE_SECTION_HEADER {
  BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // ASCII字串(節名),可自己義,只擷取8個位元組,可以8個位元組都是
名字
 union { // Misc,雙字,是該節在沒有對齊前的真實尺寸,該值可以不準確
 DWORD PhysicalAddress; // 真實寬度,這兩個值是一個聯合結構,可以使用其中的任何一個
 DWORD VirtualSize; // 一般是取後一個
 } Misc; 
 DWORD VirtualAddress; // 在記憶體中的偏移地址,加上ImageBase才是在記憶體中的真正地址
 DWORD SizeOfRawData; // 節在檔案中對齊後的尺寸
 DWORD PointerToRawData; // 節區在檔案中的偏移
 DWORD PointerToRelocations; // 除錯相關
 DWORD PointerToLinenumbers; // 除錯相關
 WORD NumberOfRelocations; // 除錯相關
 WORD NumberOfLinenumbers; // 除錯相關
 DWORD Characteristics; // 節的屬性
 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

程式碼中的註釋可以大致瞭解到每個成員的作用,其中有2個成員來描述節的大小,分別是沒有對齊前的真實尺寸和對齊後的寬度,這時候會出現一種情況就是對齊前的真實尺寸大於對齊後的寬度,這就是存在全域性變數沒有賦予初始值導致的,在檔案儲存中全域性變數沒有賦予初始值也就不佔空間,但是在記憶體中是必須要賦予初始
值的,這時候寬度就大了一些,所以在記憶體中節是誰大就按照誰去展開。

與其他結構體一樣,PE節也有屬性,這就是成員Characteristics,其資料寬度是16位(4位元組),其每一資料位
對應的屬性如下所示: