本文為轉(zhuǎn)載文章,整理自小甲魚老師講的PE結(jié)構(gòu)課程; 一,、導(dǎo)入表的結(jié)構(gòu) 在 PE文件頭的 IMAGE_OPTIONAL_HEADER32 結(jié)構(gòu)中的 DataDirectory(數(shù)據(jù)目錄表) 的第二個成員就是指向輸入表(導(dǎo)入表)的,。而輸入表是以一個 IMAGE_IMPORT_DESCRIPTOR(簡稱IID) 的數(shù)組開始。每個被 PE文件鏈接進(jìn)來的 DLL文件都分別對應(yīng)一個 IID數(shù)組結(jié)構(gòu),。在這個 IID數(shù)組中,,并沒有指出有多少個項(xiàng)(就是沒有明確指明有多少個鏈接文件),但它最后是以一個全為NULL(0) 的 IID 作為結(jié)束的標(biāo)志,。 IMAGE_IMPORT_DESCRIPTOR 結(jié)構(gòu)定義如下: IMAGE_IMPORT_DESCRIPTOR STRUCT union Characteristics DWORD ? OriginalFirstThunk DWORD ? ends TimeDateStamp DWORD ? ForwarderChain DWORD ? Name DWORD ? FirstThunk DWORD ? IMAGE_IMPORT_DESCRIPTOR ENDS 成員介紹: OriginalFirstThunk 它指向first thunk,,IMAGE_THUNK_DATA,該 thunk 擁有 Hint 和 Function name 的地址,。 TimeDateStamp 該字段可以忽略,。如果那里有綁定的話它包含時間/數(shù)據(jù)戳(time/data stamp)。如果它是0,,就沒有綁定在被導(dǎo)入的DLL中發(fā)生,。在最近,它被設(shè)置為0xFFFFFFFF以表示綁定發(fā)生,。 ForwarderChain 一般情況下我們也可以忽略該字段,。在老版的綁定中,它引用API的第一個forwarder chain(傳遞器鏈表),。它可被設(shè)置為0xFFFFFFFF以代表沒有forwarder,。 Name 它表示DLL 名稱的相對虛地址(譯注:相對一個用null作為結(jié)束符的ASCII字符串的一個RVA,該字符串是該導(dǎo)入DLL文件的名稱,,如:KERNEL32.DLL),。 FirstThunk 它包含由IMAGE_THUNK_DATA定義的 first thunk數(shù)組的虛地址,PE裝載器(loader)通過用函數(shù)虛地址初始化thunk,。在Orignal First Thunk缺席下,,它指向first thunk:Hints和The Function names的thunks。 These two import tables illustrate the different between import table with and without the original first thunk.我們看到:OriginalFirstThunk 和 FirstThunk 他們都指向兩個類型為IMAGE_THUNK_DATA 的數(shù)組,,它是一個指針大小的聯(lián)合(union)類型,。每一個IMAGE_THUNK_DATA 結(jié)構(gòu)定義一個導(dǎo)入函數(shù)信息(即指向結(jié)構(gòu)為IMAGE_IMPORT_BY_NAME 的家伙,這家伙稍后再議),,然后數(shù)組最后以一個內(nèi)容為0 的 IMAGE_THUNK_DATA 結(jié)構(gòu)作為結(jié)束標(biāo)志,。 我們得到 IMAGE_THUNK_DATA 結(jié)構(gòu)的定義如下: IMAGE_THUNK_DATA STRUC union u1 ForwarderString DWORD ? ; 指向一個轉(zhuǎn)向者字符串的RVA Function DWORD ? ; 被輸入的函數(shù)的內(nèi)存地址 Ordinal DWORD ? ; 被輸入的API 的序數(shù)值 AddressOfData DWORD ? ; 指向 IMAGE_IMPORT_BY_NAME ends IMAGE_THUNK_DATA ENDS 我們可以看出由于是union結(jié)構(gòu),所以IMAGE_THUNK_DATA 事實(shí)上是一個雙字大小,。該結(jié)構(gòu)在不同時候賦予不同的意義,。 那我們怎么來區(qū)分何時是何意義呢? 規(guī)定如下: 當(dāng) IMAGE_THUNK_DATA 值的最高位為 1時,,表示函數(shù)以序號方式輸入,,這時候低 31位被看作一個函數(shù)序號,。 當(dāng) IMAGE_THUNK_DATA 值的最高位為 0時,表示函數(shù)以字符串類型的函數(shù)名方式輸入,,這時雙字的值是一個 RVA,,指向一個 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)。 好,,那接著我們討論下指向的這個 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu),。IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)僅僅只有一個字型數(shù)據(jù)的大小,存有一個輸入函數(shù)的相關(guān)信息結(jié)構(gòu),。其結(jié)構(gòu)如下: IMAGE_IMPORT_BY_NAME STRUCT Hint WORD ? Name BYTE ? IMAGE_IMPORT_BY_NAME ENDS 結(jié)構(gòu)中的 Hint 字段也表示函數(shù)的序號,,不過這個字段是可選的,有些編譯器總是將它設(shè)置為 0,,Name 字段定義了導(dǎo)入函數(shù)的名稱字符串,,這是一個以 0 為結(jié)尾的字符串。 輸入地址表(IAT) 為什么由兩個并行的指針數(shù)組同時指向 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)呢,?第一個數(shù)組(由 OriginalFirstThunk 所指向)是單獨(dú)的一項(xiàng),,而且不能被改寫,我們前邊稱為 INT,。第二個數(shù)組(由 FirstThunk 所指向)事實(shí)上是由 PE 裝載器重寫的,。 PE 裝載器首先搜索 OriginalFirstThunk ,找到之后加載程序迭代搜索數(shù)組中的每個指針,,找到每個 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)所指向的輸入函數(shù)的地址,然后加載器用函數(shù)真正入口地址來替代由 FirstThunk 數(shù)組中的一個入口,,因此我們稱為輸入地址表(IAT),。所以,當(dāng)我們的 PE 文件裝載內(nèi)存后準(zhǔn)備執(zhí)行時,,剛剛的圖就會轉(zhuǎn)化為下圖: 此時,,輸入表中其他部分就不重要了,程序依靠 IAT 提供的函數(shù)地址就可正常運(yùn)行,。 二,、導(dǎo)出表的結(jié)構(gòu) 導(dǎo)出表就是記載著動態(tài)鏈接庫的一些導(dǎo)出信息。通過導(dǎo)出表,,DLL 文件可以向系統(tǒng)提供導(dǎo)出函數(shù)的名稱,、序號和入口地址等信息,以便Windows 加載器通過這些信息來完成動態(tài)連接的整個過程,。 友情提示:擴(kuò)展名為.exe 的PE 文件中一般不存在導(dǎo)出表,,而大部分的.dll 文件中都包含導(dǎo)出表。但注意,,這并不是絕對的,。例如純粹用作資源的.dll 文件就不需要導(dǎo)出函數(shù)啦,,另外有些特殊功能的.exe 文件也會存在導(dǎo)出函數(shù)。所以,,世事無絕對……好了,,我們接下來就對導(dǎo)出表的結(jié)構(gòu)進(jìn)行分析。 導(dǎo)出表(Export Table)中的主要成分是一個表格,,內(nèi)含函數(shù)名稱,、輸出序數(shù)等。序數(shù)是指定DLL 中某個函數(shù)的16位數(shù)字,,在所指向的DLL 文件中是獨(dú)一無二的,。在此我們不提倡僅僅通過序數(shù)來索引函數(shù)的方法,這樣會給DLL 文件的維護(hù)帶來問題,。例如當(dāng)DLL 文件一旦升級或修改就可能導(dǎo)致調(diào)用改DLL 的程序無法加載到需要的函數(shù),。 數(shù)據(jù)目錄表的第一個成員指向?qū)С霰恚且粋€IMAGE_EXPORT_DIRECTORY(以后簡稱IED)結(jié)構(gòu),,IED 結(jié)構(gòu)的定義如下: IMAGE_EXPORT_DIRECTORY STRUCT CharacteristicsDWORD ?; 未使用,,總是定義為0 TimeDateStamp DWORD ? ; 文件生成時間 MajorVersion WORD ? ; 未使用,總是定義為0 MinorVersion WORD ? ; 未使用,,總是定義為0 Name DWORD? ; 模塊的真實(shí)名稱 Base DWORD? ; 基數(shù),,加上序數(shù)就是函數(shù)地址數(shù)組的索引值 NumberOfFunctionsDWORD ?; 導(dǎo)出函數(shù)的總數(shù) NumberOfNames DWORD ? ; 以名稱方式導(dǎo)出的函數(shù)的總數(shù) AddressOfFunctionsDWORD ?; 指向輸出函數(shù)地址的RVA AddressOfNamesDWORD ?; 指向輸出函數(shù)名字的RVA AddressOfNameOrdinalsDWORD ?; 指向輸出函數(shù)序號的RVA IMAGE_EXPORT_DIRECTORY ENDS 這個結(jié)構(gòu)中的一些字段并沒有被使用,有意義的字段說明如下,。 Name:一個RVA 值,,指向一個定義了模塊名稱的字符串。如即使Kernel32.dll 文件被改名為"Ker.dll",,仍然可以從這個字符串中的值得知其在編譯時的文件名是"Kernel32.dll",。 NumberOfFunctions:文件中包含的導(dǎo)出函數(shù)的總數(shù)。 NumberOfNames:被定義函數(shù)名稱的導(dǎo)出函數(shù)的總數(shù),,顯然只有這個數(shù)量的函數(shù)既可以用函數(shù)名方式導(dǎo)出,。也可以用序號方式導(dǎo)出,剩下 的NumberOfFunctions 減去NumberOfNames 數(shù)量的函數(shù)只能用序號方式導(dǎo)出,。該字段的值只會小于或者等于 NumberOfFunctions 字段的值,,如果這個值是0,表示所有的函數(shù)都是以序號方式導(dǎo)出的,。 AddressOfFunctions:一個RVA 值,,指向包含全部導(dǎo)出函數(shù)入口地址的雙字?jǐn)?shù)組。數(shù)組中的每一項(xiàng)是一個RVA 值,,數(shù)組的項(xiàng)數(shù)等于NumberOfFunctions 字段的值,。 Base:導(dǎo)出函數(shù)序號的起始值,將AddressOfFunctions 字段指向的入口地址表的索引號加上這個起始值就是對應(yīng)函數(shù)的導(dǎo)出 序號,。假如Base 字段的值為x,,那么入口地址表指定的第1個導(dǎo)出函數(shù)的序號就是x,;第2個導(dǎo)出函數(shù)的序號就是x+1??傊?,一個導(dǎo)出函數(shù)的導(dǎo)出序號等 于Base 字段的值加上其在入口地址表中的位置索引值。 AddressOfNames 和 AddressOfNameOrdinals:均為RVA 值,。前者指向函數(shù)名字符串地址表,。這個地址表是一個雙字?jǐn)?shù)組,數(shù)組中的每一項(xiàng)指向一個函數(shù)名稱字符串的RVA,。數(shù)組的項(xiàng)數(shù)等于NumberOfNames 字段的值,,所有有名稱的導(dǎo)出函數(shù)的名稱字符串都定義在這個表中;后者指向另一個word 類型的數(shù)組(注意不是雙字?jǐn)?shù)組),。數(shù)組項(xiàng)目與文件名地址表中的項(xiàng)目一一對應(yīng),,項(xiàng)目值代表函數(shù)入口地址表的索引,這樣函 數(shù)名稱與函數(shù)入口地址關(guān)聯(lián)起來,。(舉個例子說,,加入函數(shù)名稱字符串地址表的第n 項(xiàng)指向一個字符串“MyFunction”,那么可以去查找 AddressOfNameOrdinals 指向的數(shù)組的第n 項(xiàng),,假如第n 項(xiàng)中存放的值是x,,則表示AddressOfFunctions 字段描述的地址表中的第x 項(xiàng)函數(shù)入口地址對應(yīng)的名稱就是“MyFunction”復(fù)雜吧? 沒事,接著看你就懂了,,別放棄哦~) 整個流程跟其他PE 結(jié)構(gòu)一樣說起來復(fù)雜,,但看圖說話倒是挺容易的。所以小甲魚還是本著實(shí)事求是的精神&……%¥#踏踏實(shí)實(shí)畫圖讓大家好理解一點(diǎn)吧,,來,,請上圖: 1. 從序號查找函數(shù)入口地址 下邊小甲魚帶大家來模擬一下Windows 裝載器查找導(dǎo)出函數(shù)入口地址的整個過程。如果已知函數(shù)的導(dǎo)出序號,,如何得到函數(shù)的入口地址呢 ? Windows 裝載器的工作步驟如下: 定位到PE 文件頭 從PE 文件頭中的 IMAGE_OPTIONAL_HEADER32 結(jié)構(gòu)中取出數(shù)據(jù)目錄表,,并從第一個數(shù)據(jù)目錄中得到導(dǎo)出表的RVA 從導(dǎo)出表的 Base 字段得到起始序號 將需要查找的導(dǎo)出序號減去起始序號,,得到函數(shù)在入口地址表中的索引 檢測索引值是否大于導(dǎo)出表的 NumberOfFunctions 字段的值,,如果大于后者的話,說明輸入的序號是無效的 用這個索引值在 AddressOfFunctions 字段指向的導(dǎo)出函數(shù)入口地址表中取出相應(yīng)的項(xiàng)目,,這就是函數(shù)入口地址的RVA 值,,當(dāng)函數(shù)被裝入內(nèi)存的時候,這個RVA 值加上模塊實(shí)際裝入的基地址,,就得到了函數(shù)真正的入口地址 2. 從函數(shù)名稱查找入口地址 如果已知函數(shù)的名稱,,如何得到函數(shù)的入口地址呢?與使用序號來獲取入口地址相比,,這個過程要相對復(fù)雜一點(diǎn),! Windows 裝載器的工作步驟如下: 最初的步驟是一樣的,,那就是首先得到導(dǎo)出表的地址 從導(dǎo)出表的 NumberOfNames 字段得到已命名函數(shù)的總數(shù),并以這個數(shù)字作為循環(huán)的次數(shù)來構(gòu)造一個循環(huán) 從 AddressOfNames 字段指向得到的函數(shù)名稱地址表的第一項(xiàng)開始,,在循環(huán)中將每一項(xiàng)定義的函數(shù)名與要查找的函數(shù)名相比較,,如果沒有任何一個函數(shù)名是符合的,表示文件中沒有指定名稱的函數(shù) 如果某一項(xiàng)定義的函數(shù)名與要查找的函數(shù)名符合,,那么記下這個函數(shù)名在字符串地址表中的索引值,,然后在 AddressOfNamesOrdinals 指向的數(shù)組中以同樣的索引值取出數(shù)組項(xiàng)的值,我們這里假設(shè)這個值是x 最后,,以 x 值作為索引值,,在 AddressOfFunctions 字段指向的函數(shù)入口地址表中獲取的 RVA 就是函數(shù)的入口地址 一幫情況下病毒程序就是通過函數(shù)名稱查找入口地址的,因?yàn)椴《境绦蜃鳛橐欢晤~外的代碼被附加到可執(zhí)行文件中的,,如果病毒代碼中用到某些 API 的話,,這些 API 的地址不可能在宿主文件的導(dǎo)出表中為病毒代碼準(zhǔn)備好。因此只能通過在內(nèi)存中動態(tài)查找的方法來實(shí)現(xiàn)獲取API 的地址,。關(guān)于病毒代碼具體的實(shí)現(xiàn)分析,,小甲魚在今后將跟大家共同研究討論這個話題~ |
|