→ manlike:這篇也不錯~ 只是有些地方好像怪怪的?? @@? 10/10 18:12
→ manlike:而且把 Windows dll 和 Linux so 放在一起講很酷 XD 10/10 18:13
→ purpose:有些地方小弟只有看看資料,做個人理解的講述,怕誤導大家 10/10 18:17
→ purpose:在這裡說明一下 10/10 18:17
推 nowar100:謝謝,不過我還是先研究好DSO再來研究DLL 怕混淆 10/10 19:00
修文,上述內容不變動,新增以下內容。
下面補充一下 PE 四大表裡的匯入表的前世今生,
主要提供大方向觀念用,細節請翻資料,並動手驗證我講的內容正確性。
首先我們寫了個程式碼 my.c,
將 my.c 編譯後,會產生 my.obj (目的檔;如果用 gcc 就是 my.o),
再將其連結後產生的執行檔就是 my.exe。
my.exe 是一個所謂『PE 格式』的執行檔,
更進一步說,目前 Windows 的 執行檔 (*.exe)、動態連結檔 (*.dll) 都是PE格式。
PE 格式內部可以分成好幾個區塊,其中比較重要的區塊有
.text
放置的是機器碼,比如
mov eax, 3
add eax, 4
call MessageBox
匯出表
如果你建置的 PE 中,有 __declspec(dllexport) void foo (void)
這種要匯出的函數時,就會有此表。
因此我們可以利用 Dependency Walker 查看此表,來得知這個 dll 有哪些匯出
函數。
不是 .dll 裡的所有函數都會匯出給人用,比如 RegisterServiceProcess()
這個函數 google 一下會發現很多人在用,但是你是找不到匯出資訊的。
只能用 LoadLibrary() 動態載入 kernel32.dll 後,再用 GetProcAddress
取得其函數位址。
資源表
重定位表
不重要。
匯入表
功能是幫 call MessageBox 這樣的指令,在載入到記憶體後,
能夠被轉換成正確的位址。
如果用一些偵錯軟體,比如 OllyDbg 開 my.exe 做偵錯,然後按 Alt+M 就可以看到
記憶體分配圖。可以看到 my.exe 裡的 .text、匯入表等資訊,被映射到記憶體位址
0x00400000之後。而且開始運行程式後,按個暫停,再次看記憶體分配圖 (memory map)
多數都會有載入 kernel32.dll,他一樣是 .text 被載入到記憶體裡。
簡單講 .exe 跟 .dll 都算是映像檔 (image),因為執行時關鍵部份都被映射到記憶體,
而目的檔 .obj 就不是映射檔。
回到 my.c,假定其原始碼為
int main() {
MessageBox(參數隨便);
functionForMy();
return 0;
}
int functionForMy(void) {
;
}
編譯器翻譯成機器碼時,會有
call MessageBox
call functionForMy
編譯器一開始就認定 my.exe 將會被載入到 0x0040000,所以 functionForMy 的記憶體
位址,現在可以直接給定,比如說是 call 0x00415678。
那編譯器找不到 MessageBox 的絕對位址,他要怎麼知道這不是你打錯字,而是真的有
這個函數,只不過是透過「隱式連結」。
所以通常需要一行 #pragma comment(lib, "user32.lib") 的指令,告知連結器說
有個匯入用程式庫,裡面還有記載額外的函數資訊。
透過這個檔案知道 MessageBox 確實是個「隱式連結」的函數,不必回報錯誤,讓你重
寫程式。那麼在產生 my.exe 的時候,編譯器就會知道要填寫一個匯入表,裡面用到
一個模組叫 user32.dll,而且是只有用到其中一個函數,叫 MessageBox,簡單記為
user32!MessageBox。
匯入表其實由很多資料結構組成,可以講說由四組陣列在記錄匯入資訊。
其中有一組陣列,裡面每個元素的資料型態叫做「IMAGE_THUNK_DATA」,
這個資料型態名稱你可以把他當放屁,真的!
只要記住每個元素大小是「四位元組」就好,
因為這「四位元組」是 union,在不同情況下有不同意義,扮演四種角色。
我們首先需要知道,實際上 my.exe 的 call MessageBox 指令其實在編譯成 .exe 後
就固定不會改變了,比如寫 call dword ptr[0x00479100],那 0x00479100 這個位址
是什麼?他就像函數指標一樣,會放置 MessageBox 最終的絕對位址。
執行 call MessageBox 就等於,到 0x00479100 取出四個位元組內容,將其當作位址去
call。
這裡講到的 0x00479100 其實就是「匯入表」那四組陣列剛剛我提到的那一組,
每個元素都是「四位元組」的陣列。這個陣列又叫做 IAT (Import Address Table)。
當 user32.dll 作為模組載入到 my.exe 時,Windows 還會順便去填寫 my.exe 的
IAT 每一個元素,我們假設說 IAT[0] 剛好對應 user32!MessageBox 的絕對位址
0x10203040。那必要 my.exe 一開始就知道 Windows 一定會把 IAT[0] 當作
user32!MessageBox 位址的存放處。才能把 call MessageBox 翻譯成
call dword ptr[ IAT[0] ]。
所以其實匯入表四大陣列裡面,又有另外一組陣列叫 INT (Import Name Address) 表。
INT 陣列的每個元素,其資料型態都是『IMAGE_THUNK_DATA』,換言之又是每個元素都是
「四位元組」大小。
可以想成 char *name = "MessageBox";
INT[0] = name;
那只要一開始由編譯器出面,在 my.exe 的匯入表裡面規定說,INT[0] 是代表
user32!MessageBox 函數,就能讓「my.exe 的 .text」跟 Windows Loader 有共同的
認知。我知道要去 IAT[0] 取位址,你也知道要替我把 user32!MessageBox 的最終位址
填進去此處等我拿。
對於 user32!MessageBox,要使用他不是一定要用「隱式連結」,如果用顯式
連結時,是先 LoadLibrary("user32.dll"); 再 GetProcAddree(); 來取得最終位址。
可是在 GetProcAddress() 裡面,可以是一個 "MessageBox" 字串指標,也可以是一個
序數值。只要值介於 0x0000 0000 ~ 0x0000 FFFF 就代表序數;
而 "MessageBox" 對應的字串指標在生成時,他的指標值絕對不會低於 0x0001 0000
所以不會衝突。
剛剛講的隱式連結,只講到 INT 是記錄名稱的「字串指標」,實際上不對,
INT 也有可能是記錄像 user32!MessageBox 的序數值 (0x1DD)。
當 INT 陣列的元素,比如 INT[0] 的最高位元為 1 時,代表 INT[0] 是一個序數值。
在 user32.dll 的匯出表裡面,會記錄 user32.dll!MessageBox 的序數是 0x1DD,
所以如果我在 my.exe 的 INT[0] 看到 0x8000 01DD 我就知道他是 user32!MessageBox。
那為什麼
char *name = "MessageBox";
↑這裡面,得到字串指標不會 >= 0x8000 0000
似乎超過這個位址是 kernel mode 才能用?像記錄每個行程資訊的 EPROCESS 結構
就是在 0x8xxx xxxx 一帶,可用 http://memoryhacking.com 觀看這一帶位址內容。
INT[0] 這四個位元組,可以記載「MessageBox 名稱」或「MessageBox 序數」,實際上
當記錄的是名稱時,他的指標『沒有直接指向字串陣列』!
而是指向「匯入表四大陣列」的其中一個陣列。
這個陣列之前還沒講過,他每個元素的大小是不一定的...
這些元素的資料型態名稱叫 IMAGE_BY_NAME。
前兩個 Bytes 是 Hint,記錄建議序數值用,不用管他。
從第三個 Byte 開始,是 C-Style 字串,內容就是 char str[不一定] = "MessageBox";
簡單來說 INT[0] 的內容如果是 0x00420000,這個指標會指向 IMAGE_BY_NAME 元素。
你不用管他,直接 +3 變成 0x00420003 這個位址就會放置 "MessageBox"。
而匯入表四大陣列的最後一個,他是提供大綱用的,每個元素的資料結構叫
IMAGE_IMPORT_DESCRIPTOR (IID),有 20 個 Bytes 大。
姑且叫最後這個 IID 陣列叫「匯入大綱表」。
如果 my.exe 有用到 user32.dll 跟 kernel32.dll,那麼 IID陣列 就有兩個元素,
char *DLL_NAME1 = "user32.dll";
char *DLL_NAME2 = "kernel32.dll";
IID[0].name = DLL_NAME1;
IID[1].name = DLL_NAME2;
IID[0].originalFirstThunk = 指標 = INT陣列開頭位址
IID[0].FirstThunk = 還是指標 = IAT陣列開頭位址
IID 陣列的目的是給 Windows Loader 看的,當 my.exe 被載入記憶體執行,
Loader 先到匯入表的 IID 陣列知道有兩個模組要載入到 my.exe 的 address space。
當 Loader 載入完後,假設先處理 user32.dll,那就先從 IID[0] 的成員開始處理。
從 IID[0].originalFirstThunk 知道有「user32 專用的 INT 陣列」。
從 IID[0].FirstThunk 知道有「user32 專用的 IAT 陣列」。
從 INT 陣列去知道,只有用到 MessageBox 函數,因此 Loader 去 user32.dll 找出
MessageBox 的最終位址,然後再從對應的 IAT 陣列填寫之。
這樣就完成了 dll 函數的載入時重置工作。
而 my.exe 的 call MessageBox 其實精確說是不用載入時重定位的。
※ 編輯: purpose 來自: 124.8.134.197 (10/10 21:57)
→ purpose:INT[0] 應該是 +2 變成 0x00420002,上文打錯了。 10/10 22:07
推 VictorTom:看到頭昏了, 先推再慢慢消化XD 10/10 22:45
推 xatier:先推囉,消化這篇需要時間 XD 10/11 07:39