前一篇開始進入 Pwn 之後,我們知道一個 Process 執行時,Memory 大致可以分成:
Text
Data
BSS
Heap
Stack
其中 Stack 會存放:
Local Variable
Saved Register
Return Address
而今天就要開始接觸 Pwn 中最經典的漏洞之一:
Stack Buffer Overflow
先從最基本的 Buffer 開始。
假設程式裡面有:
char name[16];
代表程式準備了一塊:
16 Bytes
的空間存放資料。
可以想像成:
+----+----+----+----+----+----+----+----+
| | | | | | | | |
+----+----+----+----+----+----+----+----+
| | | | | | | | |
+----+----+----+----+----+----+----+----+
16 Bytes
如果輸入:
shark
當然完全沒有問題。
但如果程式沒有檢查長度,我們卻塞進:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
會發生什麼?
資料不會因為 Buffer 只有 16 Bytes 就自動停下來。
它可能會繼續往後寫。
這就是:
Buffer Overflow
先來看一個簡單的程式:
#include <stdio.h>
struct user {
char name[16];
int is_admin;
};
int main() {
struct user u = {0};
printf("Name: ");
scanf("%s", u.name);
printf("is_admin = %d\n", u.is_admin);
if (u.is_admin) {
puts("Welcome, admin!");
}
return 0;
}
編譯:
gcc vuln.c -o vuln -g -O0
這裡最值得注意的是:
scanf("%s", u.name);
%s 並不知道:
u.name 只有 16 Bytes
所以只要我們輸入超過 16 Bytes,資料就會繼續往後寫。
先正常執行:
./vuln
輸入:
Name: shark
可能得到:
is_admin = 0
目前記憶體大概可以想像成:
name
↓
+------------------+----------+
| shark | is_admin |
| | 0 |
+------------------+----------+
16 Bytes 4 Bytes
因為:
is_admin = 0;
所以不會進入:
if (u.is_admin)
接著故意輸入超過 16 Bytes。
例如:
AAAAAAAAAAAAAAAAA
這裡有:
17 個 A
但 name 只有:
16 Bytes
所以前 16 個 A:
AAAAAAAAAAAAAAAA
會填滿 name。
第 17 個 A 就會繼續寫到後面的記憶體。
也就是:
+------------------+----------+
| AAAAAAAAAAAAAAAA | A....... |
+------------------+----------+
name is_admin
ASCII:
A = 0x41
在常見的 x86-64 Little Endian 環境下,就可能看到:
is_admin = 65
因為:
0x41 = 65
接著:
if (u.is_admin)
判斷的不是:
is_admin == 1
而是:
is_admin != 0
所以程式可能直接輸出:
Welcome, admin!
我們沒有正常修改:
is_admin
卻透過:
Buffer Overflow
改到了它。
問題其實非常單純。
程式以為:
User Input
↓
name[16]
但真正發生的是:
User Input
↓
name[16]
↓
繼續寫
↓
其他 Memory
Memory 本身並不知道:
這 16 Bytes 是 name
後面 4 Bytes 是 is_admin
對 CPU 來說,它們就只是:
Memory Address
如果程式沒有檢查:
Input Length
資料就可能一路寫下去。
可以用前面學過的 GDB 觀察。
gdb ./vuln
進入 main:
break main
run
可以查看兩個變數的 Address:
p/x &u.name[0]
p/x &u.is_admin
可能看到類似:
&u.name[0] = 0x7fffffffe1a0
&u.is_admin = 0x7fffffffe1b0
兩者相差:
0x10
也就是:
16 Bytes
剛好就是:
char name[16];
的大小。
所以:
name[0]
0x...e1a0
name[15]
0x...e1af
is_admin
0x...e1b0
如果輸入超過 16 Bytes:
name
↓
AAAAAAAAAAAAAAAA
↓
is_admin
就開始被覆蓋。
這也是 Dynamic Analysis 很重要的地方。
Source Code 只會告訴我們:
char name[16];
int is_admin;
但 GDB 可以直接讓我們看到:
它們實際在 Memory 的哪裡
資料又是怎麼被寫進去的
今天的範例只是覆蓋:
Local Variable
但事情真正危險的地方是:
Stack 上面不只有 Local Variable。
前面介紹 Stack 時看過:
High Address
+------------------+
| Return Address |
+------------------+
| Saved RBP |
+------------------+
| Local Variables |
+------------------+
Low Address
如果某個 Buffer 不斷 Overflow:
Buffer
↓
其他 Local Variable
↓
Saved RBP
↓
Return Address
那問題就不再只是:
is_admin 被改掉
而可能變成:
Return Address 被修改
而我們之前學 Function Call 時知道:
Return Address
決定了 Function 執行完:
ret
之後要回到哪裡。
所以如果 Return Address 可以被控制……
事情就開始有趣了。
最直接的方法就是:
不要讓輸入超過 Buffer 大小。
例如剛才:
scanf("%s", u.name);
可以限制長度:
scanf("%15s", u.name);
因為:
15 Bytes Data
+
1 Byte \0
=
16 Bytes
或者使用:
fgets(u.name, sizeof(u.name), stdin);
核心概念都是:
永遠確認:
Input Length <= Buffer Size
今天第一次真正接觸到:
Memory Corruption
整個問題可以簡化成:
Buffer Size = 16
User Input > 16
↓
Buffer 被填滿
↓
繼續寫入後面的 Memory
↓
覆蓋其他資料
今天我們只做到:
Buffer Overflow
↓
覆蓋 Local Variable
↓
改變程式狀態
但真正的 Pwn 不會只滿足於修改:
is_admin
如果 Stack Layout 是:
Buffer
↓
Saved RBP
↓
Saved RIP
那下一個問題自然就是:
到底要輸入多少 Bytes,才能剛好碰到 Return Address?
以及:
如果 Return Address 可以被覆蓋,我們能不能控制 RIP?
下一篇就來正式進入:RIP Control