天天看點

c記憶體系列(一):緩沖區溢出

轉自:http://blog.chinaunix.net/uid-20340944-id-1702253.html

在這裡強調一下,想完全看的懂這篇文章,至少需要具備一定的彙編語言,C語言和LINUX的基礎。

緩沖區溢出”在英文中可以解釋為:buffer overflow,buffer overrun,smash the

stack,trash the stack,scribble the stack, mangle the stack, memory

leak,overrun screw;

我們通常所說的“溢出”指的是緩沖區溢出,(廢話,不然要從那裡溢出呀!)先解釋一下什麼是緩沖區——緩沖區是記憶體中存放資料的地方,是程式運作時計算機記憶體中的一個連續的塊,它儲存了給定類型的資料。問題随着動态配置設定變量而出現。為了不用太多的記憶體,一個有動态配置設定變量的程式在程式運作時才決定給他們配置設定多少記憶體。當程式試圖将資料放到計算機記憶體中的某一位置,但沒有足夠空間時會發生緩沖區溢出。另一種說法,就是說程式在動态配置設定緩沖區放入太多的資料會有什麼現象?它會溢出,會漏到了别的地方。

一個緩沖區溢出應用程式使用這個溢出的資料将彙編語言代碼放到計算機的記憶體中,通常是産生root權限的地方。單單的緩沖區溢出,并不會産生安全問題。隻有将溢出送到能夠以root權限運作指令的區域才行。這樣,一個緩沖區利用程式将能運作的指令放在了有root權限的記憶體中,進而一旦運作這些指令,就是以root權限控制了計算機。

是以我們更多的時候把緩沖區溢出指的是一種系統攻擊的手段,通過往程式的緩沖區寫超出其長度的内容,造成緩沖區的溢出,進而破壞程式的堆棧,使程式轉而執行其它指令,以達到攻擊的目的。據統計,通過緩沖區溢出進行的攻擊占所有系統攻擊總數的80%以上。

世界上第一個緩沖區溢出攻擊——著名的Morris蠕蟲,發生在十年前,它曾造成了全世界6000多台網絡伺服器癱瘓。我這是我所知的最早的緩沖區溢出攻擊程式。

記住,造成緩沖區溢出的原因是程式中沒有仔細檢查使用者輸入的參數!!!

下面舉一個最為常見的,也是最簡單的溢出。

void function(char *str){

char buffer[16];

strcpy(buffer,str);

}

在這個例子中上面的buffer的長度被限制在16,而strcpy()将直接把str中的内容copy到buffer中。這樣隻要str的長度大于16,就會造成buffer的溢出,使程式運作出錯。So,我們說這個程式溢出了。

在C語言中,靜态變量是配置設定在資料段中的,動态變量是配置設定在堆棧段的。緩沖區溢出是利用堆棧段的溢出的。一個正常的程式在記憶體中通常分為程式段,資料端和堆棧三部分。程式段裡放着程式的機器碼和隻讀資料,這個段通常是隻讀,對它的寫操作是非法的。資料段放的是程式中的靜态資料。動态資料則通過堆棧來存放。在記憶體中,它們的位置如下:

  

  /――――――――  記憶體低端

  程式段

  ―――――――――

  資料段

  ―――――――――

  堆棧

  ―――――――――/記憶體高端

堆棧是記憶體中的一個連續的塊。一個叫堆棧指針的寄存器(SP)指向堆棧的棧頂。堆棧的底部是一個固定位址。堆棧有一個特點就是,後進先出。也就是說,後放入的資料第一個取出。它支援兩個操作,PUSH和POP。PUSH是将資料放到棧的頂端,POP是将棧頂的資料取出。

在進階語言中,程式函數調用和函數中的臨時變量都用到堆棧。為什麼呢?因為在調用一個函數時,我們需要對目前的操作進行保護,也為了函數執行後,程式可以正确的找到地方繼續執行,是以參數的傳遞和傳回值也用到了堆棧。通常對局部變量的引用是通過給出它們對SP的偏移量來實作的。另外還有一個基址指針(FP,在Intel晶片中是BP),許多編譯器實際上是用它來引用本地變量和參數的。通常,參數的相對FP的偏移是正的,局部變量是負的。

當程式中發生函數調用時,計算機做如下操作:首先把參數壓入堆棧;然後儲存指令寄存器(IP)中的内容,做為傳回位址(RET);第三個放入堆棧的是基址寄存器(FP);然後把目前的棧指針(SP)拷貝到FP,做為新的基位址;最後為本地變量留出一定空間,把SP減去适當的數值。

比如說下面這個程式:

void function(int a, int b, int c){

char buffer1[10];

char buffer2[15];

}

void main(){

function(1,2,3);

}

假設我們在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

  < ------ [ ] [ ] [ ] [ ] [ ] [ ] [ ]

  棧頂 棧底

  那是什麼導緻了溢出呢?緩沖區溢出就是在一個緩沖區裡寫入過多的資料。要怎麼樣利用呢?看下面這個程式:

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,

這超出了程式的位址空間,是以出現段錯誤。可見,緩沖區溢出允許我們改變一個函數的傳回位址,在這個例子中,我們可以通過修改0x41414141來改變程式傳回後的入口位址。同樣通過這種方式,就可以改變程式的執行順序了。

存在象strcpy這樣的問題的标準函數還有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循環内的getc(),fgetc(),getchar()等。