當pe檔案被執行的時候,windows裝載器將檔案裝入記憶體並將匯入表中登記的dll檔案一併裝入,再根據dll檔案中的函式匯出資訊對被執行檔案的iat表進行修正。
windows 在載入乙個程式後就在記憶體中為該程式開闢乙個單獨的虛擬位址空間,這樣的話在各個程式自己看來,自己就擁有幾乎任意位址的支配權,所以他自身的函式想放在哪個位址自己說了算。有一些函式很多程式都會用到,為每乙個程式寫乙個相同的函式看起來似乎有點浪費空間,因此windows就整出了動態鏈結庫的概念,將一些常用的函式封裝成動態鏈結庫,等到需要的時候通過直接載入動態鏈結庫,將需要的函式整合到自身中,這樣就大大的節約了記憶體中資源的存放。
動態鏈結庫是被對映到其他應用程式的位址空間中執行的,它和應用程式可以看成是「一體」的,動態鏈結庫可以使用應用程式的資源,它所擁有的資源也可以被應用程式使用,它的任何操作都是代表應用程式進行的,當動態鏈結庫進行開啟檔案、分配記憶體和建立視窗等操作後,這些檔案、記憶體和視窗都是為應用程式所擁有的。
擴充套件名為.exe 的pe 檔案中一般不存在匯出表,而大部分的.dll 檔案中都包含匯出表。但注意,這並不是絕對的。例如純粹用作資源的.dll 檔案就不需要匯出函式啦,另外有些特殊功能的.exe 檔案也會存在匯出函式。
一、匯出表的結構
1、獲取匯出表的位置
匯出表的位置和大小可以從pe檔案中image_optional_header32結構的資料目錄欄位中獲取。從image_optional_directory結構的virtualaddress欄位得到的是匯入表的rva值,如果在記憶體中查詢匯入表,那麼將rva值加上pe檔案裝入的基址就是實際的位址,如果在pe檔案中查詢匯入表,那麼需要使用前面講的將rva轉換成檔案偏移的方法進行轉換。
2、匯出表的組成
匯出表中為每個匯出函式定義了匯出序號,但函式名的定義是可選的。對於定義了函式名的函式來說,既可以使用名稱匯出,也可以使用序號匯出:對於沒有定義函式名的函式來說,只能使用序號來匯出。
匯出表的起始位置有乙個image_export_directory結構,與匯入表中有多個image_import_descriptor結構不同,匯出表中只有乙個image_export_directory結構,定義如下:
image_export_directory struct
characteristics dword ? ; 未使用,總是定義為0
timedatestamp dword ? ; 檔案生成時間
majorversion word ? ; 未使用,總是定義為0
minorversion word ? ; 未使用,總是定義為0
name dword ? ; 模組的真實名稱rva
base dword ? ; 基數,加上序數就是函式位址陣列的索引值
numberoffunctions dword ? ; 匯出函式的總數
numberofnames dword ? ; 以名稱方式匯出的函式的總數
addressoffunctions dword ? ; 指向輸出函式位址的rva
addressofnames dword ? ; 指向輸出函式名字的rva
addressofnameordinals dword ? ; 指向輸出函式序號的rva
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 型別的陣列(注意不是雙字陣列)。陣列專案與檔名位址表中的專案一一對應,專案值代表函式入口位址表的索引,這樣函 數名稱與函式入口位址關聯起來。
舉個例子,加入函式名稱字串位址表的第n 項指向乙個字串「myfunction」,那麼可以去查詢 addressofnameordinals 指向的陣列的第n 項,假如第n 項中存放的值是x,則表示addressoffunctions 字段描述的位址表中的第x 項函式入口位址(aaaa)對應的名稱就是「myfunction」,這時這個函式的全部資訊就可以如下描述。
可以看到,addressofnameordinals欄位描述的陣列僅僅起了乙個橋梁的作用。
3、從序號查詢入口位址
已知函式的匯出序號,如何得到入口位址呢?
①定位到pe 檔案頭。
②從pe 檔案頭中的 image_optional_header32 結構中取出資料目錄表,並從第乙個資料目錄中得到匯出表的rva。
③從匯出表的 base 字段得到起始序號。
④將需要查詢的匯出序號減去起始序號,得到函式在入口位址表中的索引。
⑤檢測索引值是否大於匯出表的 numberoffunctions 欄位的值,如果大於後者的話,說明輸入的序號是無效的。
⑥用這個索引值在 addressoffunctions 字段指向的匯出函式入口位址表中取出相應的專案,這就是函式入口位址的rva 值,當函式被裝入記憶體的時候,這個rva 值加上模組實際裝入的基位址,就得到了函式真正的入口位址。
4、從函式名稱查詢入口位址
已知匯出函式的名稱,如何得到入口位址呢?
①最初的步驟是一樣的,那就是首先得到匯出表的位址。
②從匯出表的numberofnames 字段得到已命名函式的總數,並以這個數字作為迴圈的次數來構造乙個迴圈。
③從addressofnames 字段指向得到的函式名稱位址表的第一項開始,在迴圈中將每一項定義的函式名與要查詢的函式名相比較,如果沒有任何乙個函式名是符合的,表示檔案中沒有指定名稱的函式。
④如果某一項定義的函式名與要查詢的函式名符合,那麼記下這個函式名在字串位址表中的索引值,然後在addressofnamesordinals 指向的陣列中以同樣的索引值取出陣列項的值,我們這裡假設這個值是x。
⑤最後,以 x 值作為索引值,在 addressoffunctions 字段指向的函式入口位址表中獲取的 rva 就是函式的入口位址。
一般情況下病毒程式就是通過函式名稱查詢入口位址的,因為病毒程式作為一段額外的**被附加到可執行檔案中的,如果病毒**中用到某些 api 的話,這些 api 的位址不可能在宿主檔案的匯出表中為病毒**準備好。因此只能通過在記憶體中動態查詢的方法來實現獲取api 的位址。
PE匯出表操作
bool enumeattable pvoid pmodulebase 下面的操作會使pdosheader e lfanew的值乘以64,再進行相加,由於pdosheader為指標型別,因此在進行相加時,會乘以結構體的大小 pntheader pimage nt headers pdosheader...
PE結構 匯出表
那麼,作業系統是如何來獲取函式位址呢,也就是getprocaddress的實現,這裡就涉及到了匯出表。匯出表,會記錄這個庫函式的位址是多少,所以簡單來說getprocaddress就是查匯出表來獲取位址,如何查就是下面的話題了。匯出意味著需要提供api給他人使用,一般來說會是一些dll之類的,所以,...
PE檔案 匯入表,匯出表
typedef struct image import descriptor dword timedatestamp dword forwarderchain dword name 庫名稱字串位址 dword firstthunk iat的位址 typedef struct image data d...