上一篇看到了 Stack Buffer Overflow——buf 只有 32 Bytes,read 卻能讀 200 Bytes,多出來的資料會覆蓋 Stack 上其他東西。
但知道漏洞存在不代表能直接利用,還有兩個問題要先解決:
今天用 GDB + pwndbg 走一次這個過程,目標就是 確認我們可以控制 RIP
#include <stdio.h>
#include <unistd.h>
void vuln()
{
char buf[32];
puts("Input:");
read(0, buf, 200);
}
int main()
{
vuln();
return 0;
}
先關掉保護機制,專心看 Stack Overflow 本身。Binary Protection 之後再介紹。
gcc chall.c -o chall -fno-stack-protector -no-pie
執行 ./chall,輸入 AAAA,程式正常結束。但如果塞一大串 A 進去:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
就會看到 Segmentation fault。程式掛了,但到底壞在哪?開 GDB 看看。
gdb ./chall
裝了 pwndbg 的話,prompt 會變成 pwndbg>。先 disassemble vuln():
push rbp
mov rbp,rsp
sub rsp,0x20
...
call read
leave
ret
sub rsp, 0x20 在 Stack 上配置 32 Bytes,對應 char buf[32]。
前面介紹過,進 Function 之後 Stack 大概長這樣:
Higher Address
+------------------+
| Saved RIP | ← ret 時 CPU 從這裡取出 Return Address
+------------------+
| Saved RBP |
+------------------+
| buf[32] |
+------------------+
Lower Address
往 buf 塞超過 32 Bytes,多出來的會先蓋掉 Saved RBP,再蓋到 Saved RIP。Saved RIP 一旦被改掉,ret 的時候 CPU 就不會回 main(),而是跳去我們寫進去的位置。
看 Stack Layout,buf 32 Bytes 加上 Saved RBP 8 Bytes,直覺猜 Offset 是 40。
不過實際上不要靠猜。Compiler 的 alignment、padding 都可能影響 Stack Layout,用 cyclic pattern 確認比較保險。
如果全部輸入 AAAA...,Crash 的時候看到 0x4141414141414141,沒辦法判斷這是第幾個 Byte 蓋上去的。
cyclic pattern 的設計是讓每個位置的 4 Bytes 都不一樣:
aaaabaaacaaadaaaeaaafaaagaaah...
這樣看到某個值就能反查它在 Input 裡的位置。
在 pwndbg 裡產生 100 Bytes 的 pattern:
pwndbg> cyclic 100
然後 run,把 pattern 貼進去,程式會 Crash。
Crash 之後用 info registers 或 pwndbg 的顯示看 Register。也可以用 telescope $rsp 看 Stack。
假設看到 Return Address 附近出現了 0x6161616b6161616a 這種值,明顯不是正常 Address,而是我們輸入的 cyclic pattern。
接著問 pwndbg 這段 pattern 在第幾個 Byte:
pwndbg> cyclic -l 0x6161616b
40
Offset = 40。也就是 Input 的第 0~39 Byte 填滿 buf 和 Saved RBP,第 40 Byte 開始蓋到 Saved RIP。
跟前面猜的一樣,但這個值每題都不同,不能直接假設 40。
知道 Offset 之後,可以精準構造 Payload:40 個 A 當 padding,後面接 8 個 B。如果 Saved RIP 變成 0x4242424242424242,就確認了我們能控制 Return Address。
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"B"*8)' > payload
GDB 裡執行:
pwndbg> run < payload
程式 Crash,觀察 Return Address 附近確實出現 0x4242424242424242。Saved RIP 被我們蓋掉了。
讓程式 Segfault 只能說明有 Memory Corruption,但不代表能利用。Crash 之後要繼續確認:我們到底控制了什麼?是 RIP?RSP?某個 Function Pointer?還是只是蓋到了不重要的地方?
今天確認的是能控制 Saved RIP,也就是 ret 之後程式跳去哪裡由我們決定。
同一行 read(0, buf, 200),Reverse 看到的是「把 Input 讀進 buf」而已。
Pwn 會想說:buf 多大?read 能讀多少?超過的部分蓋到哪?Saved RIP 離 buf 多遠?角度不一樣,一個在理解行為,一個在找怎麼打。
今天的流程:
cyclic -l 算出 Offset = 40Payload 的結構就是 Padding (40 Bytes) + New Return Address (8 Bytes)。目前塞的 0x4242424242424242 沒有實際意義,真正打的時候這裡會放 win() 的位址、ROP Gadget、或 libc 裡的東西。
Return Address 可以由我們決定了,接下來的問題是:要讓程式 Return 到哪?
不過每次手動複製 pattern、算 Offset、拼 payload 實在太煩了。Day19 來用 pwntools 把這些全部自動化。