前幾天我們已經看過 Assembly、Stack,也開始接觸 Static Analysis。
像是:
file
strings
readelf
objdump
nm
這些工具都可以幫助我們觀察 Binary。
但如果今天拿到的是一個比較大的程式,全部都使用:
objdump -d ./chall
然後從幾千、幾萬行 Assembly 裡面慢慢找,基本上會看到懷疑人生。
所以在 Reverse Engineering 中,我們通常會使用專門的 Binary Analysis Tool。
常見的工具包含:
今天主要先介紹最常見的 IDA 與 Ghidra。
簡單來說,它們可以幫我們把 Binary 裡面的 Machine Code:
Machine Code
↓
Disassembly
↓
Assembly
轉成比較容易閱讀的 Assembly。
除此之外,IDA 與 Ghidra 還提供一個非常重要的功能:
Decompiler
Decompiler 會嘗試把 Assembly 轉換成類似 C Code 的形式:
Machine Code
↓
Disassembler
↓
Assembly
↓
Decompiler
↓
Pseudo C
例如原本的程式:
if (strcmp(input, "shark") == 0) {
puts("Correct!");
}
編譯之後可能變成:
call strcmp
test eax, eax
jne wrong
Decompiler 則可能重新表示成:
if (!strcmp(input, "shark"))
{
puts("Correct!");
}
看起來是不是舒服很多。
但這邊有一個很重要的觀念:
Decompiler 顯示的程式碼不是原本的 Source Code。
它只是根據 Assembly 推測出來的 Pseudocode。
所以變數名稱、型別甚至程式結構,都有可能跟原本的程式不同。
今天可以自己準備一個簡單的程式:
#include <stdio.h>
#include <string.h>
int main() {
char input[32];
printf("Password: ");
scanf("%31s", input);
if (strcmp(input, "shark") == 0) {
puts("Correct!");
} else {
puts("Wrong!");
}
return 0;
}
接著進行編譯:
gcc main.c -o main
我們可以先使用前一天學過的方法觀察:
file main
例如:
ELF 64-bit LSB pie executable, x86-64
也可以看看裡面的字串:
strings main
可能會看到:
Password:
shark
Correct!
Wrong!
光看這裡,其實已經可以猜到這個程式可能有 Password Checking。
接下來把它丟進 IDA 或 Ghidra。
Binary 被載入之後,其中一個最重要的地方就是:
Functions
IDA / Ghidra 會分析 Binary,嘗試辨識裡面的 Function。
例如:
main
puts
printf
scanf
strcmp
在 Reverse Engineering 的時候,我通常第一件事情就是先找:
main
因為很多 CTF 題目的主要邏輯,都可以從 main() 開始追。
當然 Real World Binary 就不一定這麼單純。
但現階段從 main() 開始是最容易理解的。
點進 main() 後,我們就可以看到 Assembly。
可能會看到類似:
push rbp
mov rbp, rsp
sub rsp, 20h
前幾天學過 Stack 的話,應該已經開始有點熟悉了。
這通常是在建立 Stack Frame。
接下來可能看到:
lea rax, [rbp-20h]
mov rsi, rax
lea rdi, format
call scanf
然後進行 Password 比較:
lea rax, [rbp-20h]
lea rsi, password
mov rdi, rax
call strcmp
這邊就是:
strcmp(input, "shark")
接著:
test eax, eax
jne wrong
為什麼是 eax?
因為在 x86-64 Linux 常見的 calling convention 中,Function 的 Return Value 通常放在:
RAX
而 strcmp() 如果兩個字串相同:
return 0
所以:
test eax, eax
就是在檢查:
eax == 0 ?
這就是為什麼前面學 Assembly 很重要。
因為 Decompiler 雖然很好用,但真正執行的還是 Assembly。
接著來看 Reverse Engineering 中非常好用的功能:
Decompiler
它可能會把剛才的 Assembly 顯示成:
int main()
{
char input[32];
printf("Password: ");
scanf("%31s", input);
if (strcmp(input, "shark"))
puts("Wrong!");
else
puts("Correct!");
return 0;
}
這時候程式邏輯就非常清楚了。
我們可以直接看出:
input
↓
strcmp
↓
"shark"
如果相同:
Correct!
不同:
Wrong!
這也是很多 Reverse CTF 最基礎的分析方式。
除了單純一行一行看 Assembly,IDA 也可以使用 Graph View。
程式的控制流程可能會被表示成:
strcmp(input, "shark")
|
test eax
/ \
/ \
eax=0 eax!=0
| |
Correct! Wrong!
這其實就是程式的:
Control Flow
像 C 裡面的:
if (...) {
}
else {
}
編譯成 Assembly 後,就會變成:
cmp / test
↓
conditional jump
例如:
je
jne
jg
jl
Graph View 可以讓我們不用一直在 Assembly 裡面上下跳來跳去。
對分析複雜的 if、for、while 都非常有幫助。
另一個非常重要的功能就是:
Strings
很多時候拿到一個 Binary,我們甚至不用從 main() 開始。
可以先看看有哪些有趣的 String。
例如:
Password:
Correct!
Wrong!
shark
如果看到:
Correct!
這個字串很可能就是程式成功時會使用的。
這時候我們可以查看:
誰使用了這個字串?
這就會用到下一個非常重要的概念。
Xrefs 全名是:
Cross References
簡單來說就是:
哪些地方有使用這個東西?
例如我們找到:
Correct!
接著查看 Xrefs。
工具可能告訴我們:
main + 0x52
代表 main() 裡面某個地方會使用這個 String。
我們就可以直接跳過去。
所以 Reverse Engineering 很常見的流程是:
Strings
↓
找到有趣字串
↓
Xrefs
↓
找到使用位置
↓
分析附近 Assembly
例如 CTF 裡面看到:
Correct!
Congratulations!
Flag:
Wrong password
Access denied
都值得先查看 Xrefs。
這通常比從 Entry Point 一路慢慢 Trace 快很多。
Decompiler 非常方便,但千萬不要變成:
只看 Pseudocode,不看 Assembly。
例如 Decompiler 可能顯示:
if (v3)
但真正的 Assembly 可能是:
test eax, eax
jne loc_401230
Decompiler 只是幫你重新整理。
真正執行的仍然是:
Machine Code
因此遇到奇怪的地方時,最好回頭看:
Assembly
尤其之後遇到:
Decompiler 的結果可能會變得非常奇怪。
目前我們已經可以建立一個非常基本的 Reverse 流程:
拿到 Binary
↓
file
↓
strings
↓
丟進 IDA / Ghidra
↓
找 main()
↓
查看 Decompiler
↓
查看 Assembly
↓
Strings / Xrefs
↓
理解程式邏輯
現在先不用追求:
我要看懂整支程式
實際 Reverse 的時候,更常做的是:
我現在想知道 Password 在哪裡驗證?
或者:
哪裡會輸出 Correct?
然後只追蹤與目標有關的部分。
這也是 Reverse Engineering 很重要的一個觀念:
不一定要把整個 Binary 看完,而是找到與目標相關的程式邏輯。
如果只是學 Reverse:
兩個都可以。
優點:
優點:
現階段不用糾結哪一個比較強。
因為真正需要學的是:
Assembly
Control Flow
Function
Calling Convention
Xrefs
Program Logic
工具只是幫助我們觀察 Binary。
今天正式開始使用 Reverse Engineering Tool。
我們認識了:
Functions
Disassembly
Decompiler
Graph View
Strings
Xrefs
其中最重要的是建立這個概念:
Source Code
↓
Compiler
↓
Machine Code
↓
Disassembler
↓
Assembly
↓
Decompiler
↓
Pseudo C
Decompiler 並不是把原始 Source Code 復原。
它只是幫助我們更容易理解 Assembly。
而真正的 Reverse Engineering,仍然是在理解:
Binary 到底做了什麼?
到目前為止,我們主要都還是在:
Static Analysis
也就是不執行程式的情況下分析 Binary。
但有些東西光看 Assembly 很難判斷。
例如:
某個 Register 現在到底是多少?
這個 Function 執行完之後回傳什麼?
某個 Memory Address 裡面到底放了什麼?
這時候最直接的方法,就是:
讓程式真的跑起來,然後停在我們想看的地方。
所以下一篇:Dynamic Analysis - GDB