天天看點

緩沖區溢出

1 引言

“緩沖區溢出”對現代作業系統與編譯器來講已經不是什麼大問題,但是作為一個合格的 C/C++ 程式員,還是完全有必要了解它的整個細節。

計算機程式一般都會使用到一些記憶體,這些記憶體或是程式内部使用,或是存放使用者的輸入資料,這樣的記憶體一般稱作緩沖區。簡單的說,緩沖區就是一塊連續的計算機記憶體區域,它可以儲存相同資料類型的多個執行個體,如字元數組。而緩沖區溢出則是指當計算機向緩沖區内填充資料位數時超過了緩沖區本身的容量,溢出的資料覆寫在合法資料上。

2 C/C++中記憶體配置設定

任何一個源程式通常都包括靜态的代碼段(或者稱為文本段)和靜态的資料段,為了運作程式,作業系統首先負責為其建立程序,并在程序的虛拟位址空間中為其代碼段和資料段建立映射。但是隻有靜态的代碼段和資料段是不夠的,程序在運作過程中還要有其動态環境。

一般說來,預設的動态存儲環境通過堆棧機制建立。所有局部變量及所有按值傳遞的函數參數都通過堆棧機制自動配置設定記憶體空間。如下圖。

緩沖區溢出

程式在記憶體的映射

棧區(stack):由編譯器自動配置設定與釋放,存放為運作時函數配置設定的局部變量、函數參數、傳回資料、傳回位址等。其操作類似于資料結構中的棧。

堆區(heap):一般由程式員自動配置設定,如果程式員沒有釋放,程式結束時可能有OS回收。其配置設定類似于連結清單。

全局區(靜态區static):資料段,程式結束後由系統釋放。全局區分為已初始化全局區(data),用來存放儲存全局的和靜态的已初始化變量和未初始化全局區(bss),用來儲存全局的和靜态的未初始化變量。

常量區(文字常量區):資料段,存放常量字元串,程式結束後有系統釋放。

代碼區:存放函數體(類成員函數和全局區)的二進制代碼,這個段在記憶體中一般被标記為隻讀,任何對該區的寫操作都會導緻段錯誤(Segmentation Fault)。

需要特别注意的是,堆(Heap)和棧(Stack)是有差別的,很多程式員混淆堆棧的概念,或者認為它們就是一個概念。簡單來說,它們之間的主要差別可以表現在如下五個方面。

配置設定和管理方式不同

堆是動态配置設定的,其空間的配置設定和釋放都由程式員控制。也就是說,堆的大小并不固定,可動态擴張或縮減,其配置設定由malloc()等這類實時記憶體配置設定函數來實作。當程序調用malloc等函數配置設定記憶體時,新配置設定的記憶體就被動态添加到堆上(堆被擴張);當利用free等函數釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)。

而棧由編譯器自動管理,其配置設定方式有兩種:靜态配置設定和動态配置設定。靜态配置設定由編譯器完成,比如局部變量的配置設定。動态配置設定由alloca()函數進行配置設定,但是棧的動态配置設定和堆是不同的,它的動态配置設定是由編譯器進行釋放,無需手工控制。

申請的大小限制不同

棧是向低位址擴充的資料結構,是一塊連續的記憶體區域,棧頂的位址和棧的最大容量是系統預先規定好的,能從棧獲得的空間較小。

堆是向高位址擴充的資料結構,是不連續的記憶體區域,這是由于系統是由連結清單在存儲空閑記憶體位址,自然堆就是不連續的記憶體區域,且連結清單的周遊也是從低位址向高位址周遊的,堆的大小受限于計算機系統的有效虛拟記憶體空間,

由此空間,堆獲得的空間比較靈活,也比較大。在 32 位平台下,VC6 下預設為 1M,堆最大可以到 4G;

申請效率不同

棧由系統自動配置設定,速度快,但是程式員無法控制。

堆是有程式員自己配置設定,速度較慢,容易産生碎片,不過用起來友善。

産生碎片不同

對堆來說,頻繁執行malloc或free勢必會造成記憶體空間的不連續,形成大量的碎片,使程式效率降低;而對棧而言,則不存在碎片問題。

記憶體位址增長的方向不同

堆是向着記憶體位址增加的方向增長的,從記憶體的低位址向高位址方向增長;

棧的增長方向與之相反,是向着記憶體位址減小的方向增長,由記憶體的高位址向低位址方向增長。

假設一個程式的函數調用順序為:主函數main調用函數func1,函數func1調用函數func2。當這個程式被作業系統調入記憶體運作時,其對應的程序在記憶體中的映射結果如下圖所示

緩沖區溢出

例子中的記憶體映射

程序的棧是由多個棧幀構成的,其中每個棧幀都對應一個函數調用。當調用函數時,新的棧幀被壓入棧;當函數傳回時,相應的棧幀從棧中彈出。由于需要将函數傳回位址這樣的重要資料儲存在程式員可見的堆棧中,是以也給系統安全帶來了極大的隐患。

當程式寫入超過緩沖區的邊界時,就會産生所謂的“緩沖區溢出”。發生緩沖區溢出時,就會覆寫下一個相鄰的記憶體塊,導緻程式發生一些不可預料的結果:也許程式可以繼續,也許程式的執行出現奇怪現象,也許程式完全失敗或者崩潰等。

緩沖區溢出

對于緩沖區溢出,一般可以分為4種類型,即棧溢出、堆溢出、BSS溢出與格式化串溢出。其中,棧溢出是最簡單,也是最為常見的一種溢出方式。

沒有保證足夠的存儲空間存儲複制過來的資料

void function(char *str) 
{
   char buffer[10];
   strcpy(buffer,str);
}      

上面的strcpy()将直接把str中的内容copy到buffer中。這樣隻要str的長度大于 10 ,就會造成buffer的溢出,使程式運作出錯。存在象strcpy這樣的問題的标準函數還有strcat(),sprintf(),vsprintf(),gets(),scanf()等。對應的有更加安全的函數,即在函數名後加上_s,如scanf_s()函數。

嚴格檢查輸入長度和緩沖區長度。

常見的高危函數

函數 嚴重性 防範手段

gets() 最危險 使用 fgets(buf, size, stdin)

strcpy() 很危險 改為使用 strncpy()

strcat() 很危險 改為使用 strncat()

sprintf() 很危險 改為使用snprintf(),或者使用精度說明符

scanf() 很危險 使用精度說明符,或自己進行解析

sscanf() 很危險 使用精度說明符,或自己進行解析

fscanf() 很危險 使用精度說明符,或自己進行解析

vfscanf() 很危險 使用精度說明符,或自己進行解析

vfscanf() 很危險 改為使用 vsnprintf(),或者使用精度說明符

vscanf() 很危險 使用精度說明符,或自己進行解析

vsscanf() 很危險 使用精度說明符,或自己進行解析

streadd() 很危險 使用精度說明符,或自己進行解析

整數溢出

寬度溢出:把一個寬度較大的操作數賦給寬度較小的操作數,就有可能發生資料截斷或符号位丢失

#include<stdio.h>
int main()
{
  signed int value1 = 10;
  usigned int value2 = (unsigned int)value1;
}      

算術溢出,該程式即使在接受使用者輸入的時候對a、b的指派做安全性檢查,a+b 依舊可能溢出:

#include<stdio.h>
int main()
{
  int a;
  int b;
  int c=a*b;
  return 0;
}      

數組索引不在合法範圍内

enum {TABLESIZE = 100};
int *table = NULL;
int insert_in_table(int pos, int value) {
  if(!table) {
    table = (int *)malloc(sizeof(int) *TABLESIZE);
  }
  if(pos >= TABLESIZE) {
    return -1;
  }
  table[pos] = value;
  return 0;
}      

其中:pos為int類型,可能為負數,這會導緻在數組所引用的記憶體邊界之外進行寫入,可以将pos類型改為size_t避免

空字元錯誤

例如:

//錯誤

char array[]={'0','1','2','3','4','5','6','7','8'};

//正确的寫法應為:

char array[]={'0','1','2','3','4','5','6','7','8',’\0’};

//或者

char array[11]={'0','1','2','3','4','5','6','7','8','9’};

繼續閱讀