1.字串操作安全
1.1 確保所有字串都是以NULL結束
C語言中以 '\0' 作為字串的結束符,即NULL結束符。
沒有正確使用NULL結束符會導致緩衝區溢位和其他未定義的行為。
為了避免緩衝區溢位,常常會用相對安全的限制字元數量的字串操作函數代替一些危險函數
- strncpy() 代替 strcpy()
- strncat() 代替 strcat()
- snprintf() 代替 sprintf()
- fgets() 代替 gets()
這些函數會截斷超出指定限制的字串,但是要注意他們並不能保證目標字串總是以NULL結尾。
//錯誤釋放,a後面沒有NULL結束符char a[16];strncpy(a,"0123456789abcdef",sizeof(a));
上面的代碼調用strncpy()後,a的字串中是沒有NULL結束符的。
//正確寫法:截斷字串,保證字串以NULL結束char a[16];strncpy(a,"0123456789abcdef",sizeof(a)-1);a[sizeof(a)-1] = '\0';
1.2 不要將不明確的字串寫到固定長度的數組中
邊界不明確的字串(例如來自 gets(), getenv(), scanf()的字串)長度可能大於目標數組長度,直接拷貝到固定長度的數組中容易導致緩衝區溢位。
//錯誤樣本:char buff[256];char *editor = getenv("EDITOR");if(editor != NULL){ strcpy(buff,editor);}
editor實際長度可能大於256導致溢出。
//正確寫法:計算字串的實際長度,使用malloc分配指定長度的記憶體char *buff;char *editor = getenv("EDITOR");if(editor != NULL){ buff = malloc(strlen(editor)+1); if(buff != NULL) { strcpy(buff,editor); }}
2.整數安全
C99標準定義了整型提升、整型轉換層級以及普通算數轉換的整型操作。不過這些操作實際上也帶來了安全風險。
2.1 避免整數溢出
當一個認證被增加超過起最大值時會發生整數上溢,被減小小於其最小值時會發生下溢。帶符號和無符號的數都可能發生溢出。
//有符號和不帶正負號的整數的上溢和下溢int i;unsigned int j;i = INT_MAX; //2147483647i++;printf("i = %d\n",i); i = -214748348j = UINT_MAX; //4294967295j++;printf("j = %u\n", j); //0i = INT_MIN; //-2147483648i--;printf("i = %d\n",i); //i = 2147483647j = 0;j--;printf("j = %u\n", j); //4294967295
在長度加減時,加上長度檢查
//length 可能小於 FSM_HDRLENunsigned int length;length -= FSM_HDRLEN; //當length < FSM_HDRLEN時,發現下溢,返回一個很大的數//正確寫法: 判斷長度if(length < FSM_HDR_LEN){ return VOS_ERROR;}length -= FSM_HDRLEN;
2.2 避免符號錯誤
有時從帶符號整型轉換為無符號整型會發生符號錯誤,符號錯誤不會遺失資料,但是失去了原有的意義。
帶符號位整型轉化到無符號整型,最高位(high-order bit)會喪失其作為符號位的功能。 如果該帶符號整型的值時非負的,那麼轉換後的值不變; 如果該帶符號整型的值是負的,那麼轉換後的結果是一個非常大的正數。
//錯誤樣本:符號錯誤繞過長度檢查#define BUF_SIZE 10int main(int argc, char* argv[]){ int length; char buf[BUF_SIZE]; if(argc != 3) { return -1; } length = atoi(argv[1]); //如果atoi返回的長度為負數 if(length < BUF_SIZE) //length 為負數,長度檢查無效 { //帶符號的length被轉換為size_t類型的無符號整型,負值被解釋為一個極大的正整數。 //memcpy()調用時引發buf緩衝區溢位 memcpy(buf, argv[2], length); printf("Data copied\n"); } else { printf("Too many data\n"); } return 0;}
//正確寫法1:將length聲明為無符號整型#define BUF_SIZE 10int main(int argc, char* argv[]){ unsigned int length; char buf[BUF_SIZE]; if(argc != 3) { return -1; } length = atoi(argv[1]); //如果atoi返回的長度不可能為負數 if(length < BUF_SIZE) //length 不為負數,長度檢查有效 { memcpy(buf, argv[2], length); printf("Data copied\n"); } else { printf("Too many data\n"); } return 0;}
//正確寫法2:對length的長度判斷更有效範圍校正#define BUF_SIZE 10int main(int argc, char* argv[]){ unsigned int length; char buf[BUF_SIZE]; if(argc != 3) { return -1; } length = atoi(argv[1]); //如果atoi返回的長度為負數 if(length > 0 && length < BUF_SIZE) //length 為負數,長度檢查有效 { memcpy(buf, argv[2], length); printf("Data copied\n"); } else { printf("Too many data\n"); } return 0;}
2.3 避免截斷錯誤
將一個較大整型轉換為較小整型,並且該數的原值超出較小類型的表示範圍,就會發生截斷錯誤,原值的低位被保留,而高位被丟棄。截斷錯誤會引起資料丟失,使用截斷後的變數進行記憶體操作,很可能引發問題。
//錯誤樣本:截斷錯誤int main(int argc, char* argv[]){ //total 是 unsigned short, strlen()返回的時size_t 是 unsigned long. //如果輸入的參數長度是 65500 和 36 //那麼 total = (65500 + 36 + 1) % 65536 = 1 //strcpy() 和 strcat() 函數會緩衝區溢位 unsigned short total = strlen(argv[1]) + strlen(argv[2]) + 1; char * buffer = (char*) malloc(tatal); strcpy(buffer, argv[1]); strcat(buffer, argv[2]); free(buffer); return 0;}
//正確寫法:將涉及到計算的變數聲明為統一的類型,並檢查計算結果int main(int argc, char* argv[]){ size_t total = strlen(argv[1]) + strlen(argv[2]) + 1; if( (total < strlen(ar)) || () ) char * buffer = (char*) malloc(tatal); strcpy(buffer, argv[1]); strcat(buffer, argv[2]); free(buffer); return 0;}
3. 格式化輸出安全
3.1 確保格式字元和參數匹配
3.2 避免將使用者輸入作為格式化字串的一部分或者全部
調用格式化I/O時,不要直接或者間接將使用者輸入作為格式化字串的一部分或者全部。
攻擊者對一個格式化字串擁有部分或完全控制,存在以下風險:進程崩潰,查看棧內容,改寫記憶體,甚至執行惡意代碼。
//錯誤碼樣本:char input[1000];if(fgets(input, sizeof(input) - 1, stdin) == NULL){ return -1;}input[sizeof(input)-1] = '\0';printf(input);
上述代碼input直接來自使用者輸入,並作為格式化字元直接傳遞給printf();當使用者輸入的是”%s%s%s%s%s%s%s%s%s”,就可能觸發無效指標或未映射的地址讀取。格式化字元%s顯示棧上相應參數所指定的地址的記憶體。這裡input被當成格式化字元,而沒有提供參數,因此printf()讀取棧裡任意記憶體位置,直到格式化字元耗盡或者遇到一個無效指標或未映射地址為止。
//正確寫法:傳兩個參數,將格式化字串定下來char input[1000];if(fgets(input, sizeof(input) - 1, stdin) == NULL){ return -1;}input[sizeof(input)-1] = '\0';printf("%s\n",input);
//錯誤樣本:void check_password(char *user, char *password){ if(strcmp(password(user),password) != 0) { char *msg = malloc(strlen(user) + 100); if(!msg) { return -1; } sprintf(msg,"%s login incorrect",user); fprintf(STDERR,msg); syslog(LOG_INFO, msg); free(msg); } //...}
上述代碼檢查給定使用者名稱及其口令是否匹配,當不匹配時顯示一條錯誤資訊,並將錯誤資訊寫入日誌中。
如果user為”%s%s%s%s%s%s%s%s%s%s”,經過格式化函數sprintf()的拼裝後,msg指向的字串是”%s%s%s%s%s%s%s%s%s%s login incorrect”,那麼接下來的fprintf()和syslog就會出現格式化字元的問題。
//正確寫法:格式化字串由代碼確定,未經檢查過濾的使用者輸入只能作為參數。void check_password(char *user, char *password){ if(strcmp(password(user),password) != 0) { char *msg = malloc(strlen(user) + 100); if(!msg) { return -1; } sprintf(msg,"%s login incorrect",user); fprintf(STDERR,"%s",msg); syslog(LOG_INFO, "%s",msg); free(msg); } //...}
4. 檔案I/O安全
4.1 避免使用strlen()Function Compute位元據的長度
strlen()函數用於計算字串的長度,它返回字串中第一個NULL結束符之前的字元的數量。
因此用strlen()處理檔案I/O函數讀取內容時要小心,因為這些內容可能是二進位,也可能是文本。
//錯誤樣本:char buf[BUF_SIZE];if(fgets(buf,sizeof(buf),fp) == NULL){ //handle error}buf[strlen(buf)-1] = '\0';
上述代碼試圖從一個輸入行中刪除行尾的分行符號(\n),如果buf的第一個字元時NULL,strlen(buf)返回0,這時buf[0-1]操作會越界。
//正確寫法:在不能確定從檔案讀取到的類型時,不要使用依賴NULL結束符的字串操作函數。char buf[BUF_SIZE];char *p;if(fgets(buf,sizeof(kbuf),fp)){ p = strchr(buf,'\n'); if(p) { *p = '\0'; }}else { //handle error}
4.2 使用int類型變數來接受字元I/O函數的傳回值
字元I/O函數 fgetc(), getc() 和 getchar() 都從一個流讀取一個字元,並把它以int值的形式返回。
如果這個流到達了檔案尾或者發生讀取錯誤,函數返回EOF。
fputc(), putc(),putchar() 和 ungetc() 也返回一個字元或EOF。
如果這些I/O函數的傳回值需要與EOF比較,不要將傳回值轉換為char 類型。
因為char 是有符號8位的值,int 是32位的值。如果getchar() 返回的字元的ASCII值為 0xFF, 轉換為char類型後將被解釋為EOF。因為這個值被有符號擴充為0xFFFFFFFF(EOF)執行比較。
//錯誤樣本:char buf[BUF_SIZE];char ch;int i = 0;while((ch = getchar()) != '\n' && ch != EOF){ if(i < BUF_SIZE - 1) { buf[i++] = ch; }}buf[i] = '\0';
//正確寫法:使用int類型的變數接受getchar()的傳回值char buf[BUF_SIZE];int ch;int i = 0;while(((ch = getchar()) != '\n') && ch != EOF){ if(i < BUF_SIZE-1) { buf[i++] = ch; }}cuf[i] = '\0';
如果有些機器sizeof(int) == sizeof(char), 那麼用int接收傳回值也可能無法與EOF區分,這時要用feof()和ferror()檢查檔案尾和檔案錯誤。 5. 防止命令注入
C99函數system() 通過調用一個系統定義的命令解析器(例如UNIX的shell,Windows的CMD.exe)來執行一個指定的程式/命令。類似的還有POSIX的函數popen().
如果system()的參數由使用者的輸入組成,惡意使用者可以通過構造惡意輸入,來改變system()調用的行為。
system(sprintf("any_exe %s",input));
如果使用者輸入: happy; useradd attacker
最終shell將字串”any_exe happy; useradd attcker” 解釋為兩條獨立的命令。
//正確寫法:使用POSIX函數execve()代替system()void secuExec(char *input){ pid_t pid; char *const args[] = {"",input,NULL}; char *const envs[] = {NULL}; pid = fork(); if(pid == -1) { puts("fork error"); } else if(pid == 0) { if(execve("/usr/bin/any_exe", args, envs) == -1) { puts("Error executing any_exe"); } } return;}
Windows 環境可能對 execve() 的支援不是很完善,建議使用 Win32 API 的 CreateProcess() 代替system().