- C和C++安全編碼(原書第2版)
- (美)Robert C.Seacord
- 2543字
- 2020-10-30 17:56:38
2.2.1 無界字符串復制
無界字符串復制發(fā)生于從源數(shù)據(jù)復制數(shù)據(jù)到一個定長的字符數(shù)組時(例如,從標準輸入讀取數(shù)據(jù)到一個定長的緩沖區(qū)中)。如例2.1所示,來自ISO/IEC TR 24731-2的附錄A的一個程序利用gets()函數(shù)把字符從標準輸入讀入一個定長的字符數(shù)組,直到讀到一個換行符或者遇到文件結(jié)束標志(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()函數(shù)在C99中已廢棄并在C11中淘汰。《C安全編碼標準》[Seacord 2008],“MSC34-C.不要使用廢棄或過時的函數(shù)”規(guī)定,不允許使用此函數(shù)。
這個程序在MiCrosoft Visual C++2010中可以編譯和運行,但在警告級別/W3下會對使用gets()發(fā)出警告。當用G++4.6.1編譯時,編譯器對使用gets()發(fā)出警告,但可以無錯地編譯。
如果在提示符下輸入超過8個字符(包括空終結(jié)符),這個程序就會有不確定的行為。gets()函數(shù)的主要問題是,它沒有提供方法指定讀入的字符數(shù)的限制。這種限制在此函數(shù)的如下一致實現(xiàn)中是顯而易見的:
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 }
對于程序員而言,從無界數(shù)據(jù)源(例如stdin)讀入數(shù)據(jù)是一個有趣的問題。由于事先無法得知用戶將會輸入多少個字符,因此不可能預先分配一個長度足夠的數(shù)組。常見的解決方案是靜態(tài)分配一個認為長度遠遠大于所需的數(shù)組。在這個例子中,程序員僅僅期望用戶輸入1個字符,因此假設(shè)不會超過8個字節(jié)的數(shù)組長度。對于友好的用戶而言,這種方式可以很好地工作。但對于那些惡意用戶來說,很容易就超過一個定長字符數(shù)組的長度,從而導致發(fā)生未定義行為。在《C安全編碼標準》[Seacord 2008]的“STR35-C.不要從一個無界源復制數(shù)據(jù)到定長數(shù)組”中,禁止這種方法。
復制和連接字符串。復制和連接字符串時也容易出現(xiàn)錯誤,因為執(zhí)行這個功能的許多標準庫調(diào)用,如strcpy()、strcat()和sprintf()函數(shù),執(zhí)行無界復制操作。
從命令行讀入的參數(shù)保存在進程內(nèi)存中。當程序執(zhí)行時調(diào)用的main()函數(shù),當程序接受命令行參數(shù)時,通常聲明為如下格式:
1 int main(int argc, char *argv[]) { 2 /* ...*/ 3 }
命令行參數(shù)作為指向從argv[0]到argv[argc-1]的數(shù)組成員并以空字符結(jié)尾的字符串指針傳入main()函數(shù)。若argc大于0 [1],按照慣例,argv[0]指向的字符串是程序名。若argc大于1,從argv[0]到argv[argc-1]引用的字符串是實際程序參數(shù)。在任何條件下,argv[argc]始終保證是NULL [2]。
當分配的空間不足以復制一個程序的輸入(比如一個命令行參數(shù))時,就會產(chǎn)生漏洞。雖然按照慣例,argv[0]包含程序名,但攻擊者可以控制argv[0]的內(nèi)容,在如下的程序中,提供一個超過128個字節(jié)的字符串就會造成一個漏洞。而且,攻擊者還可以把argv[0]設(shè)置為NULL來調(diào)用這個程序。
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()發(fā)出警告。這個程序也能在G++4.7.2下編譯并運行,如果定義了_FORTIFY_SOURCE,那么在運行時,如果對strcpy()的調(diào)用導致了緩沖區(qū)溢出,由于對對象大小檢查的結(jié)果失敗,此程序會中止。
strlen()函數(shù)可用于確定由argv[0]到argv[argc-1]引用的字符串的長度,以便可動態(tài)分配足夠的內(nèi)存。記得要加一個字節(jié),以容納用于終止字符串的空字符。請注意,必須注意避免假設(shè)argv數(shù)組中的任何元素(包括argv[0])是非空的。
01 int main(int argc, char *argv[]) { 02 /* 不要假設(shè)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 /* 動態(tài)分配內(nèi)存失敗,復原 */ 10 } 11 /* ... */ 12 }
strcpy()函數(shù)的使用是絕對安全的,因為目標數(shù)組已經(jīng)被分配了適當?shù)拇笮 5{(diào)用“更安全”的函數(shù)來取代strcpy()函數(shù),以消除由編譯器或分析工具生成的診斷消息,這么做可能仍然是可取的。
POSIX的strdup()函數(shù)也可以用于復制字符串。strdup()函數(shù)接受一個指向字符串的指針,并返回一個指向新分配的復制字符串的指針。將返回的指針傳遞給free(),可以回收這些內(nèi)存。strdup()函數(shù)定義在ISO/IEC TR 24731-2[ISO/ IEC TR 24731-2:2010]中,但沒有被包括在C99或C11標準中。
sprintf()函數(shù)。另一個經(jīng)常被用來復制字符串的標準庫函數(shù)是sprintf()函數(shù)。sprintf()函數(shù)在一個格式字符串的控制之下,將輸出寫入一個數(shù)組。被寫入的字符結(jié)尾處會寫入一個空字符。因為sprintf()的后續(xù)參數(shù)指定字符串轉(zhuǎn)換格式,所以往往難以確定目標數(shù)組所需的最大尺寸。例如,在常見的ILP 32和LP 64平臺,INT_MAX =2147483647,用一個字符串來表示int類型參數(shù)的值,它會占用11個字符(逗號不能輸出,而且可能有一個減號)。浮點值的大小更是難以預料。
snprintf()函數(shù)增加了一個額外的size_t參數(shù)n。如果n為0,那么不寫任何內(nèi)容,目標數(shù)組可能是一個空指針。否則,超過第n-1位的輸出字符將被丟棄,而不是寫入數(shù)組,并在真正寫入數(shù)組的字符的末尾處把一個空字符寫入字符數(shù)組。如果n足夠大,snprintf()函數(shù)將返回會被寫入的字符數(shù)量,不計終止空字符,如果發(fā)生編碼錯誤,則返回負值。因此,當且僅當返回值是小于n的非負整數(shù)時,空字符結(jié)尾的輸出是完全寫入的。snprintf()函數(shù)是一個相對安全的函數(shù),但像其他格式的輸出函數(shù)一樣,它也容易產(chǎn)生格式化字符串漏洞。需要對snprintf()的返回值進行檢查,因為函數(shù)可能會失敗,這不僅是因為緩沖區(qū)空間不足,還有其他原因,如在函數(shù)執(zhí)行過程中發(fā)生內(nèi)存不足的狀況。詳情見《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上的特化。它提供了一些成員函數(shù),以幫助從數(shù)據(jù)流緩沖區(qū)中讀取和解釋輸入。所有格式化的輸入都通過提取操作符operator>>進行。C++同時定義了成員與非成員operator>>重載操作符,包括:
istream& operator>> (istream& is, char* str);
這個操作符提取字符并將其存入str指向的數(shù)組的連續(xù)元素。當下一個元素是有效的空白或空字符,或遇到EOF標志時,提取操作結(jié)束。如果其域?qū)挘梢杂胕os_base::width或setw()設(shè)置)設(shè)置為大于0的值,提取操作可以限制為只提取指定數(shù)量的字符(因而避免了可能的緩沖區(qū)溢出)。在這種情況下,提取操作在提取了比由域?qū)捴付ǖ臄?shù)量少一個字符的時候就會終止,以便為結(jié)尾的空字符留出空間。一次提取操作調(diào)用結(jié)束后域?qū)捵詣颖恢刂脼?。并且自動在提取出來的字符串末尾附加一個空字符。
例2.2的程序通過將域?qū)挸蓡T設(shè)置為字符數(shù)組的長度消除了上一個例子的溢出,這個例子展示了C++提取操作不存在與C的gets()函數(shù)同樣的固有缺陷。
例2.2 域?qū)挸蓡T
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 }
- AngularJS入門與進階
- Learning ROS for Robotics Programming(Second Edition)
- 我的第一本算法書
- Designing Hyper-V Solutions
- MATLAB for Machine Learning
- Visual Basic程序設(shè)計
- Java Web開發(fā)就該這樣學
- 用案例學Java Web整合開發(fā)
- 持續(xù)集成與持續(xù)交付實戰(zhàn):用Jenkins、Travis CI和CircleCI構(gòu)建和發(fā)布大規(guī)模高質(zhì)量軟件
- Red Hat Enterprise Linux Troubleshooting Guide
- Python青少年趣味編程
- 算法圖解
- Swift High Performance
- Web前端測試與集成:Jasmine/Selenium/Protractor/Jenkins的最佳實踐
- INSTANT Lift Web Applications How-to