PE文件格式學習筆記

概述

Win32平臺上(包括Windows 9x/NT/2000/XP/Server 2003/Vista/CE/7/10),可執行文件格式是PE。
PE是“Portable Executable File Format”(可移植的執行體)的縮寫。PE格式是目前Windows平臺上的主流可執行文件格式,常見的有 DLL,EXE,OCX,SYS 等。它是微軟在 UNIX 平臺的 COFF(通用對象文件格式)基礎上製作而成。最初設計用來提高程序在不同操作系統上的移植性,但實際上這種文件格式僅用在 Windows 系列操作系統下。PE文件是指 32 位可執行文件,也稱爲PE32。64位的可執行文件稱爲 PE+ 或 PE32+,是PE(PE32)的一種擴展形式(請注意不是PE64),沒有新的結構加入,只是簡單地將以前32位字段擴展成64位。對於C++代碼,Windows文件頭的配置使其擁有不明顯的區別。
事實上,一個文件是否是 PE 文件與其擴展名無關,EXE文件和DLL文件的區別完全是語義上的。它們使用完全相同的PE格式,唯一的區別就是用一個字段標識出這個文件是EXE還是DLL。

基本概念

PE文件使用的是一個平面地址空間,所有代碼和數據都被合併在一起,組成一個很大的結構。文件的內容被分割爲不同的區塊(Section,又稱區段、節等),區塊中包含代碼或數據,各個區塊按頁邊界來對齊,區塊沒有大小限制,是一個連續結構。每個塊都有它自己在內存中的一套屬性,比如:這個塊是否包含代碼、是否只讀或可讀/寫等。
認識PE文件不是作爲單一內存映射文件被裝入內存是很重要的。Windows加載器(又稱PE裝載器)遍歷PE文件並決定文件的哪一部分被映射,這種映射方式是將文件較高的偏移位置映射到較高的內存地址中。當磁盤文件一旦被裝入內存中,磁盤上的數據結構佈局和內存中的數據結構佈局是一致的。這樣如果知道在磁盤的數據結構中尋找一些內容,那麼幾乎都能在被裝入到內存映射文件中找到相同的信息。但數據之間的相對位置可能改變,其某項的偏移地址可能區別於原始的偏移位置,不管怎樣,所有表現出來的信息都允許從磁盤文件偏移到內存偏移的轉換。加載到內存中的映像的部分偏移地址之所以不同於磁盤中的偏移地址,是因爲在磁盤中,區塊的存儲是連續的,而在內存中是按頁對齊的,因此在PE頭和各區塊的尾部存在一個區域稱爲NULL填充。
如圖所示:

(一)基地址

當PE文件通過Windows加載器被裝入內存後,內存中的版本被稱作模塊(Module)。映射文件的起始地址被稱爲模塊句柄(hModule),可以通過模塊句柄訪問內存中其他的數據結構。這個初始內存地址也稱爲基地址(ImageBase)。
內存中的模塊代表着進程從這個可執行文件中所需要的代碼、數據、資源、輸入表、輸出表及其他有用的數據結構所使用的內存都放在一個連續的內存塊中,編程人員只要知道裝載程序文件映像到內存後的基地址即可。PE文件剩下的其他部分可以被讀入,但是可能不映射。比如當調試信息放到文件尾部的時候,PE的一個字段會告訴系統把文件映射到內存需要多少內存,不能被映射的數據將被放置在文件的尾部。
在32位Windows系統中可以直接調用GetModuleHandle以取得指向DLL的指針,通過指針訪問該DLL Module的內容。
例如:HMODULE GetmoduleHandle(LPCTSRT lpModuleName);
當調用該函數時,傳遞一個可執行文件或DLL文件名字符串,如果系統找到文件,則返回該可執行文件或DLL文件映像加載到的基地址。也可調用GetModuleHandle,傳遞NULL參數,則返回調用的可執行文件的基地址。
基地址的值是由PE文件本身設定的。按照默認設置,用Visual C++建立的EXE文件基地址是00400000h,DLL文件基地址是10000000h。但是,可以在創建應用程序的EXE文件時改變這個地址,方法是在鏈接應用時使用鏈接程序的/BASE選項,或者鏈接後通過REBASE應用程序進行設置。

(二)虛擬地址

在Windows系統中,PE文件被系統加載器映射到內存中。每個程序都有自己的虛擬空間,這個虛擬空間的內存地址稱爲虛擬地址(Virtual Address,VA)

(三)相對虛擬地址

在可執行文件中,有許多地方需要制定內存中的地址。例如,引用全局變量時,需要指定它的地址。PE文件儘管有一個首選的載入地址(基地址),但是它們可以載入到進程空間的任何地方,所以不能依賴於PE的載入點。由於這個原因,必須有一個方法來指定地址(不依賴PE載入點的地址)。
爲了避免在PE文件中出現絕對內存地址引入了相對虛擬地址(Relative Virtual Address,RVA)的概念。RVA只是內存中的一個簡單的、相對於PE文件載入地址的偏移地址,它是一個“相對”地址(或稱偏移量)、例如,假設一個EXE文件從400000h處載入,而且它的代碼區塊開始於401000h處,代碼區塊的RVA計算方法如下:
目標地址401000h-載入地址400000h=RVA1000h
將一個RVA轉換成真實的地址只是簡單地翻轉這個過程,即用實際的載入地址加上RVA,得到實際的內存地址。它們之間的關係如下:
虛擬地址(VA)=基地址(ImageBase)+相對虛擬地址(RVA)

(四)文件偏移地址

當PE文件儲存在磁盤上時,某個數據的位置相對於文件頭的偏移量,稱爲文件偏移地址(File Offset)或物理地址(RAW Offset)。文件偏移地址從PE文件的第一個字節開始計數,起始值爲0。用十六進制工具(例如Hex Workshop、WinHex等)打開文件所顯示的地址就是文件偏移地址。

整體介紹

(一)PE文件種類

種類 主擴展名
可執行系列 EXE、SCR
驅動程序系列 SYS、VXD
庫系列 DLL、OCX、CPL、DRV
對象文件系列 OBJ

(二)PE文件結構簡圖

對於同樣的結構,不同的人有不同的稱呼,比如section,有的資料翻譯成塊,有的資料翻譯成節,不要在名稱上糾結,主要是把握關鍵字段的含義,瞭解其整體框架和工作流程。

(三)PE文件的執行順序

  1. 當一個 PE 文件 被執行時,PE 裝載器 首先檢查 DOS header 裏的 PE header 的偏移量。如果找到,則直接跳轉到 PE header 的位置。
  2. 當 PE裝載器 跳轉到 PE header 後,第二步要做的就是檢查 PE header 是否有效。如果該 PE header 有效,就跳轉到 PE header 的尾部。
  3. 緊跟 PE header 尾部的是節表。PE裝載器執行完第二步後開始讀取節表中的節段信息,並採用文件映射( 在執行一個PE文件的時候,Windows並不在一開始就將整個文件讀入內存,而是採用與內存映射的機制,也就是說,Windows裝載器在裝載的時候僅僅建立好虛擬地址和PE文件之間的映射關係,只有真正執行到某個內存頁中的指令或者訪問某一頁中的數據時,這個頁面纔會被從磁盤提交到物理內存,這種機制使文件裝入的速度和文件大小沒有太大的關係 )的方法將這些節段映射到內存,同時附上節表裏指定節段的讀寫屬性。
  4. PE文件映射入內存後,PE裝載器將繼續處理PE文件中類似 import table (輸入表)的邏輯部分。

(四)PE文件結構說明

  1. DOS頭 是用來兼容 MS-DOS 操作系統的,目的是當這個文件在 MS-DOS 上運行時提示一段文字,大部分情況下是:This program cannot be run in DOS mode. 還有一個目的,就是指明 NT 頭在文件中的位置。
  2. NT頭 包含 windows PE 文件的主要信息,其中包括一個 'PE' 字樣的簽名,PE文件頭(IMAGE_FILE_HEADER)和 PE可選頭(IMAGE_OPTIONAL_HEADER32)。
  3. 節表:(也稱塊表)是 PE 文件後續節的描述,windows 根據節表的描述加載每個節。
  4. 節:(也稱塊)每個節實際上是一個容器,可以包含 代碼、數據 等等,每個節可以有獨立的內存權限,比如代碼節默認有讀/執行權限,節的名字和數量可以自己定義。

PE結構

(一)DOS頭

DOS頭分爲兩部分,分別是“MZ頭部”和“DOS存根”。

(1)MZ頭

MZ頭部是真正的DOS頭部,由於其開始處的兩個字節爲“MZ”,因此DOS頭也可以叫作MZ頭。該部分用於程序在DOS系統下加載,它的結構被定義爲IMAGE_DOS_HEADER。
其中,值得注意的是e_magic字段和e_lfanew字段
e_magic字段佔兩個字節,值爲0x5A4D,對應ASCII碼值爲'MZ',"MZ"其實是MS-DOS的創建者之一Mark Zbikowski名字的縮寫。
e_lfanew字段佔4個字節,位於從文件開始偏移3Ch字節處,用於指出NT頭的偏移地址

   typedef struct _IMAGE_DOS_HEADER {
     WORD e_magic; //魔術數字
     WORD e_cblp;  //文件最後頁的字節數
     WORD e_cp;    //文件頁數
     WORD e_crlc;  //重定義元素個數
     WORD e_cparhdr;  //頭部尺寸,以段落爲單位
     WORD e_minalloc; //所需的最小附加段
     WORD e_maxalloc; //所需的最大附加段
     WORD e_ss;    //初始的SS值(相對偏移量)
     WORD e_sp;    //初始的SP值
     WORD e_csum;  //校驗和
     WORD e_ip;    //初始的IP值
     WORD e_cs;    //初始的CS值(相對偏移量)
     WORD e_lfarlc; //重分配表文件地址
     WORD e_ovno;   //覆蓋號
     WORD e_res[4]; //保留字
     WORD e_oemid;  //OEM標識符(相對e_oeminfo)
     WORD e_oeminfo;  //OEM信息
     WORD e_res2[10]; //保留字
     LONG e_lfanew;   //新exe頭部的文件位置
   } IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

MZ頭的結構和大小是固定的,我們可以用一小段代碼來檢查其長度

#include<Windows.h>
#include<stdio.h>

int main()
{
   int dosheader_size;
   dosheader_size = sizeof(IMAGE_DOS_HEADER);

   printf("%d",dosheader_size);
   return 0;
}


最終求得MZ頭部的大小爲64字節

(2)DOS存根

DOS 殘留是一段簡單的程序,主要用於輸出“This program cannotbe run in DOS mode.”類似的提示字符串。爲什麼PE結構的最開始位置有這樣一段DOS頭部呢?關鍵是爲了該可執行程序可以兼容DOS系統。通常情況下,Win32下的PE程序不能在DOS下運行,因此保留了這樣一個簡單的DOS程序用於提示“不能運行於DOS模式下”。

(二)NT頭

有些資料上也叫PE頭。
NT頭部保存着 Windows 系統加載可執行文件的重要信息。NT頭部由IMAGE_NT_HEADERS定義。從該結構體的定義名稱可以看出,IMAGE_NT_HEADERS由多個結構體組合而成,包括IMAGE_NT_SIGNATRUE,IMAGE_FILE_HEADER 和 IMAGE_OPTIONAL_HEADER三部分。NT頭部在PE文件中的位置不是固定不變的,NT頭部的位置由DOS頭部的e_lfanew字段給出。
當執行體在支持PE文件結構的操作系統中執行時,PE裝載器將從IMAGE_DOS_HEADER結構的e_lfanew字段裏找到NT頭的起始偏移量,用其加上基址,得到PE文件頭的指針。

(1)起始地址

起始地址由MZ頭中的最後兩個字節給出

從圖中可以得知,這兩個字節的內容爲0x00000080(小端存儲,低字節在低地址),從而可以在該偏移地址處找到NT頭的起始位置。

(2)NT頭

NT頭總的結構定義在IMAGE_NT_HEADERS這個結構體中,其中可以分爲三個部分,分別爲“簽名”、“文件頭”和“可選頭”

    typedef struct _IMAGE_NT_HEADERS {
      DWORD IMAGE_NT_SIGNATURE; // 簽名50450000h
      IMAGE_FILE_HEADER FileHeader; // 文件頭
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;  // 可選頭
    } IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;

(3)簽名

IMAGE_NT_SIGNATURE
在NT頭的開始處是一個32位的標識信息,PE\0\0,MZ頭部的e_lfanew字段正是指向PE\0\0的。

如上圖所示,內容爲0x00004550。
#define IMAGE_NT_SIGNATURE 0x00004550

(4)NT頭:文件頭

IMAGE_FILE_HEADER
IMAGE_FILE_HEADER(映像文件頭)結構包含了PE文件的一些基本信息,最重要的是其中一個域指出了IMAGE_OPTIONAL_HEADER的大小。
這是一個COFF格式的文件頭,指明在何種機器上運行,多少個節在裏面,連接的時間,是否是可執行文件或者DLL等。DLL和可執行文件的區別:DLL不能夠啓動,只可以被其他可執行文件使用,一個可執行文件不能夠連接到另一個可執行文件。
文件頭的大小爲20字節:
#define IMAGE_SIZEOF_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;


主要掌握①②⑥⑦,③④⑤瞭解即可。
① Machine:可執行文件的目標CPU類型。PE文件可以在多種機器上使用,不同平臺上指令的機器碼不同。
我打開的這個程序中,對應的Machine字段爲0x8664
在winnt.h的第16827到第16858行定義了對應值的含義:

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_TARGET_HOST       0x0001  // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64             0xAA64  // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

②NumberOfSections:區塊(Section)的數目,塊表緊跟在IMAGE_NT_HEADERS後面。

在圖中對應的值爲0x0004表示有4個區塊。
可以使用Visual Studio 2019中自帶的dumpbin工具查看。
指令爲:dumpbin /exports 文件名
比如:dumpbin /exports E:\娛樂\網易雲音樂\crack\ncmdump\main.exe

如上圖所示,正是4個區塊。
③TimeDateStamp:表明文件是何時被創建的。這個值是自1970年1月1日以來用格林威治時間(GMT)計算的秒數,這個值是一個比文件系統的日期/時間更精確的文件創建時間指示器。將這個值翻譯爲易讀的字符串的方法是用_ctime函數(它是時區敏感型的),另一個對此字段計算有用的函數是gmtime。
但不知爲何在我打開的這個可執行文件中,對應的字段爲0。不過這個不重要。
④PointerToSymbolTable:COFF符號表的文件偏移位置。因爲已採用了較新的debug格式,所以COFF符號表在PE文件中較少見。在Visual Studio .NET之前,COFF符號表可以通過設置鏈接器開關/DEBUGTYPE:COFF來創建。COFF符號表幾乎總能在目標文件中找到,如果沒有符號表存在,將此值設爲0。
⑤NumberOfSymbols:如果有COFF符號表,它代表其中的符號數目,COFF符號是一個大小固定的結構,如果想找到COFF符號表的結束處,這個域是需要的。
⑥SizeOfOptionalHeader:緊跟着IMAGE_FILE_HEADER後面的數據大小。在PE文件中,這個數據結構叫IMAGE_OPTIONAL_HEADER,其大小依賴於是32位還是64位文件。對於32位PE文件,這個域通常是00E0h;對於64位PE32+文件,這個域是00F0h。不管怎麼樣,這些是要求的最小值,較大的值可能也會出現。

在這個程序中,SizeOfOptionalHeader的值爲00F0h,也就是對於64位PE32+文件而言的最小值。
⑦Characteristics:文件屬性,有選擇地通過幾個值的運算得到,這些標誌的有效值是定義於winnt.h內的IMAGE_FILE_xxx值。

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved external references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Aggressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.


使用方法是將這個字段的值,以二進制的形式來看,比如在下圖這個例子中,特徵值爲0223h,對應的二進制爲0000 0010 0010 0011

也就是說0001h、0002h、0020h、0200h對應的值爲1
換句話說,就是特徵值與表中的某個值通過與操作後,若結果不爲0,則說明文件具有對應屬性。比如,如果Characteristics & 0x2000 = 0x2000,那麼表明這是一個DLL文件。
對比特徵值含義表,可以得知此文件中不存在重定位信息、文件可執行等等

(5)NT頭:可選頭

IMAGE_OPTIONAL_HEADER
緊跟在文件頭的後面是IMAGE_OPTIONAL_HEADER,儘管名字是可選,但是該頭部不是一個可選的,而是一個必須存在的頭,不可以沒有。該頭被稱作“可選頭”的原因是在該頭的數據目錄數組中,有的數據目錄項是可有可無的,數據目錄項部分是可選的,因此稱爲“可選頭”。其中包含關於如何精確處理PE文件的信息。實際上,IMAGE_OPTIONAL_HEADER是對IMAGE_FILE_HEADER的一個擴充,IMAGE_FILE_HEADER結構不足以定義PE文件屬性,因此可選映像頭中定義了更多的數據,完全不必考慮兩個結構區別在哪裏,兩者連起來就是一個完整的“PE文件頭結構”。
可選頭緊挨着文件頭。NT頭又或者叫PE頭從PE文件標識符0x00004550開始,4個字節,然後是20個字節的文件頭部分,因此可選頭起始於從NT頭開始的第25字節處,當然NT頭的起始地址不固定,因爲DOS存根的大小不固定,這個起始地址由DOS頭的e_lfanew字段給出。而可選頭的大小在文件頭的SizeOfOptionalHeader字段中給出,從而可以確定出可選頭的結束位置。假設可選頭的大小是00F0h,也就是240字節,可選頭的起始位置爲0x00000098,那麼結束位置便是
0x00000098+0x00F0-1=0x00000187
可選頭定位技巧
可選頭的定位除了計算之外還有特別的技巧,起始位置比較簡單,在對應ASCII碼處找到PE字樣或者在16進制區域找到0x00004550。以該地址爲起點往後的第25字節即爲可選頭起始位置。而找可選頭的結束位置的技巧在於通常情況下(注意這裏是指通常情況下,不是手工構造的PE文件),可選頭的結尾後面跟的是第一項節表(或稱區塊表)的名稱。比如下圖中,該節表名稱爲.text。從而找到可選頭的最後一個字節在0x00000187位置處。

可選頭的數據結構
IMAGE_OPTIONAL_HEADER結構有32位和64位之分。
在winnt.h中,分別定義爲IMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER64,如下所示:

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif

IMAGE_OPTIONAL_HEADER32定義爲:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    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;

IMAGE_OPTIONAL_HEADER32不同的是,IMAGE_OPTIONAL_HEADER64沒有BaseOfData字段,
並且 ImageBase/SizeOfStackReserve/SizeOfStackCommit/SizeOfHeapReserve/SizeOfHeapCommit的數據類型是ULONGLONG,也就是unsigned __int64類型。
IMAGE_OPTIONAL_HEADER64的定義爲:

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   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;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

各字段的含義

typedef struct _IMAGE_OPTIONAL_HEADER { 
            //
            // Standard fields.                 // 標準域
            //
  +18H      WORD    Magic;                     '// 魔數 32位爲0x10B,64位爲0x20B,ROM鏡像爲0x107'
  +1AH      BYTE    MajorLinkerVersion;         // 鏈接器的主版本號 -> 05h
  +1BH      BYTE    MinorLinkerVersion;         // 鏈接器的次版本號 -> 0Ch
  +1CH      DWORD   SizeOfCode;                 // 代碼節大小,一般放在“.text”節裏,必須是FileAlignment的整數倍 -> 0x00000200
  +20H      DWORD   SizeOfInitializedData;      // 已初始化數大小,一般放在“.data”節裏,必須是FileAlignment的整數倍 -> 0x00000400
  +24H      DWORD   SizeOfUninitializedData;    // 未初始化數大小,一般放在“.bss”節裏,必須是FileAlignment的整數倍 -> 00 00 00 00
  +28H      DWORD   AddressOfEntryPoint;       '// 指出程序最先執行的代碼起始地址(RVA) -> 0x00001000
  +2CH      DWORD   BaseOfCode;                 // 代碼基址,當鏡像被加載進內存時代碼節的開頭RVA。必須是SectionAlignment的整數倍 -> 0x00001000
 
  +30H      DWORD   BaseOfData;                 // 數據基址,當鏡像被加載進內存時數據節的開頭RVA。必須是SectionAlignment的整數倍 -> 0x00002000
                                                // 在64位文件中此處被併入緊隨其後的ImageBase中。
            //
            // NT additional fields.            // 以下是屬於NT結構增加的領域。
            //
  +34H      DWORD   ImageBase;                  // 當加載進內存時,鏡像的第1個字節的首選地址。
                                                // WindowEXE默認ImageBase值爲00400000,DLL文件的ImageBase值爲10000000,也可以指定其他值。
                                                // 執行PE文件時,PE裝載器先創建進程,再將文件載入內存,
                                                // 然後把EIP寄存器的值設置爲ImageBase+AddressOfEntryPoint
 
                                                // PE文件的Body部分被劃分成若干區塊,這些區塊儲存着不同類別的數據。
  +38H      DWORD   SectionAlignment;           // SectionAlignment指定了內存中的區塊的對齊大小
  +3CH      DWORD   FileAlignment;              // FileAlignment指定了在磁盤文件中的區塊的對齊大小
                                                // SectionAlignment必須大於或者等於FileAlignment
 
  +40H      WORD    MajorOperatingSystemVersion;// 要求操作系統最低版本號的主版本號
  +42H      WORD    MinorOperatingSystemVersion;// 要求操作系統最低版本號的副版本號
  +44H      WORD    MajorImageVersion;          // 可運行於操作系統的主版本號
  +46H      WORD    MinorImageVersion;          // 可運行於操作系統的次版本號
  +48H      WORD    MajorSubsystemVersion;      // 要求最低子系統版本的主版本號
  +4AH      WORD    MinorSubsystemVersion;      // 要求最低子系統版本的次版本號
  +4CH      DWORD   Win32VersionValue;          // 從來不用的字段,不被病毒利用的話一般爲0
 
  +50H      DWORD   SizeOfImage;                // 當鏡像被加載進內存時的大小,包括所有的文件頭。向上舍入爲SectionAlignment的倍數。
                                                // 一般文件大小與加載到內存中的大小是不同的。
 
  +54H      DWORD   SizeOfHeaders;              // 所有頭的總大小,向上舍入爲FileAlignment的倍數。
                                                // 可以以此值作爲PE文件第一節的文件偏移量。
 
  +58H      DWORD   CheckSum;                   // 鏡像文件的校驗和
 
  +5CH      WORD    Subsystem;                  // 運行此鏡像所需的子系統 -> 00 02 -> 窗口應用程序
                                            // 用來區分系統驅動文件(*.sys)與普通可執行文件(*.exe,*.dll),
                                            // 參考:https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.3'
 
        WORD    DllCharacteristics;         // DLL標識 -> 00 00
        DWORD   SizeOfStackReserve;         // 最大棧大小。CPU的堆棧。默認是1MB。
        DWORD   SizeOfStackCommit;          // 初始提交的堆棧大小。默認是4KB 
        DWORD   SizeOfHeapReserve;          // 最大堆大小。編譯器分配的。默認是1MB 
        DWORD   SizeOfHeapCommit;           // 初始提交的局部堆空間大小。默認是4K 
        DWORD   LoaderFlags;                // 與調試有關,默認爲 0 
 
        DWORD   NumberOfRvaAndSizes;       '// 指定DataDirectory的數組個數,由於以前發行的Windows NT的原因,它只能爲16。
        IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; '// 數據目錄數組。詳見下文。' 
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
 
typedef struct _IMAGE_DATA_DIRECTORY {  
    DWORD   VirtualAddress;  
    DWORD   Size;  
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;


(1)Magic:是一個標記字,說明文件是ROM映像(0107h),還是普通可執行的映像(010Bh),一般是010Bh,如是PE32+,則是020Bh。

(2)MajorLinkerVersion:鏈接程序的主版本號。
(3)MinorLinkerVersion:鏈接程序的次版本號。
(4)SizeOfCode:所有帶有IMAGE_SCN_CNT_CODE屬性區塊的總共大小(只入不捨),也就是代碼塊的大小。如果有多個代碼塊的話,該值是所有代碼塊大小的總和(通常只有一個代碼塊),這個值是向上對齊FileAlignment的整數倍。例如,本例是200h,即對齊的是一個磁盤扇區字節數(200h)的整數倍。通常情況下,多數文件只有一個Code塊,所以這個字段和.text塊的大小匹配。
(5)SizeOfInitializedData:已初始化數據塊的大小,即在編譯時所構成的塊的大小(不包括代碼段)。但這個數據並不太準確。
(6)SizeOfUninitializedData:未初始化數據塊的大小,裝載程序要在虛擬地址空間中爲這些數據約定空間。這些塊在磁盤文件中不佔空間,就像“UninitializedData”這一術語所暗示的一樣,這些塊在程序開始運行時沒有指定值。未初始化數據通常在.bss塊中。
(7)AddressOfEntryPoint:程序執行的入口地址。該地址是一個相對虛擬地址,簡稱 EP (EntryPoint),這個值指向了程序第一條要執行的代碼。程序如果被加殼後會修改該字段的值。在脫殼的過程中找到了加殼前該字段的值,就說明找到了原始入口點,原始入口點被稱爲OEP。該字段的地址指向的不是 main()函數的地址,也不是WinMain()函數的地址,而是運行庫的啓動代碼的地址。如果在一個可執行文件上附加了一段代碼並想讓這段代碼首先被執行,那麼只需要將這個入口地址指向附加的代碼就可以了。對於DLL,這個入口點是在進程初始化和關閉時以及線程創建/毀滅時被調用。對於DLL來說,這個值的意義不大,因爲DLL甚至可以沒有DllMain()函數,沒有DllMain()只是無法捕獲裝載和卸載DLL時的4個消息。如果在DLL裝載或卸載時沒有需要進行處理的事件,可以將DllMain()函數省略掉。在大多數可執行文件中,這個地址並不直接指向Main、WinMain或DllMain,而是指向運行時庫代碼並由它來調用上述的函數。在DLL中這個域能被設置爲0,前面提到的通知消息都不能收到。鏈接器/NOENTRY開關可以設置這個域爲0。
(8)BaseOfCode:代碼段的起始RVA。在內存中,代碼段通常在PE文件頭之後、數據塊之前。在Microsoft鏈接器生成的執行文件中,RVA通常是1000h。Borland的Tlink32是將ImageBase加上第一個Code Section的RVA,並將結果存入該字段。
(9)BaseOfData:數據段的起始RVA。數據段通常是在內存的末尾,即PE文件頭和Code Section之後。可是,這個域的值對於不同版本的微軟鏈接器是不一致的,在64位可執行文件中是不出現的。
(10)ImageBase:

  • 《加密與解密》
    文件在內存中的首選裝入地址。如果有可能(也就是說,目前如果沒有其他佔據這塊地址,它是正確對齊的並且是一個合法的地址,等等),加載器試圖在這個地址裝入PE文件。如果可執行文件是在這個地址裝入的,那麼加載器將跳過應用基址重定位的步驟。
  • 小甲魚
    指出文件的優先裝入地址。也就是說當文件被執行時,如果可能的話,Windows優先將文件裝入到由ImageBase字段指定的地址中,只有指定的地址已經被其他模塊使用時,文件才被裝入到其他地址中。鏈接器產生可執行文件的時候對應這個地址來生成機器碼,所以當文件被裝入這個地址時不需要進行重定位操作,裝入的速度最快,如果文件被裝載到其他地址的話,將不得不進行重定位操作,這樣就要慢一點。
    對於EXE文件來說,由於每個文件總是使用獨立的虛擬地址空間,優先裝入地址不可能被其他模塊佔據,所以EXE總是能夠按照這個地址裝入,這也意味着EXE 文件不再需要重定位信息。對於DLL文件來說,由於多個DLL文件全部使用宿主EXE文件的地址空間,不能保證優先裝入地址沒有被其他的DLL使用,所以 DLL文件中必須包含重定位信息以防萬一。因此,在前面介紹的 IMAGE_FILE_HEADER 結構的 Characteristics 字段中,DLL 文件對應的 IMAGE_FILE_RELOCS_STRIPPED 位總是爲0,而EXE文件的這個標誌位總是爲1。
    在鏈接的時候,可以通過對link.exe指定/base:address選項來自定義優先裝入地址,如果不指定這個選項的話,一般EXE文件的默認優先裝入地址被定爲00400000h,而DLL文件的默認優先裝入地址被定爲10000000h。

(11)SectionAlignment:當被裝入內存時的區塊對齊大小。每個區塊被裝入的地址必定是本字段指定數值的整數倍。默認的對齊尺寸是目標CPU的頁尺寸。對於運行在Windows 9x/Me下的用戶模式可執行文件,最小的對齊尺寸是一頁1000h(4KB)。這個字段可以通過鏈接器的/ALIGN開關來設置。在IA-64上,是按8KB來排列的。
(12)FileAlignment:磁盤上PE文件內的區塊對齊大小,組成塊的原始數據必須保證從本字段的倍數地址開始。對於x86可執行文件,這個值通常是200h或1000h,這是爲了保證塊總是從磁盤的扇區開始。在文件對齊值爲1000h 時,由於與內存對齊值相同,可以加快裝載速度。而文件對齊值爲200h時,可以佔用相對較少的磁盤空間。200h是512字節,通常磁盤的一個扇區即爲512字節。

程序無論是在內存中還是磁盤上,都無法恰好滿足SectionAlignment和FileAlignment值的倍數,在不足的情況下需要補0值,這樣就導致節與節之間存在了無用的空隙。這些空隙對於病毒之類程序而言就有了可利用的價值。

(13)MajorOperatingSystemVersion:要求操作系統的最低版本號的主版本號。隨着這麼多版本的Windows的到來,這個字段明顯地變得不切題了。
(14)MinorOperatingSystemVersion:要求操作系統的最低版本號的次版本號。
(15)MajorImageVersion:該可執行文件的主版本號,由程序員定義。它不被系統使用並可以設置爲0,可以通過鏈接器的/VERSION開關設置它。
(16)MinorImageVersion:該可執行文件的次版本號,由程序員定義。
(17)MajorSubsystemVersion:要求最低子系統版本的主版本號。這個值與下一個字段一起,通常被設置爲4,可以通過鏈接器開關/SUBSYSTEM來設置。
(18)MinorSubsystemVersion:要求最低子系統版本的次版本號。
(19)Win32VersionValue:另一個從來不用的字段,通常被設置爲0。
(20)SizeOfImage:映像裝入內存後的總尺寸。它指裝入文件從Image Base到最後一個塊的大小。最後一個塊根據其大小往上取整。
(21)SizeOfHeaders:是MS-DOS頭部、PE文件頭、區塊表的總尺寸。所有這些項目出現在PE文件中所有代碼或數據區塊之前。域值四捨五入至文件對齊值的倍數。
(22)CheckSum:映像的校驗和。IMAGEHLP.DLL中的CheckSumMappedFile函數可以計算這個值。一般的EXE文件可以是0,但一些內核模式的驅動程序和系統DLL必須有一個檢驗和。當鏈接器的/RELEASE開關被使用時,校驗和被置於文件中。
(23)Subsystem:一個標明可執行文件所期望的子系統(用戶界面類型)的枚舉值。這個值只對EXE是重要的

取值 Windows.inc中的預定義值 含義
0
IMAGE_SUBSYSTEM_UNKNOWN
未知的子系統
1 IMAGE_SUBSYSTEM_NATIVE 不需要子系統(如驅動程序)
2 IMAGE_SUBSYSTEM_WINDOWS_GUI Windows圖形界面
3 IMAGE_SUBSYSTEM_WINDOWS_CUI Windows控制檯界面
5 IMAGE_SUBSYSTEM_OS2_CUI OS2控制檯界面
7 IMAGE_SUBSYSTEM_POSIX_CUI POSIX控制檯界面
8 IMAGE_SUBSYSTEM_NATIVE_WINDOWS 不需要子系統
9 IMAGE_SUBSYSTEM_WINDOWS_CE_GUI Windows CE圖形界面

(24)DllCharacteristics:DllMain()函數何時被調用,默認爲0。
(25)SizeOfStackReserve:在EXE文件裏,爲線程保留的堆棧大小。它一開始只提交其中一部分,只有在必要時,才提交剩下的部分。
(26)SizeOfStackCommit:在EXE文件裏,一開始即被委派給棧的內存。默認值是4KB。
(27)SizeOfHeapReserve:在EXE文件裏,爲進程的默認堆保留的內存。默認值是1MB。
(28)SizeOfHeapCommit:在EXE文件裏,委派給堆的內存大小。默認值是4KB。
(29)LoaderFlags:與調試有關,默認值爲0。
(30)NumberOfRvaAndSizes:數據目錄的項數。這個字段從最早的Windows NT發佈以來一直是16。
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
(31)DataDirectory[16]:數據目錄表,這是一個結構體數組,由16個相同的IMAGE_DATA_DIRECTORY結構組成,大小爲字節,指向輸出表、輸入表、資源塊等數據。
IMAGE_DATA_DIRECTORY的結構定義如下:

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

在DataDirectory這個數組中,每個元素都是一個結構體IMAGE_DATA_DIRECTORY,而根據索引值對應的結構如下所示:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 輸出表(導入表) (重要)
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // 輸入表 (重要)
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 資源目錄 (重要)
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // 異常目錄
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // 安全目錄
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // 基址重定位表 (重要)
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // 調試目錄
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // 描述信息(版權信息之類)(X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // 架構特定數據
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // 機器值
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // 線程級局部存儲目錄(重要)
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // 載入配置目錄
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // 綁定輸入目錄
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // 輸入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // 延遲加載導入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM運行時描述符
                                             15   // 保留,必須爲0


如何在十六進制文件中找到數據目錄表對應的字段?

  1. 找NT頭的起始地址
    首先找NT頭,也就是PE\0\0,對應十六進制爲0x00004550

  2. 確定文件位數
    然後找NT頭中的可選頭,在距NT頭偏移地址爲25字節的位置處,00000080h+18h=00000098h,這兩個字節是NT可選頭的Magic,代表着這個文件的類型,如果文件是32位,則爲010B,若文件是64位,則爲020B,之所以要判斷文件的類型,是因爲32位和64位,是因爲這兩種類型對應的可選頭格式不同,從而數據目錄表在NT頭中偏移地址不同,如下圖所示,010B,對應爲32位文件

  3. 確定數據目錄表起始和終止地址
    根據上一步,判斷出文件爲32位,那麼數據目錄表的起始地址相對於NT頭的偏移地址爲78h,從而可確定數據目錄表起始於00000080h+78h=000000F8h
    而數據目錄表爲十六個元素的結構體數組,每個結構體由分別表示RVA和大小的兩個雙字構成。從而,數據目錄表的大小爲16x8=128字節。終止地址爲000000f8h+80h-1=00000177h
    當然,終止地址還有一種確定方法。前面在可選頭的定位技巧中說過,可選頭的後面通常是第一項節表的名稱,比如此處是.text。而可選頭的終止地址就是數據目錄表的終止地址。

  4. 使用工具
    根據前面三個步驟,已經可以將每個字段對應的值確定下來了,不過,使用工具更加直觀。
    使用LordPE,將.exe文件拖入LordPE中,會彈出如下圖所示的窗口:

    然後,點擊“目錄”,即可打開數據目錄表,如下圖所示:

參考教程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章