標籤:des os io strong ar for 檔案 資料 div
先來看一個可執行檔的執行個體:本常式開啟一PE檔案,將所有引入dll和對應的函數名讀入一編輯控制項,同時顯示 IMAGE_IMPORT_DESCRIPTOR 結構各域值。
C:\QQDownload\blah.EXE
================[ IMAGE_IMPORT_DESCRIPTOR ]=============
OriginalFirstThunk = 303C
TimeDateStamp = 0
ForwarderChain = 0
Name = KERNEL32.dll
FirstThunk = 3064
Hint Function
-----------------------------------------
0 CreateFileA
0 FreeLibrary
0 ExitProcess
0 LoadLibraryA
0 ReadFile
0 WriteFile
0 GetProcAddress
================[ IMAGE_IMPORT_DESCRIPTOR ]=============
OriginalFirstThunk = 305C
TimeDateStamp = 0
ForwarderChain = 0
Name = USER32.dll
FirstThunk = 3084
Hint Function
-----------------------------------------
0 MessageBoxA
上面的資料我們如何得到的呢?
理論:
一.首先,您得瞭解什麼是引入函數。一個引入函數是被某模組調用的但又不在調用者模組中的函數,因而命名為"import(引入)"。引入函數實際位於一個或者更多的DLL裡。調用者模組裡只保留一些函數資訊,包括函數名及其駐留的DLL名。現在,我們怎樣才能找到PE檔案中儲存的資訊呢? 轉到 data directory 尋求答案吧。再回顧一把,下面就是 PE header:
IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS
optional header 最後一個成員就是 data directory(資料目錄):
IMAGE_OPTIONAL_HEADER32 STRUCT
....
LoaderFlags dd ?
NumberOfRvaAndSizes dd ?
DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS
data directory 是一個 IMAGE_DATA_DIRECTORY 結構數組,共有16個成員。如果您還記得節表可以看作是PE檔案各節的根目錄的話,也可以認為 data directory 是儲存在這些節裡的邏輯元素的根目錄。明確點,data directory 包含了PE檔案中各重要資料結構的位置和尺寸資訊。每個成員包含了一個重要資料結構的資訊。
Member |
Info inside |
0 |
Export symbols |
1 |
Import symbols |
2 |
Resources |
3 |
Exception |
4 |
Security |
5 |
Base relocation |
6 |
Debug |
7 |
Copyright string |
8 |
Unknown |
9 |
Thread local storage (TLS) |
10 |
Load configuration |
11 |
Bound Import |
12 |
Import Address Table |
13 |
Delay Import |
14 |
COM descriptor |
上面那些金色顯示的是我熟悉的。瞭解 data directory 包含域後,我們可以仔細研究它們了。data directory 的每個成員都是 IMAGE_DATA_DIRECTORY 結構類型的,其定義如下所示:
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress dd ?
isize dd ?
IMAGE_DATA_DIRECTORY ENDS
VirtualAddress 實際上是資料結構的相對虛擬位址(RVA)。比如,如果該結構是關於import symbols的,該域就包含指向IMAGE_IMPORT_DESCRIPTOR 數組的RVA。
isize 含有VirtualAddress所指向資料結構的位元組數。
下面就是如何找尋PE檔案中重要資料結構的一般方法:
1. 從 DOS header 定位到 PE header
2. 從 optional header 讀取 data directory 的地址。
3. IMAGE_DATA_DIRECTORY 結構尺寸乘上找尋結構的索引號: 比如您要找尋import symbols的位置資訊,必須用IMAGE_DATA_DIRECTORY 結構尺寸(8 bytes)乘上1(import symbols在data directory中的索引號)。
4. 將上面的結果加上data directory地址,我們就得到包含所查詢資料結構資訊的 IMAGE_DATA_DIRECTORY 結構項。
小段總結:以上說明data directory是一個結構數組,這裡麵包含16個結構成員,每一個結構成員是IMAGE_DATA_DIRECTORY 結構類型,而這種結構類型包含VirtualAddress dd ?
isize dd ?
二.現在我們開始真正討論引入表了。
從上面已經分析過了,這是一個IMAGE_DATA_DIRECTORY 結構類型,這個結構類型中包含VirtualAddress,這個裡面放的是引入表地址
什麼是引入表呢?
引入表實際上是一個 IMAGE_IMPORT_DESCRIPTOR 結構數組。如下:
IMAGE_IMPORT_DESCRIPTOR STRUCT
union
Characteristics dd ?
OriginalFirstThunk dd ?
ends
TimeDateStamp dd ?
ForwarderChain dd ?
Name1 dd ?
FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS
結構第一項是一個union子結構。 事實上,這個union子結構只是給 OriginalFirstThunk 增添了個別名,您也可以稱其為"Characteristics"。 該成員項含有指向一個 IMAGE_THUNK_DATA 結構數組的RVA。
什麼是 IMAGE_THUNK_DATA? 這是一個dword類型的集合。通常我們將其解釋為指向一個 IMAGE_IMPORT_BY_NAME 結構的指標。注意 IMAGE_THUNK_DATA 包含了指向一個IMAGE_IMPORT_BY_NAME 結構的指標: 而不是結構本身。
我們用通俗的語言表示就是:
現有幾個 IMAGE_IMPORT_BY_NAME 結構,我們收集起這些結構的RVA (IMAGE_THUNK_DATA)組成一個數組,並以0結尾,然後再將數組的RVA放入 OriginalFirstThunk。
此 IMAGE_IMPORT_BY_NAME 結構存有一個引入函數的相關資訊。再來研究 IMAGE_IMPORT_BY_NAME 結構到底是什麼樣子的呢:
IMAGE_IMPORT_BY_NAME STRUCT
Hint dw ?
Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS
Hint 指示本函數在其所駐留DLL的引出表中的索引號。該域被PE裝載器用來在DLL的引出表裡快速查詢函數。該值不是必須的,一些連接器將此值設為0。
Name1 含有引入函數的函數名。函數名是一個ASCIIZ字串。注意這裡雖然將Name1的大小定義成位元組,其實它是可變尺寸域,只不過我們沒有更好方法來表示結構中的可變尺寸域。The structure is provided so that you can refer to the data structure with descriptive names.
小段總結: 可以這樣理解上面這段話:
OriginalFirstThunk中的每個成員項實際上是一個指向IMAGE_THUNK_DATA結構數組中的每個成員
IMAGE_THUNK_DATA結構數組中存放的是指向IMAGE_IMPORT_BY_NAME 結構數組的指標
IMAGE_IMPORT_BY_NAME結構數組存放的是每個函數的RAV
三.好了,如果您還在犯糊塗,就朝這邊看過來: 現在有幾個 IMAGE_IMPORT_BY_NAME 結構,同時您又建立了兩個結構數組,並同樣寸入指向那些 IMAGE_IMPORT_BY_NAME 結構的RVAs,這樣兩個數組就包含相同數值了(可謂相當精確的複製啊)。 最後您決定將第一個數組的RVA賦給 OriginalFirstThunk,第二個數組的RVA賦給 FirstThunk,這樣一切都很清楚了。
OriginalFirstThunk |
|
IMAGE_IMPORT_BY_NAME |
|
FirstThunk |
| |
|
|
|
| |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
... |
IMAGE_THUNK_DATA |
|
---> |
---> |
---> |
---> |
---> |
---> |
|
Function 1 |
Function 2 |
Function 3 |
Function 4 |
... |
Function n |
|
<--- |
<--- |
<--- |
<--- |
<--- |
<--- |
|
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
... |
IMAGE_THUNK_DATA |
|
現在您應該明白我的意思。不要被IMAGE_THUNK_DATA這個名字弄糊塗: 它僅是指向 IMAGE_IMPORT_BY_NAME 結構的RVA。 如果將 IMAGE_THUNK_DATA 字眼想象成RVA,就更容易明白了。OriginalFirstThunk 和 FirstThunk 所指向的這兩個數組大小取決於PE檔案從DLL中引入函數的數目。比如,如果PE檔案從kernel32.dll中引入10個函數,那麼IMAGE_IMPORT_DESCRIPTOR 結構的 Name1域包含指向字串"kernel32.dll"的RVA,同時每個IMAGE_THUNK_DATA 數組有10個元素。
下一個問題是: 為什麼我們需要兩個完全相同的數組? 為了回答該問題,我們需要瞭解當PE檔案被裝載到記憶體時,PE裝載器將尋找IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 這些結構數組,以此決定引入函數的地址。然後用引入函數真真實位址來替代由FirstThunk指向的 IMAGE_THUNK_DATA 數組裡的元素值。因此當PE檔案準備執行時,已轉換成:
OriginalFirstThunk |
|
IMAGE_IMPORT_BY_NAME |
|
FirstThunk |
| |
|
|
|
| |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
IMAGE_THUNK_DATA |
... |
IMAGE_THUNK_DATA |
|
---> |
---> |
---> |
---> |
---> |
---> |
|
Function 1 |
Function 2 |
Function 3 |
Function 4 |
... |
Function n |
|
|
Address of Function 1 |
Address of Function 2 |
Address of Function 3 |
Address of Function 4 |
... |
Address of Function n |
|
由OriginalFirstThunk 指向的RVA數組始終不會改變,所以若還反過頭來尋找引入函數名,PE裝載器還能找尋到。
當然再簡單的事物都有其複雜的一面。有些情況下一些函數僅由序數引出,也就是說您不能用函數名來調用它們: 您只能用它們的位置來調用。此時,調用者模組中就不存在該函數的IMAGE_IMPORT_BY_NAME 結構。不同的,對應該函數的 IMAGE_THUNK_DATA 值的低位字指示函數序數,而最高二進位 (MSB)設為1。例如,如果一個函數只由序數引出且其序數是1234h,那麼對應該函數的 IMAGE_THUNK_DATA 值是80001234h。Microsoft提供了一個方便的常量來測試dword值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值為80000000h。
假設我們要列出某個PE檔案的所有引入函數,可以照著下面步驟走:
- 校正檔案是否是有效PE。
- 從 DOS header 定位到 PE header。
- 擷取位於 OptionalHeader 資料目錄位址。
- 轉至資料目錄的第二個成員提取其VirtualAddress值。
- 利用上值定位第一個 IMAGE_IMPORT_DESCRIPTOR 結構。
- 檢查 OriginalFirstThunk值。若不為0,順著 OriginalFirstThunk 裡的RVA值轉入那個RVA數組。若 OriginalFirstThunk 為0,就改用FirstThunk值。有些連接器產生PE檔案時會置OriginalFirstThunk值為0,這應該算是個bug。不過為了安全起見,我們還是檢查 OriginalFirstThunk值先。
- 對於每個數組元素,我們比對元素值是否等於IMAGE_ORDINAL_FLAG32。如果該元素值的最高二進位為1, 那麼函數是由序數引入的,可以從該值的低位元組提取序數。
- 如果元素值的最高二進位為0,就可將該值作為RVA轉入 IMAGE_IMPORT_BY_NAME 數組,跳過 Hint 就是函數名字了。
- 再跳至下一個數組元素提取函數名一直到數組底部(它以null結尾)。現在我們已遍曆完一個DLL的引入函數,接下去處理下一個DLL。
- 即跳轉到下一個 IMAGE_IMPORT_DESCRIPTOR 並處理之,如此這般迴圈直到數組見底。(IMAGE_IMPORT_DESCRIPTOR 數組以一個全0域元素結尾)。
破解軟體感悟-PE檔案格式之Import Table(引入表)(四)