昨天我們介紹了 Control Flow,開始學著從:
Basic Block
Branch
CFG
去理解一個 Function 裡面的執行流程。
但實際的程式通常不會只有一個 Function。
例如:
main()
├── read_input()
├── check_password()
└── print_flag()
這時候只盯著 main() 裡面的 Assembly 看,很快就會遇到另一個問題:
這個 Function 是誰呼叫的?
這個 String 又是在哪裡被使用的?
這就是今天要看的:
Xref
Function Analysis
先來準備一個簡單的程式:
#include <stdio.h>
#include <string.h>
int check_password(char *input) {
if (strcmp(input, "shark_is_cute") == 0) {
return 1;
}
return 0;
}
void success() {
puts("Correct!");
}
int main() {
char input[32];
printf("Password: ");
scanf("%31s", input);
if (check_password(input)) {
success();
} else {
puts("Wrong!");
}
return 0;
}
Compile:
gcc chall.c -o chall
接著把它丟進 IDA 或 Ghidra。
Xref 全名是:
Cross Reference
可以先把它理解成:
誰有使用這個東西?
這個「東西」可以是:
Function
String
Global Variable
Address
Data
例如 Binary 裡面有:
Correct!
我們可以問:
哪裡使用了 "Correct!"?
工具就可能告訴我們:
success()
再查看 success() 的 Xref,又可以知道:
誰呼叫了 success()?
答案可能是:
main()
所以我們就可以一路反推:
"Correct!"
↑
success()
↑
main()
這就是 Xref 很重要的地方。
前面 Static Analysis 的時候有介紹過:
strings chall
可能會看到:
Password:
Correct!
Wrong!
shark_is_cute
其中:
Correct!
就是一個很值得注意的 String。
因為如果這是一題 Crackme,我們通常想知道的就是:
怎麼走到成功的地方?
這時不一定需要從 main() 一行一行往下看。
可以直接在 IDA / Ghidra 的 Strings View 找:
Correct!
接著查看它的 Xrefs。
可能會跳到:
void success()
{
puts("Correct!");
}
於是我們知道:
Correct!
↑
success()
下一步再問:
誰呼叫 success()?
繼續查看 success() 的 Xrefs。
可能就會找到:
if (check_password(input)) {
success();
}
到這邊,分析方向就很明顯了。
Xref 不只能用在 String。
Function 本身也可以查看 Cross Reference。
例如:
check_password()
我們想知道:
誰呼叫 check_password()?
查看 Xrefs 後可能得到:
main
Assembly 可能看到:
lea rax, [rbp-20h]
mov rdi, rax
call check_password
test eax, eax
je wrong
前面已經學過:
RDI → 第一個 Argument
RAX → Return Value
因此可以推測:
input
↓
check_password(input)
↓
RAX
↓
判斷成功 / 失敗
這時候我們就不用把整個 main() 都看懂。
只需要順著我們關心的 Function 追下去。
分析 Function 時會很常遇到兩個詞:
Caller
Callee
假設:
main() {
check_password(input);
}
那麼:
main → Caller
check_password → Callee
也就是:
Caller
│
│ call
▼
Callee
所以分析一個 Function 時,可以問兩個問題:
誰呼叫它?
它又呼叫誰?
例如:
main
├── printf
├── scanf
├── check_password
│ └── strcmp
│
├── success
│ └── puts
│
└── puts
這樣其實就已經開始形成一個:
Call Graph
當 Binary 稍微大一點之後,最常遇到的問題就是:
sub_401156
sub_4011A0
sub_401230
sub_4012F4
...
如果 Binary 被 Strip 過,原本像:
check_password
success
read_input
這些名稱可能全部消失。
IDA 可能只會幫它取名成:
sub_401156
這時候就要靠 Function Analysis 去判斷它在做什麼。
例如看到:
int sub_401156(char *a1)
{
return strcmp(a1, "shark_is_cute") == 0;
}
那其實我們已經可以推測:
sub_401156
很可能是在做:
check_password
這時就可以直接 Rename:
sub_401156
↓
check_password
很多人剛開始 Reverse 的時候,會一直看:
v1
v2
v3
a1
sub_401156
sub_401220
看久之後很容易直接混亂。
所以分析時很重要的一個習慣就是:
看懂一個東西,就幫它重新命名。
例如原本:
int sub_401156(char *a1)
{
int v2;
v2 = strcmp(a1, "shark_is_cute");
return v2 == 0;
}
理解之後可以整理成:
int check_password(char *input)
{
int result;
result = strcmp(input, "shark_is_cute");
return result == 0;
}
雖然 Binary 本身完全沒有改變,但可讀性差非常多。
Reverse Engineering 很大一部分其實就是:
未知資訊
↓
分析
↓
建立假設
↓
重新命名
↓
逐漸還原程式語意
這也是我自己覺得 Reverse 很重要的一個觀念。
假設一個 Binary 裡面有:
300 Functions
是不是代表:
我要把 300 個 Function 全部看完?
通常不是。
我們真正需要的是:
找到與目標有關的 Function。
例如 Crackme 想找 Password,可以從:
Correct!
Wrong!
Password:
strcmp
memcmp
這些線索開始。
假設先找到:
Correct!
接著:
Correct!
↓ Xref
success()
↓ Xref
main()
↓
check_password()
↓
strcmp()
最後可能只分析了:
4 ~ 5 個 Function
就已經理解主要邏輯。
所以 Reverse 並不是:
從第一行看到最後一行
更像是在 Binary 裡面:
找線索
↓
追 Xref
↓
找到關鍵 Function
↓
建立程式邏輯
除了 String 以外,一些 Function 本身也是很好的線索。
例如看到:
strcmp
memcmp
strncmp
scanf
fgets
malloc
free
open
read
write
都可以思考:
為什麼程式會使用它?
例如 Crackme 裡看到:
strcmp
很可能代表程式正在比較:
Input
Password
Flag
Command
查看 strcmp() 的 Xrefs,就可以找到:
哪些地方正在做 String Comparison?
例如:
mov rsi, offset secret
mov rdi, rax
call strcmp
很快就可以發現:
User Input
↓
strcmp
↑
secret
因此:
Xref 不一定要從自己寫的 Function 開始追,也可以從 Library Function 反推程式邏輯。
除了 Function 和 String,其實 Data 也可以有 Xref。
例如程式裡有:
int is_admin = 0;
某個 Function:
if (is_admin) {
print_flag();
}
我們找到:
is_admin
之後,可以查看:
誰讀取 is_admin?
誰修改 is_admin?
最後可能找到:
login()
↓
修改 is_admin
admin_panel()
↓
讀取 is_admin
形成:
login()
↓
is_admin
↓
admin_panel()
這種分析方式之後在:
Reverse
Malware Analysis
Firmware
Vulnerability Research
裡面都會非常常見。
昨天介紹的是 Control Flow。
例如:
check_password()
│
test eax
/ \
/ \
Success Failed
它主要是在分析:
一個 Function 裡面的程式怎麼走?
今天的 Xref 則比較像:
Function、String、Data 之間彼此有什麼關係?
可以簡單理解成:
Control Flow
↓
Function 內部怎麼走
Xref / Call Graph
↓
不同 Function / Data 之間怎麼連
兩個搭配起來,就可以慢慢建立整個 Binary 的程式結構。
到目前為止,我們的 Reverse Workflow 可以再稍微更新一下:
Binary
↓
file / strings
↓
IDA / Ghidra
↓
找 Interesting String / Function
↓
Xrefs
↓
找到相關 Function
↓
Decompiler / Assembly
↓
Control Flow
↓
Rename
↓
繼續追 Xrefs
↓
建立程式邏輯
而不是:
打開 Binary
↓
從 main 第一行開始看
↓
一路看到最後
這樣分析速度通常會快非常多。
今天主要介紹了:
Xref
Caller
Callee
Function Analysis
Call Graph
Rename
Data Reference
其中最重要的概念其實只有一個:
不要試圖一次看懂整個 Binary,而是順著關係找到真正重要的地方。
例如:
Interesting String
↓
Xref
↓
Interesting Function
↓
Caller / Callee
↓
Control Flow
↓
理解程式邏輯
當我們看到:
sub_401156
v3
a1
loc_401230
這些看起來很混亂的東西時,也不用急著全部理解。
先找到它們彼此之間的關係,再慢慢:
Rename
Comment
建立假設
驗證假設
Binary 原本看起來可能是一團:
sub_401156
sub_4011A0
sub_401230
sub_401300
分析到最後,就可能逐漸變成:
main
├── read_input
├── check_password
│ └── strcmp
│
└── success
└── puts
而這就是 Reverse Engineering 很核心的一個過程:
從不知道程式在做什麼,到逐步還原出程式真正的行為。