簡介
要想了解輸入表,首先還得先從DLL檔案入手。日常生活中我們會看見一些大型軟體有很多的DLL格式的檔案,它們是“
動態程式庫檔案”,這些檔案中有很多的導入函式,這些函式不會直接被執行,當一個程式(EXE)運行時,導入函式是被程式調用執行的,其執行的代碼是不在主程式(EXE)中的一小部分函式,其真正的代碼卻在DLL檔案中。這時我們就會想,那么EXE主程式是如何找到這些需要導入的函式呢,這就要歸結於“輸入表”了,輸入表就相當於EXE檔案與DLL檔案溝通的鑰匙,形象的可以比喻成兩個城市之間交流的高速公路,所有的導入函式信息都會寫入輸入表中,在PE檔案
映射到記憶體後,windows將相應的DLL檔案裝入,EXE檔案通過“輸入表”找到相應的DLL中的導入函式,從而完成程式的正常運行,這一動態連線的過程都是由“輸入表”參與的。
PE檔案
PE(Portable Executable)檔案格式是微軟制定的一種檔案標準, 它是從普遍運用於 UNIX 作業系統的 COFF(Common ObjectFile Format)發展而來, 在 Windows 作業系統中扮演著非常重要的角色, 其格式中的數據結構通常定義在 WINNT.H 中。 本文試圖對 PE 的格式及其最新擴展進行分析 , 以便能較好地去理解Windows 作業系統以及目前微軟的最新的開發平台.Net。
PE 檔案格式是一種檔案組織的方式,裡面除了程式運行的代碼和數據外,還有一些檔案相關的重要信息,如檔案由磁碟裝載到記憶體中的起始地址、程式執行的入口地址、調用 DLL 動態程式庫函式地址等。PE 檔案主要由 DOS M Z H eader(DOS頭)、DOS Stub (DOS 塊)、PE H eader (PE 頭)、SectionTable(節表)、Sections(各個節)五部分組成。DOS M Z header 和 DOS stub 稱為 DOS 部分。緊隨 DOS stub 的是 PE 頭,其中包含 PE 裝載器用到的許多重要數據。PE 頭後面是節表,包含了指向各個節的數據信息。各個節存放了 PE 檔案的代碼、數據和資源等內容。
結構
輸入函式
輸入函式,表示被程式調用但是它的代碼不在程式代碼中的,而在dll中的函式。對於這些函式,磁碟上的執行檔只是保留相關的函式信息,如函式名,dll檔案名稱等。在程式運行前,程式是沒有保存這些函式在記憶體中的地址。當程式運行起來時,windows載入器會把相關的dll裝入記憶體,並且將輸入函式的指令與函式真在記憶體中正的地址聯繫起來。輸入表(導入表)就是用來保存這些函式的信息的。
Data Directory
在IMAGE_OPTIONAL_HEADER 中的 DataDirectory[16] 數組保存了 輸入表的RVA跟大小。通過RVA可以在OD中載入程式通過 ImageBase+RVA 找到 輸入表,或者通過RVA計算出檔案偏移地址,查看磁碟中的執行檔,通過檔案偏移地址找到輸入表。
輸入表是以一個IMAGE_IMPORT_DESCRIPTOR(IID)數組 開始的,每一個被PE檔案隱式的連結進來的dll都有一個IID,IID數組的最後一個單元用NULL表示。
IMAGE_IMPORT_DESCRIPTOR 結構:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { _ANONYMOUS_UNION union { //00h DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; //04h DWORD ForwarderChain; //08h DWORD Name; //0Ch DWORD FirstThunk; //10h } IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
其中Name是dll名字的指針。OriginalFirstThunk指向一個IMAGE_THUNK_DATA數組叫做輸入名稱表Import Name Table(INT),用來保存函式,FirstThunk也指向IMAGE_THUNK_DATA數組叫做輸入地址表Import Address Table(IAT)。
IMAGE_THUNK_DATA 結構:
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;
當IMAGE_THUNK_DATA 的值最高位為1時,表示函式是以序號方式輸入,這時低31為被當作函式序號。當最高位是0時,表示函式是以字元串類型的函式名方式輸入的,這時,IMAGE_THUNK_DATA 的值為指向IMAGE_IMPORT_BY_NAME 的結構的RVA。
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
Hint 表示這個函式在其所駐留dll的輸出表的序號,不是必須的。
Name 表示 函式名,是一個ASCII字元串以0結尾,大小不固定。
INT保存的是這個程式導入這個dll中函式信息,它是固定的不會被修改。但是IAT會在程式載入時被重寫,當程式載入時,它會被PE載入器重寫成 這些函式的在記憶體中的真正地址。即把它原來指向的IMAGE_IMPORT_BY_NAME 改成 函式真正的地址。
要兩個 IMAGE_THUNK_DATA 數組的原因
當程式載入時,IAT 會被PE載入器重寫,PE載入器先搜尋INT,PE載入器疊代搜尋INT數組中的每個指針,找出 INT所指向的IMAGE_IMPORT_BY_NAME結構中的函式在記憶體中的真正的地址,並把它替代原來IAT中的值。當完成後,INT就沒有用了,程式只需要IAT就可以正常運行了。
看下面圖,這個是可執行程式在磁碟中的時候:
這個是當程式被載入的是後:
實例分析
先找到輸入表RVA,通過IMAGE_OPTIONAL_HEADER 中的最後一個項 IMAGE_DATA_DIRECTORY 可以知道 輸入表相對與PE檔案頭的偏移量為80h可以找到輸入表達RVA。
圖片1
但這個是RVA不是檔案偏移地址。通過轉換可以知道,輸入表的檔案偏移地址為850h,
查看850h,即IID,可以看到這個程式有兩個IID,即連結了 兩個dll,看到一個IID,可以知道它的OriginalFirstThunk 是 2098h FirstThunk 為200Ch,它們轉換後的檔案偏移地址分別為898h 和 80Ch
圖片2 IID數組
查看OriginalFirstThunk跟FirstThunk ,可以發現這裡INT跟IAT的內容是一樣的,先看看第一個函式的信息,因為第一位為1,所以這裡00002122h 表示 IMAGE_IMPORT_BY_NAME 的RVA,轉化為檔案偏移值為922h
圖片3 INT跟IAT
查看922h 可以看函式名。
圖片4:
我們可以從上面看到程式在磁碟中時 INT 與IAT內容一樣,都是指向 IMAGE_IMPORT_BY_NAME 。用OD載入程式,查看INT與IAT的內容
圖片5 INT
圖片6 IAT
可以發現INT沒有發生變化,IAT變成了例如77D3C702h,IAT中的RVA被改成了函式的真正的地址。
查看77D3C702h,就可以看到這個函式
圖片7