中介語言的特性#

不是所有應用程式都以原生執行檔的形式散布。以 .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 執行檔

實際上,.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.NetSystem.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/libjce.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 執行檔算你走運,有大量現成工具可用。當然,如果開發者做了混淆,分析會變難——但那也是逆向工程樂趣的一部分。