前面幾天我們已經介紹過:
今天就來第一次把它們全部串在一起,實際分析一個簡單的 Crackme
那這是什麼?
可以理解成:
一個刻意設計來讓你 Reverse Engineering 的小程式。
通常我們的目標可能是:
找到正確 Password
或:
找出程式的驗證邏輯
今天先從最基本的 Password Crackme 開始。
假設今天拿到一個:
chall
直接執行:
./chall
看到:
Password: test
Wrong!
目前我們知道的資訊只有:
程式會要求輸入 Password
輸入錯誤時:
Wrong!
我們的目標就是想辦法找到:
正確的 Password
前面 Static Analysis 有提過。
拿到未知 Binary,我通常會先做最基本的資訊蒐集:
file chall
例如:
chall: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped
可以先知道:
ELF
x86-64
Dynamic Linking
not stripped
接著:
strings chall
可能看到:
Password:
Correct!
Wrong!
strcmp
這些 String 已經提供不少線索。
尤其是:
strcmp
strcmp 是 C 裡面很常見的字串比較 Function。
例如:
strcmp(input, password)
所以看到它時,可以先猜測:
這個程式可能直接使用
strcmp驗證 Password。
注意這時候還只是:
Hypothesis
我們還沒有真的證明它。
接著把:
chall
丟進 IDA 或 Ghidra。
第一個目標通常不是:
從第一條 Assembly 開始看
而是先找:
main
因為程式主要邏輯通常會從這裡開始。
可能會看到類似:
int main()
{
char input[32];
printf("Password: ");
scanf("%31s", input);
if (check(input))
puts("Correct!");
else
puts("Wrong!");
return 0;
}
光看 Decompiler,我們就可以快速整理出程式流程:
取得 Input
↓
呼叫 check()
↓
判斷結果
↓
Correct / Wrong
所以接下來真正重要的地方就是:
check()
進入:
check
之後,可能看到:
int check(char *input)
{
char expected[11];
expected[0] = 'r';
expected[1] = '3';
expected[2] = 'v';
expected[3] = '_';
expected[4] = '1';
expected[5] = 's';
expected[6] = '_';
expected[7] = 'f';
expected[8] = 'u';
expected[9] = 'n';
expected[10] = 0;
return strcmp(input, expected) == 0;
}
這時候其實答案已經非常接近了。
把:
expected[0]
expected[1]
expected[2]
...
串起來:
r3v_1s_fun
也就是:
Password = r3v_1s_fun
但我們先不要急著結束。
因為今天的重點不是只有:
找到答案
而是:
怎麼證明自己的分析是對的。
如果切到 Assembly View,可能會看到:
call strcmp@plt
test eax, eax
jne .wrong
昨天以前的文章其實已經把這幾個東西都介紹過了。
首先:
call strcmp@plt
呼叫:
strcmp
在 Linux x86-64 中,Function 前兩個 Argument 通常會放在:
RDI
RSI
所以在呼叫 strcmp 前:
RDI → input
RSI → expected
接著 strcmp 的 Return Value 會放在:
RAX
更精確來說這裡通常會看:
EAX
如果兩個 String 相等:
strcmp(...) == 0
所以:
test eax, eax
就是在檢查:
EAX 是否為 0
接著:
jne .wrong
代表:
如果不是 0
就跳到 Wrong
因此整段可以翻回:
if (strcmp(input, expected) != 0) {
goto wrong;
}
這就是 Reverse 很常做的事情:
Assembly
↓
理解 Control Flow
↓
還原 High Level Logic
接著就用前一篇介紹的 GDB 來驗證。
啟動:
gdb ./chall
因為我們已經知道程式會使用:
strcmp
所以可以直接:
break strcmp@plt
接著:
run
輸入一個錯誤的 Password:
AAAA
程式會停在:
strcmp
前面提過 Linux x86-64 Calling Convention:
RDI → 第一個 Argument
RSI → 第二個 Argument
所以直接查看:
x/s $rdi
可能看到:
0x7fffffffe000: "AAAA"
這就是我們輸入的內容。
再看:
x/s $rsi
可能直接看到:
0x7fffffffdfe0: "r3v_1s_fun"
這時候就成功驗證:
strcmp(
"AAAA",
"r3v_1s_fun"
)
所以真正的 Password 就是:
r3v_1s_fun
離開 GDB 後:
./chall
輸入:
Password: r3v_1s_fun
結果:
Correct!
成功。
這題其實非常簡單。
但它剛好可以把前幾篇的東西全部串起來。
首先:
file
strings
讓我們知道 Binary 的基本資訊,並找到:
strcmp
Correct!
Wrong!
接著:
IDA / Ghidra
找到:
main
↓
check
↓
strcmp
透過 Decompiler 和 Assembly 還原:
Program Flow
最後再用:
GDB
直接觀察:
RDI
RSI
驗證我們的推測。
所以完整流程大概就是:
Binary
↓
file / strings
↓
找到可疑 String / Function
↓
IDA / Ghidra
↓
main()
↓
找到 Validation Logic
↓
追 Function
↓
理解 Assembly / Control Flow
↓
GDB 驗證
↓
得到答案
這其實就是一個非常基本的:
Reverse Engineering Workflow
另外有一件事情也很重要。
假設今天:
strings chall
直接看到:
super_secret_password
然後輸入之後真的成功。
那其實:
題目就解完了
沒有規定一定要:
IDA
Ghidra
GDB
全部打開才算 Reverse。
真正重要的是:
用最低成本取得足夠的資訊。
所以拿到 Binary 時,不妨按照:
簡單的方法
↓
複雜的方法
慢慢往下走。
例如:
file
↓
strings
↓
objdump / readelf
↓
IDA / Ghidra
↓
GDB
如果前面就可以解掉,就不一定需要繼續。
做到這裡,可以開始發現 Reverse Engineering 並不是:
把所有 Assembly 看懂
而比較像是:
找到重要的地方
↓
提出推測
↓
追蹤資料
↓
理解條件
↓
驗證推測
例如今天:
看到 strcmp
我們推測:
可能在比 Password
接著在 IDA 找到:
check()
再透過 GDB 看到:
RDI = User Input
RSI = Expected Password
最後確認:
r3v_1s_fun