前面幾天我們花了不少時間在 Reverse Engineering。
從:
Assembly
Static Analysis
Dynamic Analysis
GDB
Control Flow
Xref
Crackme
一路到昨天實際分析一個稍微完整一點的 Reverse Challenge。
而從今天開始,我們要正式進入這個系列的另外一半:
Pwn
如果說 Reverse Engineering 的目標是:
理解 Binary 到底在做什麼。
那 Pwn 更像是:
找到 Binary 裡面的漏洞,並利用它改變原本的程式行為。
而 Binary Exploitation 中最經典的起點,就是:
Buffer Overflow
在 CTF 裡面,Pwn 通常指的是:
Binary Exploitation
也就是針對 Binary 中的漏洞進行利用。
例如一個正常的程式原本只能:
Input
↓
Program Logic
↓
Output
但如果程式存在漏洞,我們可能可以讓流程變成:
Input
↓
Memory Corruption
↓
改變 Program Control Flow
↓
執行原本不應該執行的程式碼
例如:
取得 Flag
呼叫某個 Function
控制 RIP
執行 Shellcode
ROP
取得 Shell
這些都是之後會慢慢碰到的東西。
其實 Reverse 和 Pwn 並不是完全分開的兩個東西。
例如我們前面分析 Crackme 時會問:
Password 在哪裡檢查?
哪個 Function 負責驗證?
Success Path 在哪裡?
程式什麼時候會 print_flag()?
這些都是 Reverse。
而 Pwn 則會進一步問:
有沒有辦法不照原本的邏輯走?
有沒有 Memory Corruption?
能不能控制 Return Address?
能不能直接跳到 print_flag()?
所以很多 Pwn 題目的流程其實是:
Reverse
↓
理解程式
↓
找到 Vulnerability
↓
Exploit
也就是說,前面學的 Reverse 並沒有白學。
反而是接下來做 Pwn 很重要的基礎。
先來看一個很簡單的 C Program:
#include <stdio.h>
#include <unistd.h>
void vuln() {
char buffer[32];
puts("Input:");
read(0, buffer, 128);
}
int main() {
vuln();
return 0;
}
乍看之下程式非常簡單:
建立一個 buffer
↓
讀取使用者輸入
↓
程式結束
但仔細看:
char buffer[32];
代表:
buffer 只有 32 bytes
可是下面卻:
read(0, buffer, 128);
允許使用者最多輸入:
128 bytes
所以就出現一個問題:
如果我輸入超過 32 bytes,剩下的資料會去哪裡?
這就是今天的重點。
Buffer Overflow 中文通常翻成:
緩衝區溢位
假設我們有:
char buffer[8];
Memory 原本只準備:
buffer
+---+---+---+---+---+---+---+---+
| | | | | | | | |
+---+---+---+---+---+---+---+---+
8 bytes
如果我們輸入:
AAAAAAAA
剛好:
8 bytes
沒有問題。
但如果輸入:
AAAAAAAAAAAAAAAA
也就是:
16 bytes
那前面 8 bytes 會進入 buffer。
剩下的資料就可能繼續往旁邊的 Memory 寫。
概念上就會變成:
Buffer Other Data
──────── ──────────
AAAAAAAA AAAAAAAA
^^^^^^^^ ^^^^^^^^
合法範圍 Overflow
這就是:
Buffer Overflow
如果只是多寫幾個 A,好像也沒什麼。
真正的問題在於:
Stack 上除了 Local Variable,還有其他很重要的資料。
前面介紹 Stack 的時候有提到 Function Call。
簡化來看的話,一個 Stack Frame 可能類似:
High Address
+------------------+
| Return Address |
+------------------+
| Saved RBP |
+------------------+
| |
| buffer |
| |
+------------------+
Low Address
其中非常重要的一個東西就是:
Return Address
它會告訴 CPU:
這個 Function 執行結束之後,要回去哪裡繼續執行?
假設:
main() {
vuln();
puts("Done");
}
流程可能是:
main()
↓
vuln()
↓
ret
↓
回到 main()
↓
puts("Done")
CPU 必須知道:
vuln() 結束後要回 main() 的哪個位置
這個位置就是 Return Address。
回到剛才的程式:
char buffer[32];
read(0, buffer, 128);
如果我們一直輸入:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
資料可能一路超過:
buffer
繼續覆蓋:
Saved RBP
甚至最後碰到:
Return Address
概念上變成:
+------------------+
| AAAAAAAAAAAAAAAA | ← Return Address
+------------------+
| AAAAAAAAAAAAAAAA | ← Saved RBP
+------------------+
| AAAAAAAAAAAAAAAA |
| AAAAAAAAAAAAAAAA | ← buffer
+------------------+
這時候 Function 執行:
ret
CPU 原本應該拿到:
正常的 Return Address
結果現在拿到的卻可能是:
0x4141414141414141
為什麼是 41?
因為 ASCII:
'A' = 0x41
所以:
AAAAAAAA
在 Memory 裡面就是:
41 41 41 41 41 41 41 41
先編譯剛才的程式。
這邊為了學習 Buffer Overflow,先把部分保護關掉:
gcc chall.c -o chall \
-fno-stack-protector \
-no-pie \
-g
接著正常執行:
./chall
輸入:
AAAA
程式正常結束。
但如果一次塞很多資料:
python3 -c 'print("A"*100)' | ./chall
就可能看到:
Segmentation fault
也就是:
程式 Crash 了
這時候我們已經成功做到第一件事:
使用我們可以控制的 Input,破壞程式的 Memory。
前面學過的 GDB 現在就派上用場了。
先產生一個 Input:
python3 -c 'print("A"*100)' > input
接著:
gdb ./chall
執行:
run < input
程式可能會在 Function Return 附近 Crash。
這時可以看看 Stack:
x/10gx $rsp
可能會看到類似:
0x7fffffffe000: 0x4141414141414141
0x7fffffffe008: 0x4141414141414141
0x7fffffffe010: 0x4141414141414141
也就是 Stack 上出現大量:
0x4141414141414141
這代表:
我們輸入的 A
已經不只存在 buffer 裡面了。
而是一路 Overflow 到其他 Stack Data。
這邊有一個很重要的觀念。
我們現在做到的是:
Buffer Overflow
↓
Memory Corruption
↓
Crash
但:
讓程式 Crash 跟真正完成 Exploit 是兩回事。
如果今天只能:
AAAAAAAAAAAA
↓
Segmentation Fault
那頂多只能算找到一個 Crash。
我們真正想做到的是:
Input
↓
Buffer Overflow
↓
精確覆蓋 Return Address
↓
控制 RIP
↓
讓程式跳到我們指定的位置
也就是從:
Crash
進一步變成:
Control
這才是 Binary Exploitation 真正有趣的地方。
在 x86-64 中:
RIP
代表目前正在執行的 Instruction Address。
例如:
RIP = 0x401156
代表 CPU 現在正在執行:
0x401156
附近的 Instruction。
如果我們有辦法控制:
RIP
那就代表:
我們可能可以決定 CPU 下一步去哪裡執行。
例如 Binary 裡面剛好存在:
void win() {
system("/bin/sh");
}
正常情況下程式可能永遠不會呼叫:
win()
但如果 win() 位址是:
0x401176
那如果我們可以把 Return Address 改成:
0x401176
Function ret 時:
ret
↓
0x401176
↓
win()
程式就可能直接跑進 win()。
這也是之後會介紹的經典:
ret2win
到這裡應該可以看到 Reverse 和 Pwn 是怎麼接起來的。
Reverse 時,我們會找:
win() 在哪裡?
Function Address 是多少?
程式有哪些 Branch?
Return Address 怎麼運作?
Pwn 則會問:
我能不能利用漏洞:
Return Address
↓
改成
↓
win()
所以:
Reverse
↓
知道程式可以去哪裡
Pwn
↓
想辦法讓程式真的去那裡
這也是為什麼 Binary Exploitation 通常離不開 Reverse Engineering。
如果 Buffer Overflow 只需要:
塞爆 Buffer
↓
改 Return Address
↓
跳到 Shellcode
那事情就太簡單了。
現代 Binary 通常還會有各種 Exploit Mitigation:
Stack Canary
NX
PIE
ASLR
RELRO
可以先使用:
checksec --file=chall
查看 Binary Protection。
不同 Protection 會限制不同的 Exploit 手法。
例如:
Canary
→ 防止簡單 Stack Overflow
NX
→ Stack 通常不能直接執行程式碼
PIE + ASLR
→ Address 不再固定
RELRO
→ 保護部分 GOT 結構
這些東西之後都會慢慢介紹。
現在第一階段先專注在:
Stack
Buffer Overflow
Return Address
RIP
就好。
之後拿到一題簡單的 Pwn Challenge,可以先按照這個方向:
Binary
↓
file / checksec
↓
執行程式
↓
Reverse
↓
找到 Input
↓
找到 Vulnerability
↓
嘗試 Crash
↓
GDB
↓
觀察 Stack
↓
找到 Offset
↓
控制 RIP
↓
Exploit
今天我們目前做到:
找到 Vulnerability
↓
成功 Crash
下一個問題就是:
到底要輸入幾個 Byte,才會剛好碰到 Return Address?
總不能每次都:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
然後慢慢猜。
所以接下來就要開始找:
Offset
今天正式從 Reverse Engineering 進入 Pwn。
首先認識了:
Pwn
Binary Exploitation
Buffer Overflow
Stack Overflow
Return Address
RIP
Memory Corruption
最重要的概念可以整理成:
固定大小的 Buffer
↓
程式卻允許輸入過多資料
↓
Buffer Overflow
↓
覆蓋 Stack Data
↓
Return Address
↓
Control Flow
但今天我們只是成功讓:
Program Crash
真正的 Exploit 還需要做到:
Crash
↓
找到 Offset
↓
控制 Return Address
↓
控制 RIP
所以下一篇就來實際處理:
Stack Buffer Overflow
+
Cyclic Pattern
+
Offset
+
控制 RIP
也就是第一次真正開始把:
Buffer Overflow
變成:
Binary Exploitation