前幾天我們開始慢慢把 Reverse 的流程串起來:
Control Flow
↓
Xref
↓
Function Analysis
在前面的範例裡,我們常常可以很舒服地看到:
main
check_password
verify
strcmp
但實際拿到 Binary 時,事情通常不會這麼順利。
有時候把 Binary 丟進 IDA / Ghidra,看到的可能不是:
check_password()
而是:
sub_401176
sub_4011A3
sub_401210
原因之一就是:
Stripped Binary
今天就來看看:
當 Function Name 都消失之後,我們還能怎麼 Reverse?
假設我們今天寫一個簡單的程式:
#include <stdio.h>
#include <string.h>
int check_password(char *input)
{
return strcmp(input, "rev_is_fun") == 0;
}
int main()
{
char input[32];
printf("Password: ");
scanf("%31s", input);
if (check_password(input))
puts("Correct!");
else
puts("Wrong!");
return 0;
}
Compile:
gcc chall.c -o chall
接著可以使用:
nm chall
可能看到:
0000000000001159 T check_password
0000000000001185 T main
這些:
check_password
main
就是 Symbol。
它可以幫助 Debugger、Linker 或分析工具知道:
這個 Address 對應到哪個 Function
對 Reverse Engineering 來說,有 Symbol 當然非常舒服。
因為:
check_password
光看名字就已經告訴我們:
這個 Function 八成跟 Password Validation 有關。
但 Release Binary 通常不一定會留下這些資訊。
我們可以自己試試:
strip chall
接著再:
file chall
可能會看到:
ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
最後面的:
stripped
就是今天的主角。
再執行:
nm chall
可能會得到:
nm: chall: no symbols
原本的:
main
check_password
都不見了。
這時候再把 Binary 丟進 IDA。
原本可能看到:
main
check_password
現在可能變成:
sub_1159
sub_1185
Ghidra 則可能看到:
FUN_00101159
FUN_00101185
因為分析工具不知道:
這個 Function 原本叫什麼
所以只能用:
Address
幫它暫時命名。
例如:
sub_1159
基本上可以理解成:
位於 0x1159 的 Function
但這不代表我們不能分析。
只是代表:
Function Name 不再能直接告訴我們答案。
第一件事情還是可以從最便宜的方法開始。
strings chall
我們可能還是可以找到:
Password:
Correct!
Wrong!
rev_is_fun
這裡要注意:
Strip != 刪除所有 String
Strip 主要處理的是 Symbol 等資訊。
程式真正執行時需要使用的:
Password:
Correct!
Wrong!
通常還是得存在 Binary 裡。
所以即使 Binary 被 Strip:
strings
依然可能提供非常多線索。
假設我們在 IDA 裡看到:
"Correct!"
這時就可以使用上一篇介紹的:
Xref
查看:
誰使用了這個 String?
例如:
"Correct!"
↑
Xref
↑
sub_1185
那我們就知道:
sub_1185
很可能是負責處理 Password 驗證結果的地方。
接著進去看。
可能看到:
undefined8 FUN_00101185(void)
{
char input[32];
printf("Password: ");
scanf("%31s", input);
if (FUN_00101159(input) != 0)
puts("Correct!");
else
puts("Wrong!");
return 0;
}
雖然名字變成:
FUN_00101159
但程式邏輯其實沒有消失。
我們依然可以知道:
User Input
↓
FUN_00101159
↓
Return Value
↓
Correct / Wrong
因此我們可以合理推測:
FUN_00101159
就是某種:
Validation Function
再進入:
FUN_00101159
可能看到:
int FUN_00101159(char *param_1)
{
int result;
result = strcmp(param_1, "rev_is_fun");
return result == 0;
}
這時候即使原本的:
check_password
已經消失,其實也完全不影響我們理解它。
因為:
strcmp
已經告訴我們這個 Function 在做什麼。
我們可以自己把它 Rename:
FUN_00101159
改成:
check_password
再把:
FUN_00101185
改成:
main
Reverse 過程中非常重要的一個習慣就是:
不要被工具產生的名字綁住。
看到:
sub_401234
不代表你要一路記著:
401234
當你理解它的功能之後,就可以直接 Rename:
sub_401234
↓
parse_input
或:
sub_401500
↓
check_password
慢慢把 Binary 重新整理成:
自己看得懂的程式
這時可能會有一個問題。
既然 Binary 已經 Strip:
為什麼 strcmp、puts、printf 還看得到?
因為這些 Function 很多是:
Dynamic Linking
需要使用的 External Symbol。
例如:
strcmp@plt
puts@plt
printf@plt
程式執行時還需要知道怎麼找到這些外部 Function。
因此 Stripped Binary 裡:
自己寫的 Function Name
通常比較容易消失。
但:
Imported Function
仍然可能留下非常重要的線索。
這也是 Reverse 時很常看的東西。
例如看到:
strcmp
可能想到:
字串比較
Password / Token Validation
看到:
malloc
free
可能想到:
Heap
Dynamic Memory
看到:
open
read
write
可能想到:
File / IO
Function Name 本身就是一種線索。
Stripped Binary 裡還有另一個很常遇到的問題:
main 不見了
但程式執行時:
main
當然還是存在。
只是:
main 這個名字
消失了。
Linux ELF 執行時通常不是直接:
Entry Point
↓
main
而比較接近:
Entry Point
↓
_start
↓
Runtime Initialization
↓
main
因此如果真的完全不知道 main 在哪裡,我們也可以從:
readelf -h chall
找到:
Entry point address
再從 Entry Point 開始往下追。
不過在 CTF 的簡單題目中,通常不需要每次都從 _start 慢慢追。
我們還是可以優先利用:
Strings
Xref
Imported Functions
Control Flow
找到真正重要的 Function。
所以今天的流程可以整理成:
Stripped Binary
↓
file
↓
strings
↓
找有意義的 String
↓
Xref
↓
找到使用 String 的 Function
↓
觀察 Caller / Callee
↓
分析 Function Behavior
↓
Rename
↓
慢慢重建程式邏輯
例如今天:
"Correct!"
↓
Xref
↓
FUN_00101185
↓
呼叫 FUN_00101159
↓
FUN_00101159 呼叫 strcmp
↓
理解它在驗證 Password
↓
Rename
最後就可以重新得到:
main
↓
check_password
↓
strcmp
也就是說:
Symbol 被拿掉,不代表程式邏輯被拿掉。
剛開始學 Reverse 的時候,很容易一直看到:
FUN_00101234
FUN_00101453
FUN_00101842
然後看到最後自己也不知道誰是誰。
所以我自己覺得 Rename 是一個非常重要的習慣。
例如分析過程中:
FUN_00101234
發現它負責讀 User Input。
就改成:
read_input
另一個:
FUN_00101453
發現負責比對 Password。
改成:
check_password
另一個:
FUN_00101842
負責輸出結果。
改成:
print_result
原本:
FUN_00101234
FUN_00101453
FUN_00101842
很快就會變成:
read_input
↓
check_password
↓
print_result
Binary 的結構也會慢慢變得比較容易理解。
今天介紹的 Stripped Binary,其實只是把 Reverse 再往真實世界推進一步。
前面的 Binary 很友善:
main
check_password
verify
但當 Symbol 消失之後:
sub_1159
FUN_00101185
sub_401234
我們就不能再依賴 Function Name。
而是開始依賴:
String
Imported Function
Xref
Control Flow
Caller / Callee
Function Behavior
這也是前面幾天一直在介紹這些東西的原因。
因為真正 Reverse 一個 Binary 時,我們做的事情其實就是:
找到線索
↓
追蹤 Reference
↓
理解 Function
↓
Rename
↓
建立更多線索
↓
逐漸還原整個 Program
到了這裡,我們已經差不多把 Reverse Engineering 最基本的一套 Workflow 串起來了。
下一篇就來做一次比較完整的 Reverse 實戰!