1. 程式人生 > >PE檔案結構詳解(四)PE匯入表

PE檔案結構詳解(四)PE匯入表

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 00 00 19 30氣質FF 15表示這是一個間接呼叫,即call dword ptr [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;
使用RtlImageDirectoryEntryToData並將索引號傳1,會得到一個如上結構的指標,實際上指向一個上述結構的陣列,每個匯入的DLL都會成為陣列中的一項,也就是說,一個這樣的結構對應一個匯入的DLL。

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;
ForwarderString是轉發用的,暫時不用考慮,Function表示函式地址,如果是按序號匯入Ordinal就有用了,若是按名字匯入AddressOfData便指向名字資訊。可以看出這個結構體就是一個大的union,大家都知道union雖包含多個域但是在不同時刻代表不同的意義那到底應該是名字還是序號,該如何區分呢?可以通過Ordinal判斷,如果Ordinal的最高位是1,就是按序號匯入的,這時候,低16位就是匯入序號,如果最高位是0,則AddressOfData是一個RVA,指向一個IMAGE_IMPORT_BY_NAME結構,用來儲存名字資訊,由於Ordinal和AddressOfData實際上是同一個記憶體空間,所以AddressOfData其實只有低31位可以表示RVA,但是一個PE檔案不可能超過2G,所以最高位永遠為0,這樣設計很合理的利用了空間。實際編寫程式碼的時候微軟提供兩個巨集定義處理序號匯入:IMAGE_SNAP_BY_ORDINAL判斷是否按序號匯入,IMAGE_ORDINAL用來獲取匯入序號。

這時我們可以回頭看看OriginalFirstThunk與FirstThunk,OriginalFirstThunk指向的IMAGE_THUNK_DATA陣列包含匯入資訊,在這個陣列中只有Ordinal和AddressOfData是有用的,因此可以通過OriginalFirstThunk查詢到函式的地址。FirstThunk則略有不同,在PE檔案載入以前或者說在匯入表未處理以前,他所指向的陣列與OriginalFirstThunk中的陣列雖不是同一個,但是內容卻是相同的,都包含了匯入資訊,而在載入之後,FirstThunk中的Function開始生效,他指向實際的函式地址,因為FirstThunk實際上指向IAT中的一個位置,IAT就充當了IMAGE_THUNK_DATA陣列,載入完成後,這些IAT項就變成了實際的函式地址,即Function的意義。還是上個圖對比一下:


上圖是載入前。


上圖是載入後。

最後總結一下:

  1. 匯入表其實是一個IMAGE_IMPORT_DESCRIPTOR的陣列,每個匯入的DLL對應一個IMAGE_IMPORT_DESCRIPTOR。
  2. IMAGE_IMPORT_DESCRIPTOR包含兩個IMAGE_THUNK_DATA陣列,陣列中的每一項對應一個匯入函式。
  3. 載入前OriginalFirstThunk與FirstThunk的陣列都指向名字資訊,載入後FirstThunk陣列指向實際的函式地址。

by evil.eagle 轉載請註明出處。