當PE 文件被執行的時候,Windows 加載器將文件裝入內存並將導入表(Export Table) 登記的動態鏈接庫(一般是DLL 格式)文件一並裝入地址空間,再根據DLL 文件中的函數導出信息對被執行文件的IAT 進行修正。
導出表就是記載着動態鏈接庫的一些導出信息。通過導出表,DLL 文件可以向系統提供導出函數的名稱、序號和入口地址等信息,比便Windows 加載器通過這些信息來完成動態連接的整個過程。
注意:擴展名為.exe 的PE 文件中一般不存在導出表,而大部分的.dll 文件中都包含導出表。但注意,這並不是絕對的。例如純粹用作資源的.dll 文件就不需要導出函數啦,另外有些特殊功能的.exe 文件也會存在導出函數。
導出表結構
導 出表(Export Table)中的主要成分是一個表格,內含函數名稱、輸出序數等。序數是指定DLL 中某個函數的16位數字,在所指向的DLL 文件中是獨一無二的。在此我們不提倡僅僅通過序數來索引函數的方法,這樣會給DLL 文件的維護帶來問題。例如當DLL 文件一旦升級或修改就可能導致調用改DLL 的程序無法加載到需要的函數。
數據目錄表的第一個成員指向導出表,是一個IMAGE_EXPORT_DIRECTORY(以后簡稱IED)結構,IED 結構的定義如下:
1 IMAGE_EXPORT_DIRECTORY STRUCT【導出表,共40字節】 2 { 3 +00 h DWORD Characteristics ; 未使用,總是定義為0 4 +04 h DWORD TimeDateStamp ; 文件生成時間 5 +08 h WORD MajorVersion ; 未使用,總是定義為0 6 +0A h WORD MinorVersion ; 未使用,總是定義為0 7 +0C h DWORD Name ; 模塊的真實名稱 8 +10 h DWORD Base ; 基數,加上序數就是函數地址數組的索引值 9 +14 h DWORD NumberOfFunctions ; 導出函數的總數 10 +18 h DWORD NumberOfNames ; 以名稱方式導出的函數的總數 11 +1C h DWORD AddressOfFunctions ; 指向輸出函數地址的RVA 12 +20 h DWORD AddressOfNames ; 指向輸出函數名字的RVA 13 +24 h DWORD AddressOfNameOrdinals ; 指向輸出函數序號的RVA 14 };IMAGE_EXPORT_DIRECTORY ENDS
這個結構中的一些字段並沒有被使用,有意義的字段說明如下。
Name:
一個RVA 值,指向一個定義了模塊名稱的字符串。如即使Kernel32.dll 文件被改名為"Ker.dll",仍然可以從這個字符串中的值得知其在編譯時的文件名是"Kernel32.dll"。
NumberOfFunctions:
文件中包含的導出函數的總數。
NumberOfNames:
被 定義函數名稱的導出函數的總數,顯然只有這個數量的函數既可以用函數名方式導出。也可以用序號方式導出,剩下 的NumberOfFunctions 減去NumberOfNames 數量的函數只能用序號方式導出。該字段的值只會小於或者等於 NumberOfFunctions 字段的值,如果這個值是0,表示所有的函數都是以序號方式導出的。
AddressOfFunctions:
一個RVA 值,指向包含全部導出函數入口地址的雙字數組。數組中的每一項是一個RVA 值,數組的項數等於NumberOfFunctions 字段的值。
Base: 導出函數序號的起始值,將AddressOfFunctions 字段指向的入口地址表的索引號加上這個起始值就是對應函數的導出 序號。假如Base 字段的值為x,那么入口地址表指定的第1個導出函數的序號就是x;第2個導出函數的序號就是x+1。總之,一個導出函數的導出序號等 於Base 字段的值加上其在入口地址表中的位置索引值。
AddressOfNames 和 AddressOfNameOrdinals:
均 為RVA 值。前者指向函數名字符串地址表。這個地址表是一個雙字數組,數組中的每一項指向一個函數名稱字符串的RVA。數組的項數等於NumberOfNames 字段的值,所有有名稱的導出函數的名稱字符串都定義在這個表中;后者指向另一個word 類型的數組(注意不是雙字數組)。數組項目與文件名地址表中的項目一一對應,項目值代表函數入口地址表的索引,這樣函 數名稱與函數入口地址關聯起來。
整個流程跟其他PE 結構圖:
1. 從序號查找函數入口地址
大家來模擬一下Windows 裝載器查找導出函數入口地址的整個過程。如果已知函數的導出序號,如何得到函數的入口地址呢 ?
Windows 裝載器的工作步驟如下:
定位到PE 文件頭
從PE 文件頭中的 IMAGE_OPTIONAL_HEADER32 結構中取出數據目錄表,並從第一個數據目錄中得到導出表的RVA
從導出表的 Base 字段得到起始序號
將需要查找的導出序號減去起始序號,得到函數在入口地址表中的索引
檢測索引值是否大於導出表的 NumberOfFunctions 字段的值,如果大於后者的話,說明輸入的序號是無效的
用這個索引值在 AddressOfFunctions 字段指向的導出函數入口地址表中取出相應的項目,這就是函數入口地址的RVA 值,當函數被裝入內存的時候,這個RVA 值加上模塊實際裝入的基地址,就得到了函數真正的入口地址
2. 從函數名稱查找入口地址
如果已知函數的名稱,如何得到函數的入口地址呢?與使用序號來獲取入口地址相比,這個過程要相對復雜一點!
Windows 裝載器的工作步驟如下:
最初的步驟是一樣的,那就是首先得到導出表的地址
從導出表的 NumberOfNames 字段得到已命名函數的總數,並以這個數字作為循環的次數來構造一個循環
從 AddressOfNames 字段指向得到的函數名稱地址表的第一項開始,在循環中將每一項定義的函數名與要查找的函數名相比較,如果沒有任何一個函數名是符合的,表示文件中沒有指定名稱的函數
如果某一項定義的函數名與要查找的函數名符合,那么記下這個函數名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的數組中以同樣的索引值取出數組項的值,我們這里假設這個值是x
最后,以 x 值作為索引值,在 AddressOfFunctions 字段指向的函數入口地址表中獲取的 RVA 就是函數的入口地址