昨天我們實際分析了第一個 Crackme,並把前面學過的 Static Analysis, IDA / Ghidra, GDB 全部串在一起。
但昨天的 Crackme 有一個很大的問題,就是他的驗證邏輯太單純了。
只要找到 strcmp(),甚至直接觀察傳入的 Argument,就很容易知道 Password。
但實際的 Reverse 題目或者實際漏洞研究並不會這麼友善。
我們更常看到的是:
cmp eax, 5
jne loc_401200
test edx, edx
je loc_401250
cmp ecx, 0x539
jg loc_401280
jmp loc_401300
看到這種程式時,如果只是一行一行讀 Assembly,很快就會不知道:
現在到底跳到哪裡去了?
所以今天要開始學 Reverse Engineering 很重要的一個概念:Control Flow
Control Flow 就是:
程式執行的流程。
正常情況下 CPU 會依序執行:
Instruction 1
Instruction 2
Instruction 3
Instruction 4
但程式不可能永遠從上跑到下。
例如 C Code:
if (password == 1337) {
puts("Correct");
} else {
puts("Wrong");
}
程式必須根據條件決定:
password == 1337
│
┌───┴───┐
Yes No
│ │
Correct Wrong
這就是最基本的 Control Flow。
先看一個簡單的程式:
#include <stdio.h>
int check(int value) {
if (value == 1337) {
return 1;
}
return 0;
}
int main() {
int input;
printf("Number: ");
scanf("%d", &input);
if (check(input)) {
puts("Correct!");
} else {
puts("Wrong!");
}
return 0;
}
編譯:
gcc chall.c -o chall
如果從 Source Code 看,很容易理解:
Input
↓
check()
↓
value == 1337 ?
↓
Yes / No
但 Reverse Engineering 的時候我們沒有 Source Code。
看到的可能是:
cmp DWORD PTR [rbp-0x4], 0x539
jne loc_wrong
mov eax, 1
jmp loc_end
loc_wrong:
mov eax, 0
loc_end:
ret
其中:
0x539 = 1337
所以其實就是:
if (value == 1337)
Control Flow 裡面最重要的一個東西就是:
Branch
也就是「分支」。
前面的 Assembly:
cmp eax, 0x539
jne loc_wrong
可以理解成:
eax == 1337 ?
接著程式可能往兩個方向走:
cmp eax, 1337
│
┌─────┴─────┐
Equal Not Equal
│ │
↓ ↓
Correct Wrong
這就是一個 Branch。
前面的 Assembly 文章已經介紹過:
je
jne
jg
jl
jge
jle
真正重要的是:
看到 Conditional Jump 時,要開始思考它把程式切成了哪些路徑。
接著介紹 Reverse 裡非常常看到的一個詞:
Basic Block
Basic Block 可以先簡單理解成:
一段會按照順序執行,不會在中間突然跳走的 Instruction。
例如:
mov eax, DWORD PTR [rbp-0x4]
cmp eax, 0x539
jne loc_wrong
可以視為一個 Basic Block。
接著:
mov eax, 1
jmp loc_end
可能是另一個 Basic Block。
而以下又是另一個:
loc_wrong:
mov eax, 0
所以原本看起來像:
一大坨 Assembly
其實可以拆成:
Block A
│
┌─┴─┐
↓ ↓
B C
\ /
D
當 Binary 越來越大時,這種看法會比逐行閱讀容易很多。
把這些 Basic Block 與它們之間的 Jump 關係連起來,就會得到 Control Flow Graph 簡稱 CFG
例如剛才的 check():
┌───────────────┐
│ value == 1337 │
└───────┬───────┘
│
┌─────┴─────┐
│ │
True False
│ │
▼ ▼
┌────────┐ ┌────────┐
│ return │ │ return │
│ 1 │ │ 0 │
└────────┘ └────────┘
這就是一個非常簡單的 CFG。
前面介紹 IDA / Ghidra 時有稍微看過 Graph View。
現在就可以知道 Graph View 到底在畫什麼了。
它並不是單純把 Assembly 排得比較漂亮,而是在顯示:
Basic Block
+
Branch
+
Jump
形成的:
Control Flow Graph
例如 IDA 裡可能看到:
┌─────────────┐
│ cmp eax,539 │
│ jne loc_x │
└──────┬──────┘
│
┌──────┴──────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ mov eax,1│ │ mov eax,0│
└─────┬────┘ └─────┬────┘
│ │
└──────┬───────┘
▼
ret
這時候就不需要一直追:
0x401163
0x401172
0x401184
0x40119a
而是可以直接看:
程式在哪裡判斷?
判斷後有幾條路?
哪一條是 Success?
哪一條是 Failure?
假設今天看到:
cmp eax, 0
je loc_fail
cmp ebx, 10
jl loc_fail
cmp ecx, 0x539
jne loc_fail
call print_flag
不要急著一行一行翻譯。
可以先整理成:
eax == 0 ?
│
Yes → Fail
│
No
↓
ebx < 10 ?
│
Yes → Fail
│
No
↓
ecx == 1337 ?
│
No → Fail
│
Yes
↓
print_flag()
瞬間就會比較清楚。
這也是我覺得 Reverse 很重要的一個觀念:
不一定要把每一條 Assembly 都翻成 C,先把程式的邏輯流程思考出來通常更重要。
除了 if / else 之外,Loop 也是 Control Flow。
例如:
for (int i = 0; i < 10; i++) {
sum += i;
}
Assembly 可能出現:
mov DWORD PTR [rbp-0x4], 0
loop:
cmp DWORD PTR [rbp-0x4], 10
jge end
add DWORD PTR [rbp-0x8], eax
add DWORD PTR [rbp-0x4], 1
jmp loop
可以畫成:
┌──────────┐
│ i < 10 ? │
└────┬─────┘
│
┌─────┴─────┐
Yes No
│ │
▼ ▼
Loop End
│
└────────────┐
│
└── 回到判斷
所以看到:
jmp
往前跳時,也可以特別注意:
這裡是不是一個 Loop?
之後拿到一個 Function,可以先不要試著理解全部 Instruction。
先找:
cmp
test
je
jne
jg
jl
jmp
接著問:
這裡在判斷什麼?
↓
True 往哪裡?
False 往哪裡?
↓
哪一條會到 Success?
哪一條會到 Failure?
最後再回頭分析:
要滿足什麼條件
才能走到我們想要的那一條路?
這其實就是很多 Crackme 的核心。
前面幾天我們比較像是在學:
mov 是什麼
cmp 是什麼
call 是什麼
je 是什麼
但到了現在,目標要慢慢變成:
Assembly
↓
Basic Block
↓
Branch
↓
Control Flow
↓
Program Logic
真正做 Reverse Engineering 時,我們通常不需要知道每一條 Instruction 的所有細節。
我們真正想知道的是:
Input 從哪裡進來?
↓
經過哪些 Function?
↓
在哪裡被驗證?
↓
有哪些 Branch?
↓
Success Path 在哪裡?
能回答這些問題,就已經開始從看 Assembly 進入 Reverse Engineering 了
今天介紹了 Reverse Engineering 中很重要的幾個概念:
Control Flow
Branch
Basic Block
CFG
簡單來說:
Basic Block
↓
透過 Jump / Branch 連接
↓
Control Flow Graph
↓
還原 Program Logic
昨天的 Crackme 還可以直接找到 strcmp()。
但之後遇到比較複雜的驗證邏輯時,我們就必須開始追:
Input
↓
Function
↓
Branch
↓
Branch
↓
Success
而當 Binary 越來越大的時候,又會遇到另一個問題:
我知道這個 Function 很重要,但到底有哪些地方會呼叫它?這個字串又在哪裡被使用?
下一篇就來介紹 Reverse Engineering 非常好用的功能:Cross Reference(XREF)