天天看點

PE檔案自學筆記(一):DOS頭與PE頭解析

Windows下所謂PE檔案即Portable Executable,意為可移植的可執行的檔案。常見的.EXE、.DLL、.OCX、.SYS、.COM都是PE檔案。PE檔案有一個共同特點:前兩個位元組為4D 5A(MZ)。如果一個檔案前兩個位元組不是4D 5A則其肯定不是可執行檔案。比如用16進制文本編輯器打開一個“.xls”檔案其前兩個位元組為:0XD0 0XCF;打開一個“.pdf”其前兩個位元組為:0X25 0X50。

PE檔案結構:DOS頭+PE頭+節表+.data/.rdata/.text。而今天我們就來具體了解一下PE檔案的DOS頭和PE頭的結構成員與部分成員的作用。注意:一個exe檔案本身是一個PE檔案,但是由于包含dll庫,是以一個exe檔案也是許多PE檔案組成的(包含多個dll)一個PE檔案。

本文中所用到的執行個體程式write.exe存放路徑位于本機的C:\Windows目錄下,是利用WinHex編譯器來打開的。

1、DOS頭:共40H(64位元組)

DOS頭中聲明用的寄存器(我們可以看到e_ss、e_sp、e_ip、e_cs還是16位的寄存器),是以在32位/64為系統中用到的隻有兩個成員了(第一個和最後一個),也是我們務必要記住的: 

①e_magic:判斷一個檔案是不是PE檔案; 

②e_lfanew:相對于檔案首的偏移量,用于找到PE頭;

具體結構如下(前面的十六進制數表示該成員相對于結構的偏移量,WORD2位元組變量、DWORD4位元組變量):

//注釋掉的不需要重點分析
struct _IMAGE_DOS_HEADER{
    0X00 WORD e_magic;      //※Magic DOS signature MZ(4Dh 5Ah):MZ标記:用于标記是否是可執行檔案
    //0X02 WORD e_cblp;     //Bytes on last page of file
    //0X04 WORD e_cp;       //Pages in file
    //0X06 WORD e_crlc;     //Relocations
    //0X08 WORD e_cparhdr;  //Size of header in paragraphs
    //0X0A WORD e_minalloc; //Minimun extra paragraphs needs
    //0X0C WORD e_maxalloc; //Maximun extra paragraphs needs
    //0X0E WORD e_ss;       //intial(relative)SS value
    //0X10 WORD e_sp;       //intial SP value
    //0X12 WORD e_csum;     //Checksum
    //0X14 WORD e_ip;       //intial IP value
    //0X16 WORD e_cs;       //intial(relative)CS value
    //0X18 WORD e_lfarlc;   //File Address of relocation table
    //0X1A WORD e_ovno;     //Overlay number
    //0x1C WORD e_res[4];   //Reserved words
    //0x24 WORD e_oemid;    //OEM identifier(for e_oeminfo)
    //0x26 WORD e_oeminfo;  //OEM information;e_oemid specific
    //0x28 WORD e_res2[10]; //Reserved words
    0x3C DWORD e_lfanew;    //※Offset to start of PE header:定位PE檔案,PE頭相對于檔案的偏移量
};
           

我們檢視下面所示write.exe檔案的結構資訊: 

PE檔案自學筆記(一):DOS頭與PE頭解析

64位元組(共4行)的DOS頭,第一個成員2個位元組是可執行檔案的标志資訊;最後一個成員4位元組是PE頭的偏移位址為000000E0H,我們可以根據000000E0H來擷取PE頭的位址。而DOS頭和PE頭中間的空餘位置是一些垃圾值以及編譯器填充的一些“is program cannot be run in DOS mode.”或“This program must be run under Win32”等資訊。

2、PE頭:

PE頭分為标準PE頭和可選PE頭,其同為NT結構的成員:

//NT頭
//pNTHeader = dosHeader + dosHeader->e_lfanew;
struct _IMAGE_NT_HEADERS{
    0x00 DWORD Signature;   //PE檔案辨別:ASCII的"PE\0\0"
    0x04 _IMAGE_FILE_HEADER FileHeader;
    0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
};
           

根據DOS頭的e_lfanew成員我們就可以找到NT頭,NT頭的第一個成員是”PE\0\0”(0X50 0X45 0X00 0X00四位元組的簽名,可以在上圖00000100H位址處觀察),後兩個成員則分别是:

标準PE頭(_IMAGE_FILE_HEADER)從第四個位元組開始

可選PE頭(_IMAGE_OPTIONAL_HEADER)從從第二十四個位元組開始

(1)、_IMAGE_FILE_HEADER:

//标準PE頭:最基礎的檔案資訊,共20位元組
struct _IMAGE_FILE_HEADER{
    0x00 WORD Machine;                  //※程式執行的CPU平台:0X0:任何平台,0X14C:intel i386及後續處理器
    0x02 WORD NumberOfSections;         //※PE檔案中區塊數量
    0x04 DWORD TimeDateStamp;           //時間戳:連接配接器産生此檔案的時間距1969/12/31-16:00P:00的總秒數
    //0x08 DWORD PointerToSymbolTable;  //COFF符号表格的偏移位置。此字段隻對COFF除錯資訊有用
    //0x0c DWORD NumberOfSymbols;       //COFF符号表格中的符号個數。該值和上一個值在release版本的程式裡為0
    //0x10 WORD SizeOfOptionalHeader;   //IMAGE_OPTIONAL_HEADER結構的大小(位元組數):32位預設E0H,64位預設F0H(可修改)
    0x12 WORD Characteristics;          //※描述檔案屬性,eg:
                                        //單屬性(隻有1bit為1):#define IMAGE_FILE_DLL 0x2000  //File is a DLL.
                                        //組合屬性(多個bit為1,單屬性或運算):0X010F 可執行檔案
};
           

我們依舊來看wirte.exe的檔案資訊: 

PE檔案自學筆記(一):DOS頭與PE頭解析

首先四位元組是NT第一個簽名成員“PE\0\0”。接着便是2位元組的CPU平台資訊:08664,即X64平台(其它平台可見下圖): 

PE檔案自學筆記(一):DOS頭與PE頭解析

然後是區塊數量0005H即5個塊;可以這樣了解,PE格式檔案,是由各種區塊組成的,區塊就是相關資料的集合。比如,PE格式檔案,需外來要dll函數及資料資訊(在.idata區塊)、還需要一些資源資訊(什麼圖示、菜單、位圖,在.rsrc區塊)等等

第三個成員為時間戳:4A5BC9ACH 十六進制轉換成十進制就是2898877258 秒   可以通過時間戳線上轉換中原標準時間 。  

第四個和第五個成員各占4位元組且均為0;

第六個成員占2位元組為預設值00F0H(即可選PE頭的大小為224位元組,other.exe中為X64預設值F0H);最後一個屬性成員占2位元組為“0022H”,該成員是按bit位來看的,屬性值也是多個屬性的組合(或運算)。每一位的資訊如下: 

PE檔案自學筆記(一):DOS頭與PE頭解析

0022  即0000|0000|0020|0002   (按bit來算的) 即:

檔案可執行以及高位址警告

(2)、_IMAGE_OPTIONAL_HEADER: 

可選PE頭緊接着标準PE頭,其大小在标準PE頭中給出:大小為F0H即256位元組(15*16=256)。_IMAGE_OPTIONAL_HEADER結構如下所示:

//可選PE頭
struct _IMAGE_OPTIONAL_HEADER{
    0x00 WORD Magic;                    //※幻數(魔數),0x0107:ROM image,0x010B:32位PE,0X020B:64位PE 
    //0x02 BYTE MajorLinkerVersion;     //連接配接器主版本号
    //0x03 BYTE MinorLinkerVersion;     //連接配接器副版本号
    0x04 DWORD SizeOfCode;              //所有代碼段的總和大小,注意:必須是FileAlignment的整數倍,存在但沒用
    0x08 DWORD SizeOfInitializedData;   //已經初始化資料的大小,注意:必須是FileAlignment的整數倍,存在但沒用
    0x0c DWORD SizeOfUninitializedData; //未經初始化資料的大小,注意:必須是FileAlignment的整數倍,存在但沒用
    0x10 DWORD AddressOfEntryPoint;     //※程式入口位址OEP,這是一個RVA(Relative Virtual Address),通常會落在.textsection,此字段對于DLLs/EXEs都适用。
    0x14 DWORD BaseOfCode;              //代碼段起始位址(代碼基址),(代碼的開始和程式無必然聯系)
    0x18 DWORD BaseOfData;              //資料段起始位址(資料基址)
    0x1c DWORD ImageBase;               //※記憶體鏡像基址(預設裝入起始位址),預設為4000H
    0x20 DWORD SectionAlignment;        //※記憶體對齊:一旦映像到記憶體中,每一個section保證從一個「此值之倍數」的虛拟位址開始
    0x24 DWORD FileAlignment;           //※檔案對齊:最初是200H,現在是1000H
    //0x28 WORD MajorOperatingSystemVersion;    //所需作業系統主版本号
    //0x2a WORD MinorOperatingSystemVersion;    //所需作業系統副版本号
    //0x2c WORD MajorImageVersion;              //自定義主版本号,使用連接配接器的參數設定,eg:LINK /VERSION:2.0 myobj.obj
    //0x2e WORD MinorImageVersion;              //自定義副版本号,使用連接配接器的參數設定
    //0x30 WORD MajorSubsystemVersion;          //所需子系統主版本号,典型數值4.0(Windows 4.0/即Windows 95)
    //0x32 WORD MinorSubsystemVersion;          //所需子系統副版本号
    //0x34 DWORD Win32VersionValue;             //總是0
    0x38 DWORD SizeOfImage;         //※PE檔案在記憶體中映像總大小,sizeof(ImageBuffer),SectionAlignment的倍數
    0x3c DWORD SizeOfHeaders;       //※DOS頭(64B)+PE标記(4B)+标準PE頭(20B)+可選PE頭+節表的總大小,按照檔案對齊(FileAlignment的倍數)
    0x40 DWORD CheckSum;            //PE檔案CRC校驗和,判斷檔案是否被修改
    //0x44 WORD Subsystem;          //使用者界面使用的子系統類型
    //0x46 WORD DllCharacteristics;   //總是0
    0x48 DWORD SizeOfStackReserve;  //預設線程初始化棧的保留大小
    0x4c DWORD SizeOfStackCommit;   //初始化時實際送出的線程棧大小
    0x50 DWORD SizeOfHeapReserve;   //預設保留給初始化的process heap的虛拟記憶體大小
    0x54 DWORD SizeOfHeapCommit;    //初始化時實際送出的process heap大小
    //0x58 DWORD LoaderFlags;       //總是0
    0x5c DWORD NumberOfRvaAndSizes; //目錄項數目:總為0X00000010H(16)
    0x60 _IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
};
           

繼續接着标準PE頭來分析: 

PE檔案自學筆記(一):DOS頭與PE頭解析

    前三十個成員60H,最後一個成員是一個結構體數組144位元組(16*9)

第1個成員(Magic,2Byte):幻數020B,表示該檔案為32位PE,其它情況如下: 

PE檔案自學筆記(一):DOS頭與PE頭解析

第4個成員(SizeOfCode,4Byte):代碼段總大小為0001000 H; 

第5個成員(SizeOfInitializedData,4Byte):已初始化資料大小為00001A00H; 

第6個成員(SizeOfUninitializedData,4Byte):未初始化資料大小為0,即均已初始化; 

第7個成員(AddressOfEntryPoint,4Byte):程式入口位址OEP=000015A4H; 

第8個成員(BaseOfCode,4Byte):代碼段基址 = 00001000H; 

第9個成員(BaseOfData,4Byte):資料段基址 = 00000000H; 

第10個成員(ImageBase,4Byte):記憶體鏡像基址 = 00000001H,預設的值為4000H; 

第11個成員(SectionAlignment,4Byte):記憶體對齊 = 00001000H,即4096位元組; 

第12個成員(FileAlignment,4Byte):檔案對齊 = 00000200H,即512位元組(檔案對齊和記憶體對齊的目的是提高效率); 

第20個成員(SizeOfImage,4位元組):PE映像在記憶體中總大小 = 00006000H,是SectionAlignment的6倍(整數倍); 

第21個成員(SizeOfHeaders,4位元組):所有頭+節表總大小 = 00000400H; 

第22個成員(CheckSum,4位元組):PE檔案CRC校驗和 = 0000883FH 

第25個成員(SizeOfStackReserve,4位元組):為線程初始棧保留虛拟記憶體的預設值 = 00080000H = )_8x2^16個Byte; 

第26個成員(SizeOfStackCommit,4位元組):為線程的初始棧送出的實際虛拟記憶體大小 = 00000000H = 0KB; 

第27個成員(SizeOfHeapReserve,4位元組):為程序的初始堆保留虛拟記憶體的預設值 = 00002000H = 2x2^12個Byte; 

第28個成員(SizeOfHeapCommit,4位元組):為程序的初始堆送出的實際虛拟記憶體大小 = 00000000H = 0KB; 

第30個成員(NumberOfRvaAndSizes,4位元組):目錄項總數預設 = 00000000H = 0個;

最後一個成員為_IMAGE_DATA_DIRECTORY的結構體數組(本次隻分析DOS頭和PE頭基本成員,該結構體數組以後重點分析),該結構體成員如下:

struct _IMAGE_DATA_DIRECTORY

{ 
   DWORD VirtualAddress; 

   DWORD Size; 

}; 

//占用16*9 = 144Byte = 90H = F0H(可選PE頭預設大小) - 60H(前面所有成員固定占用大小)
           

3、幾個重點的資料成員分析:

(1)、檔案對齊(FileAlignment)和記憶體對齊(SectionAlignment): 

一個PE檔案加載進記憶體中可能大于在硬碟上的大小,并且無論是在記憶體中還是硬碟上,都是是分塊管理(分節),一塊和一塊存儲空間之間是空隙。在硬碟上空隙有可能小于記憶體中空隙;在記憶體中空隙較大(相較于硬碟)。而存在間隙的原因則是分塊管理。 

分塊的一個原因是節省硬碟:比如notepad.exe,由于是早期的程式,當時硬碟容量比較小,編譯器在生成可執行檔案時,不僅要考慮效率問題使得記憶體對齊/檔案對齊,還需要設計成節省硬碟空間的結構。是以這種結構遵循的對齊原則:記憶體對齊(1000H)和硬碟對齊(200H),對齊的補充資料(0X0000)便是間隙。硬碟的對齊值較小,補充間隙自然小,是以同一個可執行程式在記憶體中可能比在硬碟上大。但是現如今的硬碟空間更大,是以編譯器生成的可執行程式在硬碟上與記憶體中對齊方式都是1000H。統一對齊為1000H的目的依舊是提高效率。 

而分塊的另一個目的是節省記憶體空間,比如同時在電腦上運作登入多個QQ賬号,就需要運作多次QQ可執行程式。而代碼段為隻讀資料需要一份即可,資料段則需要為每個賬号均開辟一份,,多個QQ程式共享代碼塊,單獨使用資料塊,這樣就節省了多份代碼塊的記憶體。(這些塊是使用結構體來維護的,分塊即建立結構體)。

(2)、鏡像位址/基址ImageBase的作用: 

FileBuffer是磁盤上.exe檔案在記憶體中的一份拷貝,但是FileBuffer無法直接在記憶體中運作,必須經過PE loader(裝載器)裝載以後成為ImageBuffer。ImageBuffer是FileBuffer的”拉伸”。即”.exe–>FileBuffer–>ImageBuffer”

①.exe首位址(基址)為0 

②FileBuffer首位址也為0 

③ImageBuffer首位址為ImageBase 

④而真正的程式入口位址是:ImageBase + AddressOfEntryPoint(OEP)

一個exe檔案預設鏡像位址為400000H(有可能不是,總之有一個預設值),如果一個exe檔案中用到了多個dll,而dll檔案作為一個PE檔案,其預設鏡像位址也均是400000H,作業系統不會修改exe的鏡像基址。因為.exe先被加載,在.exe中才加載的dll庫,由于400000已經被.exe占用,是以裝載器會修改dll的鏡像基址。而采用ImageBase + OEP的目的也就是:采用偏移位址的方式可以更友善地修改基址,使得任何一個dll檔案基址修改後程式依舊不會出錯。比如:dll和exe基址有沖突,本隻需要将沖突的.dll的檔案基址修改為600000H(假設是編譯器為其配置設定的是600000H);如果不采用“基址+偏移位址”的方式,而采用絕對位址,那麼要修改的就不是一個基址為600000H了,而是dll中所有的位址統一加上200000H(因為原來預設為400000H)。