常見緩沖區溢出函數
C 和 C++ 不能夠自動地做邊界檢查,邊界檢查的代價是效率。一般來講,C 在大多數情況下注重效率。然而,獲得效率的代價是,C 程序員必須十分警覺以避免緩沖區溢出問題。
C語言標準庫中的許多字符串處理和IO流讀取函數是導致緩沖區溢出的罪魁禍首。我們有必要了解這些函數,在編程中多加小心。
一、字符串處理函數
1. strcpy()
strcpy()函數將源字符串復制到緩沖區。沒有指定要復制字符的具體數目!如果源字符串碰巧來自用戶輸入,且沒有專門限制其大小,則有可能會造成緩沖區溢出!
我們也可以使用strncpy來完成同樣的目的:
- strncpy(dst, src, dst_size-1);
如果 src 比 dst 大,則該函數不會拋出一個錯誤;當達到最大尺寸時,它只是停止復制字符。注意上面調用 strncpy() 中的 -1。如果 src 比 dst 長,則那給我們留有空間,將一個空字符放在 dst 數組的末尾。
但是! strncpy()也不完全安全,也有可能把事情搞糟。即使“安全”的調用有時會留下未終止的字符串,或者會發生微妙的相差一位錯誤。
確保 strcpy() 不會溢出的另一種方式是,在需要它時就分配空間,確保通過在源字符串上調用 strlen() 來分配足夠的空間。
- dst = (char *)malloc(strlen(src));
- strcpy(dst, src);
2. strcat()
strcat()函數非常類似于 strcpy(),除了它可以將一個字符串合并到緩沖區末尾。它也有一個類似的、更安全的替代方法 strncat()。如果可能,使用 strncat() 而不要使用 strcat()。
3. sprintf()、vsprintf()
函數 sprintf()和 vsprintf()是用來格式化文本和將其存入緩沖區的通用函數。它們可以用直接的方式模仿 strcpy() 行為。換句話說,使用 sprintf() 和 vsprintf() 與使用 strcpy() 一樣,都很容易對程序造成緩沖區溢出。
sprintf() 的許多版本帶有使用這種函數的更安全的方法。可以指定格式字符串本身每個自變量的精度。sprintf 采用”*”來占用一個本來需要一個指定寬度或精度的常數數字的位置,而實際的寬度或精度就可以和其它被打印的變量一樣被提供出來。
例如:
- sprintf(usage, "USAGE: %*s\n", BUF_SIZE, argv[0]);
二、字符讀取函數
1. gets()
永遠不要使用 gets()。該函數從標準輸入讀入用戶輸入的一行文本,它在遇到 EOF 字符或換行字符之前,不會停止讀入文本。也就是:gets() 根本不執行邊界檢查。因此,使用 gets() 總是有可能使任何緩沖區溢出。
作為一個替代方法,可以使用方法 fgets()。它可以做與 gets() 所做的同樣的事情,但它接受用來限制讀入字符數目的大小參數,因此,提供了一種防止緩沖區溢出的方法。
2. getchar()、fgetc()、getc()、read()
如果在循環中使用這些函數,確保檢查緩沖區邊界
3. scanf()系列
sscanf()、fscanf()、vfscanf()、vscanf()、vsscanf()
scanf系列的函數也設計得很差。目的地緩沖區也可能會發生溢出。
同樣地,我們用設置寬度也可以解決這個問題。
4. getenv()
使用系統調用getenv() 的最大問題是您從來不能假定特殊環境變量是任何特定長度的。
三、使用安全版本的代碼庫
微軟對于有緩沖溢出危險的API使用其開發的安全版本的庫來替代。
SafeCRT自Visual Studio 2005起開始支持。當代碼中使用了禁用的危險的CRT函數,Visual Studio 2005編譯時會報告相應警告信息,以提醒開發人員考慮將其替代為Safe CRT中更為安全。
1. 有關字符串拷貝的API
例如:strcpy, wcscpy等
替代的Safe CRT函數:strcpy_s
2. 有關字符串合并的API
例如:strcat, wcscat等
替代的Safe CRT函數:strcat_s
3. 有關sprintf的API
例如:sprintf, swprintf等
替代的Safe CRT函數:
- _snprintf_s
- _snwprintf_s
其它被禁用的API還有scanf, strtok, gets, itoa等等。 ”n”系列的字符串處理函數,例如strncpy等,也在被禁用之列。
舉個栗子
破解下面的密碼防護代碼:
- #include <stdio.h>int main(int argc, char *argv[])
- {
- int flag = 0;
- char passwd[10];
- memset(passwd,0,sizeof(passwd));
- strcpy(passwd, argv[1]);
- if(0 == strcmp("LinuxGeek", passwd))
- {
- flag = 1;
- }
- if(flag)
- {
- printf("\n Password cracked \n");
- }
- else {
- printf("\n Incorrect passwd \n");
- }
- return 0;
- }
如果把命令行輸入的文字當作密碼的話,會有很大的漏洞:
首先如果我輸入11個字符且最后一個字符是大于0的話,就慘了,strcpy是要copy到’/0’的。他會一直把這11個字符都copy到passwd數組中,此時數組越界了,最后一個字符就把flag標志位個賦值了,if條件就滿足了,密碼就被破解了!
經過上面我們的討論,我們可以對用戶輸入動態分配同樣大小的空間,而不是提前分配固定的空間。
- passwd = (char *)malloc(strlen(argv[1]));
- strcpy(passwd, argv[1]);
注意:
不要用strncpy(),它會造成最后一位的丟失,造成隱藏的錯誤。
四、關于緩沖區溢出問題
由于函數調用棧頭部會保存其調用者棧的基地址%ebp,如果破壞了存儲%ebp的值,那么基址寄存器就不能正確地恢復,因此調用者就不能正確地引用它的局部變量或參數。
如果破壞了存儲的返回地址,那么ret指令會使程序跳轉到完全意想不到的地方。
緩沖區溢出的一個更加致命的使用就是讓程序執行它本來不愿意執行的函數。這是一種最常見的通過計算機網絡攻擊系統安全的方法。通常,輸入給程序一個字符串,這個字符串包含一些可執行代碼的字節編碼,稱為攻擊代碼,另外還有一些字節會用一個指向攻擊代碼的指針覆蓋返回地址。那么,執行ret指令的效果就是跳轉到攻擊代碼。
五、對抗緩沖區溢出攻擊
1. 棧隨機化
為了在系統中插入攻擊代碼,攻擊者不但要插入代碼,還要插入指向這段代碼的指針,這個指針也是攻擊字符串的一部分。產生這個指針需要知道這個字符串放置的棧地址。在過去,程序的棧地址非常容易預測,在不同的機器之間,棧的位置是相當固定的。
棧隨機化的思想使得棧的位置在程序每次運行時都有變化。因此,即使許多機器都運行相同的代碼。它們的棧地址都是不同的。
實現的方式是:程序開始時,在棧上分配一段0--n字節之間的隨機大小空間。程序不使用這段空間,但是它會導致程序每次執行時后續的棧位置發生了變化。
在Linux系統中,棧隨機化已經變成了標準行為。(在linux上每次運行相同的程序,其同一局部變量的地址都不相同)
2. 棧破壞檢測
在C語言中,沒有可靠的方法來防止對數組的越界寫,但是,我們能夠在發生了越界寫的時候,在沒有造成任何有害結果之前,嘗試檢測到它。
最近的GCC版本在產生的代碼中加入了一種棧保護者機制,用來檢測緩沖區越界,其思想是在棧中任何局部緩沖區與棧狀態之間存儲一個特殊的金絲雀值。這個金絲雀值是在程序每次運行時隨機產生的,因此,攻擊者沒有簡單的辦法知道它是什么。
在恢復寄存器狀態和從函數返回之前,程序檢查這個金絲雀值是否被該函數的某個操作或者函數調用的某個操作改變了。如果是,那么程序異常終止。
3. 限制可執行代碼區域
限制那些能夠存放可執行代碼的存儲器區域。在典型的程序中,只有保存編譯器產生的代碼的那部分存儲器才需要是可執行的,其他部分可以被限制為只允許讀和寫。
現在的64位處理器的內存保護引入了”NX”(不執行)位。有了這個特性,棧可以被標記為可讀和可寫,但是不可執行,檢查頁是否可執行由硬件來完成,效率上沒有損失。
【本文是51CTO專欄作者elknot的原創文章,轉載請通過51CTO獲取授權】