收藏.PE文件詳解

PE文件頭分兩大部分:
1:DOS ‘MZ’ HEADER
2:IMAGE_NT_HEADERS
其中IMAGE_NT_HEADERS中包含
PE signature
IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER(其中包含Data Direcotry)
文件頭後緊跟着爲
Section Table (array of IMAGE_SECTION_HEADERs)
在DELPHI的windows.pad中已經有定義的有:
TImageDosHeader;
TImageNtHeaders;
TImageSectionHeader; { size of Tim..der is $28 }
定義變量後按住Ctrl可以察看具體的項目,這裏我就不多說了,這方面的東西也很多。
而其他的如TImageResourceDirectory等,在DELPHI中卻沒有定義,察看其他資料,我在這裏給出他們的結構和簡單說明:
以下是我寫的PEDump.exe的類型說明:

type
PIMAGE_RESOURCE_DIRECTORY = ^TImageResourceDirectory;
_IMAGE_RESOURCE_DIRECTORY = packed record
Characteristics:DWORD;
TimeDateStamp:DWORD;
MajorVersion:WORD;
MinorVersion:WORD;
NumberOfNamedEntries:WORD;
NumberOfIdEntries:WORD;
end;
TImageResourceDirectory = _IMAGE_RESOURCE_DIRECTORY;
{ 資源目錄的格式說明 }

PIMAGE_RESOURCE_DIRECTORY_ENTRY = ^TImageResourceDirectoryEntry;
_IMAGE_RESOURCE_DIRECTORY_ENTRY = packed record
Name:DWORD; { NameOffset:31,NameIsString:1 }
// Id:WORD;
OffsetToData:DWORD; { OffsetToDirectory:31,DataIsDirectory:1 }
end;
TImageResourceDirectoryEntry = _IMAGE_RESOURCE_DIRECTORY_ENTRY;
{ 資源目錄進入點的格式說明 }

PIMAGE_RESOURCE_DIRECTORY_STRING = ^TImageResourceDirectoryString;
_IMAGE_RESOURCE_DIRECTORY_STRING = packed record
Length:WORD;
NameString:CHAR;
end;
TImageResourceDirectoryString = _IMAGE_RESOURCE_DIRECTORY_STRING;
{ 資源目錄名的格式說明 }

PIMAGE_RESOURCE_DIR_STRING_U = ^TImageResourceDirStringU;
_IMAGE_RESOURCE_DIR_STRING_U = packed record
Length:WORD;
NameString:WCHAR;
end;
TImageResourceDirStringU = _IMAGE_RESOURCE_DIR_STRING_U;
{ unicode形式的資源目錄名的格式說明 }

PIMAGE_RESOURCE_DATA_ENTRY = ^TImageResourceDataEntry;
_IMAGE_RESOURCE_DATA_ENTRY = packed record
OffsetToData:DWORD;
Size:DWORD;
CodePage:DWORD;
Reserved:DWORD;
end;
TImageResourceDataEntry = _IMAGE_RESOURCE_DATA_ENTRY;
{ 資源目錄數據進入點的格式說明 }

const
IMAGE_RESOURCE_NAME_IS_STRING = $80000000;
{ 檢測TImageResourceDirectoryEntry.Name的最高爲是否設立,
是則說明剩下的31位指向IMAGE_RESOURCE_DIR_STRING_U的偏移,
否則說明剩下的31位爲一個整數ID。 }
IMAGE_RESOURCE_DATA_IS_DIRECTORY = $80000000;
{ 檢測TImageResourceDirectoryEntry.OffsetToData的最高爲是否設立,
是則說明剩下的31位指向另一個IMAGE_RESOURCE_DIRECTORY的偏移,
否則說明剩下的31位指向IMAGE_RESOURCE_DATA_ENTRY的偏移。 }

{ 以下是文件屬性具體值常量說明 }
{ File Characteristics }
IMAGE_FILE_RELOCS_STRIPPED = $0001; // Relocation info stripped from file.
IMAGE_FILE_EXECUTABLE_IMAGE = $0002; // File is executable.
IMAGE_FILE_LINE_NUMS_STRIPPED = $0004; // Line nunbers stripped from file.
IMAGE_FILE_LOCAL_SYMS_STRIPPED = $0008; // Local symbols stripped from file.
IMAGE_FILE_AGGRESIVE_WS_TRIM = $0010; // Agressively trim working set
IMAGE_FILE_LARGE_ADDRESS_AWARE = $0020; // App can handle >2gb addresses
IMAGE_FILE_BYTES_REVERSED_LO = $0080; // Bytes of machine word are reversed.
IMAGE_FILE_32BIT_MACHINE = $0100; // 32 bit word machine.
IMAGE_FILE_DEBUG_STRIPPED = $0200;
// Debugging info stripped from file in .DBG file
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP = $0400;
// If Image is on removable media, copy and run from the swap file.
IMAGE_FILE_NET_RUN_FROM_SWAP = $0800;
// If Image is on Net, copy and run from the swap file.
IMAGE_FILE_SYSTEM = $1000; // System File.
IMAGE_FILE_DLL = $2000; // File is a DLL.
IMAGE_FILE_UP_SYSTEM_ONLY = $4000; // File should only be run on a UP machine
IMAGE_FILE_BYTES_REVERSED_HI = $8000; // Bytes of machine word are reversed.

{ 以下是文件頭機器屬性值的具體說明 }
{ Machine }
IMAGE_FILE_MACHINE_UNKNOWN = $0;
IMAGE_FILE_MACHINE_I386 = $014c; // Intel 386.
IMAGE_FILE_MACHINE_R3000 = $0162; // MIPS little-endian, $160 big-endian
IMAGE_FILE_MACHINE_R4000 = $0166; // MIPS little-endian
IMAGE_FILE_MACHINE_R10000 = $0168; // MIPS little-endian
IMAGE_FILE_MACHINE_WCEMIPSV2 = $0169; // MIPS little-endian WCE v2
IMAGE_FILE_MACHINE_ALPHA = $0184; // Alpha_AXP
IMAGE_FILE_MACHINE_SH3 = $01a2; // SH3 little-endian
IMAGE_FILE_MACHINE_SH3E = $01a4; // SH3E little-endian
IMAGE_FILE_MACHINE_SH4 = $01a6; // SH4 little-endian
IMAGE_FILE_MACHINE_SH5 = $01a8; // SH5
IMAGE_FILE_MACHINE_ARM = $01c0; // ARM Little-Endian
IMAGE_FILE_MACHINE_THUMB = $01c2;
IMAGE_FILE_MACHINE_ARM33 = $01d3;
IMAGE_FILE_MACHINE_POWERPC = $01F0; // IBM PowerPC Little-Endian
IMAGE_FILE_MACHINE_IA64 = $0200; // Intel 64
IMAGE_FILE_MACHINE_MIPS16 = $0266; // MIPS
IMAGE_FILE_MACHINE_ALPHA64 = $0284; // ALPHA64
IMAGE_FILE_MACHINE_MIPSFPU = $0366; // MIPS
IMAGE_FILE_MACHINE_MIPSFPU16 = $0466; // MIPS
// IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
IMAGE_FILE_MACHINE_AMD64 = $0500; // AMD K8
IMAGE_FILE_MACHINE_TRICORE = $0520; // Infineon
IMAGE_FILE_MACHINE_CEF = $0CEF;

{ 以下是SECTION的屬性值具體說明 }
{ Section characteristics }
// IMAGE_SCN_TYPE_REG = $00000000; // Reserved.
// IMAGE_SCN_TYPE_DSECT = $00000001; // Reserved.
// IMAGE_SCN_TYPE_NOLOAD = $00000002; // Reserved.
// IMAGE_SCN_TYPE_GROUP = $00000004; // Reserved.
IMAGE_SCN_TYPE_NO_PAD = $00000008; // Reserved.
// IMAGE_SCN_TYPE_COPY = $00000010; // Reserved.

IMAGE_SCN_CNT_CODE = $00000020; // Section contains code.
IMAGE_SCN_CNT_INITIALIZED_DATA = $00000040; // Section contains initialized data.
IMAGE_SCN_CNT_UNINITIALIZED_DATA = $00000080; // Section contains uninitialized data.

IMAGE_SCN_LNK_OTHER = $00000100; // Reserved.
IMAGE_SCN_LNK_INFO = $00000200;
// Section contains comments or some other type of information.
// IMAGE_SCN_TYPE_OVER = $00000400; // Reserved.
IMAGE_SCN_LNK_REMOVE = $00000800;
// Section contents will not become part of image.
IMAGE_SCN_LNK_COMDAT = $00001000; // Section contents comdat.
// = $00002000; // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete = $00004000;
IMAGE_SCN_NO_DEFER_SPEC_EXC = $00004000;
// Reset speculative exceptions handling bits in the TLB entries for this section.
IMAGE_SCN_GPREL = $00008000;
// Section content can be accessed relative to GP
IMAGE_SCN_MEM_FARDATA = $00008000;
// IMAGE_SCN_MEM_SYSHEAP - Obsolete = $00010000;
IMAGE_SCN_MEM_PURGEABLE = $00020000;
IMAGE_SCN_MEM_16BIT = $00020000;
IMAGE_SCN_MEM_LOCKED = $00040000;
IMAGE_SCN_MEM_PRELOAD = $00080000;

IMAGE_SCN_ALIGN_1BYTES = $00100000; //
IMAGE_SCN_ALIGN_2BYTES = $00200000; //
IMAGE_SCN_ALIGN_4BYTES = $00300000; //
IMAGE_SCN_ALIGN_8BYTES = $00400000; //
IMAGE_SCN_ALIGN_16BYTES = $00500000;
// Default alignment if no others are specified.
IMAGE_SCN_ALIGN_32BYTES = $00600000; //
IMAGE_SCN_ALIGN_64BYTES = $00700000; //
IMAGE_SCN_ALIGN_128BYTES = $00800000; //
IMAGE_SCN_ALIGN_256BYTES = $00900000; //
IMAGE_SCN_ALIGN_512BYTES = $00A00000; //
IMAGE_SCN_ALIGN_1024BYTES = $00B00000; //
IMAGE_SCN_ALIGN_2048BYTES = $00C00000; //
IMAGE_SCN_ALIGN_4096BYTES = $00D00000; //
IMAGE_SCN_ALIGN_8192BYTES = $00E00000; //
// Unused = $00F00000;
IMAGE_SCN_ALIGN_MASK = $00F00000;

IMAGE_SCN_LNK_NRELOC_OVFL = $01000000; // Section contains extended relocations.
IMAGE_SCN_MEM_DISCARDABLE = $02000000; // Section can be discarded.
IMAGE_SCN_MEM_NOT_CACHED = $04000000; // Section is not cachable.
IMAGE_SCN_MEM_NOT_PAGED = $08000000; // Section is not pageable.
IMAGE_SCN_MEM_SHARED = $10000000; // Section is shareable.
IMAGE_SCN_MEM_EXECUTE = $20000000; // Section is executable.
IMAGE_SCN_MEM_READ = $40000000; // Section is readable.
IMAGE_SCN_MEM_WRITE = $80000000; // Section is writeable.

我寫了檢測是否包含此屬性的函數
function BeTrue(fg:Cardinal,Value):Boolean;
begin
Result:=fg and not Value=0;
end;
如果fg的屬性值在Value中,則爲True,否則爲False;
例如 BeTrue(IMAGE_FILE_RELOCS_STRIPPED,PENTHead.FileHeader.Characteristics);

至於資源目錄的讀取,至少需要兩重循環來定位,具體實現就要靠你的算法功力了:)


[文檔]

NT頭---可選頭---IMAGE_DATA_DIRECTORY---IMAGE_DIRECTORY_ENTRY_RESOURCE--->
IMAGE_SECTION_HEADER[](節頭/表)
……

節n---->IMAGE_RESOURCE_DIRECTORY_ENTRY[]---IMAGE_RESOURCE_DIRECTORY[]



-----------------0:DOS頭

-----------------1:NT頭
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;

-----------------1.1:文件頭
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;

-----------------1.2:可選頭
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];//=16
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

-----------------1.2.1:數據目錄?
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //表的RVA地址
DWORD Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

-----------------1.2.2數據入口
// Directory Entries
#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

-----------------1.2.2.0導出函數表?
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

-----------------1.2.2.1引入函數表
-----------------1.2.2.2資源表
-----------------1.2.2.3異常表?
-----------------1.2.2.4安全表?
-----------------1.2.2.5重定向表
-----------------1.2.2.6調試信息表
……
-----------------2:節表(段表)
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;

-----------------3:節……

-----------------3.1資源目錄(_IMAGE_RESOURCE_DIRECTORY)
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

----------------3.2資源目錄入口(_IMAGE_RESOURCE_DIRECTORY_ENTRY)
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

-----------------3.211資源目錄名
typedef struct _IMAGE_RESOURCE_DIRECTORY_STRING {
WORD Length;
CHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIRECTORY_STRING, *PIMAGE_RESOURCE_DIRECTORY_STRING;

-----------------3.212資源目錄Unicode名
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length;
WCHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

-----------------3.22資源數據入口
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;//偏移地址。並非在文件中的偏移!
DWORD Size; //大小
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

-----------------9:其他
如果是在資源根目錄,id爲:
1: cursor
2: bitmap
3: icon
4: menu
5: dialog
6: string table
7: font directory
8: font
9: accelerators
10: unformatted resource data
11: message table
12: group cursor
14: group icon
16: version information


題目:深度探索Win32可執行文件格式
來自:不詳
作者:Matt Pietrek
翻譯:姜慶東



摘要

對可執行文件的深入認識將帶你深入到系統深處。如果你知道你的exe/dll裏是些什麼東東,你就是一個更有知識的程序員。作爲系列文章的第一章,將關注這幾年來PE格式的變化,同時也簡單介紹一下PE格式。經過這次更新,作者加入了PE格式是如何與.NET協作的及PE文件表格(PE FILE SECTIONS),RVA,The DataDirectory,函數的輸入等內容。

====================

很久以前,我給Microsoft Systems Journal(現在的MSDN)寫了一篇名爲“Peering Inside the PE: A Tour of the Win32 Portable Executable File Format”的文章。後來比我期望的還流行,到現在我還聽說有人在用它(它還在MSDN裏)。不幸的是,那篇文章的問題依舊存在,WIN32的世界靜悄悄地變了好多,那篇文章已顯得過期了。從這個月開始我將用這兩篇文章來彌補。

你可能會問爲什麼我應當瞭解PE格式,答案依舊:操作系統的可執行文件格式和數據結構暴露出系統的底層細節。通過了解這些,你的程序將編的更出色。

當然,你可以閱讀微軟的文檔來了解我將要告訴你的。但是,像很多文檔一樣,‘寧可晦澀,但爲瓦全’。

我把焦點放在提供一些不適合放在正式文檔裏的內容。另外,這篇文章裏的一些知識不見得能在官方文檔裏找到。

1. 裂縫的撕開

讓我給你一些從1994年我寫那篇文章來PE格式變化的例子。WIN16已經成爲歷史,也就沒有必要作什麼比較和說明了。另外一個可憎的東西就是用在WINDOWS 3.1 中的WIN32S,在它上面運行程序是那麼的不穩定。

那時候,WINDOWS 95(也叫Chicago)還沒有發行。NT還是3.5版。微軟的連接器還沒開始大規模的優化,儘管如此,there were MIPS and DEC Alpha implementations of Windows NT that added to the story.

那麼究竟,這麼些年來,有些什麼新的東西出來呢?64位的WINDOWS有了它自己的PE變種,WINDOWS CE 支持各種CPU了,各種優化如DLL的延遲載入,節表的合併,動態捆綁等也已出臺。

有很多類似的東西發生了。

讓我們最好忘了.NET。它是如何與系統切入的呢?對於操作系統,.NET的可執行文件格式是與舊的PE格式兼容的。雖然這麼說,在運行時期,.NET還是按元數據和中間語言來組織數據的,這畢竟是它的核心。這篇文章當中,我將打開.NET元數據這扇門,但不做深入討論。

如果WIN32的這些變化都不足以讓我重寫這篇文章,就是原來的那些錯誤也讓我汗顏。比如我對TLS的描述只是一帶而過,我對時間戳的描述只有你生活在美國西部才行等等。還有,一些東西已是今是作非了, 我曾說過.RDATA幾乎沒排上用場,今天也是,我還說過.IDATA節是可讀可寫的,但是一些搞API攔截的人發現好像是錯的。

在更新這篇文章的過程當中,我也檢查了PEDUMP這個用來傾印PE文件的程序.這個程序能夠在0X86和IA-64平臺下編譯和運行。

2. PE格式概覽

微軟的可執行文件格式,也就是大家熟悉的PE 格式,是官方文檔的一部分。但是,它是從VAX/VMS上的COFF派生出來的,就WINDOWS NT小組的大部分是從DEC轉過來的看來,這是可以理解的。很自然,這些人在NT的開發上會用他們以往的代碼。

採用術語“PORTABLE EXECUTABLE”是因爲微軟希望有一個通用在所有WINDOWS平臺上和所有CPU上的文件格式。從大的方面講,這個目標已經實現。它適用於NT及其後代,95及其後代,和CE.

微軟產生的OBJ文件是用COFF格式的。當你看到它的很多域都是用八進制的編碼的,你會發現她是多麼古老了。COFF OBJ文件用到了很多和PE一樣的數據結構和枚舉,我馬上會提到一些。

64位的WINDOWS只對PE格式作了一點點改變。這個新的格式叫做PE32+。沒有增加一個字段,且只刪了一個字段。其他的改變就是把以前的32位字段擴展成64位。對於C++代碼,通過宏定義WINDOWS的頭文件已經屏蔽了這些差別。

EXE與DLL的差別完全是語義上的。它們用的都是同樣一種文件格式-PE。唯一的區別就是其中有一個字段標識出是EXE還是DLL .還有很多DLL的擴展比如OCX,CPL等都是DLL.它們有一樣的實體。

你首先要知道的關於PE的知識就是磁盤中的數據結構佈局和內存中的數據結構佈局是一樣的。 載入可執行文件(比如LOADLIBARY)的首要任務就是把磁盤中的文件映射到進程的地址空間.因此像IMAGE_NT_HEADER(下面解釋)在磁盤和內存中是一樣的。關鍵的是你要懂得你怎樣在磁盤中獲得PE文件某些信息的, 當它載入內存時你可以一樣獲得,基本上是沒什麼不同的(即內存映射文件)。但是知道與映射普通的內存映射文件不同是很重要的。WINDOWS載入器察看PE文件才決定映射到哪裏,然後從文件的開始處 往更高的地址映射,但是有的東西在文件中的偏移和在內存中的偏移會不一樣。儘管如此,你也有了足夠的信息把文件偏移轉化成內存偏移。

當Windows載入器把PE載入內存,在內存中它稱作模塊(MODULE), 文件從HMODULE這個地址開始映射。記住這點:給你個HMODULE,從那你可以知道一個數據結構(IMAGE_DOS_HEADER),然後你還可以知道所有得數據結構。這個強大的功能對於API攔截特別有意義。(準確地說:對於WINDOWS CE,這是不成立的,不過這是後話)。

內存中的模塊代表着進程從這個可執行文件中所需要的所有代碼,數據,資源。其他部分可以被讀入,但是可能不映射(如,重定位節)。還有一些部分根本就不映射,比如當調試信息放到文件的尾部的時候。有一個字段告訴系統把文件映射到內存需要多少內存。不需要的數據放在文件的尾部,而在過去,所有部分都映射。 在WINNT.H描述了PE格式。在這個文件中,幾乎有所有的關於PE的數據結構,枚舉,#DEFINE。當然,其它地方也有相關文檔,但是還是WINNT.H說了算。

有很多檢測PE文件的工具,有VISUAL STUDIO的DUMPBIN,SDK中的DEPENDS,我比較喜歡DEPENDS,因爲它以一種簡潔的方式檢測出文件的引入引出。一個免費的PE察看器,PEBrowse,來自smidgenosoft。我的pedump也是很有用的,它和dumpbin有一樣的功能。

從api的立場看,imagehlp.dll提供了讀寫pe文件的機制。

在開始討論pe文件前,回顧一下pe文件的一些基本概念是有意義的。在下面幾節,我將討論:pe 節,相對虛擬地址(rva),數據目錄,函數的引入。


3. PE節

PE節以某鍾順序表示代碼或數據。代碼就是代碼了,但是卻有多種類型的數據,可讀寫的程序數據(如全局變量),其它的節包含API的引入引出表,資源,重定位。 每個節有自己的屬性,包括是否是代碼節,是否只讀還是可讀可寫,節的數據是否全局共享。

通常,節中的數據邏輯上是關聯的。PE文件一般至少要有兩個節,一個是代碼,另一個爲數據。一般還有一個其它類型的數據的節。後面我將描述各種類型的節。

每個節都有一個獨特的名字。這個名字是用來傳達這個節的用途的。比如,.RDATA表示一個只讀節, 節的名字對於操作系統毫無意義,只是爲了人們便於理解。把一個節命名爲FOOBAR和.TEXT是一樣有用的。微軟給他們的節命名了個有特色的名字,但是這不是必需的。Borland的連接器用的是code和data。

一般編譯器將產生一系列標準的節,但這沒有什麼不可思議的。你可以建立和命名自己的節,連接器會自動在程序文件中包含它們。在visual c++中,你能用#pragma指令讓編譯器插入數據到一個節中。像下面這樣:

 #pragma data_seg("MY_DATA")
 ...有必要初始化
 #pragma data_seg()

你也可以對.data做同樣的事。大部分的程序都只用編譯器產生的節,但是有時候你卻需要這樣。比如建立一個全局共享節。

節並不是全部由連接器確定的,他們可以在編譯階段由編譯器放入obj文件。連接器的工作就是合併所有obj和庫中需要的節成一個最終的合適的節。比如,你的工程中的所有obj可能都有一個包含代碼的.text節,連接器把這些節合併成一個.text節。同樣對於.data等。這些主題超出了這篇文章的範圍了。還有更多的規則關於連接器的。在obj文件中是專門給linker用的,並不放入到pe文件中,這種節是用來給連接器傳遞信息的。

節有兩個關於對齊的字段,一個對應磁盤文件,另一個對應內存中的文件。Pe文件頭指出了這兩個值,他們可以不一樣。每個節的偏移從對齊值的倍數開始。比如,典型的對齊值是0x200,那麼每個節的的偏移必須是0x200的倍數。一旦載入內存,節的起始地址總是以頁對齊。X86cpu的頁大小爲4k,al-64爲8k。

下面是pedump傾印出的Windows XP KERNEL32.DLL.的.text .data節的信息:

 Section Table
 01 .text VirtSize: 00074658 VirtAddr: 00001000
 raw data offs: 00000400 raw data size: 00074800
 ...
 02 .data VirtSize: 000028CA VirtAddr: 00076000
 raw data offs: 00074C00 raw data size: 00002400

建立一個節在文件中的偏移和它相對於載入地址的偏移相同的pe文件是可能的。在98/me中,這會加速大文件的載入。Visual studio 6.0 的默認選項 /opt:win98j就是這樣產生文件的。在Visual studio.net中是否用/opt:nowin98取決於文件是否夠小。

一個有趣的連接器特徵是合併節的能力。如果兩個節有相似兼容的屬性,連接的時候就可以合併爲一個節。這取決於是否用/merger開關。像下面就把.rdata和.text合併爲一個節.text


 /MERGE:.rdata=.text

合併節的優點就是對於磁盤和內存節省空間。每個節至少佔用一頁內存,如果你可以把可執行文件的節數從4減到3,很可能就可以少用一頁內存。當然,這取決於兩個節的空餘空間加起來是否達到一頁。

當你合併節事情會變得有意思,因爲這沒有什麼硬性和容易的規則。比如你可以合併.rdata到.text,

但是你不可以把.rsrc.reloc.pdata合併到別的節。先前Visual Studio .NET允許把.idata合併,後來又不允許了。但是當發行的時候,連接器還是可以把.idata合併到別的節。

因爲引入節的一部分在載入器載入時將被寫入,你可能驚奇它是如何被放入一個只讀節的。是這樣的,在載入的時候系統會臨時改變那些包含引入節的頁爲可讀可寫,初始化完成後,又恢復原來屬性。


4. 相對虛擬地址

在可執行文件中,有很多地方需要指定內存地址,比如,引用全局變量時,需要指定它的地址。Pe文件儘管有一個首選的載入地址,但是他們可以載入到進程空間的任何地方,所以你不能依賴於pe的載入點。由於這點,必須有一個方法來指定地址而不依賴於pe載入點的地址。爲了避免把內存地址硬編碼進pe文件,提出了RVA。RVA是一個簡單的相對於PE載入點的內存偏移。比如,PE載入點爲0X400000,那麼代碼節中的地址0X401000的RVA爲(target address) 0x401000 - (load address)0x400000 = (RVA)0x1000。把RVA加上PE的載入點的實際地址就可以把RVA轉化實際地址。順便說一下,按PE的說法,內存中的實際地址稱爲VA(VIRTUAL ADDRESS).不要忘了早點我說的PE的載入點就是HMODULE。

想對探索內存中的任意DLL嗎?用GetModuleHanle(LPCTSTR)取得載入點,用你的PE知識來幹活吧


5. 數據目錄

PE文件中有很多數據結構需要快速定位。顯然的例子有引入函數,引出函數,資源,重定位。這些東西是以一致的方式來定位的,這就是數據目錄。

數據目錄是一個結構數組,包含16個結構。每個元素有一個定義好的標識,如下:


 // Export Directory
 #define IMAGE_DIRECTORY_ENTRY_EXPORT 0

 // Import Directory

 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1

 // Resource Directory

 #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2

 // Exception Directory

 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3

 // Security Directory

 #define IMAGE_DIRECTORY_ENTRY_SECURITY 4

 // Base Relocation Table

 #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5

 // Debug Directory

 #define IMAGE_DIRECTORY_ENTRY_DEBUG 6

 // Description String

 #define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7

 // Machine Value (MIPS GP)

 #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8

 // TLS Directory

 #define IMAGE_DIRECTORY_ENTRY_TLS 9

 // Load Configuration Directory

 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10

 typedef struct _IMAGE_DATA_DIRECTORY {

   ULONG VirtualAddress;

   ULONG Size;

 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

6. 引入函數

當你使用別的DLL中的代碼或數據,稱爲引入。當PE載入時,載入器的工作之一就是定位所有引入函數及數據,使那些地址對於載入的PE可見。具體細節在後面討論,在這裏只是大概講一下。

當你用到了一個DLL中的代碼或數據,你就暗中連接到這個DLL。但是你不必爲“把這些地址變得對你的代碼有效”做任何事情,載入器爲你做這些。方法之一就是顯式連接,這樣你就要確定DLL已被載入,及函數的地址。調用LOADLIBARY和GETPROCADDRESS就可以了。

當你暗式連接DLL,LOADLIBARY和GETPROCADDRESS同樣還是執行了的。只不過載入器爲你做了這些。載入器還保證PE文件所需得任何附加的DLL都已被載入。比如,當你連接了KERNEL32.DLL,而它又引入了NTDLL.DLL的函數,又比如當你連接了GDI32.DLL,而它又依賴於USER32, ADVAPI32,NTDLL, 和 KERNEL32 DLLs的函數,載入器會保證這些DLL被載入及函數的決議。

暗式連接時,決議過程在PE文件在載入時就發生了。如果這時有什麼問題(比如這個DLL文件找不到),進程終止。

VISUAL C++ 6.0 加入了DLL的延遲載入的特徵。它是暗式連接和顯式連接的混合。當你延遲載入DLL,連接器做出一些和引入標準規則DLL類似的東西,但是操作系統卻不管這些東西,而是在第一次調用這個DLL中的函數的時候載入(如果還沒載入),然後調用GetProcAddress取得函數的地址。

對於pe文件要引入的dll都有一個對應的結構數組,每個結構指出這個dll的名字及指向一個函數指針數組的指針,這個函數指針數組就是所謂的 IAT(IMORT ADDRESS TABLE)。每個輸入函數,在IAT中都有一個保留槽,載入器將在那裏寫入真正的函數地址。最後特別重要一點的是:模塊一旦載入,IAT中包含所要調用的引入函數的地址。

把所有輸入函數放在IAT一個地方是很有意義的,這樣無論代碼中多少次調用一個引入函數,都是通過IAT中的一個函數指針。

讓我們看看是怎樣調用一個引入函數的。有兩種情況需要考慮:有效率的和效率差的。最好的情況像下面這樣:

 CALL DWORD PTR [0x00405030]

直接調用[0x405030]中的函數,0x405030位於IAT部分。效率差的方式如下:

 CALL 0x0040100C

 ...

 0x0040100C:

 JMP DWORD PTR [0x00405030]

這種情況,CALL把控制權轉到一個子程序,子程序中的JMP指令跳轉到位於IAT中的0x00405030,簡單說,它多用了5字節和JMP多花的時間。

你可能驚訝引入函數就採用了這種方式,有個很好的解釋,編譯器無法區別引入函數的調用和普通函數調用,對於每個函數調用,編譯器只產生如下指令:

 CALL XXXXXXXX

XXXXXXXX是一個由連接器填入的RVA。注意,這條指令不是通過函數指針來的,而是代碼中的實際地址。

爲了因果的平衡,連接器必須產生一塊代碼來代替取代XXXXXXXX,簡單的方法就是象上面所示調用一個JMP STUB.

那麼JMP STUB 從那裏來呢?令人驚異的是,它取自輸入函數的引入庫。如果你去察看一個引入庫,在輸入函數名字的關聯處,你會發現與上面JMP STUB相似的指令。

接着,另一個問題就是如何優化這種形式,答案是你給編譯器的修飾符,__declspec(import) 修飾符告訴編譯器,這個函數來自另一個dll,這樣編譯器就會產生第一種指令。另外,編譯器將給函數加上__imp_前綴然後送給連接器決議,這樣可以直接把__imp_xxx送到iat,就不需要jmp stub了。

對於我們這有什麼意義呢,如果你在寫一個引出函數的東西並提供一個頭文件的話,別忘了在函數前加上修飾符__declspec(import)

 __declspec(dllimport) void Foo(void);

在winnt.h等系統頭文件中就是這樣做的。


7. PE 文件結構

現在讓我們開始研究PE文件格式,我將從文件的頭部開始,描述每個PE文件中都有的各種數據結構,然後,我將討論更多的專門的數據結構比如引入表和資源,除非特殊說明,這些結構都定義在WINNT.H中。

一般地,這些結構都有32和64位之分,如IMAGE_NT_HEADERS32 ,IMAGE_NT_HEADER64等,他們基本上是一樣的,除了64位的擴展了某些字段。通過#DEFINE WINNT.H都屏蔽了這些區別,選擇那個數據結構取決於你要如何編譯了(如,是否定義_WIN64)

The MS-DOS Header

每個PE文件是以一個DOS程序開始的,這讓人想起WINDOWS在沒有如此可觀的使用者的早期年代。當可執行文件在非WINDOWS平臺上運行的時候至少可以顯示出一條信息表示它需要WINDOWS。

PE文件的開頭是一個IMAGE_DOS_HEADER結構,結構中只有兩個重要的字段e_magic and e_lfanew。e_lfanew指出pe file header的偏移,e_magic需要設定位0x5a4d,被#define 成IMAGE_DOS_SIGNATURE 它的ascii爲’MZ’,Mark Zbikowski的首字母,DOS 的原始構建者之一。


The IMAGE_NT_HEADERS Header

這個結構是PE文件的主要定位信息的所在。它的偏移由IMAGE_DOS_HEADER的e_lfanew給出

確實有64和32位之分,但我在討論中將不作考慮,他們幾乎沒有區別。

 typedef struct _IMAGE_NT_HEADERS {

  DWORD Signature;

  IMAGE_FILE_HEADER FileHeader;

  IMAGE_OPTIONAL_HEADER32 OptionalHeader;

 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;


在一個有效的pe文件裏,Signture被設爲0x00004500,ascii 爲’PE00’,#define IMAGE_NT_SIGNTURE 0X00004500;第二個字段是一個IMAGE_FILE_HEADER結構,它包含文件的基本信息,特別重要的是它指出了IMAGE_OPTIONAL_HEADER的大小(重要嗎?);在PE文件中,IMAGE_OPTIONAL_HEADER是非常重要的,但是仍稱作IMAGE_OPTIONAL_HEADER。

IMAGE_OPTIONAL_HEADER結構的末尾就是用來定位pe文件中重要信息的地址簿-數據目錄,它的定義如下:

 typedef struct _IMAGE_DATA_DIRECTORY {

  DWORD VirtualAddress; // RVA of the data

  DWORD Size; // Size of the data

 };


The Section Table


緊接着IMAGE_NT_HEADERS後的就是節表,節表就是IMAGE_SECTION_HEADER的數組。IMAGE_SECTION_HEADER包含了它所關聯的節的信息,如位置,長度,特徵;該數組的數目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections指出。具體見下圖


PE中的節的大小的總和最後是要對齊的,Visual Studio 6.0中的默認值是4k,除非你使用/OPT:NOWIN98 或/ALIGN開關;在.NET中,依然用了默認的/OPT:WIN98,但是如果文件小於一特定大小時,就會採用0X200爲對齊值。

.NET文檔中有關於對齊的另一件有趣的事。.NET文件的內存對齊值爲8K而不是普通X86平臺上的4K,這樣就保證了在X86平臺編譯的程序可以在IA-64平臺上運行。如果內存對齊值爲4K,那麼IA-64的載入器就不能載入這個程序,因爲它的頁爲8K。  

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