中介語言的特性#
不是所有應用程式都以原生執行檔的形式散布。以 .NET 和 Java 這類受管語言(managed languages) 撰寫的應用程式,會編譯成一種中介機器語言,這種語言通常刻意設計成與 CPU 及作業系統無關;應用程式執行時,再由虛擬機或執行期環境(runtime)執行這些程式碼。在 .NET 中,這個中介機器語言叫通用中介語言(common intermediate language, CIL);在 Java 中則稱為 Java byte code。
這些中介語言含有大量中繼資料(metadata),例如類別名稱、所有對內與對外的方法名稱。而且與原生編譯的程式碼不同,受管語言的輸出相當可預測,非常適合反編譯。
遇到受管語言的目標算是好運:你多半能直接拿回接近原始碼的內容,而不是只有組合語言。
.NET 應用程式#
.NET 的執行期環境叫通用語言執行期(common language runtime, CLR)。.NET 應用程式除了依賴 CLR,也依賴一個龐大的基礎功能函式庫,稱為基底類別庫(base class library, BCL)。
.NET 主要是 Microsoft Windows 平台的產物(畢竟由 Microsoft 開發),但也有多個較具可攜性的版本,最知名的是 Mono Project,它可在 Unix-like 系統上執行,並涵蓋 SPARC、MIPS 等多種 CPU 架構。
看 .NET 應用程式散布的檔案時,你會看到 .exe 與 .dll 副檔名,很容易誤以為它們是原生執行檔;但若把這些檔案丟進 x86 反組譯器,你只會得到一則「這不是原生程式碼」之類的訊息。

圖表 6-17:x86 反組譯器中的 .NET 執行檔
.exe 與 .dll 檔案格式當成盛裝 CIL 程式碼的便利容器,在 .NET 執行期中這些容器稱為組件(assemblies)。
組件包含一個以上的類別、列舉或結構。每個型別以名稱指涉,通常由命名空間(namespace) 加一個簡短名稱組成。命名空間降低名稱衝突的機率,也方便分類——例如任何位於 System.Net 命名空間下的型別都與網路功能有關。
使用 ILSpy#
你幾乎不需要直接面對原始的 CIL,因為 Reflector 與 ILSpy 這類工具能把 CIL 資料反編譯成 C# 或 Visual Basic 原始碼,並同時顯示原始 CIL。ILSpy 是免費的開源工具,介面分成兩塊:左側是 ILSpy 已載入的所有組件的樹狀清單,可展開檢視組件所含的命名空間與型別;右側顯示反編譯出的原始碼,內容對應左側選取的組件。

圖表 6-18:ILSpy 的主介面
ILSpy 的搜尋與分析操作
載入與搜尋
- 按
CTRL+O並在對話框中選取應用程式即可載入。若開啟的是應用程式主執行檔,ILSpy 會視需要自動載入執行檔中參照的所有組件。 - 按
F3搜尋所有已載入的組件,右側會出現搜尋視窗。輸入搜尋詞可過濾所有已載入的型別;也能從下拉選單改成搜尋成員或常數——例如要找字面字串就選 Constant。 - 找到想檢視的項目(例如
TcpNetworkListener)後雙擊,ILSpy 會自動反編譯該型別或方法。

圖表 6-19:ILSpy 的 Search 視窗
分析參照關係

圖表 6-20:ILSpy 分析一個型別
除了直接搜尋特定型別與成員,你也可以反過來找「使用了內建網路或密碼學函式庫」的區域。BCL 含有大量低階 socket API,以及 HTTP、FTP 等高階協定的函式庫。在左側視窗對型別或成員按右鍵選 Analyze,右側會出現一個樹狀視窗,展開後顯示可對該項目執行的分析類型。選項取決於你選的是什麼:
分析型別時會有三個選項,通常只需用到其中兩種:
- Instantiated By:顯示哪些方法建立了此型別的新實例
- Exposed By:顯示哪些方法或屬性在其宣告或參數中使用了此型別
分析成員、方法或屬性時則有兩個選項:
- Uses:顯示所選成員用到了哪些其他成員或型別
- Used By:顯示哪些其他成員使用了所選成員(例如呼叫該方法)
所有項目都可以展開。
靜態分析 .NET 應用程式大致就這樣:找到感興趣的程式碼、檢視反編譯結果,然後開始分析網路協定。
.NET 的核心功能大多在隨執行期環境散布的 BCL 中,所有 .NET 應用程式都能取用。BCL 的組件提供多個基礎的網路與密碼學函式庫,實作網路協定的應用程式很可能會用到。留意引用
System.Net與System.Security.Cryptography命名空間下型別的區域,它們主要實作在 MSCORLIB 與 System 組件中。從這些關鍵 API 的呼叫往回追,就能找到應用程式處理網路協定的地方。
Java 應用程式#
Java 與 .NET 的差別在於:Java 編譯器不會把所有型別合併成單一檔案,而是把每個原始碼檔編譯成一個副檔名為 .class 的 Class 檔。由於散落在檔案系統目錄中的 Class 檔不方便在系統間傳輸,Java 應用程式常被打包成 Java archive(JAR)——JAR 檔其實就是一個 ZIP 檔,外加幾個支援 Java 執行期的額外檔案(用一般 ZIP 解壓縮程式就能打開檢視)。

圖表 6-21:以 ZIP 程式開啟的 JAR 檔範例
反編譯 Java 程式推薦使用 JD-GUI,運作方式基本上和 ILSpy 反編譯 .NET 應用程式相同。以 Java 安裝時預設附帶、通常位於 JAVAHOME/lib 的 jce.jar 為例:

圖表 6-22:開啟 JAR 檔的 JD-GUI
- 你可以一次開啟單一 class 檔或多個 JAR 檔,視你要逆向的應用程式結構而定。
- 開啟 JAR 檔時,JD-GUI 會剖析中繼資料以及類別清單,並以樹狀結構呈現。例如你會看到
javax.crypto套件,它定義了各種 Java 密碼學操作的類別;套件名稱底下就是該套件中定義的類別,例如CryptoAllPermissionCollection.class。 - 在左側視窗點選類別名稱,右側會顯示該類別反編譯後的版本。你可以捲動反編譯結果,或點選類別公開的欄位與方法,直接跳到反編譯程式碼中的對應位置。
- 反編譯程式碼中任何加底線的識別字都可以點擊,工具會導航到它的定義處。例如點擊加底線的
all_allowed,介面就會跳到目前反編譯類別中all_allowed欄位的定義。
對付混淆#
.NET 或 Java 應用程式所帶的大量中繼資料,讓逆向工程師更容易看懂應用程式在做什麼。但擁有「祕方」網路協定的商業開發者往往不樂見這件事——這些語言容易反編譯,也讓人相對容易發現自訂網路協定中糟糕的安全漏洞。有些開發者不希望你知道這些,於是把「隱晦」當成安全解方。
你很可能會遇到刻意混淆(obfuscation)過的應用程式,工具如 Java 的 ProGuard 或 .NET 的 Dotfuscator。這些工具對編譯後的應用程式施加各種修改,目的就是讓逆向工程師吃鱉:可能單純把所有型別與方法名稱改成無意義的值,也可能更精巧,例如在執行期解密字串與程式碼。不論用哪種手法,混淆都會讓反編譯更困難。
混淆過的應用程式,用一般反編譯器很難判斷它在做什麼——這正是混淆的目的。別把讀不懂當成自己的問題。

圖表 6-23:原始與混淆後的 class 檔比較
面對混淆時可用的幾個技巧:
- 從外部函式庫下手:外部函式庫的型別與方法(例如核心類別庫)無法被混淆。只要應用程式有做網路通訊,對 socket API 的呼叫就一定存在,去搜它們。
- 寫測試載入器:.NET 與 Java 都很容易動態載入與執行,你可以寫一個簡單的測試框架(test harness)載入被混淆的應用程式,直接呼叫它的字串或程式碼解密常式。
- 盡量改用動態逆向:在執行期檢視型別,藉此判斷它們的用途。
逆向工程資源#
延伸資訊來源
以下網址提供逆向工程軟體的優質資訊來源,也涵蓋執行檔格式等相關主題:
- OpenRCE 論壇:
http://www.openrce.org/ - ELF 檔案格式:
http://refspecs.linuxbase.org/elf/elf.pdf - macOS Mach-O 格式:Apple 開發者文件中的 Mach-O Runtime 參考(可透過 Web Archive 取得)
- PE 檔案格式:Microsoft MSDN 上的 PE 格式說明文件
本章使用到的工具(包含下載位置)詳見附錄 A。
結語#
逆向工程需要時間與耐心,別期待一夜之間學會。要理解作業系統與架構如何協同運作、要在反組譯器裡解開最佳化過的 C 所產生的一團亂麻、要靜態分析反編譯出的程式碼,這些都需要時間。
幾個實務上的建議:
- 從你已經懂的小程式開始。把這些小執行檔的原始碼與反組譯出的機器碼對照,你會更清楚編譯器是怎麼翻譯原始語言的。
- 別忘了動態逆向與除錯器。有時直接把程式跑起來,會比靜態分析更有效率。單步執行不只幫你理解電腦架構如何運作,也讓你能把一小段程式碼徹底分析透。
- 遇到 .NET 或 Java 執行檔算你走運,有大量現成工具可用。當然,如果開發者做了混淆,分析會變難——但那也是逆向工程樂趣的一部分。