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

建議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ā)生。

主站蜘蛛池模板: 呼伦贝尔市| 凉山| 天台县| 南丹县| 佛冈县| 东城区| 呼伦贝尔市| 河西区| 廉江市| 临沂市| 普格县| 湟中县| 西贡区| 利川市| 黄冈市| 延吉市| 娄底市| 怀集县| 塔河县| 蓝田县| 兖州市| 泰兴市| 合水县| 鄂尔多斯市| 云龙县| 和平县| 漳浦县| 裕民县| 卓尼县| 手游| 阿拉善右旗| 哈尔滨市| 河池市| 台北市| 侯马市| 浦北县| 鄂伦春自治旗| 综艺| 聂荣县| 武安市| 阜宁县|