PE文件結構詳解

我們大家都知道,在Windows 9x、NT、2000下,所有的可執行文件都是基於Microsoft設計的一種新的文件格式Portable Executable File Format(可移植的執行體),即PE格式。有一些時候,我們需要對這些可執行文件進行修改,下面文字試圖詳細的描述PE文件的格式及對PE格式文件的 修改。

PE文件框架構成
DOS MZ header
DOS Stub
PE header
Section table
Section 1
Section 2
Section...
Section n

    上表是PE文件結構的總體層次分佈。所有 PE文件(甚至32位的 DLLs) 必須以一個簡單的 DOS MZ header開始,在偏移0處有DOS下可執行文件的“MZ標誌”,有了它,一旦程序在DOS下執行,DOS就能識別出這是有效的執行體,然後運行緊隨 MZ header之後的DOS Stub。緊接着DOS Stub的是PE header。PE header是PE相關結構IMAGE_NT_HEADERS的簡稱,其中包含了許多PE裝載器用到的重要域。可執行文件在支持PE文件結構的操作系統 中執行時,PE裝載器將從DOS MZ header的偏移3CH處找到PE header的起始偏移量。因而跳過了DOS Stub直接定位到真正的文件頭PE header。

    小知識:DOS Stub實際上是個有效的EXE,在不支持PE文件格式的操作系統中,它將簡單顯示一個錯誤提示,類似於字符串“This program cannot run in DOS mode”或者程序員可根據自己的意圖實現完整的DOS代碼。通常DOS Stub由彙編器/編譯器自動生成,對我們的用處不是很大,它簡單調用中斷21h服務9來顯示字符串“This program cannot run in DOS mode”。

    PE文件的真正內容劃分成塊,稱之爲Sections(節)。每節是一塊擁有共同屬性的數據,比如“.text”節等,那麼,每一節的內容都是什麼呢?實 際上PE格式的文件把具有相同屬性的內容放入同一個節中,而不必關心類似“.text”、“.data”的命名,其命名只是爲了便於識別,所有,我們如果 對PE格式的文件進行修改,理論上講可以寫入任何一個節內,並調整此節的屬性就可以了。

    PE header 接下來的數組結構Section table(節表)。每個結構包含對應節的屬性、文件偏移量、虛擬偏移量等。如果PE文件裏有5個節,那麼此結構數組內就有5個成員。

    以上就是PE文件格式的物理分佈,下面將總結一下裝載一PE文件的主要步驟:

1.PE文件被執行,PE裝載器檢查DOS MZ header裏的PE header偏移量。如果找到,則跳轉到PE header。 
2.PE裝載器檢查PE header的有效性。如果有效,就跳轉到PE header的尾部。 
3.緊跟 PE header的是節表。PE裝載器讀取其中的節信息,並採用文件映射方法將這些節映射到內存 ,同時附上節表裏指定的節屬性。 
4.PE文件映射入內存後,PE裝載器將處理PE文件中類似Import table(引入表)邏輯部分。

    PE文件頭定義

我們可以在Winnt.h這個文件中找到關於PE文件頭的定義:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature
//PE文件頭標誌 :“PE/0/0”。在開始DOS header的偏移3CH處所指向的地址開始
IMAGE_FILE_HEADER FileHeader;        //PE文件物理分佈的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader;    //PE文件邏輯分佈的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_FILE_HEADER {
WORD    Machine;            //該文件運行所需要的CPU,對於Intel平臺是14Ch
WORD    NumberOfSections;        //文件的節數目
DWORD   TimeDateStamp;        //文件創建日期和時間
DWORD   PointerToSymbolTable;    //用於調試
DWORD   NumberOfSymbols;        //符號表中符號個數
WORD    SizeOfOptionalHeader;    //OptionalHeader 結構大小
WORD    Characteristics;        //文件信息標記,區分文件是exe還是dll
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD    Magic;            //標誌字(總是010bh)
BYTE    MajorLinkerVersion;        //連接器版本號
BYTE    MinorLinkerVersion;        //
DWORD   SizeOfCode;            //代碼段大小
DWORD   SizeOfInitializedData;    //已初始化數據塊大小
DWORD   SizeOfUninitializedData;    //未初始化數據塊大小
DWORD   AddressOfEntryPoint;
     

PE裝載器準備運行的PE文件的第一個指令的RVA,若要改變整個執行的流程,可以將該值指定到新的RVA,這樣新RVA處的指令首先被執行(以往許多文章都有介紹RVA,請大家先了解)。

DWORD   BaseOfCode;            //代碼段起始RVA
DWORD   BaseOfData;            //數據段起始RVA
DWORD   ImageBase;            //PE文件的裝載地址
DWORD   SectionAlignment;        //塊對齊
DWORD   FileAlignment;        //文件塊對齊
WORD    MajorOperatingSystemVersion;//所需操作系統版本號
WORD    MinorOperatingSystemVersion;//
WORD    MajorImageVersion;        //用戶自定義版本號
WORD    MinorImageVersion;        //
WORD    MajorSubsystemVersion;    //win32子系統版本。若PE文件是專門爲Win32設計的
WORD    MinorSubsystemVersion;    //該子系統版本必定是4.0否則對話框不會有3維立體感
DWORD   Win32VersionValue;        //保留
DWORD   SizeOfImage;            //內存中整個PE映像體的尺寸
DWORD   SizeOfHeaders;        //所有頭+節表的大小
DWORD   CheckSum;            //校驗和
WORD    Subsystem;            //NT用來識別PE文件屬於哪個子系統
WORD    DllCharacteristics;        //
DWORD   SizeOfStackReserve;        //
DWORD   SizeOfStackCommit;        //
DWORD   SizeOfHeapReserve;        //
DWORD   SizeOfHeapCommit;        //
DWORD   LoaderFlags;            //
DWORD   NumberOfRvaAndSizes;    //
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 結構數組。每個結構給出一個重要數據結構的RVA,比如引入地址表等
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD   VirtualAddress;        //表的RVA地址
DWORD   Size;                //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PE文件頭後是節表,在winnt.h下如下定義
typedef struct _IMAGE_SECTION_HEADER {
BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];//節表名稱,如“.text”
union {
    DWORD   PhysicalAddress;    //物理地址            
    DWORD   VirtualSize;        //真實長度
} Misc;
DWORD   VirtualAddress;        //RVA
DWORD   SizeOfRawData;        //物理長度
DWORD   PointerToRawData;        //節基於文件的偏移量
DWORD   PointerToRelocations;    //重定位的偏移
DWORD   PointerToLinenumbers;    //行號表的偏移
WORD    NumberOfRelocations;    //重定位項數目
WORD    NumberOfLinenumbers;    //行號表的數目
DWORD   Characteristics;        //節屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

以上結構就是在Winnt.h中關於PE文件頭的定義,如何我們用C/C++來進行PE可執行文件操作,就要用到上面的所有結構,它詳細的描述了PE文件頭的結構。

修改PE可執行文件
    現在讓我們把一段代碼寫入任何一個PE格式的可執行文件,代碼如下:
-- test.asm --
.386p
.model flat, stdcall
option casemap:none

include /masm32/include/windows.inc
include /masm32/include/user32.inc
includelib /masm32/lib/user32.lib

.code

start:
    INVOKE MessageBoxA,0,0,0,MB_ICONINFORMATION or MB_OK
    ret
end start
以上代碼只顯示一個MessageBox框,編譯後得到二進制代碼如下:
unsigned char writeline[18]=;

好,現在讓我們看看該把這些代碼寫到那。現在用Tdump.exe顯示一個PE格式得可執行文件信息,可以發現如下描述:
Object table:
#   Name      VirtSize    RVA     PhysSize Phys off Flags   
-- -------- -------- -------- -------- -------- --------
01 .text     0000CCC0 00001000 0000CE00 00000600 60000020 [CER]
02 .data     00004628 0000E000 00002C00 0000D400 C0000040 [IRW]
03 .rsrc     000003C8 00013000 00000400 00010000 40000040 [IR]

Key to section flags:
C - contains code
E - executable
I - contains initialized data
R - readable
W - writeable

    上面描述此文件中存在3個段及每個段的信息,實際上我們的代碼可以寫入任何一個段,這裏我選擇“.text”段。用光盤中提供的代碼可以得到一個PE格式可執行文件的頭信息。

    由於在PE格式的文件中,所有的地址都使用RVA地址,所以一些函數調用和返回地址都要經過計算纔可以得到。

發佈了216 篇原創文章 · 獲贊 11 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章