我獨自逆向:Reverse - 課程筆記
第二部分:逆向工程核心概念
逆向工程介紹 (Introduction to Reverse Engineering)
- 廣義定義: 理解某個事物(軟體、硬體、遊戲等)的內部運作原理。
- 應用範疇:惡意程式分析、軟體授權破解、開發硬體替代零件或遊戲插件等。
- 狹義定義 (本課程焦點):
- 將高階程式語言編譯後的機器碼或組合語言,逆向推導回原始的程式邏輯。
- 主要流程:高階語言 -> (編譯) -> 組合語言 -> (組譯) -> 機器碼。逆向工程則是反向操作。
- 為何學習: 提升安全性、學術研究、改進與創新、恢復遺失的資料。
- 如何進行:
- 靜態分析: 在不執行程式的情況下分析其程式碼、結構與資源。
- 動態分析: 在程式執行時觀察其行為、記憶體變化與系統互動。
- 本課程聚焦: 以 C/C++ 編寫、基於 x86 指令集架構的應用程式逆向工程。
執行檔分析 (Executable File Analysis)
- 常見格式:
- ELF (Executable and Linkable Format): Linux, Unix-like 系統。
- PE (Portable Executable): Windows 系統 (.exe, .dll)。
- Mach-O (Mach Object): macOS, iOS 系統。
- ELF Header:
- 作業系統透過讀取檔案的 Header 來了解其格式與執行方式。
- Header 包含魔術數字 (Magic Number)
7f 45 4c 46(.ELF),以及程式的進入點位址 (Entry point address)、架構 (e.g., x86-64)、Program headers/Section headers 的位置等資訊。
- Program Header:
- 描述作業系統如何將檔案載入記憶體,定義了多個 Segment (如 LOAD, INTERP)。
- 每個 Segment 都有指定的虛擬位址 (VirtAddr) 和權限 (Read, Write, Execute)。
- Section Header:
- 將檔案內容劃分為不同的 Section,各自有不同用途與權限。
- .text (R-X): 存放可執行的程式碼。
- .rodata (R–): 存放唯讀資料,如字串常數。
- .data (RW-): 存放已初始化的全域變數。
- .bss (RW-): 存放未初始化的全域變數。
- 虛擬位址 (Virtual Address, VA):
- 程式執行時使用的記憶體位址,由作業系統的虛擬記憶體管理器 (VMM) 映射到實體記憶體。
- 好處包含:各程序記憶體空間獨立、支援位址空間配置隨機化 (ASLR) 以增強安全性。
第三部分:C 語言與 x86 組合語言
Linux & C 開發基礎
- C 語言特性:
- 因其較接近底層,需手動管理記憶體 (malloc/free),有助於理解電腦運作。
- 是學習逆向工程的良好起點,也常用於開發惡意程式。
- 基本語法回顧:
- 編譯: 使用 GCC
gcc -o <output_file> <source_file.c>。 - 資料型態:
char(1 byte),int(4 bytes),long long(8 bytes) 等。 - 有號數: 使用二補數 (Two’s Complement) 表示,最高位元為符號位。
- 位元運算:
&(AND),|(OR),^(XOR),~(NOT),<<(左移),>>(右移)。 - 指標:
&(取址),*(解引用)。陣列的名稱本質上就是指向第一個元素的指標。
- 編譯: 使用 GCC
x86 指令集
- CPU 運作: 遵循「取指令 (Fetch) -> 解碼 (Decode) -> 執行 (Execute) -> 寫回 (Write Back)」的指令週期。
- 暫存器 (Registers): CPU 內部的高速儲存空間。
- 通用暫存器:
RAX(累加器/回傳值),RBX(基底指標),RCX(計數器),RDX(資料)。 - 指標暫存器:
RSP(堆疊頂端指標),RBP(堆疊基底指標),RIP(指令指標)。 - 索引暫存器:
RSI(來源索引),RDI(目標索引)。 - EFLAGS: 狀態暫存器,包含
ZF(零旗標),SF(符號旗標),CF(進位旗標),OF(溢位旗標) 等,用於條件判斷。
- 通用暫存器:
- 常用指令:
- 資料傳輸:
mov(移動),push(推入堆疊),pop(彈出堆疊),lea(載入有效位址)。 - 算術運算:
add(加),sub(減),mul(無號乘),imul(有號乘),div(無號除)。 - 邏輯與比較:
and,or,xor,cmp(比較),test(測試)。 - 流程控制:
jmp(無條件跳轉),je/jz(相等/為零則跳轉),jne/jnz(不相等/不為零則跳轉),call(呼叫函式),ret(返回)。
- 資料傳輸:
- x86 vs x86-64:
- 主要差別在於暫存器大小(32-bit vs 64-bit)與可定址的記憶體空間。
- x86-64 作業系統可向下相容執行 x86 程式。
- 系統呼叫方式不同 (
int 0x80vssyscall)。
第四部分:函式呼叫與資料結構
呼叫約定 (Calling Convention)
- 定義: 一組規則,規範函式如何傳遞參數、回傳值以及誰負責清理堆疊。
- 重要性: 影響應用程式二進位介面 (ABI),若呼叫者與被呼叫者使用不同約定會導致程式崩潰。
- 常見範例:
- x86-64 System V (Linux/macOS): 前六個整數/指標參數依序使用
RDI,RSI,RDX,RCX,R8,R9暫存器傳遞。 - Microsoft x64 (Windows): 使用
RCX,RDX,R8,R9。
- x86-64 System V (Linux/macOS): 前六個整數/指標參數依序使用
Stack Frame
- 概念: 每次函式呼叫時,在堆疊 (Stack) 上建立的一個獨立區域,用於儲存區域變數、函式參數、返回位址等資訊。
- 函式前序 (Function Prologue):
push rbp: 將舊的rbp(呼叫者函式的堆疊基底) 推入堆疊保存。mov rbp, rsp: 將rbp指向目前的rsp,建立新函式的堆疊基底。sub rsp, N: 將rsp向下移動,為區域變數預留空間。
*
- 函式結語 (Function Epilogue):
leave: 相當於mov rsp, rbp(釋放區域變數空間) 和pop rbp(恢復舊的rbp)。ret: 從堆疊彈出返回位址到RIP,回到呼叫者函式繼續執行。
Struct
- 在組合語言中,存取
struct成員是透過計算相對於該struct實例基底位址的偏移量 (offset) 來完成的。 - 編譯器可能會為了效能,在
struct成員之間插入 padding,以確保資料對齊 (alignment),避免因cache line split導致的存取延遲。
Endianness
- 定義: 多位元組資料在記憶體中的儲存順序。
- Little-Endian (小端序): 低位元組 (least significant byte) 存在低位址。這是 x86 和 ARM 架構的常見模式。
- 例如:
0x12345678在記憶體中存為78 56 34 12。
- 例如:
- Big-Endian (大端序): 高位元組 (most significant byte) 存在低位址。常見於網路協議。
第五部分:進階主題與工具
逆向工具 (Reverse Engineering Tools)
- 靜態分析工具:
- IDA Pro: 功能強大的反組譯器與反編譯器,支援多種 CPU 架構與腳本擴充。常用快捷鍵:
F5(反編譯),x(交叉引用),g(跳轉),n(重新命名)。 - Ghidra: 由 NSA 開源的免費工具。
- 其他: Radare2, Binary Ninja。
- IDA Pro: 功能強大的反組譯器與反編譯器,支援多種 CPU 架構與腳本擴充。常用快捷鍵:
- 動態分析工具 (Debugger):
- GDB: GNU Debugger,常用於 Linux 環境,可搭配 GEF、Pwndbg 等插件強化功能。
- 常用指令:
b(設斷點),r(執行),c(繼續),si(單步進入),ni(單步越過),x/(檢視記憶體)。
- 輔助工具:
strings: 掃描檔案中的可列印字串。objdump: 顯示二進位檔案的資訊,如反組譯碼。
程式進入點 (Entry Point)
- C 語言程式的真正執行起點並非
main()函式。 - 在 Linux 中,起點通常是 C 執行時環境 (CRT) 的
_start函式。 _start會呼叫__libc_start_main(),此函式負責初始化環境 (如執行.init_array中的函式),然後才呼叫開發者寫的main()。
編譯器優化 (Compiler Optimization)
- 編譯器為了提升程式執行效率,會對程式碼進行優化,可能導致產生的組合語言與原始碼的直接對應關係變得複雜。
- 強度削弱 (Strength Reduction): 是一個常見的優化技巧,例如當除數為常數時,會將較慢的
div指令替換為較快的乘法與位移運算。
Strip
strip是一個 Unix-like 指令,用於移除執行檔中的符號表 (symbol table) 與除錯資訊。- 這樣做可以縮小檔案大小,但同時也會移除函式和變數的名稱,大幅增加逆向工程的難度。