PE檔案解析-輸入表、輸出表與重定位表
一、 輸入表
1、輸入表地址定位
PE檔案頭可選映像頭中資料目錄表的第二成員指向輸入表,輸入表以一個 IAMGE_IMPORT_DESCRITPTOR 陣列開始,每個被PE檔案隱式地連結進來的DLL都有一個IID,在這個陣列中沒有欄位指出該結構陣列的項數,但他最後一個單元是NULL。
資料目錄表的第二成員 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] 就對應輸入表。回憶上節文章內容可知,資料目錄表 IMAGE_DATA_DIRECTORY 中結構成員 VirtualAddress 就是輸入表的RVA了,而區塊表 IMAGE_SECTION_HEADER 中結構成員 VirtualAddress 對應區塊的RVA。通過輸入表的RVA與區塊的RVA比較,我們就能知道輸入表在哪個區塊裡面,一般輸入表都在".idata"區塊裡面。
2、輸入表結構
IID的結構如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; //指向輸入名稱表(INT)的RVA }; DWORD TimeDateStamp; //一個32位的時間標誌 DWORD ForwarderChain; //這是一個被轉向API的索引,一般為0 DWORD Name; //DLL名字,是個以00結尾的ASCII字元的RVA地址 DWORD FirstThunk; //指向輸入地址表(IAT)的RVA } IMAGE_IMPORT_DESCRIPTOR;
通過上圖 OriginalFisrtThunk 和 FirstThunk 非常相似,兩個陣列都有一個IMAGE_THUNK_DATA結構型別的元素,他是一個指標大小的聯合,每一個IAMGE_THUNK_DATA元素對應於一個從可執行檔案輸入的函式,兩個陣列的結束通過一個值為零的IAMGE_THUNK_DATA元素表示的,IMAGE_THUNK_DATA結構實際上是一個雙字,該結構不同時刻有不同的含義,定義如下:
typedef struct _IMAGE_THUNK_DATA32 { union { PBYTE ForwarderString; PDWORD Function; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; } u1; } IMAGE_THUNK_DATA32;
(1)OriginalFirstThunk:它指向輸入名稱表(簡稱INT),INT是一個 IMAGE_THUNK_DATA 結構的陣列,陣列中的每個IMAGE_THUNK_DATA 結構的成員 AddressOfData都指向IMAGE_IMPORT_BY_NAME結構。
(2)TimeDateStamp:一個32位時間標誌,該欄位可以忽略。
(3)ForwarderChain:當程式引用一個DLL中的API,而這個API又引用別的DLL的API時使用,這種情況很少出現。
(4)Name:它表示DLL 名稱的相對虛地址(譯註:相對一個用null作為結束符的ASCII字串的一個RVA,該字串是該匯入DLL檔案的名稱,如:KERNEL32.DLL)。
(5)FirstThunk:它指向輸入地址表(簡稱IAT),IAT是一個 IMAGE_THUNK_DATA 結構的陣列。
IAMGE_THUNK_DATA 值的最高位為1時,表示函式以序列號方式輸入,這時低31位(或者一個64位可執行檔案的低63位)被看做是一個函式序號,當雙字的最高位為0時,表示函式以字串型別的函式名方式輸入,這時雙字的值是一個RVA,指向一個IAMGE_IMPORT_BY_NAME結構,該結構定義如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //本函式在其所駐留DLL的輸出表中的序號
BYTE Name[1]; //輸入函式的函式名,函式名是一個ASCII碼字串,以NULL結尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
為什麼由兩個並行的指標陣列指向IAMGE_IMPORT_BY_NAME結構呢?
第一個陣列(由OriginalFirstThunk所指向)是但單獨的一項,而且不可改變,成為INT,第二個陣列(由FirstThunk所指向)是由PE裝載器重寫的,PE裝載器首先搜尋OriginalFirstThunk,如果找到了,載入程式迭代搜尋陣列中的每個指標,找到每個 IMAGE_IMPORT_BY_NAME 結構所指向的輸入函式的地址,然後載入器用函式真實入口地址來代替由FirstThunk指向的 IAMGE_IMPORT_BY_NAME 陣列所指向數組裡的元素值,JMP dword ptr[xxxxxxxx]中的[xxxxxxxx]指向FirstThunk
陣列中的一個入口,所以當PE檔案裝載記憶體後準備執行時,所有函式入口地址被排列在一起,此時輸入表中其他就不重要的,依靠IAT提供地址就可以正常執行。
有些情況,一些函式僅由序號引出,也就是說不能用函式名來呼叫它,只能通過位置呼叫它,此時 IMAGE_THUNK_DATA 的值低位字指示函式序數,而高二進位(MSB)設為1,Microsoft提供了一個方便的常量測量DWORD值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值是80000000h。第二是程式的 OriginalFirstThunk 的值為0,初始化時,系統根據FirstThunk 的值指向函式名的地址串,由地址串找到函式名,再根據函式名入口地址,然後用入口地址取代 FirstThunk
指向的地址串的原值。
二、輸出表
輸出表(Export Table)包含函式名稱,輸出序數等,序數是指定DLL中某個函式的16位數字,在所指向的DLL裡是獨一無二的。資料目錄表中的第一個成員 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] 指向輸出表,對應區塊一般為".edata"。輸出表 IAMGE_EXPORT_DIRECTORY 結構如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //未使用,總為0
DWORD TimeDateStamp; //建立輸出表建立時間(GMT時間)
WORD MajorVersion; //主版本號,一般為0
WORD MinorVersion; //次版本號,一般為0
DWORD Name; //模組的真實名稱
DWORD Base; //基數,加上序數就是函式陣列的索引值
DWORD NumberOfFunctions; //AddressOfFunctions陣列中的元素個數
DWORD NumberOfNames; //AddressOfNameS陣列中的元素個數
DWORD AddressOfFunctions; //指向函式地址陣列
DWORD AddressOfNames; //函式名字的指標地址
DWORD AddressOfNameOrdinals; //指向輸出序號陣列
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Name:指向一個ASCII字串的RVA,既模組的名字。
Base:序號的基數,按序號匯出函式的序號值從Base開始遞增。當通過序數來查詢一個輸出函式時,這個值從序數裡被減去,結果被用作進入輸出地址表(EAT)的索引。
NumberOfFunctions:輸出地址表(EAT)中的條目數量,即所有匯出函式的數量。
NumberOfNames:輸出名稱表(ENT)中的條目數量,即按名稱匯出函式的數量,這個值總是小於或者等於NumberOfFunctions的值。
AddressOfFunctions:EAT的RVA,指向一個DWORD陣列,陣列中的每一項是一個匯出函式的RVA,順序與匯出序號相同。
AddressOfNames:ENT的RVA,指向一個DWORD陣列,陣列中的每一項仍然是一個RVA,指向一個表示函式名字。
AddressOfNameOrdinals:輸出序數表的RVA,指向一個WORD陣列。陣列中的每一項與AddressOfNames中的每一項對應,表示該名字的函式在AddressOfFunctions中的序號。
下圖是一個經典的輸出表結構:
三、重定位表
重定位就是你本來這個程式理論上要佔據這個地址,但是由於某種原因,這個地址現在不能讓你霸佔,你必須轉移到別的地址,這就需要基址重定位。如果可執行檔案不在首選的地址裝入,那麼檔案中每一個定位都需要被修正。對載入器來說,它不需要知道關於地址如何使用的任何細節,它只需要知道有一系列的資料需要以某種一致的方式來修正就可以了。
對於EXE檔案來說,每個檔案總是使用獨立的虛擬地址空間,所以EXE總能夠按照這個地址裝入,不需要重定位資訊。而對於DLL來說,由於多個DLL檔案全部使用宿主EXE檔案的地址空間,不能保證裝入地址沒有被其它的DLL使用,所以DLL檔案中必須包含重定位資訊。
下面以例項 DllDemo.DLL為例講述其定位過程。
......
:0040100E 6800204000 push 00402000
......
在這個例子中彙編語句將一個指標壓棧,402000h是某一字串的指標。指令是來自一個基置為 00400000h 的DLL檔案,因此這個字串的RVA是2000h。如果DLL確實在 00400000h 處裝入,那麼指令能夠按照現在的樣子正確執行。假設當DLL執行時,Windows載入器決定將其對映到 870000h 處,此時就需要進行基址重定位,計算方式如下:
402000h + (870000h - 400000h)= 872000h
基址重定位表(Base Relocation Table)位於一個叫".reloc" 的區塊內,但是找到它們正確方式是通過資料目錄表的 IMAGE_DIRECTORY_ENTRY_BASERELOC 條目。基址重定位資料組織方法採用類似按頁分割的方法,其許多重定位塊串接在成的,每個塊存放4KB(1000h)大小的重定位資訊,每個重定位資料塊的大小必須以DWORD(4位元組)對齊,他們以IMAGE_BASE_RELOCATION 結構開始,格式如下:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; //重定位資料開始RVA地址
DWORD SizeOfBlock; //重定位塊的長度
// WORD TypeOffset[1]; //重定項位陣列
} IMAGE_BASE_RELOCATION;
VirtualAddress:這是一組重定位資料的開始RVA地址,各重定位項的地址加上這個值才是重定位項完整的RVA地址。
SizeOfBlock:是當前重定位結構的大小,因為VirtualAddress 和 SizeOfBlock 大小都是固定的4個位元組,因此這個項減去8,則是TypeOffset 大小。
TypeOffset:是一個數組。陣列每項大小為兩個位元組,共16位,它又分為高4位與低12位,高四位代表重定位型別,低12位是重定位的地址,與VirtualAddress 相加即是指向PE映像中需要修改地址資料的指標。
對於X86 可執行檔案,所有的基址重定位型別都是IMAGE_REL_BASED_HIGHLOW,在一組重定位結束的地方會出現一個型別是IAMGE_REL_BASED_ABSOLUTE的重定位,這些重定位什麼都不做,在哪裡只是填充,以便下一個IAMGE_BASE_RELOCATION 是以4個位元組分界線來對齊,所有重定位最終以一個 VirtualAddress 欄位為0的 IAMGE_BASE_RELOCATION 結構對齊。
重定位表的結構如下圖,由數個 IMAGE_BASE_RELOCATION 結構組成,每個結構VirtualAddress,SizeOfSlock 和 TypeOffset 三部分組成。