gets函式的不安性詳解
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。
相關文章
- C語言關於指標,gets()和gets_s()函式的理解C語言指標函式
- Python區域性函式及用法詳解Python函式
- TypeScript中的函式詳解TypeScript函式
- SetupDiGetClassDevs函式詳解dev函式
- 詳解Java函式式介面Java函式
- 緩衝區溢位漏洞那些事:C -gets函式函式
- mysql常用函式詳解MySql函式
- 箭頭函式詳解函式
- python socket函式詳解Python函式
- 建構函式詳解函式
- 函式引數詳解函式
- 3.11 solidity 函式詳解Solid函式
- fcntl函式用法詳解函式
- ORALCE函式:LAG()和LEAD() 分析函式詳解函式
- 平凡的函式 線性篩積性函式函式
- SQL中常用的字串LEFT函式和RIGHT函式詳解!SQL字串函式
- 函式詳解 | VLOOKUP 函式:最為人熟知的偵探函式
- 詳解SQL操作的視窗函式SQL函式
- 詳解常見的損失函式函式
- Oracle分析函式之開窗函式over()詳解Oracle函式
- Oracle中pivot函式詳解Oracle函式
- Python中Numpy函式詳解Python函式
- Java建構函式詳解Java函式
- 箭頭函式this指向詳解函式
- PHP BC Math 函式詳解PHP函式
- vimscript-expand函式詳解函式
- jquery.ajaxSetup()函式詳解jQuery函式
- Oracle中的正規表示式(及函式)詳解Oracle函式
- Golang建立建構函式的方法詳解Golang函式
- OpenCV中的findContours函式引數詳解OpenCV函式
- Python正規表示式 findall函式詳解Python函式
- C++中建構函式,拷貝建構函式和賦值函式的詳解C++函式賦值
- re模組 函式模式詳解函式模式
- MySQL中count(*)函式原理詳解MySql函式
- 詳細講解函式呼叫原理函式
- JavaScript函式柯里化詳解JavaScript函式
- 匿名函式(lambda)詳解 C++函式C++
- vue3函式setUp和reactive函式詳細講解Vue函式React
- PyTorch 中 torch.matmul() 函式的文件詳解PyTorch函式