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檔案的結構資訊:
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的檔案資訊:
首先四位元組是NT第一個簽名成員“PE\0\0”。接着便是2位元組的CPU平台資訊:08664,即X64平台(其它平台可見下圖):
然後是區塊數量0005H即5個塊;可以這樣了解,PE格式檔案,是由各種區塊組成的,區塊就是相關資料的集合。比如,PE格式檔案,需外來要dll函數及資料資訊(在.idata區塊)、還需要一些資源資訊(什麼圖示、菜單、位圖,在.rsrc區塊)等等
第三個成員為時間戳:4A5BC9ACH 十六進制轉換成十進制就是2898877258 秒 可以通過時間戳線上轉換中原標準時間 。
第四個和第五個成員各占4位元組且均為0;
第六個成員占2位元組為預設值00F0H(即可選PE頭的大小為224位元組,other.exe中為X64預設值F0H);最後一個屬性成員占2位元組為“0022H”,該成員是按bit位來看的,屬性值也是多個屬性的組合(或運算)。每一位的資訊如下:
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頭來分析:
前三十個成員60H,最後一個成員是一個結構體數組144位元組(16*9)
第1個成員(Magic,2Byte):幻數020B,表示該檔案為32位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)。