最近做逆向破解的學習需要有關PE格式的相關知識,之前看過一點但還是模模糊糊的,剛好借這個機會來個徹底的學習,對PE的結構爭取有個更深刻一點的認識
下面是我自己的學習筆記.
1. DOS文件頭,這是PE文件最開始的一個結構體,定義了一個DOS小程序(注意在磁盤中是0x200對齊而內存中0x1000對齊,所以DOS文件頭中不足的地方補0)
_IMAGE_DOS_HEADER
{
WORD e_magic; // MZ 0x5A4D
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // 指向PE文件頭的地址的指針,在這里是0x200h
}
2. IMAGE_NT_HEADERS文件頭,這是一個大架構體,也就是e_lfanew = 0x200h指向的地方,即NT文件頭,這里包含了PE文件的很多重要的信息。
_IMAGE_NT_HEADERS
{
DWORD Signature; //PE文件頭的標志:PE\0\0 0x00004550
IMAGE_FILE_HEADER FileHeader; //PE文件頭的結構體,這是一種結構體中的結構體,它包含了關於文件的一些基本信息。最重要的是,其中有一個域指明了跟在這個結構后面的可選文件頭的大小
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}
下面開始以遞歸的方式探究PE的一層層結構
2.2 IMAGE_FILE_HEADER PE文件頭
_IMAGE_FILE_HEADER
{
WORD Machine; //目標平台CPU的類型:0x014c // Intel 386 0x0200 // Intel 64
WORD NumberOfSections; //指示節表中節的數目。節表緊跟着IMAGE_NT_HEADERS結構, 也即是說節表是第 3 部分
DWORD TimeDateStamp; //指示文件創建時間。這個值是從格林尼治時間(GMT)1970年1月1日00:00以來的總秒數
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //就是上面說到的那個域,指示了OptionalHeader數據的大小對於32位PE文件來說,它通常是224;對於64位PE32+文件來說,它通常是240。但是,它們只是最小值,可能有更大的值
WORD Characteristics; //指示文件屬性的一組位標志,它的取值來自與下面的宏定義
}
文件屬性宏定義:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // 文件是 DLL, 這個域用來區分exe和dll.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
2.3 IMAGE_OPTIONAL_HEADER32 PE可選文件頭,雖說是可選,但是PE要求必須要有這個結構體域
_IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
WORD Magic; //一個特征字,用於表明文件頭的類型 IMAGE_NT_OPTIONAL_HDR32_MAGIC:0x10b IMAGE_NT_OPTIONAL_HDR64_MAGIC:0x2b
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //帶有IMAGE_SCN_CNT_CODE 屬性的所有節的總大小。
DWORD SizeOfInitializedData; //所有由已初始化的數據組成的節的總大小。
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;//這個域很重要!文件中首先被執行的代碼的第一個字節的RVA(注意是RVA)
DWORD BaseOfCode; //加載進內存之后代碼的第一個字節的RVA(這個值一般和AddressOfEntryPoint是一樣的)。
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase; //這個文件在內存中的首選加載地址,對於EXE來說,默認的ImageBase為0x400000;對於DLL來說,它是0x10000000
DWORD SectionAlignment; //加載進內存(注意是內存)之后節(注意是節)的對齊值。這個對齊值必須大於或等於文件對齊值
DWORD FileAlignment; //節在PE文件中的對齊值。對於x86可執行文件來說,它或者是0x200,或者是0x1000。不同版本的Microsoft鏈接器的默認值不一樣。這個值必須是2的冪
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //SizeOfImage包含了假設存在於最后一個節之后的那個節的RVA。這等效於把此文件加載進內存時系統需要保留的內存數量。這個域的值必須是節的對齊值的倍數,如果這個域被惡意修改了,造成內存分配錯誤
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //在IMAGE_NT_HEADERS結構末尾處是一個IMAGE_DATA_DIRECTORY結構數組。這個域包含了這個數組的元素數目。由於以前發行的Windows NT的原因,它被設置為16。即導入表導出表那些東西
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //這可以說是PE中最重要的部分了,IMAGE_DATA_DIRECTORY結構數組。每個結構包含可執行文件中一些重要部分(例如導入表、導出表以及資源等)的RVA和大小。下面會重點介紹這個結構
}
2.3.1 IMAGE_OPTIONAL_HEADER結構末尾的DataDirectory數組就像是可執行文件中重要位置的地址簿。
IMAGE_DATA_DIRECTORY 數據表
_IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress; //數據的RVA
DWORD Size; //數據的大小
}
3. 緊跟着IMAGE_NT_HEADERS結構的是節表(section table)。這是PE結構的第3部分,既然是表,顧名思義,只是一個存放一些信息的結構體,下面還有節。節表是一個IMAGE_SECTION_HEADER結構數組。此結構提供了與它相關的節的信息,其中包括位置、長度和屬性
_IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //節的名稱(ASCII碼)。節名並不保證以NULL結尾。如果你指定的節名大於8個字節,鏈接器在生成可執行文件時將其截斷為8個字符。在OBJ文件中存在一種機制可以讓節名更長。節名通常以圓點開始,但這並不是必需的
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //指示節實際占用的內存大小。這個域的值可能比SizeOfRawData域的值大或小
} Misc;
DWORD VirtualAddress; //在可執行文件中,它表示在內存中節的起始RVA。在OBJ文件中它被設置為0
DWORD SizeOfRawData; //可執行文件或OBJ文件中的節中存儲的數據的大小(以字節計)。對於可執行文件來說,它必須是PE文件頭中給出的文件對齊值的倍數
DWORD PointerToRawData; //節中數據起始的文件偏移。對於可執行文件來說,它必須是PE文件頭中給出的文件對齊值的倍數
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //指示節屬性的標志(可以用“或”連接,這也是這種用bit位表示屬性信息的常用思路,linux中的文件屬性744之類的也是這種屬性位圖思想)下面列出了常用的節屬性標志
}
Section characteristics:
IMAGE_SCN_CNT_CODE //節中包含代碼。
IMAGE_SCN_MEM_EXECUTE //節是可執行的。
IMAGE_SCN_CNT_INITIALIZED_DATA //節中包含已初始化的數據。
IMAGE_SCN_CNT_UNINITIALIZED_DATA //節中包含未初始化的數據。
IMAGE_SCN_MEM_DISCARDABLE //這個節在最終的可執行文件中可以被丟棄。用於保存鏈接器使用的信息,包括.debug$節。
IMAGE_SCN_MEM_NOT_PAGED //這個節不能被交換到頁面文件中,因此它應該總是存在於物理內存中。經常用於內核模式驅動程序,可以使用#pragma page來聲明。
IMAGE_SCN_MEM_SHARED //包含這個節的物理頁面將在加載這個可執行文件的所有進程之間共享。因此每個進程看到的這個節中的數據的值完全一樣。對於在進程的所有實例之間共享全局變量比較有用。要共享某個節,使用/SECTION:節名,S鏈接器選項
IMAGE_SCN_MEM_READ //節是可讀的。幾乎總是設置這個值。
IMAGE_SCN_MEM_WRITE //節是可寫的。
IMAGE_SCN_LNK_INFO //節中包含鏈接器使用的信息。僅存在於OBJ文件中。
IMAGE_SCN_LNK_REMOVE //這個節中的內容將不成為最終的映像的一部分。僅用於OBJ文件。
IMAGE_SCN_LNK_COMDAT //節中的內容是公共數據(comdat)。公共數據(Communal data)是可以被定義在多個OBJ文件中的數據(或代碼)。鏈接器只將其中的一份副本包含進最終的可執行文件中。Comdats對於支持C++的模板函數和函數級的鏈接至關重要。它僅存在於OBJ文件中。
IMAGE_SCN_ALIGN_xBYTES //這個節中的數據在最終的可執行文件中的對齊值。有各種各樣的值(_4BYTES,_8BYTES,_16BYTES等)。如果不指定,默認為16字節。僅在OBJ文件中才設置這些標志。
4. PE的第4部分: 節。緊接着節表就是具體的節。它們是一個個單獨的IMAGE_SECTION_DATA結構(這是從010editor上看到的,在winnt.h中貌似沒有看到這個結構,我的理解是節沒有一個固定的結構了,是具體的代碼或着數據)
下面列出常見的節:
.text 默認的代碼節。
.data 默認的可讀/可寫數據節。全局變量通常在這個節中。
.rdata 默認的只讀數據節。字符串常量和C++/COM虛表就放在這個節中。
.idata 導入表。實際上,鏈接器經常把.idata節合並到其它節中(或者是明確指定的,或者是通過鏈接器的默認行為)。默認情況下,鏈接器僅在創建發行版的程序時才把.idata節合並到其它節中。
.edata 導出表。當創建要導出函數或數據的可執行文件時,鏈接器會創建一個.EXP文件。這個.EXP文件包含一個.edata節,這個節被添加到最后的可執行文件中。與.idata節一樣,.edata節也經常被合並到.text節或.rdata節中。
.rsrc 資源節。這個節是只讀的。它不應該被命名為其它名稱,也不應該被合並到其它節中。
.bss 未初始化的數據節。在最新的鏈接器創建的可執行文件中很少見到。鏈接器擴展可執行文件的.data節的VirtualSize域以便容納未初始化的數據。
.crt 添加到可執行文件中的數據,用來支持C++運行時庫(CRT)。一個比較好的例子就是用於調用靜態C++對象的構造函數和析構函數的指針。要獲取更詳細的信息,可以參考2001年1月的Under The Hood專欄。
.tls 這個節中的數據用來支持使用__declspec(thread)語法創建的線程局部存儲變量。它包括數據的初始值,以及運行時需要的附加變量。
.reloc 可執行文件中的基址重定位節。通常DLL需要基址重定位信息而EXE並不需要。在創建發行版的程序時,鏈接器並不為EXE文件生成基址重定位信息。可以使用/FIXED鏈接器選項移除基址重定位信息。
以上就是PE結構的大致全貌,下面開始我們要重點介紹一下_IMAGE_NT_HEADERS文件頭中的IMAGE_FILE_HEADER結構體中的IMAGE_DATA_DIRECTORY結構,因為我們以后在研究逆向破解,進程注入之類的時候會頻繁的涉及關於導入表和導出表的相關知識,而這些表都存在這個DataDirectory數組中。
關於導出,導入表:
這里有一點要注意的是,在文件頭中的那個IMAGE_DATA_DIRECTORY[16]數組中存的並不是實際的導入導出表,而只是存了RVA和SIZE,而實際存放數據的地方是節區:.idata和.edata這兩個節,所以我們查找導出導出函數的具體地址要通過導入導出表的指針跳轉到具體的節區才能實際尋址。
IMAGE_DATA_DIRECTORY[0]:導出表
IMAGE_EXPORT_DIRECTORY就是導出表中的指針指向的導出節對應的結構體,導出目錄(Export Directory)指向三個數組和一個ASCII碼字符串表
當一個EXE或DLL導出函數或變量時,其它EXE或DLL就可以使用這些導出的函數或變量。為了簡單起見,我把導出的函數和導出的變量統稱為“符號”。當導出一些符號時,最起碼導出符號的地址需要能夠以一種已定義好的方式被獲取。每個導出的符號都有一個與之關聯的序數,它可以用來查找這個符號。同時,幾乎總有一個ASCII碼格式的字符串名稱與這個導出的符號關聯。一般來說,導出的符號名與源文件中的符號名是一樣的,盡管它們可以被修改的不一樣。
_IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics; //導出標志。當前未定義任何值。
DWORD TimeDateStamp; //導出數據的創建時間。這個域的定義與IMAGE_NT_HEADERS.FileHeader.TimeDateStamp相同
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //與導出符號相關的DLL的名稱ASCII字符串的RVA(例如KERNEL32.DLL),這個字段很重要,shellcode中通過PEB搜索dll文件獲取cmd的時候就回用到這個字段
DWORD Base; //這個域包含了這個可執行文件的導出符號所使用的序數值的起始值。通常情況下這個值為1,但並不總是這樣。當通過序數查找導出符號時,將序數值減去這個域的值就得到了這個導出符號在導出地址表(Export Address Table ,EAT)中的索引。
DWORD NumberOfFunctions; //EAT中的元素數。注意EAT中的某些元素可能為0,這表明沒有 代碼/數據使用那個序數值導出。
DWORD NumberOfNames; //導出名稱表(Export Names Table,ENT)中的元素數。這個域的值總是小於或等於NumberOfFunctions域的值。當某些符號僅使用序數導出時,它就小於那個域的值。如果導出序數之間有間隔,它同樣也小於那個域的值。這個域的值也是導出序數表的大小
DWORD AddressOfFunctions; //EAT的RVA。EAT中的每個元素都是一個RVA。其中每個非0的RVA都對應一個導出符號。
DWORD AddressOfNames; //ENT的RVA。ENT中的每個元素都是一個ASCII碼字符串的RVA。其中的每個ASCII碼字符串都對應一個由名稱導出的符號。這些字符串是按一定順序排列的。這就使得加載器在查找導出符號時可以進行二進制搜索
DWORD AddressOfNameOrdinals; //導出序號表的RVA。這個表是一個WORD類型的數組。它將ENT中的索引映射到導出地址表中相應的元素上。
}
導出目錄(Export Directory)指向三個數組(EAT,ENT,EOT)和一個ASCII碼字符串表。其中只有導出地址表是必需的,它是一個由指向導出函數的指針組成的數組
假設你調用GetProcAddress來獲取KERNEL32中的AddAtomA這個API的地址。這時系統開始查找KERNEL32的IMAGE_EXPORT_DIRECTORY結構。它從那里獲取了導出名稱表的起始地址,知道了在這個數組中有0x3A0個元素,它通過二進制搜索來查找字符串“AddAtomA”。
假設加載器發現AddAtomA是這個數組中的第二個元素。然后它從導出序數表(Export Ordinal Table)中讀取相應的第二個值。這個值就是AddAtomA的導出序數。將這個導出序數作為EAT的索引(加上Base域的值),它最終獲取AddAtomA的相對虛擬地址(RVA)是0x82C2。將此值與KERNEL32的加載地址相加就得到了AddAtomA的實際地址。
這就是函數尋址的過程。可見,我們通過名稱對ENT進行查找,得到在EOT中的index,又通過EOT獲得導出函數的真實序號,在到EAT中得到RVA,最后加上IMAGE_OPTIONAL_HEADER.ImageBase得到函數在內存中的VA,完成尋址。
導出轉發
導出表一個特別聰明的地方是它能將一個導出函數轉發(Forwarding)到其它DLL。例如在Windows NT®、Windows® 2000和Windows XP中,KERNEL32中的HeapAlloc函數被轉發到了NTDLL導出的RtlAllocHeap函數上。轉發是在鏈接時通過.DEF文件中的EXPORTS節中的一種特殊語法形式來實現的。對於HeapAlloc這個例子,KERNEL32的.DEF文件一定包含下面的內容: EXPORTS ••• HeapAlloc = NTDLL.RtlAllocHeap
怎樣才能區別轉發的函數與正常導出的函數呢?這需要一些技巧。通常EAT中包含的是導出符號的RVA。但是如果這個RVA位於導出表中(通過相應的DataDirectory中的VirtualAddress域和Size域進行判斷),那么它就是轉發的。
當轉發一個符號時,它的RVA很明顯不能是當前模塊中的代碼或數據的地址。實際上,它的RVA指向一個由DLL和轉發到的符號名稱組成的字符串。在前面的例子中,這個字符串就是NTDLL.RtlAllocHeap。
IMAGE_DATA_DIRECTORY[1]:導入表
與導出函數或變量相反的就是導入它們。為了與前面保持一致,我仍然使用“符號”這個術語來指代導入的函數和變量。
與導出表有一點不太一樣。
導入數據被保存在IMAGE_IMPORT_DESCRIPTOR結構中。對應着導入表的數據目錄項就指向由這個結構組成的數組。每個IMAGE_IMPORT_DESCRIPTOR結構都與一個導入的可執行文件對應
也就是說PE頭中的那個IMAGE_DATA_DIRECTORY[16]數組只是一個指針,真正的導入表在.idata這個節中。
在.idata節中對應的結構體是一連串的IMAGE_IMPORT_DESCRIPTOR,每一個IMAGE_IMPORT_DESCRIPTOR一般都對應着一個dll文件愛你,編程中一般是強制類型轉換獲取到。
_IMAGE_IMPORT_DESCRIPTOR
{
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)。它包含導入名稱表的RVA。導入名稱表是一個IMAGE_THUNK_DATA結構數組。這個域被設置為0表示IMAGE_IMPORT_DESCRIPTOR結構數組的結尾,這里我的理解是一個程序可以導出不止一個的dll文件,而一個dll文件又有不止一個的導出函數,所以會有IMAGE_IMPORT_DESCRIPTOR結構體數組,且每個IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk又指向一個IAT導入名稱表
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // 導入的DLL名稱字符串(ASCII碼格式)的RVA
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses).導入地址表的RVA。IAT是一個IMAGE_THUNK_DATA結構數組
}

每個IMAGE_IMPORT_DESCRIPTOR結構指向兩個數組,這兩個數組實際上是一樣的。它們有好幾種叫法,但最常用的名稱是導入地址表(Import Address Table,IAT)和導入名稱表(Import Name Talbe,INT)
OriginalFirstThunk->INT
FirstThunk->IAT
IAT個INT一樣,都是一個IMAGE_THUNK_DATA的結構體數組,這兩個數組最后都以一個值為0的IMAGE_THUNK_DATA結構作為結尾
IMAGE_THUNK_DATA這個結構是一個與指針大小相同的共用體(或者稱為聯合)。每個IMAGE_THUNK_DATA結構對應着從可執行文件中導入的一個函數。
IMAGE_THUNK_DATA可以有如下幾種含義:
DWORD ForwarderString;// 轉發函數字符串的RVA(見上文)
DWORD Function; // 導入函數的內存地址
DWORD Ordinal; // 導入函數的序數
DWORD AddressOfData; // IMAGE_IMPORT_BY_NAME和導入函數名稱的RVA
IAT中的IMAGE_THUNK_DATA結構的用途可以分為兩種。在可執行文件中,它們或者是導入函數的序數,或者是一個IMAGE_IMPORT_BY_NAME結構的RVA。IMAGE_IMPORT_BY_NAME結構只是一個WORD類型的值,它后面跟着導入函數的名稱字符串。這個WORD類型的值是一個“提示(hint)”,它提示加載器導入函數的序號可能是什么。
當加載器加載可執行文件時,它用導入函數的實際地址來覆蓋IAT中的每個元素。這一點是理解下文的關鍵(這句話很重要)
_IMAGE_IMPORT_BY_NAME
{
WORD Hint;
BYTE Name[1];
}
另一個數組INT,本質上與IAT是一樣的。它也是一個IMAGE_THUNK_DATA結構數組。關鍵的區別在於當加載器將可執行文件加載進內存時,它並不覆蓋INT。為什么對於從DLL中導入的每組API都需要有兩個並列的數組呢?答案在於一個稱為綁定(binding)的概念。當在綁定過程(后面我會講到)中覆蓋可執行文件的IAT時,需要以某種方式保存原來的信息。而作為這個信息的副本的INT,正是這個用途。
綁定
當可執行文件被綁定時(例如通過Bind程序),其IAT中的IMAGE_THUNK_DATA結構中是導入函數的實際地址。也就是說,磁盤上的可執行文件的IAT中存儲的就是其導入的DLL中的函數在內存中的實際地址。當加載一個被綁定的可執行文件時,Windows加載器可以跳過查找每個導入函數並覆蓋IAT這一步。因為IAT中已經是正確的地址了。但是這只有正確對齊時才行
你也許會懷疑將可執行文件綁定是否保險。你可能會想,如果綁定了可執行文件,但它導入的DLL發生了變化,這時怎么辦呢?當這種情況發生時,IAT中的地址已經失效了。加載器會檢查這種情況並隨機應變。如果IAT中的地址已經失效,加載器會根據INT中的信息重新解析導入函數的地址。
確定綁定信息有效性的一個關鍵數據結構是IMAGE_BOUND_IMPORT_DESCRIPTOR。被綁定的可執行文件中有一個此結構的列表。每個IMAGE_BOUND_IMPORT_DESCRIPTOR結構表示一個綁定到的
DLL的日期/時間戳。這個列表的RVA由數據目錄中索引為IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT的元素給出。
_IMAGE_BOUND_IMPORT_DESCRIPTOR
{
DWORD TimeDateStamp; //這是包含導入的DLL的日期/時間戳的一個DWORD類型的值
WORD OffsetModuleName; //這是包含導入的DLL的名稱字符串偏移地址的一個WORD類型 的值。這個域是相對於首個IMAGE_BOUND_IMPORT_DESCRIPTOR結構的偏移(而不是RVA)。
WORD NumberOfModuleForwarderRefs;
}
一般情況下,每個導入的DLL對應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構簡單地組成一個數組。但是當綁定的API轉發到了另一個DLL上時,這個轉發到的DLL的有效性也需要檢查。在這種情況下,IMAGE_BOUND_FORWARDER_REF結構就與IMAGE_BOUND_IMPORT_DESCRIPTOR結構交叉在了一起
基址重定位
基址重定位(Base Relocations)信息告訴加載器可執行文件不能被加載到其首選地址時需要進行修改的每一個位置。對於加載器來說,幸運的是它並不需要知道地址使用的細節問題。它只知道有一個地址列表,其中的每一個地址都需要以同樣的方式進行修改
簡而言之,基址重定位信息只是可執行文件中的一個地址列表,當加載進內存時,這些地址中的值都要再加上△。為了提高系統性能,可執行文件的頁面只有在需要時才會被加載進內存(可執行文件的加載與內存映射文件類似),基址重定位信息的格式就反映了這個特性
基址重定位信息所在的節通常被稱為.reloc節,但是查找它的正確方法是通過數據目錄中索引為IMAGE_DIRECTORY_ENTRY_BASERELOC的那個元素。
基址重定位信息是一些非常簡單的IMAGE_BASE_RELOCATION結構
_IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress; //需要進行重定位的內存范圍的起始RVA
DWORD SizeOfBlock; //重定位信息的大小,其中包括IMAGE_BASE_RELOCATION自身的大小
}
至此,對PE文件格式的大致剖析結束了,以后要用到更詳細的內容的時候以后再說。
