- 編寫高質(zhì)量代碼:改善C程序代碼的125個建議
- 馬偉 著
- 1512字
- 2019-01-01 01:33:09
建議2-7:防止有符號整數(shù)溢出
整數(shù)溢出是一種常見、難預(yù)測且嚴(yán)重的軟件漏洞,由它引發(fā)的程序Bug可能比格式化字符串與緩沖區(qū)溢出等缺陷更難于發(fā)現(xiàn)。C99標(biāo)準(zhǔn)中規(guī)定,當(dāng)兩個操作數(shù)都是有符號整數(shù)時,就有可能發(fā)生整數(shù)溢出,它將會導(dǎo)致“不能確定的行為”。也就是說整數(shù)溢出是一種未定義的行為,這也就意味著編譯器在處理有符號整數(shù)的溢出時具有很多的選擇,遵循標(biāo)準(zhǔn)的編譯器可以做它們想做的任何事,比如完全忽略該溢出或終止進(jìn)程。大多數(shù)編譯器都會忽略這種溢出,這可能會導(dǎo)致不確定的值或錯誤的值保存在整數(shù)變量中。
整數(shù)溢出有時候是很難發(fā)現(xiàn)的,一般情況下在整數(shù)溢出發(fā)生之前,你都無法知道它是否會發(fā)生溢出,即使你的代碼經(jīng)過仔細(xì)審查,有時候溢出也是不可避免的。因此,程序很難區(qū)分先前計算出的結(jié)果是否正確,而且如果計算結(jié)果將作為一個緩沖區(qū)的大小、數(shù)組的下標(biāo)、循環(huán)計數(shù)器與內(nèi)存分配函數(shù)的實參等時將會非常危險。當(dāng)然,因為無法直接改寫內(nèi)存單元,所以大多數(shù)整數(shù)溢出是沒有辦法利用的。但是,有時候整數(shù)溢出將會導(dǎo)致其他類型的缺陷發(fā)生,比如很容易發(fā)生的緩沖區(qū)溢出等。代碼清單1-13是一個簡單的整數(shù)溢出示例。
代碼清單1-13 整數(shù)溢出示例
#include <stdio.h> int main(void) { int s1 = 2147483647; int s2 = 1073741824; int s3 = -1879048193; int s4=1; int s5=4; printf("%d(0x%x)+%d(0x%x)=%d(0x%x)\n", s1, s1, s4, s4, s1+s4, s1+s4); printf("%d(0x%x)-%d(0x%x)=%d(0x%x)\n", s2, s2, s3, s3, s2-s3, s2-s3); printf("%d(0x%x)*%d(0x%x)=%d(0x%x)\n", s2, s2, s5, s5, s2*s5, s2*s5); return 0; }
在32位操作系統(tǒng)中,類型int的取值范圍為“-2147483647~2147483647”,限制是由INT_MIN與INT_MAX宏指定的,如下面的代碼所示:
#define INT_MIN (-2147483647 - 1) #define INT_MAX 2147483647
而在代碼清單1-13中,當(dāng)程序執(zhí)行語句“s1+s4、s2-s3與s2*s5”時,其結(jié)果都超過類型int的取值范圍,因此發(fā)生溢出行為,運行結(jié)果如圖1-15所示。

圖1-15 代碼清單1-13的運行結(jié)果
當(dāng)然,面對這些簡單的有符號整數(shù)運算溢出,簡單地通過對操作數(shù)進(jìn)行預(yù)測的方法就能夠避免發(fā)生有符號整數(shù)運算溢出。比如,代碼清單1-14就采用了補碼的表示形式來對操作數(shù)進(jìn)行預(yù)測。
代碼清單1-14 采用補碼的表示形式來對操作數(shù)進(jìn)行預(yù)測
#include <stdio.h> #include <stdlib.h> int main(void) { int s1 = 2147483647; int s2 = 1073741824; int s3 = -1879048193; int s4=1; if(((s1^s4)|(((s1^(~(s1^s4) &(1<<(sizeof(int)*CHAR_BIT-1))))+s4)^s4))>=0) { /*處理溢出條件*/ } else { printf("%d(0x%x)+%d(0x%x)=%d(0x%x)\n", s1, s1,s4,s4, s1+s4, s1+s4); } if(((s2^s3)&(((s2^((s2^s3) &(1<<(sizeof(int)*CHAR_BIT-1))))-s3)^s3))<0) { /*處理溢出條件*/ } else { printf("%d(0x%x)-%d(0x%x)=%d(0x%x)\n", s2, s2, s3, s3, s2-s3, s2-s3); } return 0; }
如上面的代碼所示,這種方式可以有效地避免發(fā)生簡單的有符號整數(shù)運算溢出,有興趣的朋友可以自己測試。其實,不只算術(shù)運算可能造成溢出,任何企圖改變該有符號整型變量值的操作都可能造成溢出。示例如代碼清單1-15所示。
代碼清單1-15 溢出示例
#include <stdio.h> int main(void) { int si1= 1073741824; int si2=0; int si3= -1073741824; int si4=4; int si5=-1; printf("si1 = %d (0x%x)\n", si1, si1); si2 = si1 + si3; printf("si1 + %d(0x%x) = %d (0x%x)\n",si3,si3, si2, si2); si2 = si1 * si4; printf("si1 * %d(0x%x) = %d (0x%x)\n",si4,si4, si2, si2); si2 = si1 - si5; printf("si1 - %d(0x%x) = %d (0x%x)\n",si5,si5, si2, si2); return 0; }
代碼清單1-15的運行結(jié)果如圖1-16所示。
與無符號整數(shù)的回繞相似,并不是每種運算符號都會令有符號操作數(shù)運算產(chǎn)生溢出,表1-6給出了可能會導(dǎo)致溢出的操作符。

圖1-16 代碼清單1-15的運行結(jié)果
表1-6 可能導(dǎo)致溢出的操作符

與前面所講的無符號整數(shù)回繞一樣,有符號整數(shù)的這種溢出也很容易導(dǎo)致緩沖區(qū)溢出,同時也很容易讓攻擊者可執(zhí)行任意代碼,演示示例如代碼清單1-16所示。
代碼清單1-16 溢出導(dǎo)致的結(jié)果示例
#include <stdio.h> #include <stdlib.h> int copychar(char *c1,int len1, char *c2, int len2); int main(int argc, char *argv[]) { copychar(argv[1],atoi(argv[2]),argv[3],atoi(argv[4])); return 0; } int copychar(char *c1,int len1, char *c2, int len2) { char buf[100]; if((len1 + len2) > 100) { printf("超出buf容納范圍(100)!\n"); return -1; } memcpy(buf, c1, len1); memcpy(buf+len1, c2, len2); printf("復(fù)制%d+%d=%d個字節(jié)到buf!\n",len1,len2,len1+len2); printf("buf=%s\n", buf); return 0; }
在代碼清單1-16中,程序需要將c1與c2的內(nèi)容復(fù)制到buf中,并分別由len1與len2來指定復(fù)制的字節(jié)數(shù)。這里需要特別注意的語句是“if((len1+len2)>100)”,我們利用該語句進(jìn)行了相對嚴(yán)格的大小檢查:如果len1+len2的值大于buf數(shù)組的大小(100),則不進(jìn)行復(fù)制。
運行代碼清單1-16,當(dāng)我們執(zhí)行命令“1-16 Hello!6 C 2”時,程序運行正常,并成功地將字符串復(fù)制到buf中,運行結(jié)果如圖1-17所示。

圖1-17 代碼清單1-16(執(zhí)行“1-16 Hello!6 C 2”)的運行結(jié)果
當(dāng)我們執(zhí)行命令“1-16 Hello!50 C 51”時,程序同樣運行正常,運行結(jié)果如圖1-18所示。

圖1-18 代碼清單1-16(執(zhí)行“1-16 Hello!50 C 51”)的運行結(jié)果
可當(dāng)我們執(zhí)行命令“1-16 Hello!2147483647 C 2”時,程序卻意外地繞過大小檢查語句“if((len1+len2)>100)”來執(zhí)行相關(guān)的操作。是什么原因?qū)е逻@種情況發(fā)生的呢?
其實很簡單,就是由于整數(shù)溢出而導(dǎo)致的。從執(zhí)行的命令“1-16 Hello!2147483647 C 2”可以得出,len1的值為2147483647(即十六進(jìn)制為0x7fffffff),len2值為2(即十六進(jìn)制為0x00000002),當(dāng)執(zhí)行語句“l(fā)en1+len2(即0x7fffffff+0x00000002)”時會發(fā)生溢出,所得結(jié)果為-2147483647(即十六進(jìn)制為0x80000001)。因為-2147483647遠(yuǎn)遠(yuǎn)小于100,從而使程序繞過大小檢查語句“if((len1+len2)>100)”來執(zhí)行余下的操作。也正因為如此,在執(zhí)行語句“memcpy(buf,c1,len1)”時便導(dǎo)致異?!癠nhandled exception at 0x65726f66 in 1-16.exe:0xC0000005:Access violation reading location 0x65726f66”的發(fā)生。
- Getting Started with Gulp(Second Edition)
- Python金融數(shù)據(jù)分析
- Java Web開發(fā)技術(shù)教程
- Building a Quadcopter with Arduino
- 自制編程語言
- Kotlin從基礎(chǔ)到實戰(zhàn)
- MATLAB for Machine Learning
- Create React App 2 Quick Start Guide
- 軟件測試實用教程
- ASP.NET開發(fā)與應(yīng)用教程
- Python Essentials
- Python編程:從入門到實踐(第3版)
- C語言編程魔法書:基于C11標(biāo)準(zhǔn)
- jBPM6 Developer Guide
- Node.js核心技術(shù)教程