1. 程式人生 > >PE檔案格式學習(二):總體結構

PE檔案格式學習(二):總體結構

1.概述

PE檔案分為幾個部分,分別是:

  • DOS頭
  • DOS Stub
  • NT頭(PE頭)
    • 檔案頭
    • 可選頭
  • 區段頭(一個數組,每個元素都是一個結構體,稱之為IMAGE_SECTION_HEADER)
  • .text
  • .rdata
  • .data
  • .rsrc
  • .reloc
  • ...

本篇文章主要圍繞整體結構做一個梳理,不會做太多細節的講解。(比如說可選頭裡的資料目錄表,它包含16種表,會在接下來的系列文章分別進行講解,熟悉了資料目錄表就可以說熟悉了PE)

2.DOS頭和DOS Stub

DOS頭和DOS Stub在現在的Windows系統上基本都是擺設(對於軟體安全還是有一定價值的,PE的變形、壓縮、植入都可能利用Stub部分,以後有機會我會寫一個系列專門講解PE檔案操作相關的應用技術),它存在的原因是為了向下相容、適配DOS系統,對於DOS頭和它的Stub部分就只要稍微瞭解一些主要結構體欄位就可以了。

PE檔案開頭的兩個位元組,在上篇文章介紹過,是字元“MZ”,也就是0x5A4D,它就是DOS頭的開端,從這裡開始的64個位元組大小都是DOS頭的範圍。

DOS頭的結構體是:

struct _IMAGE_DOS_HEADER{
    0X00 WORD magic;
    0X02 WORD e_cblp; 
    0X04 WORD e_cp;    
    0X06 WORD e_crlc;   
    0X08 WORD e_cparhdr;  
    0X0A WORD e_minalloc; 
    0X0C WORD e_maxalloc; 
    0X0E WORD e_ss;
    0X10 WORD e_sp;       
    0X12 WORD e_csum;     
    0X14 WORD e_ip;      
    0X16 WORD e_cs;       
    0X18 WORD e_lfarlc;   
    0X1A WORD e_ovno;     
    0x1C WORD e_res[4];  
    0x24 WORD e_oemid;   
    0x26 WORD e_oeminfo;  
    0x28 WORD e_res2[10]; 
    0x3C DWORD e_lfanew;
};

DOS頭的結構體欄位雖然很多,但是絕大部分現在都可以不用瞭解(除非你想做計算機歷史學家,^_^)。對於不重要的欄位就不解釋了,下面出現的結構體也只會摘出重要的欄位做出解釋。

magic:它幾乎永遠是5A4D,也就是字元“MZ”。

e_lfanew:指向PE頭的檔案偏移。下圖中e_lfanew的值是0x100,它也幾乎總是這個值。PE頭就在地址0x100處開始,它通常是0x00004550,也就是PE\0\0。

下圖顯示了一些資訊,其中0x3c處就是e_lfanew!在十六進位制工具中明明顯示的是00010000,也就是0x10000啊!怎麼會是0x100呢?這就需要了解一些小端排序的知識了。

儲存在Windows系統中的資料基本上都是小端排序,資料讀取的順序不再是從左往右,而是從高地址往低地址,一個位元組一個位元組的讀。上圖中的e_flanew從高地址到低地址依次是:00 00 01 00,因此它其實是0x100。小端排序是一種CPU遵循的標準或者說格式,為了使CPU能夠正確讀資料,系統中的大部分程式都被編譯成了小端排序(Java這樣的基於虛擬機器編寫的程式除外)。

DOS頭的大小是64個位元組也就是0x40,從0x40開始到e_flanew指向的0x100為止的部分就是DOS Stub了。

DOS Stub是DOS系統的可執行檔案,在DOS系統中會執行裡面的程式碼,只需要將一個包含完整DOS頭和DOS Stub的Win32程式放到DOS系統中執行就可以執行裡面的程式碼了。

DOS頭和DOS Stub就介紹到這裡吧。

3.NT頭

上面說道e_lfanew指向的檔案偏移是0x100,它指向的位置的值是0x00004550,也幾乎總是這個值,它轉成ASCII碼就是PE\0\0。這裡就是NT頭的開始。下面介紹一下NT頭的結構體。

struct _IMAGE_NT_HEADERS{
    0x00 DWORD Signature;
    0x04 _IMAGE_FILE_HEADER FileHeader;
    0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
};

Signatue:也就是“PE\0\0”,佔4個位元組。
FileHeader:檔案頭,本身也是一個結構體,後面會說
OptionalHeader:可選頭,本身也是一個結構體,後面會說

3.1 檔案頭

檔案頭佔20個位元組,檔案頭也是一個結構體。

struct _IMAGE_FILE_HEADER{
    0x00 WORD Machine;
    0x02 WORD NumberOfSections;
    0x04 DWORD TimeDateStamp; 
    0x08 DWORD PointerToSymbolTable; 
    0x0c DWORD NumberOfSymbols;  
    0x10 WORD SizeOfOptionalHeader;   
    0x12 WORD Characteristics;
};

Machine:目標平臺,對照下面的表

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#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
#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_CEE               0xC0EE

NumberOfSections:區段表數量

SizeOfOptionalHeader:可選頭的大小(位元組數):32位預設E0H,64位預設F0H(可修改)

Characteristics:檔案型別,對照下面的表

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel 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  // Agressively 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.

3.2 可選頭

介紹完NT頭的Signature和檔案頭還有最後一個欄位:可選頭。雖然它叫可選頭,但是它非常的重要,叫“必選頭”才對!可選頭包含了非常多的資訊,它在32位系統中的大小是E0h(224個位元組),64位系統中的大小是F0h(240個位元組)。

它的結構體:

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;

Magic:0x10B是32位PE,0x20B是64位PE,0x107是ROM映像

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b  // 32位PE可選頭
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b  // 64位PE可選頭
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107  // ROM映像

AddressOfEntryPoint://程式入口的RVA值,程式最先執行程式碼的地址

BaseOfImage:PE檔案的裝載地址

SizeOfImage:記憶體中整個PE映像體的尺寸

SectionAlignment:區段在記憶體中的對齊值,一般為0x1000

FileAlignment:區段在檔案中的對齊值,一般為0x200

SizeOfHeaders:所有頭+節表的大小,即整個PE頭的大小

DllCharacteristics:DLL檔案的屬性值,DLL檔案才會生效,參見下面的表

#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040     // DLL can move.
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY    0x0080     // Code Integrity Image
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT    0x0100     // Image is NX compatible
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200     // Image understands isolation and doesn't want it
#define IMAGE_DLLCHARACTERISTICS_NO_SEH       0x0400     // Image does not use SEH.  No SE handler may reside in this image
#define IMAGE_DLLCHARACTERISTICS_NO_BIND      0x0800     // Do not bind this image.
//                                            0x1000     // Reserved.
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER   0x2000     // Driver uses WDM model
//                                            0x4000     // Reserved.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE     0x8000

NumberOfRvaAndSizes:資料目錄成員的數量,一般為0x10

DataDirectory:資料目錄表陣列,每個結構給出一個重要資料結構的RVA和size,參見下面的表

#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

資料目錄表的結構體是這樣的:

struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;
    DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

資料目錄表的解析後面的文章會逐一介紹,它是PE檔案格式的重中之重。

4.區段頭和區段

區段頭是一個數組,裡面的每個元素都是一個結構體,這個結構體叫做IMAGE_SECTION_HEADER,每個結構體的大小是40個位元組,如下:

struct _IMAGE_SECTION_HEADER {          
    BYTE Name[8];         
    union {                                     
        DWORD PhysicalAddress;              
                WORD VirtualSize;
    } Misc
    DWORD VirtualAddress;   
    DWORD SizeOfRawData;                        
    DWORD PointerToRawData;                     
    DWORD PointerToRelocations;                 
    DWORD PointerToLinenumbers;                 
    WORD NumberOfRelocations;                   
    WORD NumberOfLinenumbers;                   
    DWORD Characteristics;                                              
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 

Name:區段名

PhysicalAddress:實體地址,它的值一般跟VirtualSize相同,所以它有些不準確。

VirtualSize:真實大小,區段未做對齊處理之前的大小,跟PhysicalAddress同屬一個聯合體,可以使用任何一個,一般使用VirtualSize。

VirtualAddress:區段在記憶體中的RVA

SizeOfRawData:區段在磁碟中的大小

PointerToRawData:區段在檔案中的偏移

PointerToRelocations:在obj檔案中使用,指向重定位表的指標

PointerToLinenumbers:行號表的位置,供除錯使用,一般為0x0000

NumberOfRelocations:在obj檔案中使用,此區段重定位表項的數量

NumberOfLinenumbers:行號表中的行號數量

Characteristics:區段屬性,參見下表

IMAGE_SCN_CNT_CODE          0x00000020      該節區含程式碼
IMAGE_SCN_MEM_SHARED        0x10000000      該節區為可共享
IMAGE_SCN_MEM_EXECUTE       0x20000000      該節區為可執行
IMAGE_SCN_MEM_READ          0x40000000      該節區為可讀
IMAGE_SCN_MEM_WRITE         0x80000000      該節區為可寫

區段頭介紹完了,已經說明過區段頭是一個數組,檔案頭中有區段的數量,這個數量就是區段頭陣列的大小,緊接著區段頭後面的就是各種區段,常見的區段見下面的表:

 .text: 預設的程式碼區塊,內容都是指令程式碼 
 .data:預設的讀寫資料塊,全域性變數、靜態變數一般放在這裡。
 .rdata: 預設的只讀資料塊  一般很少用到。
 .idata:包含外來的DLL資料及資料資訊,也就是輸入表 ,通常情況下把他合併到.rdata中。
 .edata: 當建立一個用於輸出資料的可執行檔案時(輸出表),資料會放在這裡,通常情況下會被合併到.text 或.tdata中。
 .rsrs:資源塊 包含一切圖示選單等。

5.總結

本文對PE的結構有了一個總體的介紹,到此應該對PE的結構有了一個整體性的瞭解。PE檔案看似簡單,實際上使用的時候靈活度很大,對PE檔案的變形壓縮等操作的前提是要對PE每一個欄位、每一處結構有充分的瞭解,所以PE檔案中重要的欄位要非常的熟練。

檔案結構中有一些欄位需要記憶,以加深對PE的印象。

比如說,有大量對屬性、型別的定義:

  1. DOS頭中的magic欄位:4D5A,"MZ"
  2. 檔案頭中的 Machine:目標平臺的型別
  3. 檔案頭中的Characteristics:檔案的型別
  4. 可選頭中的Magic: 檔案的型別,重點分別32位還是64位
  5. 可選頭中的 DllCharacteristics:只有DLL檔案才有效,定義DLL的一些屬性
  6. 區段頭中的Characteristics: 區段的屬性

還有各個結構中的重點欄位:

  1. DOS頭中的magic、e_lfanew
  2. 檔案頭中的machine、characteristics、numberOfSections、sizeOfOptionalHeader
  3. 可選頭中的magic、DllCharacteristics、addressOfEntry、imageOfBase、sizeOfHeaders、sizeOfImage
  4. 區段頭中的VirtualSize、VirtualAddress、PointerToRawData、sizeOfRawData、numberOfRelocations、pointerToRelocations、numberOfLinenumbers、pointerToLinenumbers、name、Characteristics

下面的圖示明瞭PE檔案各部分的範圍:

上圖中,區段頭有五個成員,每一個長度都是28h,意味著這個檔案有5個區段。

區段頭的後面5個區段依次排列。每個區段的起始點在區段頭的pointerToRawdata中記錄,長度在sizeOfRawData中記錄。

再後面的內容儲存了很多表的內容以及雜項。這些表都在可選頭的資料目錄表裡記錄,起始地址就儲存在資料目錄表裡,下篇文章開始介紹資料目錄表裡的內容。