Import表的重建

看雪資料發表於2015-11-15

Import表的重建

標題:重建 PE 檔案的輸入表

原著:TiTi/BLiZZARD

翻譯:Sun Bird [CCG]

日期:2000年5月24日


1. 前言
=======

大家好 :) 我之所以寫這篇短文,是由於我在 Dump 時發現,很多加壓、加密軟體都使
得輸入表(Import Table)不可用,所以 Dump 出的可執行檔案必須要重建輸入表。而在普
通的講授 Win32 彙編的站點上我沒有找到這樣的介紹,所以如果你對此感興趣,那麼這篇
短文對你會有些幫助。

例如,為了讓從記憶體中 Dump 出的經 PETite v2.1 壓縮過的可執行檔案正常執行,必
須重建輸入表。(對於 ASPack、PEPack、PESentry……也同樣)這就是所有 Dump 軟體都
具備重建輸入表功能的原因(例如 G-RoM/UCF 製作的 Phoenix Engine(ProcDump 內含),
或者由 Virogen/PC 和我製作的 PE Rebuilder)。

鑑於這個問題十分特殊,而且比較複雜,所以我假定你已經瞭解了 PE 檔案結構。(你
需要閱讀有關 PE 檔案的文件)

2. 預備知識
===========

首先是一些關於輸入表和 RVA/VA 的簡介。

輸入表的相對虛擬地址(RVA)儲存在 PE 檔案頭部的相應目錄入口(它的偏移量為
[ PE 檔案頭偏移量+80h ])。由於是虛擬偏移量,所以它和檔案輸入表中的偏移量(VA)
是不匹配的(除非檔案純粹是剛剛從記憶體中 Dump 出來的)。於是我們首先要做的事情是,
找到 PE 檔案的輸入表,將 RVA 轉換為相應的 VA。為此,我們可以採用不同的辦法:你可
以自行編制軟體來分析塊(Sections)目錄並計算 VA,但最簡單的辦法是使用專門為此設
計的應用程式介面(API)。這個 API 包括在 IMAGEHLP.DLL(Win9X 和 NT 系統都使用的
一個庫)中,名為 imageRvaToVa。下面是對它的描述(完整的內容詳見 MSDN 庫):

# LPVOID imageRvaToVa(
# IN PIMAGE_NT_HEADERS NtHeaders,
# IN LPVOID Base,
# IN DWORD Rva,
# IN OUT PIMAGE_SECTION_HEADER *LastRvaSection
#);
#
# 引數:
#
# NtHeaders
#
# 指示一個 IMAGE_NT_HEADERS 結構。透過呼叫 imageNtHeader 函式可以獲得這個結構。
#
# Base
#
# 指定透過呼叫 MapViewOfFile 函式對映入記憶體的一個映象的基址(Base Address)。
#
# Rva
#
# 指定相對虛擬地址的位置。
#
# LastRvaSection
#
# 指向一個指定的最終 RVA 塊的 IMAGE_SECTION_HEADER 結構。這是一個可選引數。當被
#指定時,它指向一個變數,該變數包含指定映象的最後塊值,以便將 RVA 轉換為 VA。

就這麼簡單。你只需要將 PE 檔案對映入記憶體,然後呼叫這個函式就能夠得到輸入表的正
確 VA。

注意,下面我會忽略所有的 RVA/VA 註釋,但是,當你對重建的 PE 檔案進行讀出或寫入
RVAs 操作時,不要忘記它們之間的轉換。

3. 完整說明
===========

這裡是一個完整改變輸入表的例子(這個 PE 檔案的輸入表已經被 PETite v2.1 壓縮過,
並且是直接從記憶體中 Dump 出來的):

我們用“`”表示 00,用“-”表示非字串

0000C1E8h : 00 00 00 00 00 00 00 00 00 00 00 00 BA C2 00 00 ````````````----
0000C1F8h : 38 C2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ----````````````
0000C208h : C5 C2 00 00 44 C2 00 00 00 00 00 00 00 00 00 00 --------````````
0000C218h : 00 00 00 00 D2 C2 00 00 54 C2 00 00 00 00 00 00 ````--------````
0000C228h : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ````````````````
0000C238h : 7F 89 E7 77 4C BC E8 77 00 00 00 00 E6 9F F1 77 --------````----
0000C248h : 1A 38 F1 77 10 40 F1 77 00 00 00 00 4F 1E D8 77 --------````----
0000C258h : 00 00 00 00 00 00 4D 65 73 73 61 67 65 42 6F 78 ``````MessageBox
0000C268h : 41 00 00 00 77 73 70 72 69 6E 74 66 41 00 00 00 A```wsprintfA```
0000C278h : 45 78 69 74 50 72 6F 63 65 73 73 00 00 00 4C 6F ExitProcess```Lo
0000C288h : 61 64 4C 69 62 72 61 72 79 41 00 00 00 00 47 65 adLibraryA````Ge
0000C298h : 74 50 72 6F 63 41 64 64 72 65 73 73 00 00 00 00 tProcAddress````
0000C2A8h : 47 65 74 4F 70 65 6E 46 69 6C 65 4E 61 6D 65 41 GetOpenFileNameA
0000C2B8h : 00 00 55 53 45 52 33 32 2E 64 6C 6C 00 4B 45 52 ``USER32.dll`KER
0000C2C8h : 4E 45 4C 33 32 2E 64 6C 6C 00 63 6F 6D 64 6C 67 NEL32.dll`comdlg
0000C2D8h : 33 32 2E 64 6C 6C 00 00 00 00 00 00 00 00 00 00 32.dll``````````

正如你看到的,這個輸入表被分成三個主要部分:

- C1E8h - C237h:IMAGE_IMPORT_DESCRIPTOR 結構部分,對應著每一個需要輸入的動態
連結庫(DLL)。這部分以關鍵字 00 結束。

IMAGE_IMPORT_DESCRIPTOR struct
OriginalFirstThunk dd 0 ;原拆分 IAT 的 RVA
TimeDateStamp dd 0 ;沒有使用
ForwarderChain dd 0 ;沒有使用
Name dd 0 ;DLL 名字串的 RVA
FirstThunk dd 0 ;IAT 部分的 RVA
IMAGE_IMPORT_DESCRIPTOR ends

- C238h - C25Bh:這部分雙字(DWord) 稱作“IAT”,由 IMAGE_IMPORT_DESCRIPTOR
結構中的 FirstThunk 部分指明。這部分每一個 DWord 對應一個輸入函式。

- C25Ch - C2DDh : 這裡是輸入函式和 DLL 檔案的名稱。問題是,這些是沒有規定順序
的:有時候 DLL 檔案在函式前面,有時候正好相反,另外一些時候它們混在一起。

輸入表的簡介
------------

OriginalFirstThunk 是 IAT 的一部分,它是 PE 檔案引導時首先要搜尋的。如果存在,PE
檔案的引導部分將使用它來糾正在 FirstThunk IAT 部分的問題。當調入記憶體後,FirstThunk
的每一個 Dword (包含有函式名字串的 RVA),將被 RVA 替換為函式的真實地址(當呼叫這
些函式時,它們調入記憶體的位置將被執行)。所以,只要 OriginalFirstThunk 沒有被改變,基
本上這裡不存在輸入表的問題。

下面來看我們的問題
------------------

好了,經過簡單描述後,下面來看我們的問題。如果你試圖執行包含上面顯示的輸入表的可
執行檔案,它不會被調入,Windows 會顯示一個錯誤資訊。為什麼?很簡單,因為
OriginalFirstThunk 被刪除了。事實上,你應該注意到,在這個輸入表的每一個IMAGE_IMPORT_DESCRIPTOR 結構,OriginalFirstThunk 的內容都是 00000000h。嗯,所以我們
可以推測出,當我們執行這個可執行程式時,PE 檔案的引導部分試圖從 FirstThunk 部分獲得
輸入函式的名字。但是,正象你注意到的,這部分根本沒有包含函式名字串的 RVA,但是函式
地址的 RVA 在記憶體中。

我們需要怎麼做
--------------

現在,為了讓這個可執行檔案執行,我們需要重建 FirstThunk 部分的內容,讓它們指向我
們在輸入表第三部分看到的函式名字串。這不是一項很困難的任務,但是,我們需要知道哪個
IAT 對應哪個函式,而函式字串和 FirstThunk 內容並不採用同樣的儲存方法。所以,對於每
一個 IAT,我們需要驗證它對應的是哪個函式名(事實上,根據 IMAGE_IMPORT_
DESCRIPTOR.Name DWord 我們已經有了 DLL 名稱,這些並沒有被改變)。

如何驗證每一個函式
------------------

正向我們上面所見到的,在記憶體中,每一個被破壞的 IAT 都有一個函式地址的 RVA。這些
地址並沒有被破壞,所以,我們只要重新找回指向錯誤 IAT 的函式地址,把它們指向函式名字
符串。
為此,在 Kernel32.dll 中有一個非常有用的 API:GetProcAddress。它允許你得到給定函
數的地址。這裡是它的描述:

GetProcAddress(

HMODULE hModule, // DLL 模組的控制程式碼
LPCSTR lpProcName // 函式名
);

所以,對於每一個被破壞的 IAT,在 GetProcAddress 返回我們尋找的函式地址之前,只需
要分析包含在輸入表第三部分的所有函式名。

- hModule 引數是 DLL 模組的控制程式碼(也就是說,模組映象在記憶體中的基址),我們可以通
過 GetModuleHandleA API 得到:

HMODULE GetModuleHandle(
LPCTSTR lpModuleName // 返回模組名地址控制程式碼
);

(lpModuleName 只需要指向我們從 IMAGE_IMPORT_DESCRIPTOR.Name 部分得到的 DLL 檔案
名字串)

- lpProcName 僅指向函式名字串。

注意,有時候函式是按序號輸入的。這些序號是在每個 [ 函式名偏移量-2 ] 處的單字(WORDS)。
所以,你在分析程式時需要檢查函式是按名稱還是按序號輸入的。

使用上面輸入表的例項
--------------------

針對上面輸入表的例子,我將說明如何修復第一個輸入 DLL 的第一個輸入函式。

1. 我們來看第一個 IMAGE_IMPORT_DESCRIPTOR 結構部分(C1E8h),.Name 部分(C1E4h,指向
C1BAh)指出了 DLL 名。我們看到,那是 USER32.dll。

2. 我們來看 .FirstThunk 部分,它們指向 IAT 部分;每個對應一個這個 DLL(user32.dll)的
輸入函式。在這裡是 C1F8h,指向 C238h。所以,在 C238h,我們可以修復被破壞的 IATs。(你
會注意到,這個 IAT 部分包含二個 DWords,所以,這個 DLL 有二個函式輸入)

3. 我們得到了第一個被破壞的 IAT。它的值是 77E7897Fh。這是函式在記憶體中的地址。

4. 對每一個輸入表第三部分中的函式,我們呼叫 GetProcAddress API。當該 API 返回 7E7897Fh
時就意味著,我們到達了正確的函式。所以我們讓被破壞的 IAT 指向正確函式名(在本例中為 'wsprintfA')。

5. 現在我們只需要將 IAT 指向:偏移量(函式名字串)-2。為什麼是 -2 ?因為有時候使用了
函式序列。
所以在本例中,我們改變地址 C238h,讓它指向 C26Ah(以代替 77E7897Fh)。

6. 就這樣,這個函式被修復了,下面你只需要對所有的 IATs 重複這個過程就可以了。

後記
----

我描述的是一般的操作過程。當然只有在 DLLs 被正常調入記憶體後才能夠這樣做。對於其他情
況,你需要將它們調入,或者你需要仔細研究它們的輸出表才能找到正確的函式地址。

 

相關文章