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_magic和e_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位元組),其每一資料位
對應的屬性如下所示: