天天看點

小甲魚PE詳解之輸入表(導出表)詳解(PE詳解09)

上一講:小甲魚PE詳解之輸入表(導入表)詳解2

小甲魚PE詳解之輸出表(導出表)詳解(PE詳解09)

當PE 檔案被執行的時候,Windows 加載器将檔案裝入記憶體并将導入表(Export Table) 登記的動态連結庫(一般是DLL 格式)檔案一并裝入位址空間,再根據DLL 檔案中的函數導出資訊對被執行檔案的IAT 進行修正。

( 基礎補充:很多朋友可能看到這裡會有點懵,各位看官請允許小甲魚啰嗦一下,照顧初學者。我們都明白Windows 在加載一個程式後就在記憶體中為該程式開辟一個單獨的虛拟位址空間,這樣的話在各個程式自己看來,自己就擁有幾乎任意位址的支配權,是以他自身的函數想放在哪個位址自己說了算。有一些函數很多程式都會用到,為每一個程式寫一個相同的函數看起來似乎有點浪費空間,是以Windows就整出了動态連結庫的概念,将一些常用的函數封裝成動态連結庫,等到需要的時候通過直接加載動态連結庫,将需要的函數整合到自身中,這樣就大大的節約了記憶體中資源的存放。如圖:

小甲魚PE詳解之輸入表(導出表)詳解(PE詳解09)

有一個重要的概念需要記住:動态連結庫是被映射到其他應用程式的位址空間中執行的,它和應用程式可以看成是“一體”的,動态連結庫可以使用應用程式的資源,它所擁有的資源也可以被應用程式使用,它的任何操作都是代表應用程式進行的,當動态連結庫進行打開檔案、配置設定記憶體和建立視窗等操作後,這些檔案、記憶體和視窗都是為應用程式所擁有的。是以,動态連結庫用小甲魚的話說就是“寄生蟲”! )

那導出表是幹啥用的呢? 導出表就是記載着動态連結庫的一些導出資訊。通過導出表,DLL 檔案可以向系統提供導出函數的名稱、序号和入口位址等資訊,比便Windows 加載器通過這些資訊來完成動态連接配接的整個過程。

友情提示:擴充名為.exe 的PE 檔案中一般不存在導出表,而大部分的.dll 檔案中都包含導出表。但注意,這并不是絕對的。例如純粹用作資源的.dll 檔案就不需要導出函數啦,另外有些特殊功能的.exe 檔案也會存在導出函數。是以,世事無絕對……好了,我們接下來就對導出表的結構進行分析。

導出表結構

導出表(Export Table)中的主要成分是一個表格,内含函數名稱、輸出序數等。序數是指定DLL 中某個函數的16位數字,在所指向的DLL 檔案中是獨一無二的。在此我們不提倡僅僅通過序數來索引函數的方法,這樣會給DLL 檔案的維護帶來問題。例如當DLL 檔案一旦更新或修改就可能導緻調用改DLL 的程式無法加載到需要的函數。

資料目錄表的第一個成員指向導出表,是一個IMAGE_EXPORT_DIRECTORY(以後簡稱IED)結構,IED 結構的定義如下:

IMAGE_EXPORT_DIRECTORY STRUCT Characteristics DWORD ? ; 未使用,總是定義為0 TimeDateStamp DWORD ?      ; 檔案生成時間 MajorVersion WORD ? ; 未使用,總是定義為0 MinorVersion WORD ? ; 未使用,總是定義為0 Name DWORD ? ; 子產品的真實名稱 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 項函數入口位址對應的名稱就是“MyFunction”複雜吧? 沒事,接着看你就懂了,别放棄哦~)

整個流程跟其他PE 結構一樣說起來複雜,但看圖說話倒是挺容易的。是以小甲魚還是本着實事求是的精神&……%¥#踏踏實實畫圖讓大家好了解一點吧,來,請上圖:

小甲魚PE詳解之輸入表(導出表)詳解(PE詳解09)

1. 從序号查找函數入口位址

下邊小甲魚帶大家來模拟一下Windows 裝載器查找導出函數入口位址的整個過程。如果已知函數的導出序号,如何得到函數的入口位址呢 ?

Windows 裝載器的工作步驟如下:

  1. 定位到PE 檔案頭
  2. 從PE 檔案頭中的 IMAGE_OPTIONAL_HEADER32 結構中取出資料目錄表,并從第一個資料目錄中得到導出表的RVA
  3. 從導出表的 Base 字段得到起始序号
  4. 将需要查找的導出序号減去起始序号,得到函數在入口位址表中的索引
  5. 檢測索引值是否大于導出表的 NumberOfFunctions 字段的值,如果大于後者的話,說明輸入的序号是無效的
  6. 用這個索引值在 AddressOfFunctions 字段指向的導出函數入口位址表中取出相應的項目,這就是函數入口位址的RVA 值,當函數被裝入記憶體的時候,這個RVA 值加上子產品實際裝入的基位址,就得到了函數真正的入口位址

2. 從函數名稱查找入口位址

如果已知函數的名稱,如何得到函數的入口位址呢?與使用序号來擷取入口位址相比,這個過程要相對複雜一點!

Windows 裝載器的工作步驟如下:

  1. 最初的步驟是一樣的,那就是首先得到導出表的位址
  2. 從導出表的 NumberOfNames 字段得到已命名函數的總數,并以這個數字作為循環的次數來構造一個循環
  3. 從 AddressOfNames 字段指向得到的函數名稱位址表的第一項開始,在循環中将每一項定義的函數名與要查找的函數名相比較,如果沒有任何一個函數名是符合的,表示檔案中沒有指定名稱的函數
  4. 如果某一項定義的函數名與要查找的函數名符合,那麼記下這個函數名在字元串位址表中的索引值,然後在 AddressOfNamesOrdinals 指向的數組中以同樣的索引值取出數組項的值,我們這裡假設這個值是x
  5. 最後,以 x 值作為索引值,在 AddressOfFunctions 字段指向的函數入口位址表中擷取的 RVA 就是函數的入口位址

一幫情況下病毒程式就是通過函數名稱查找入口位址的,因為病毒程式作為一段額外的代碼被附加到可執行檔案中的,如果病毒代碼中用到某些 API 的話,這些 API 的位址不可能在宿主檔案的導出表中為病毒代碼準備好。是以隻能通過在記憶體中動态查找的方法來實作擷取API 的位址。關于病毒代碼具體的實作分析,小甲魚在今後将跟大家共同研究讨論這個話題~

輸出表結構執行個體分析(具體過程将在視訊中示範,這裡不啰嗦啦~)

工具:PEinfo.exe, UltraEdit, W32DasmV10.0

解剖對象:Counter.dll

友情提示:以上工具均可在此處下載下傳無病毒無二次污染版本,在其他地方下載下傳謹慎病毒威脅。

下一講:小甲魚PE詳解之 基址重定位 詳解

繼續閱讀