相信有開發 MS Windows native 程式的人都會遇過找不到 DLL 的問題。在 Windows 下面好解決,因為開發人員可以把執行檔和 DLL 檔放在同樣的目錄(資料夾)下面,而不像 Linux 需要設定 .rpath
才有這樣的效果。
要放 DLL 首先還要知道執行檔需要的 DLL ,以及那些 DLL 需要的 DLL 有哪些。在 Windows 之下,有個叫做 dependency walker 的工具可以使用。不過要用這工具的話,首先必須要有 Windows 環境或者是 Wine ;接著,此工具似乎沒有批次的功能。
為了鋪設未來的康莊(偷懶)大道,或許可以自己試著解剖 PE 檔案來看裡面的內容。
需求 / 目標
要列出相依性,有兩種層次
相依的對象存在
相依的對象存在,且具有同名的函數可供使用
完成前者的話,只需要檢查輸入表即可;若要完成後者目標,則要輸出表以及輸入表的資料。
那,輸出表和輸入表是啥呢?
我想要的東西,以及你願意分享的東西
所謂的輸入表( import table ),是執行檔或是動態連結程式庫用來列出自己需要哪些動態程式庫,以及需要那些動態程式庫上面有哪些函數( function )的需求表。
而所謂的輸出表( export table ),是動態連結程式庫用來列出自己輸出了哪些函數的表格。
利用這些表格,就可以在不載入執行檔的狀況下推得在 執行檔載入階段 時會使用到的程式庫。也就是利用輸出輸入表並無法得知利用 LoadLibrary
等 Win32API 載入的程式庫。不過,理論上載入階段的相依相對來說比較重要,後來才動態載入的程式庫,有很高的機會是可選用的元件;或者即使不是可選用的,也會給出相對豐富的錯誤訊息讓使用者可以處理。
PE 檔裡面究竟有…
關於 PE 檔案的結構可以參見 微軟的說明文件 。除此之外, 這裡 有畫得相當精美的 PE 檔案結構圖。
簡單的說, PE 執行檔或是程式庫裡面有 DOS 程式片段(用來說明這不是 MS-DOS 程式),接著在 0x3C
的位置紀錄了 PE 檔案真正的檔頭。
利用檔頭,首先確認檔案的位元順序是 Big-Endian 或是 Little-Endian ,接著就是許多關於這個 PE 檔案的資訊,對本文的目的來說,重要的有
機器架構
PE32 or PE32+
Section 的數量
輸出、輸入表的位置以及大小
檔頭讀完後,接著是各式各樣的 section 了。以本文的目的來說,只要找到裝著輸出入表的 section 即可。
值得注意的是,有時輸入表會藏在 .rdata
之中,而非 .idata
之中。此時就要特別去看 .rdata
的範圍是否包括了在 PE header 上面所紀錄的輸入表位置。
PE 檔位置的標記: relative virtual address
看說明文件的時候,可以看見所謂的 relative virtual address (相對虛擬位置, RVA )。 RVA 用來描述一個 PE 影像檔載入 虛擬記憶空間 後 相對 於 image base 的 位址 。假設 image base 是 0x4000 ,然後 RVA 是 0x20 ,那在這個 PE 檔載入記憶體後,若沒有調整位置的話,那該位置就會是 0x4020 。
現在的問題是,該如何藉著 RVA 推得實際資料的存放位置呢?千秋使用的是下面的公式
|
|
夠多前言了,何不來點程式碼?
口說無憑,當然來點程式碼。有了程式碼才可以用各式各樣的方法來觀察自己看規格文件時的理解是否正確。
在這裡,先選用 python 來製作程式的原型 (prototype) 。這是為了測試自己的理解是否正確。也可以替程式流程做一個簡單初步的規劃。
目前,千秋有弄出可以檢視輸入以及輸出表的程式:該 python 程式可以在 這裡 看到,以 GPL3 發行。要執行的話可以這樣做
|
|
這樣的指示可以看到完整的追蹤輸出。若只想看輸入表的話可以搭配 grep
使用
|
|
若只想看輸出表的話則
|
|
之後較為完整的程式應該會使用 Rust 來製作,畢竟需要練習 Rust 的機會 XD