- C和C++安全編碼(原書第2版)
- (美)Robert C.Seacord
- 2543字
- 2020-10-30 17:56:38
2.2.1 無界字符串復制
無界字符串復制發生于從源數據復制數據到一個定長的字符數組時(例如,從標準輸入讀取數據到一個定長的緩沖區中)。如例2.1所示,來自ISO/IEC TR 24731-2的附錄A的一個程序利用gets()函數把字符從標準輸入讀入一個定長的字符數組,直到讀到一個換行符或者遇到文件結束標志(EOF)為止。
例2.1 從標準輸入讀入
01 #include <stdio.h> 02 #include <stdlib.h> 03 04 void get_y_or_n(void) { 05 char response[8]; 06 puts("Continue? [y] n: "); 07 gets(response); 08 if (response[0] == 'n') 09 exit(0); 10 return; 11 }
本例只使用C99中的接口,盡管gets()函數在C99中已廢棄并在C11中淘汰。《C安全編碼標準》[Seacord 2008],“MSC34-C.不要使用廢棄或過時的函數”規定,不允許使用此函數。
這個程序在MiCrosoft Visual C++2010中可以編譯和運行,但在警告級別/W3下會對使用gets()發出警告。當用G++4.6.1編譯時,編譯器對使用gets()發出警告,但可以無錯地編譯。
如果在提示符下輸入超過8個字符(包括空終結符),這個程序就會有不確定的行為。gets()函數的主要問題是,它沒有提供方法指定讀入的字符數的限制。這種限制在此函數的如下一致實現中是顯而易見的:
01 char *gets(char *dest) { 02 int c = getchar(); 03 char *p = dest; 04 while (c != EOF && c != '\n') { 05 *p++ = c; 06 c = getchar(); 07 } 08 *p = '\0'; 09 return dest; 10 }
對于程序員而言,從無界數據源(例如stdin)讀入數據是一個有趣的問題。由于事先無法得知用戶將會輸入多少個字符,因此不可能預先分配一個長度足夠的數組。常見的解決方案是靜態分配一個認為長度遠遠大于所需的數組。在這個例子中,程序員僅僅期望用戶輸入1個字符,因此假設不會超過8個字節的數組長度。對于友好的用戶而言,這種方式可以很好地工作。但對于那些惡意用戶來說,很容易就超過一個定長字符數組的長度,從而導致發生未定義行為。在《C安全編碼標準》[Seacord 2008]的“STR35-C.不要從一個無界源復制數據到定長數組”中,禁止這種方法。
復制和連接字符串。復制和連接字符串時也容易出現錯誤,因為執行這個功能的許多標準庫調用,如strcpy()、strcat()和sprintf()函數,執行無界復制操作。
從命令行讀入的參數保存在進程內存中。當程序執行時調用的main()函數,當程序接受命令行參數時,通常聲明為如下格式:
1 int main(int argc, char *argv[]) { 2 /* ...*/ 3 }
命令行參數作為指向從argv[0]到argv[argc-1]的數組成員并以空字符結尾的字符串指針傳入main()函數。若argc大于0 [1],按照慣例,argv[0]指向的字符串是程序名。若argc大于1,從argv[0]到argv[argc-1]引用的字符串是實際程序參數。在任何條件下,argv[argc]始終保證是NULL [2]。
當分配的空間不足以復制一個程序的輸入(比如一個命令行參數)時,就會產生漏洞。雖然按照慣例,argv[0]包含程序名,但攻擊者可以控制argv[0]的內容,在如下的程序中,提供一個超過128個字節的字符串就會造成一個漏洞。而且,攻擊者還可以把argv[0]設置為NULL來調用這個程序。
1 int main(int argc, char *argv[]) { 2 /* ... */ 3 char prog_name[128]; 4 strcpy(prog_name, argv[0]); 5 /* ... */ 6 }
這個程序可以在MiCrosoft Visual C++2012下編譯并運行,但在警告級別/W3下會對使用strcpy()發出警告。這個程序也能在G++4.7.2下編譯并運行,如果定義了_FORTIFY_SOURCE,那么在運行時,如果對strcpy()的調用導致了緩沖區溢出,由于對對象大小檢查的結果失敗,此程序會中止。
strlen()函數可用于確定由argv[0]到argv[argc-1]引用的字符串的長度,以便可動態分配足夠的內存。記得要加一個字節,以容納用于終止字符串的空字符。請注意,必須注意避免假設argv數組中的任何元素(包括argv[0])是非空的。
01 int main(int argc, char *argv[]) { 02 /* 不要假設argv[0] 不許為空 */ 03 const char * const name = argv[0] ? argv[0] : ""; 04 char *prog_name = (char *)malloc(strlen(name) + 1); 05 if (prog_name != NULL) { 06 strcpy(prog_name, name); 07 } 08 else { 09 /* 動態分配內存失敗,復原 */ 10 } 11 /* ... */ 12 }
strcpy()函數的使用是絕對安全的,因為目標數組已經被分配了適當的大小。但調用“更安全”的函數來取代strcpy()函數,以消除由編譯器或分析工具生成的診斷消息,這么做可能仍然是可取的。
POSIX的strdup()函數也可以用于復制字符串。strdup()函數接受一個指向字符串的指針,并返回一個指向新分配的復制字符串的指針。將返回的指針傳遞給free(),可以回收這些內存。strdup()函數定義在ISO/IEC TR 24731-2[ISO/ IEC TR 24731-2:2010]中,但沒有被包括在C99或C11標準中。
sprintf()函數。另一個經常被用來復制字符串的標準庫函數是sprintf()函數。sprintf()函數在一個格式字符串的控制之下,將輸出寫入一個數組。被寫入的字符結尾處會寫入一個空字符。因為sprintf()的后續參數指定字符串轉換格式,所以往往難以確定目標數組所需的最大尺寸。例如,在常見的ILP 32和LP 64平臺,INT_MAX =2147483647,用一個字符串來表示int類型參數的值,它會占用11個字符(逗號不能輸出,而且可能有一個減號)。浮點值的大小更是難以預料。
snprintf()函數增加了一個額外的size_t參數n。如果n為0,那么不寫任何內容,目標數組可能是一個空指針。否則,超過第n-1位的輸出字符將被丟棄,而不是寫入數組,并在真正寫入數組的字符的末尾處把一個空字符寫入字符數組。如果n足夠大,snprintf()函數將返回會被寫入的字符數量,不計終止空字符,如果發生編碼錯誤,則返回負值。因此,當且僅當返回值是小于n的非負整數時,空字符結尾的輸出是完全寫入的。snprintf()函數是一個相對安全的函數,但像其他格式的輸出函數一樣,它也容易產生格式化字符串漏洞。需要對snprintf()的返回值進行檢查,因為函數可能會失敗,這不僅是因為緩沖區空間不足,還有其他原因,如在函數執行過程中發生內存不足的狀況。詳情見《C安全編碼標準》[Seacord 2008],“FIO04-C.檢測和處理輸入和輸出錯誤”和“FIO33-C.檢測和處理導致未定義行為的輸入輸出錯誤”。
無界字符串復制問題不僅存在于C語言中。舉個例子,對于以下的C++程序,如果用戶輸入多于11個字符,也會導致寫越界。
1 #include <iostream> 2 3 int main(void) { 4 char buf[12]; 5 6 std::cin >> buf; 7 std::cout << "echo: " << buf << '\n'; 8 }
在微軟Visual C++2012中,當警告級別是/W4時,這個程序可以正確編譯。在G++4.7.2中,當選項是-Wall -Wextra -pedantic時,它也可以正確編譯。
標準的std::cin對象類型是std::istream類。istream類其實是std::basic_istream類模板在字符類型char上的特化。它提供了一些成員函數,以幫助從數據流緩沖區中讀取和解釋輸入。所有格式化的輸入都通過提取操作符operator>>進行。C++同時定義了成員與非成員operator>>重載操作符,包括:
istream& operator>> (istream& is, char* str);
這個操作符提取字符并將其存入str指向的數組的連續元素。當下一個元素是有效的空白或空字符,或遇到EOF標志時,提取操作結束。如果其域寬(可以用ios_base::width或setw()設置)設置為大于0的值,提取操作可以限制為只提取指定數量的字符(因而避免了可能的緩沖區溢出)。在這種情況下,提取操作在提取了比由域寬指定的數量少一個字符的時候就會終止,以便為結尾的空字符留出空間。一次提取操作調用結束后域寬自動被重置為0。并且自動在提取出來的字符串末尾附加一個空字符。
例2.2的程序通過將域寬成員設置為字符數組的長度消除了上一個例子的溢出,這個例子展示了C++提取操作不存在與C的gets()函數同樣的固有缺陷。
例2.2 域寬成員
1 #include <iostream> 2 3 int main(void) { 4 char buf[12]; 5 6 std::cin.width(12); 7 std::cin >> buf; 8 std::cout << "echo: " << buf << '\n'; 9 }
- R語言數據分析從入門到精通
- Learning RabbitMQ
- Ray分布式機器學習:利用Ray進行大模型的數據處理、訓練、推理和部署
- GitLab Repository Management
- Android Native Development Kit Cookbook
- R Deep Learning Cookbook
- Advanced Oracle PL/SQL Developer's Guide(Second Edition)
- Getting Started with Laravel 4
- C/C++程序員面試指南
- 編程菜鳥學Python數據分析
- Scala Data Analysis Cookbook
- 一本書講透Java線程:原理與實踐
- PHP編程基礎與實踐教程
- INSTANT Silverlight 5 Animation
- 區塊鏈架構之美:從比特幣、以太坊、超級賬本看區塊鏈架構設計