從原始碼到機器碼#
多數應用程式是用高階程式語言寫成的,例如 C/C++、C#、Java 或各種腳本語言。開發時寫下的原始語言稱為原始碼(source code),但電腦看不懂原始碼,必須先把高階語言轉換成機器碼(machine code)——處理器能直接執行的原生指令。
轉換方式有兩條常見路徑:直譯(interpreting) 原始碼,或編譯(compiling) 成原生碼。
程式的執行方式決定了你該用什麼方式逆向它。先判斷目標是直譯、原生編譯,還是受管語言(managed language),再決定拿出哪套工具。
直譯語言#
Python、Ruby 這類直譯語言(interpreted languages) 有時被稱為腳本語言(scripting languages),因為它們的應用常以文字檔形式的短腳本執行。
- 優點:動態、開發速度快。
- 缺點:直譯器執行程式比已轉成機器碼的程式慢,因為機器碼是電腦能直接理解的形式。
編譯語言#
編譯語言(compiled languages) 用編譯器(compiler) 剖析原始碼並產生機器碼,過程中通常會先產生一種中介語言。對原生碼生成而言,這個中介通常是目標 CPU 專用的組合語言(assembly language)(例如 32 位元或 64 位元組語),它是底層處理器指令集的人類可讀形式。組合語言接著由組譯器(assembler) 轉換成機器碼。
以 C 語言為例,整條產線是:
C 原始碼 → (編譯器) → 組合語言 → (組譯器) → 機器碼
圖表 6-1:C 語言的編譯流程
要把原生二進位檔還原回原始碼,需要反轉編譯過程,這叫反編譯(decompilation)。可惜反編譯機器碼相當困難,所以逆向工程師通常只反轉組譯這一步,也就是反組譯(disassembly)。
這正是逆向實務上大量使用 IDA Pro 之類反組譯器的原因:拿回組合語言遠比拿回 C 原始碼務實得多。
靜態連結與動態連結#
對極簡單的程式來說,編譯完就能產生可運作的執行檔。但多數應用程式會從外部函式庫匯入大量程式碼,這個步驟由編譯後執行的連結器(linker) 完成,稱為連結(linking)。
- 靜態連結(static linking):連結器把編譯器產生的應用程式機器碼,連同所有用到的外部函式庫,全部嵌進最終執行檔。產物是一個自給自足、不依賴原始函式庫的單一執行檔。
- 動態連結(dynamic linking):編譯器不嵌入機器碼,只在執行檔中留下對動態函式庫與所需函式的參照,等應用程式執行時由作業系統負責解析這些參照。
為什麼不乾脆全部靜態連結?因為某些工作在不同作業系統上的做法差異極大——例如「寫入磁碟檔案」在 Windows 與 Linux 的系統呼叫就完全不同——把所有程式碼塞成一顆大二進位檔,一旦 OS 專屬實作變動就會出問題。因此編譯器普遍以動態連結的方式連結到作業系統專屬的函式庫。
動態連結對逆向工程是好消息:執行檔必須明確留下它引用了哪些外部函式的名稱,這些匯入資訊往往是你追蹤網路 API 呼叫的第一條線索。