PE格式詳細講解

PE是英文Portable Executable(可移植的執行體)的縮寫,從縮寫可以看出它是跨平臺的,即使在非intel的CPU上也能正常運行的。它是 Win32環境自身所帶的執行體文件格式。其實不光是EXE文件是PE格式,其它的一些重要文件,例如動態鏈接庫文件(DLL),驅動文件(SYS)等也是PE格式的,所以學好PE格式是非常重要的,以下我把這類文件統稱爲PE文件。學習PE文件結構不僅可以使我們知道可執行文件是怎樣運行的,也可以使我們瞭解一下windows操作系統的一些工作機制,精通PE是成爲計算機高手的必經之路。
    其實說白了PE文件格式就是一種文件組織的方式,裏面對一些重要信息的存放做了一些規定,比如文件要運行,我們就得先知道入口地址,可是我們從哪去得到入口地址呢,我們必須把保存有入口地址信息的有關數據放在固定的位置,這樣不管是哪個文件我們就能從那個固定位置取得入口地址,而這固定位置就是PE標準所規定的。現在我們就來正式學習這套標準,我覺得從整體到部分是個很好的學習方法,先從整體把握全局,然後再重點各個擊破,逐漸深入,下面我就以這個思路來介紹PE文件格式,方便大家快速掌握。它總體上由五大部分組成:
1.DOS MZ header(DOS頭)
2.DOS stub
3.PE header(PE頭)
4.section table(節表)
5.section(各個節)
所有的PE文件必須以一個DOS MZ header開始,其實它是一個IMAGE_DOS_HEADER類型的結構,這個結構的定義我們可以在WINNT.H頭文件找到,此結構中有兩個重要的成員是我們必須知道的,第一個e_magic,它是一個DWORD類型的變量,這個變量只有一個用處,就是當我們要判斷一個文件是不是PE文件時,我們首先需要把這個變量的值與IMAGE_DOS_SIGNATURE比較,相等就完成了判斷的第一步,當然後面我們還得判斷一個標誌,不等的話就說明當前文件不是PE文件。第二個成員就是非常重要的了,可以說只要它一出錯我想這個文件就壞了,它就是e_lfanew,這是一個LONG類型的變量,裏面存放了PE頭在這個文件中的偏移量,它是定位PE頭的關鍵數據,知道這兩個成員的意義後DOS頭學習基本就完成了。接下來的DOS stub實際上是個EXE,噹噹前系統不支持PE文件結構時它能輸出一個錯誤提示“This program requires Windows”,不是很重要。接下來是重頭戲PE頭,PE頭是一個IMAGE_NT_HEADERS類型的結構,下面是這個結構在WINNT.H中的定義:
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
它有三個數據成員,Signature是一個標誌變量,這個就是當我們判斷一個文件是否是PE文件時第二步需要判斷的,若這個值等於"PE\0\0"時就是一個PE文件,當然我們也可以像上面一樣直接與IMAGE_NT_SIGNATURE這個常量比較,其實是等效的,因爲IMAGE_NT_SIGNATURE是一個宏定義,它的實際值就是"PE\0\0"。第二個成員是一個IMAGE_FILE_HEADER類型的對象,通常我們叫它文件頭,這個結構在頭文件中的定義是:
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可以看出它有7個成員,下面我就對其中比較重要的成員做一說明,NumberOfSections這個成員保存了節的數目,至於節是什麼東東,後面再說,在這隻要知道它的數目保存在文件頭的NumberOfSections中就可以了,當我們要遍歷節時需要訪問這個對象。PointerToSymbolTable,NumberOfSymbols這兩個成員在調試時用的着,這裏不做過多說明。SizeOfOptionalHeader這個成員保存了PE頭中OptionalHeader這個成員的大小,最後一個成員是一個關於文件的標記,即這個文件是EXE還是DLL文件。總的來說文件頭中包含了PE文件的物理分佈的有關信息,比較重要的就是第二個成員了。接下來我們來學習PE頭的第三個成員OptionalHeader,這是PE文件結構中最重要的一個部分,因爲大部分重要的數據結構都得通過它去定位,可以說它保存了PE文件邏輯分佈的信息,但是這部分也是學習PE結構過程中的一個難點,因爲層次結構比較多,我們定位一個結構需要抽絲剝繭般一層層進行下去,在此過程還得保持頭腦清醒,對變量類型還得有比較深層次的理解。好了,我們還是那個原則,開始不要先涉入過深,先對這個成員有個總體的印象,它是一個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;
從這個結構的長度就可看出,它保存的信息不少,肯定很重要,在正式學習這個結構的成員前,我們還要理解一個重要的術語--RVA,RVA 代表相對虛擬地址。它是一個DWORD類型的數據,那RVA到底是什麼呢?簡單的說它是一個偏移量,這個偏移量是虛擬空間中相對於參考點的偏移,它的值就是偏移的大小,值得一說的是這個值只有當PE文件被PE文件裝載器載入內存時纔有作用,否則的話我們不能用這個值去直接定位與它相關的數據結構。如果在PE文件沒有被PE裝載器載入內存,而我們又想在PE文件中直接定位相關數據該怎麼辦呢,我們需要把這個值轉換成文件偏移量,也就是相對於文件開頭的偏移量,我們可以用RVAToOffset這個函數去完成地址的相關轉換,至於這個函數的實現可以從網上找到。好了,準備工作做好了,下面我們就進入正題吧。由於這個結構成員較多,有些也不是很常用,還有有些我也不是很明白,所以就不說了,下面我就對一些比較常用的一一進行介紹吧。還希望大家不要像我一樣一知半解的,要有寧可錯殺一千,不可放過一個的學習精神。

AddressOfEntryPoint: 
   PE裝載器準備運行的PE文件的第一個指令的RVA。若您要改變整個執行的流程,可以將該值指定到新的RVA,這樣新RVA處的指令首先被執行。ImageBase PE文件的優先裝載地址。比如,如果該值是400000h,PE裝載器將嘗試把文件裝到虛擬地址空間的400000h處。字眼"優先"表示若該地址區域已被其他模塊佔用,那PE裝載器會選用其他空閒地址。 

SectionAlignment:
   內存中節對齊的粒度。例如,如果該值是4096 (1000h),那麼每節的起始地址必須是4096的倍數。若第一節從401000h開始且大小是10個字節,則下一節必定從402000h開始,即使401000h和402000h之間還有很多空間沒被使用。 

FileAlignment:
    文件中節對齊的粒度。例如,如果該值是(200h),,那麼每節的起始地址必須是512的倍數。若第一節從文件偏移量200h開始且大小是10個字節,則下一節必定位於偏移量400h: 即使偏移量512和1024之間還有很多空間沒被使用/定義。
 
MajorSubsystemVersion
MinorSubsystemVersion :
    win32子系統版本。若PE文件是專門爲Win32設計的,該子系統版本必定是4.0否則對話框不會有3維立體感。 

SizeOfImage:
   內存中整個PE映像體的尺寸。它是所有頭和節經過節對齊處理後的大小。 
 
SizeOfHeaders:
   所有頭+節表的大小,也就等於文件尺寸減去文件中所有節的尺寸。可以以此值作爲PE文件第一節的文件偏移量。 

Subsystem:
    NT用來識別PE文件屬於哪個子系統。 對於大多數Win32程序,只有兩類值: Windows GUI 和 Windows CUI (控制檯)。 

DataDirectory:
    一IMAGE_DATA_DIRECTORY 結構數組。每個結構給出一個重要數據結構的RVA,比如引入地址表等。 可以告訴大家這個成員是PE結構的重中之重了,在這先給大家提個醒,讓大家多看它兩眼,加深對它的印象。

    好了,到此爲止,這個PE文件結構在總體劃分上就只剩下節表和節沒有介紹了,現在我就來說說節表和節吧,在正式介紹它之前,我先說一點它話,當然絕對是有助於你理解節表的,書大家都看過是吧,現在你想一想我們拿到一本新書時,做的第一件事是什麼呢?首先我們翻開的是書的目錄,然後從書的目錄中去搜尋我們感興趣的東西。可以打個形象的比喻,節表就相當於書的目錄,而書中的各個章節就相當於PE文件結構中的節,通過目錄我們能很快找到書中我們感興趣的內容,同樣通過節表我們很快能找到PE文件中的各個節。好了,有了定性的認識後我們還得繼續昇華,還得定量的學習它,這纔是科學的學習方法。節表從數據結構的角度來說它是一個結構數組,所謂結構數組就是這個數組的每個成員都是同一種結構類型的變量。它們不光在邏輯上是連續的,在存儲介質中也是連續的。同樣我們也可在WINNT.H中找到這個結構的定義:
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
這個結構總共包含了10個成員變量,由於有些並不是很重要,所以我就對其中比較重要的做個介紹,如果你是那種追根究底的人,你自己完全可以繼續查閱相關資料,瞭解一下其它成員的實際意義,不過要在你對我介紹的都掌握之後,學習把握主次是很重的,捨本逐末只會撿了芝麻丟了西瓜。好了,現在開始介紹吧。

Name[IMAGE_SIZEOF_SHORT_NAME]:
     這個成員是一個字節型的數組,很明顯它保存的是一個名字,我想你能猜到了吧,這就是節的名字,不過要注意一下,這個數組的上限是8,最多隻能保存8個字符,還有就是它不是一個ASCIIZ字符串,因爲它不是以null結尾的。

VirtualAddress:
      本節的RVA(相對虛擬地址)。PE裝載器將節映射至內存時會讀取本值,因此如果域值是1000h,而PE文件裝在地址400000h處,那麼本節就被載到401000h。
這個很重要,定位節時需要它。

SizeOfRawData:
     經過文件對齊處理後節尺寸,PE裝載器提取本域值瞭解需映射入內存的節字節數。(譯者注: 假設一個文件的文件對齊尺寸是0x200,如果前面的 VirtualSize域指示本節長度是0x388字節,則本域值爲0x400,表示本節是0x400字節長)。 

PointerToRawData:
     這是節基於文件的偏移量,PE裝載器通過本域值找到節數據在文件中的位置。 

Characteristics:
     包含標記以指示節屬性,比如節是否含有可執行代碼、初始化數據、未初始數據,是否可寫、可讀等。

基本上比較重要的成員都介紹了,我想你看了這麼久肯定有點迷糊了,介紹了這麼久的節表,那到底什麼是節呢?還專門爲它建立一個目錄好像很重要似的。確實,可以說節纔是PE內容的真正載體, 節其實我們可以想象成塊,這樣更形象點,因爲一節就是一塊數據,而且這塊數據擁有共同的屬性,比如是代碼還是數據,是隻讀的還是讀寫的。學習節要牢牢記住幾個字---共同的屬性。就是說多個數據只要是具有共同屬性我們就能把它放在同一節中,而不用去考慮這些數據是不是在邏輯上有什麼關聯。真正以數據之間邏輯關聯建立的目錄是PE頭的第三個成員OptionalHeader,我們已經稍微瞭解了下了。
   好了,對PE文件結構的總體構造已經介紹完了,但學習PE文件結構的任務還遠遠沒有完成,因爲還有導入表,象DLL還有導出表,還有到底怎麼去定位PE中的重要結構等都沒有介紹。我只能對你說,不要着急,一口喫不成胖子,學習決非一日之寒,尤其是計算機的學習,它是一個系統的認知過程,知識有很強的連貫性。所以對於決心學好的計算機的人,我有一句良言相贈----找準目標,持之以恆,絕不放棄。上面介紹的內容我參考了《軟件加密技術內幕》這本書,由於我也是第一次真正接觸PE,所以有些概念理解的不是很準確,希望大家多多包含,有時間大家可以看看原版。當然這只是這個PE教程的第一部分,如果大家不拿雞蛋砸我的話,我還會寫出後續部分。我的知識來自網絡,所以我也要回饋網絡,雖然現在水平還不是很高,但盡力了就行了。
    附上PE信息查看器的下載地址,可以用這個小工具輔助學習,效果更好。
http://bbs.pediy.com/showthread.php?t=97970

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