PE文件結構詳解(二)可執行文件頭的最後展示了一個數組,PE文件結構詳解(三)PE導出表中解釋了其中第一項的格式,本篇文章來揭示這個數組中的第二項:IMAGE_DIRECTORY_ENTRY_IMPORT,即導入表。
也許大家注意到過,在IMAGE_DATA_DIRECTORY中,有幾項的名字都和導入表有關係,其中包括:IMAGE_DIRECTORY_ENTRY_IMPORT,IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT,IMAGE_DIRECTORY_ENTRY_IAT和IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT這幾個導入都是用來幹什麼的,他們之間又是什麼關係呢?聽我慢慢道來。
- IMAGE_DIRECTORY_ENTRY_IMPORT就是我們通常所知道的導入表,在PE文件加載時,會根據這個表裏的內容加載依賴的DLL,並填充所需函數的地址。
- IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT叫做綁定導入表,在第一種導入表導入地址的修正是在PE加載時完成,如果一個PE文件導入的DLL或者函數多那麼加載起來就會略顯的慢一些,所以出現了綁定導入,在加載以前就修正了導入表,這樣就會快一些。
- IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT叫做延遲導入表,一個PE文件也許提供了很多功能,也導入了很多其他DLL,但是並非每次加載都會用到它提供的所有功能,也不一定會用到它需要導入的所有DLL,因此延遲導入就出現了,只有在一個PE文件真正用到需要的DLL,這個DLL纔會被加載,甚至於只有真正使用某個導入函數,這個函數地址纔會被修正。
- IMAGE_DIRECTORY_ENTRY_IAT是導入地址表,前面的三個表其實是導入函數的描述,真正的函數地址是被填充在導入地址表中的。
這個代碼調用了一個RegOpenKeyW的導入函數,我們看到其opcode是FF 15 0000 19 30氣質FF 15表示這是一個間接調用,即call dwordptr [30190000];這表示要調用的地址存放在30190000這個地址中,而30190000這個地址在導入地址表的範圍內,當模塊加載時,PE加載器會根據導入表中描述的信息修正30190000這個內存中的內容。
那麼導入表裏到底記錄了那些信息,如何根據這些信息修正IAT呢?我們一起來看一下導入表的定義:
- typedef struct _IMAGE_IMPORT_DESCRIPTOR {
- union {
- DWORD Characteristics; // 0 for terminating null import descriptor
- DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
- } DUMMYUNIONNAME;
- 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;
- DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
- } IMAGE_IMPORT_DESCRIPTOR;
- typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
Characteristics和OriginalFirstThunk:一個聯合體,如果是數組的最後一項Characteristics爲0,否則OriginalFirstThunk保存一個RVA,指向一個IMAGE_THUNK_DATA的數組,這個數組中的每一項表示一個導入函數。
TimeDateStamp:映象綁定前,這個值是0,綁定後是導入模塊的時間戳。
ForwarderChain:轉發鏈,如果沒有轉發器,這個值是-1。
Name:一個RVA,指向導入模塊的名字,所以一個IMAGE_IMPORT_DESCRIPTOR描述一個導入的DLL。
FirstThunk:也是一個RVA,也指向一個IMAGE_THUNK_DATA數組。
既然OriginalFirstThunk與FirstThunk都指向一個IMAGE_THUNK_DATA數組,而且這兩個域的名字都長得很像,他倆有什麼區別呢?爲了解答這個問題,先來認識一下IMAGE_THUNK_DATA結構:
- typedef struct _IMAGE_THUNK_DATA32 {
- union {
- DWORD ForwarderString; // PBYTE
- DWORD Function; // PDWORD
- DWORD Ordinal;
- DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
- } u1;
- } IMAGE_THUNK_DATA32;
- typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
這時我們可以回頭看看OriginalFirstThunk與FirstThunk,OriginalFirstThunk指向的IMAGE_THUNK_DATA數組包含導入信息,在這個數組中只有Ordinal和AddressOfData是有用的,因此可以通過OriginalFirstThunk查找到函數的地址。FirstThunk則略有不同,在PE文件加載以前或者說在導入表未處理以前,他所指向的數組與OriginalFirstThunk中的數組雖不是同一個,但是內容卻是相同的,都包含了導入信息,而在加載之後,FirstThunk中的Function開始生效,他指向實際的函數地址,因爲FirstThunk實際上指向IAT中的一個位置,IAT就充當了IMAGE_THUNK_DATA數組,加載完成後,這些IAT項就變成了實際的函數地址,即Function的意義。還是上個圖對比一下:
上圖是加載前。
上圖是加載後。
最後總結一下:
- 導入表其實是一個IMAGE_IMPORT_DESCRIPTOR的數組,每個導入的DLL對應一個IMAGE_IMPORT_DESCRIPTOR。
- IMAGE_IMPORT_DESCRIPTOR包含兩個IMAGE_THUNK_DATA數組,數組中的每一項對應一個導入函數。
- 加載前OriginalFirstThunk與FirstThunk的數組都指向名字信息,加載後FirstThunk數組指向實際的函數地址。