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()等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章