1 為什麼gets()函數還在我們的代碼中?
好吧,最終還是發生了。我們遇到了一個非常嚴重,并且非常普遍的緩沖區溢出問題。這個問題造成了非常大的影響,修複這個問題的過程,将會非常艱難,非常 慢,代價非常高。在我看來,可能在這個世界上,會有不少軟體産品經理這樣問程式員們:“為什麼你沒有警告過我?”,估計這些被問到的程式員中,有很多都會 直接回答道:“我警告過你了,你什麼沒有聽進去?“
在軟體開發的過程中,總是存在一個沖突:正确的解決問題和快速的解決問題。這個問題在安全領域更加的突出。是以在接下來的幾周時間裡,我們來聊聊這個沖突。這個沖突的如下兩個方面,在我們聊的過程中相當的重要:
不管你針對問題的解決方案有多完美,如果沒有人使用這個解決方案,都是無用功
不管是處于什麼目的,如果你沒有使用完美解決方案,那所有的考慮都是白費功夫。因為你的代碼裡沒有實作該解決方案
讓我們從這個看起來非常俗氣的例子開始吧:C标準庫中的 gets() 函數。 這個函數的定義如下:
char * gets ( char * str );
gets() 函數的形參隻有一個指針。它會從标準輸入流中讀字元到一塊連續的記憶體位址空間中。這塊位址空間的開始位置就是指針 str 指向的位置。當在輸入流中遇到檔案結束符( EOF )或者換行符(n)時,讀取操作結束。當讀入換行符(n)時,該字元不會被放入那塊連續的位址空間中。在讀取結束時, gets() 會自動在記憶體空間的末尾追加一個 NULL 字元。經過上述這些操作,對于程式員來說,這個函數得到的就是從标準輸入進來的,以 NULL 字元結尾的C字元串。如果讀入的字元流是一整行的話,行尾的換行符将會被舍去。
這個函數友善,也有局限性。 C程式員們經常使用它讀取标準輸入。下面的代碼是一種典型的應用場景:
代碼如下
複制代碼
char input[100];
printf("Yes or no?n");
gets(input);
/* and so on… */
在過去的30年裡,許多C程式設計社群的同仁們都已經意識到 gets() 函數不安全,而且在保證接口不變的情況下,也無法被改良。原因也比較直覺,這個函數隻有一個指針作為參數,該指針指向的記憶體空間将用于儲存讀入資料。但是 gets() 函數無法知道它需要使用多大的記憶體空間。如果在标準輸入中讀入足夠長的,不包含換行符的字元留, gets() 函數肯定會覆寫掉指定的記憶體區域,而程式員對此無能為力。
此外,除了 gets() 函數缺乏安全性,還有它的小夥伴 fgets() 也有問題。 這個函數的原型如下:
char * fgets ( char * str, int num, FILE * stream );
str 是一個指針,指向一塊記憶體區域,讀入的資料将會存儲到這塊記憶體空間。num 是一個整數,指定了記憶體空間的大小, stream 是一個檔案指針,指定了可以從哪裡讀取。可能第一眼看過去,你會和我當時一樣,覺得前面的那段不安全代碼,可以使用 fgets() 函數重寫,來避免遇到緩沖區溢出的問題。
fgets(input, 100, stdin);
不過, gets() 函數和 fgets() 函數有個不同點。fgets() 函數會在遇到換行符時停止,并且其儲存到記憶體中的資料會包含該換行符,而 gets() 函數會排除換行符。是以,就簡單的這麼重寫代碼無法實作完全同等的功能。而為了保證代碼安全,又實作完全相同的功能,我們就需要檢查記憶體位址中的字元,如 果在結尾有換行符,就将其删除。
是以我們可以用拍腦袋的方式, 得到下面的代碼。這段代碼既安全,又能保證和 gets() 函數的行為相同。
/* This code doesn't work! */
char *last = input + strlen(input) – 1;
if (*last == 'n')
*last = '';
可是,雖然代碼變複雜了,但是還是存在一個隐藏問題,該問題會導緻程式崩潰,或者有安全隐患。當程式執行時,如果标準輸入流已經得到了所有可用的字元,但 是還沒有遇到檔案結束符( EOF), fgets() 函數将會通過将 input[0] 标記為 NULL 字元的形式,直接傳回一個 NULL 字元串。此時, strlen(intput) 的傳回值為0, 是以導緻 last 指針指向 input 數組之前的那個字元。因為不能确定這個字元到底是什麼,這段代碼的行為将是以無法判斷。
做個随堂小練習吧, 請自行修複一下這段代碼。 點選這裡檢視修複方法
在我過去工作過的一家公司裡,曾經的經理是一個對安全非常敏感的人,他要求 gets() 函數從所有本地的C庫中移除。這個要求,就導緻我們經常需要重寫從其他地方拿到的代碼。是以有下面這段對話,也就不足為奇了。
A:你發給我的那段代碼,你看了嗎?我們需要重寫裡面的部分代碼,去掉對 gets() 函數的調用
B:為什麼 gets() 函數不能出現在代碼中?
A:<長篇大論的解釋>此處忽略5421個字
B:哈,有意思
A:如果你需要的話,我們很樂意發給你修改後的代碼
B: 好的,我很樂意,發給我吧。不過現在我能告訴你的是,我們暫時還不能做什麼,因為我們隻能在客戶發現并報告此問題的情況下,才能修改代碼。
各位通讀了文章的朋友,能否回答如下幾個問題:
在讀此文之前,你知道 gets() 函數是不安全的嗎?
你所工作的地方,有限制使用 gets() 函數的相關規定嗎?
你曾經沖寫過代碼來避免使用 gets() 函數嗎?
關于 gets() 函數,你有什麼想了解的嗎?
請下周繼續關注此讨論。
2 實戰:如何解決 gets() 函數的安全問題
2.1 工具鍊的安全警告
目前GCC預設就會為包含對 gets() 函數調用的代碼,報出警告資訊。
比如下面的代碼:
int main(void)
{
char c[5];
gets(c);
puts(c);
}
就會給出下面的提示資訊:
gets_warn.c:(.text+0xd): warning: the `gets’ function is dangerous and should not be used.
2.2 安全的gets()實作
C11 标準(ISO/IEC 9899:201x)中, gets() 函數被删除, 引入了新的函數 gets_s().
C11 K.3.5.4.1 The gets_s function
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);
因為目前GCC中還沒有完全實作此标準, 是以 gets_s() 函數尚未包含在目前的GNU 工具鍊中。Clang裡也暫時沒有增加對 gets_s 的支援。
是以最通用的做法,可能是自己實作一個。 如下是一種實作方式:
char *gets_s(char * str, int num)
if (fgets(str, int, stdin) != 0)
{
size_t len = strlen(str);
if (len > 0 && buffer[len-1] == 'n')
buffer[len-1] = '';
return buffer;
}
return 0;
2.3 C标準庫中其他存在安全隐患的函數
除了像 gets() 函數這類,非常不安全的函數外。C語言中因為缺少對數組越界的檢查,指針的廣泛使用,導緻不少函數如果使用不當,容易被黑客利用, 存在安全隐患。
strcpy : 建議使用 strncpy
strcat : 建議使用 strncat
sprintf : 建議使用 snprintf
如果你想自己實作一些字元串操作函數,那麼下面這種接口設計值得推薦。即務必要規定好目标位址空間的大小:
size_t foobar(char *dest, size_t buf_size, /* operands here */)
本文轉自 ye小灰灰 51CTO部落格,原文連結:http://blog.51cto.com/10704527/1763072,如需轉載請自行聯系原作者