官术网_书友最值得收藏!

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  }

[1] 在某些特殊情況下,如被execlp函數(shù)調(diào)用的程序,其argc 會等于0。—譯者注
[2] 這里的NULL 是表示空指針。—譯者注
主站蜘蛛池模板: 图们市| 潮安县| 西乌珠穆沁旗| 福贡县| 林甸县| 洪湖市| 天全县| 宁城县| 灌南县| 龙泉市| 普格县| 武清区| 会理县| 稻城县| 兴义市| 石棉县| 息烽县| 晋城| 博客| 始兴县| 上高县| 盱眙县| 日照市| 石门县| 资兴市| 长阳| 屯昌县| 宜兴市| 天全县| 新平| 霍山县| 明溪县| 长丰县| 玉田县| 司法| 晋城| 互助| 漠河县| 湖口县| 祁门县| 宁乡县|