gets函式的不安性詳解

餘二五發表於2017-11-14

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() 函式重寫,來避免遇到緩衝區溢位的問題。

 程式碼如下 複製程式碼


char input[100];

printf(“Yes or no?n”);

fgets(input, 100, stdin);

/* and so on… */




不過, gets() 函式和 fgets() 函式有個不同點。fgets() 函式會在遇到換行符時停止,並且其儲存到記憶體中的資料會包含該換行符,而 gets() 函式會排除換行符。因此,就簡單的這麼重寫程式碼無法實現完全同等的功能。而為了保證程式碼安全,又實現完全相同的功能,我們就需要檢查記憶體地址中的字元,如 果在結尾有換行符,就將其刪除。

因此我們可以用拍腦袋的方式, 得到下面的程式碼。這段程式碼既安全,又能保證和 gets() 函式的行為相同。

 程式碼如下 複製程式碼


/* This code doesn`t work! */

char input[100];

printf(“Yes or no?n”);

fgets(input, 100, stdin);

char *last = input + strlen(input) – 1;

if (*last == `n`)

      *last = “;

/* and so on… */




可是,雖然程式碼變複雜了,但是還是存在一個隱藏問題,該問題會導致程式崩潰,或者有安全隱患。當程式執行時,如果標準輸入流已經得到了所有可用的字元,但 是還沒有遇到檔案結束符( EOF), fgets() 函式將會通過將 input[0] 標記為 NULL 字元的形式,直接返回一個 NULL 字串。此時, strlen(intput) 的返回值為0, 因此導致 last 指標指向 input 陣列之前的那個字元。因為不能確定這個字元到底是什麼,這段程式碼的行為將因此無法判斷。


做個隨堂小練習吧, 請自行修復一下這段程式碼。 點選這裡檢視修復方法


在我過去工作過的一家公司裡,曾經的經理是一個對安全非常敏感的人,他要求 gets() 函式從所有本地的C庫中移除。這個要求,就導致我們經常需要重寫從其他地方拿到的程式碼。所以有下面這段對話,也就不足為奇了。


    A:你發給我的那段程式碼,你看了嗎?我們需要重寫裡面的部分程式碼,去掉對 gets() 函式的呼叫

    B:為什麼 gets() 函式不能出現在程式碼中?

    A:<長篇大論的解釋>此處忽略5421個字

    B:哈,有意思

    A:如果你需要的話,我們很樂意發給你修改後的程式碼

    B: 好的,我很樂意,發給我吧。不過現在我能告訴你的是,我們暫時還不能做什麼,因為我們只能在客戶發現並報告此問題的情況下,才能修改程式碼。


雖然 gets() 函式早就被公認為不安全的,但是它仍然存在於 C89 和 C99 標準,並最終在 C2011 標準中移除了。但這僅僅是在語言標準中的移除,當我檢查自己的一些程式碼時,發現仍有地方用到了它。而且以我目前對C的瞭解,更有意思的是,目前在C語言庫中,還沒有一個安全並且方便的取代 gets() 函式的方法。


各位通讀了文章的朋友,能否回答如下幾個問題:


    在讀此文之前,你知道 gets() 函式是不安全的嗎?

    你所工作的地方,有限制使用 gets() 函式的相關規定嗎?

    你曾經衝寫過程式碼來避免使用 gets() 函式嗎?

    關於 gets() 函式,你有什麼想了解的嗎?


請下週繼續關注此討論。


2 實戰:如何解決 gets() 函式的安全問題


2.1 工具鏈的安全警告


目前GCC預設就會為包含對 gets() 函式呼叫的程式碼,報出警告資訊。


比如下面的程式碼:

 程式碼如下 複製程式碼
#include<stdio.h>

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 */)


微軟在MSDN中也就如何安全的使用C語言標準庫介面給出了建議,感興趣的朋友,可以看看 https://msdn.microsoft.com/en-us/library/bb288454.aspx。

本文轉自 ye小灰灰  51CTO部落格,原文連結:http://blog.51cto.com/10704527/1763072,如需轉載請自行聯絡原作者


相關文章