0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內(nèi)不再提示

詳解C語言中整形溢出問題

dyquk4xk2p3d ? 來源:酷殼-COOLSHELL ? 2023-11-06 10:58 ? 次閱讀

整型溢出有點老生常談了,bla, bla, bla… 但似乎沒有引起多少人的重視。整型溢出會有可能導致緩沖區(qū)溢出,緩沖區(qū)溢出會導致各種黑客攻擊。

今天分享一篇文章,希望大家都了解一下整型溢出,編譯器的行為,以及如何防范,以寫出更安全的代碼。

什么是整型溢出

C語言的整型問題相信大家并不陌生了。對于整型溢出,分為無符號整型溢出和有符號整型溢出。

對于unsigned整型溢出,C的規(guī)范是有定義的——“溢出后的數(shù)會以2^(8*sizeof(type))作模運算”,也就是說,如果一個unsigned char(1字符,8bits)溢出了,會把溢出的值與256求模。例如:

unsignedcharx=0xff;
printf("%d
",++x);

上面的代碼會輸出:0 (因為0xff + 1是256,與2^8求模后就是0)

對于signed整型的溢出,C的規(guī)范定義是“undefined behavior,也就是說,編譯器愛怎么實現(xiàn)就怎么實現(xiàn)。對于大多數(shù)編譯器來說,算得啥就是啥。比如:

signedcharx=0x7f;//注:0xff就是-1了,因為最高位是1也就是負數(shù)了
printf("%d
",++x);

上面的代碼會輸出:-128,因為0x7f + 0x01得到0x80,也就是二進制的1000 0000,符號位為1,負數(shù),后面為全0,就是負的最小數(shù),即-128。

另外,千萬別以為signed整型溢出就是負數(shù),這個是不定的。比如:

signedcharx=0x7f;
signedchary=0x05;
signedcharr=x*y;
printf("%d
",r);

上面的代碼會輸出:123

相信對于這些大家不會陌生了。

整型溢出的危害

下面說一下,整型溢出的危害。

示例一:整形溢出導致死循環(huán)

......
......
shortlen=0;
......
while(len

上面這段代碼可能是很多程序員都喜歡寫的代碼(我在很多代碼里看到過多次),其中的MAX_LEN 可能會是個比較大的整型。

比如32767,我們知道short是16bits,取值范圍是-32768 到 32767 之間。但是,上面的while循環(huán)代碼有可能會造成整型溢出,而len又是個有符號的整型,所以可能會成負數(shù),導致不斷地死循環(huán)。

示例二:整形轉型時的溢出

intcopy_something(char*buf,intlen)
{
#defineMAX_LEN256
charmybuf[MAX_LEN];
......
......

if(len>MAX_LEN){//<----?[1]
?????????return?-1;
?????}

?????return?memcpy(mybuf,?buf,?len);
}

上面這個例子中,還是[1]處的if語句,看上去沒有會問題,但是len是個signed int,而memcpy則需一個size_t的len,也就是一個unsigned 類型。

于是,len會被提升為unsigned,此時,如果我們給len傳一個負數(shù),會通過了if的檢查,但在memcpy里會被提升為一個正數(shù),于是我們的mybuf就是overflow了。這個會導致mybuf緩沖區(qū)后面的數(shù)據(jù)被重寫。

示例三:分配內(nèi)存

關于整數(shù)溢出導致堆溢出的很典型的例子是,OpenSSH Challenge-Response SKEY/BSD_AUTH 遠程緩沖區(qū)溢出漏洞。下面這段有問題的代碼摘自OpenSSH的代碼中的auth2-chall.c中的input_userauth_info_response() 函數(shù):

nresp=packet_get_int();
if(nresp>0){
response=xmalloc(nresp*sizeof(char*));
for(i=0;i

上面這個代碼中,nresp是size_t類型(size_t一般就是unsigned int/long int),這個示例是一個解數(shù)據(jù)包的示例。一般來說,數(shù)據(jù)包中都會有一個len,然后后面是data。

如果我們精心準備一個len,比如:1073741825(在32位系統(tǒng)上,指針占4個字節(jié),unsigned int的最大值是0xffffffff,我們只要提供0xffffffff/4 的值——0x40000000,這里我們設置了0x4000000 + 1), nresp就會讀到這個值,然后nresp * sizeof(char * )就成了 1073741825 * 4,于是溢出,結果成為了 0x100000004,然后求模,得到4。于是,malloc(4),于是后面的for循環(huán)1073741825 次,就可以干壞環(huán)事了。

經(jīng)過0x40000001的循環(huán),用戶的數(shù)據(jù)早已覆蓋了xmalloc原先分配的4字節(jié)的空間以及后面的數(shù)據(jù),包括程序代碼,函數(shù)指針,于是就可以改寫程序邏輯。關于更多的東西,你可以看一下這篇文章《Survey of Protections from Buffer-Overflow Attacks》)。

示例四:緩沖區(qū)溢出導致安全問題

intfunc(char*buf1,unsignedintlen1,
char*buf2,unsignedintlen2)
{
charmybuf[256];

if((len1+len2)>256){//<---?[1]
???????return?-1;
???}?

???memcpy(mybuf,?buf1,?len1);
???memcpy(mybuf?+?len1,?buf2,?len2);?

???do_some_stuff(mybuf);?

???return?0;
}

上面這個例子本來是想把buf1和buf2的內(nèi)容copy到mybuf里,其中怕len1 + len2超過256 還做了判斷,但是,如果len1+len2溢出了,根據(jù)unsigned的特性,其會與2^32求模。

所以,基本上來說,上面代碼中的[1]處有可能為假的。(注:通常來說,在這種情況下,如果你開啟-O代碼優(yōu)化選項,那個if語句塊就全部被和諧掉了——被編譯器給刪除了)比如,你可以測試一下 len1=0x104, len2 = 0xfffffffc 的情況。

示例五:size_t 的溢出

for(inti=strlen(s)-1;i>=0;i--){...}
for(inti=v.size()-1;i>=0;i--){...}

上面這兩個示例是我們經(jīng)常用的從尾部遍歷一個數(shù)組的for循環(huán)。第一個是字符串,第二個是C++中的vector容器。strlen()和vector::size()返回的都是 size_t,size_t在32位系統(tǒng)下就是一個unsigned int。

你想想,如果strlen(s)和v.size() 都是0呢?這個循環(huán)會成為個什么情況?于是strlen(s) – 1 和 v.size() – 1 都不會成為 -1,而是成為了 (unsigned int)(-1),一個正的最大數(shù)。導致你的程序越界訪問。

這樣的例子有很多很多,這些整型溢出的問題如果在關鍵的地方,尤其是在搭配有用戶輸入的地方,如果被黑客利用了,就會導致很嚴重的安全問題。

關于編譯器的行為

在談一下如何正確的檢查整型溢出之前,我們還要來學習一下編譯器的一些東西。請別怪我羅嗦。

編譯器優(yōu)化

如何檢查整型溢出或是整型變量是否合法有時候是一件很麻煩的事情,就像上面的第四個例子一樣,編譯的優(yōu)化參數(shù)-O/-O2/-O3基本上會假設你的程序不會有整形溢出。會把你的代碼中檢查溢出的代碼給優(yōu)化掉。

關于編譯器的優(yōu)化,在這里再舉個例子,假設我們有下面的代碼(又是一個相當相當常見的代碼):

intlen;
char*data;

if(data+len

上面這段代碼中,len 和 data 配套使用,我們害怕len的值是非法的,或是len溢出了,于是我們寫下了if語句來檢查。這段代碼在-O的參數(shù)下正常。但是在-O2的編譯選項下,整個if語句塊被優(yōu)化掉了。

你可以寫個小程序,在gcc下編譯(我的版本是4.4.7,記得加上-O2和-g參數(shù)),然后用gdb調試時,用disass /m命信輸出匯編,你會看到下面的結果(你可以看到整個if語句塊沒有任何的匯編代碼——直接被編譯器和諧掉了):

7intlen=10;
8char*data=(char*)malloc(len);
0x00000000004004d4<+4>:mov$0xa,%edi
0x00000000004004d9<+9>:callq0x4003b8

9
10if(data+len:add$0x8,%rsp
0x00000000004004e2<+18>:retq

對此,你需要把上面 char* 轉型成 uintptr_t 或是 size_t,說白了也就是把char*轉成unsigned的數(shù)據(jù)結構,if語句塊就無法被優(yōu)化了。如下所示:

if((uintptr_t)data+len

關于這個事,你可以看一下C99的規(guī)范說明《 ISO/IEC 9899:1999 C specification 》第 §6.5.6 頁,第8點,我截個圖如下:(這段話的意思是定義了指針+/-一個整型的行為,如果越界了,則行為是undefined)

3ea79ca6-7ab7-11ee-939d-92fbcf53809c.png

注意上面標紅線的地方,說如果指針指在數(shù)組范圍內(nèi)沒事,如果越界了就是undefined,也就是說這事交給編譯器實現(xiàn)了,編譯器想咋干咋干,那怕你想把其優(yōu)化掉也可以。在這里要重點說一下,C語言中的一個大惡魔—— Undefined! 這里都是“野獸出沒”的地方,你一定要小心小心再小心。

正確檢測整型溢出

在看過編譯器的這些行為后,你應該會明白——“在整型溢出之前,一定要做檢查,不然,就太晚了”。

我們來看一段代碼:

voidfoo(intm,intn)
{
size_ts=m+n;
.......
}

上面這段代碼有兩個風險:1)有符號轉無符號2)整型溢出。

這兩個情況在前面的那些示例中你都應該看到了。所以,你千萬不要把任何檢查的代碼寫在 s = m + n 這條語名后面,不然就太晚了。

undefined行為就會出現(xiàn)了——用句純正的英文表達就是——“Dragon is here”——你什么也控制不住了。(注意:有些初學者也許會以為size_t是無符號的,而根據(jù)優(yōu)先級 m 和 n 會被提升到unsigned int。其實不是這樣的,m 和 n 還是signed int,m + n 的結果也是signed int,然后再把這個結果轉成unsigned int 賦值給s)

比如,下面的代碼是錯的:

voidfoo(intm,intn)
{
size_ts=m+n;
if(m>0&&n>0&&(SIZE_MAX-m

上面的代碼中,大家要注意(SIZE_MAX – m < n)這個判斷,為什么不用m + n > SIZE_MAX呢?因為,如果 m + n 溢出后,就被截斷了,所以表達式恒真,也就檢測不出來了。另外,這個表達式中,m和n分別會被提升為unsigned。

但是上面的代碼是錯的,因為:

1)檢查的太晚了,if之前編譯器的undefined行為就已經(jīng)出來了(你不知道什么會發(fā)生)。

2)就像前面說的一樣,(SIZE_MAX – m < n) 可能會被編譯器優(yōu)化掉。

3)另外,SIZE_MAX是size_t的最大值,size_t在64位系統(tǒng)下是64位的,嚴謹點應該用INT_MAX或是UINT_MAX

所以,正確的代碼應該是下面這樣:

voidfoo(intm,intn)
{
size_ts=0;
if(m>0&&n>0&&(UINT_MAX-m

在《蘋果安全編碼規(guī)范》(PDF)中,第28頁的代碼中:

3ed5330a-7ab7-11ee-939d-92fbcf53809c.png

如果n和m都是signed int,那么這段代碼是錯的。正確的應該像上面的那個例子一樣,至少要在n * m時要把 n 和 m 給 cast 成 size_t。因為,n*m可能已經(jīng)溢出了,已經(jīng)undefined了,undefined的代碼轉成size_t已經(jīng)沒什么意義了。(如果m和n是unsigned int,也會溢出),上面的代碼僅在m和n是size_t的時候才有效。

不管怎么說,《蘋果安全編碼規(guī)范》絕對值得你去讀一讀。

二分取中搜索算法中的溢出

我們再來看一個二分取中搜索算法(binary search),大多數(shù)人都會寫成下面這個樣子:

intbinary_search(inta[],intlen,intkey)
{
intlow=0;
inthigh=len-1;

while(low<=high?)?{
????????int?mid?=?(low?+?high)/2;
????????if?(a[mid]?==?key)?{
????????????return?mid;
????????}
????????if?(key?

上面這個代碼中,你可能會有這樣的想法:

1) 我們應該用size_t來做len, low, high, mid這些變量的類型。沒錯,應該是這樣的。但是如果這樣,你要小心第四行 int high = len -1; 如果len為0,那么就“high大發(fā)了”。

2) 無論你用不用size_t。我們在計算mid = (low+high)/2; 的時候,(low + high) 都可以溢出。正確的寫法應該是:

intmid=low+(high-low)/2;

上溢出和下溢出的檢查

前面的代碼只判斷了正數(shù)的上溢出overflow,沒有判斷負數(shù)的下溢出underflow。讓們來看看怎么判斷:

對于加法,還好。

#include

voidf(signedintsi_a,signedintsi_b){
signedintsum;
if(((si_b>0)&&(si_a>(INT_MAX-si_b)))||
((si_b

對于乘法,就會很復雜(下面的代碼太夸張了):

voidfunc(signedintsi_a,signedintsi_b)
{
signedintresult;
if(si_a>0){/*si_aispositive*/
if(si_b>0){/*si_aandsi_barepositive*/
if(si_a>(INT_MAX/si_b)){
/*Handleerror*/
}
}else{/*si_apositive,si_bnonpositive*/
if(si_b0){/*si_aisnonpositive,si_bispositive*/
if(si_a

更多的防止在操作中整型溢出的安全代碼可以參看《INT32-C. Ensure that operations on signed integers do not result in overflow》

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • C語言
    +關注

    關注

    180

    文章

    7575

    瀏覽量

    134041
  • 代碼
    +關注

    關注

    30

    文章

    4671

    瀏覽量

    67765
  • 編譯器
    +關注

    關注

    1

    文章

    1602

    瀏覽量

    48895

原文標題:全面總結C語言中整形溢出問題

文章出處:【微信號:良許Linux,微信公眾號:良許Linux】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    嵌入式C語言中的goto語句詳解

    goto語句被稱為C語言中的跳轉語句。用于無條件跳轉到其他標簽。它將控制權轉移到程序的其他部分。
    發(fā)表于 07-19 16:08 ?2895次閱讀
    嵌入式<b class='flag-5'>C</b><b class='flag-5'>語言中</b>的goto語句<b class='flag-5'>詳解</b>

    C語言中宏定義的應用

    C語言中,宏定義是一種預處理指令,用于在代碼中定義和使用常量、函數(shù)或代碼片段的替代。
    發(fā)表于 08-17 15:33 ?612次閱讀

    C語言中if語句、if-else語句和switch語句詳解

    C語言中,有三種條件判斷結構:if語句、if-else語句和switch語句。
    發(fā)表于 08-18 16:36 ?5861次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語言中</b>if語句、if-else語句和switch語句<b class='flag-5'>詳解</b>

    C語言中賦值運算符詳解

    C語言中,賦值運算符用于將一個值賦給變量。
    發(fā)表于 08-18 16:38 ?1612次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語言中</b>賦值運算符<b class='flag-5'>詳解</b>

    C語言中int型強制類型轉換成short型的溢出問題怎么解決?

    如何判斷STM32編碼器模式中電機的正反轉?C語言中int型強制類型轉換成short型的溢出問題怎么解決?
    發(fā)表于 10-19 06:59

    C語言中常見的數(shù)據(jù)溢出情況有哪些

    C語言中有幾種基本數(shù)據(jù)類型呢?C語言中常見的數(shù)據(jù)溢出情況有哪些?
    發(fā)表于 02-25 07:55

    MSP430 C語言編程的程序堆棧溢出分析

    MSP430 C語言編程的程序堆棧溢出分析
    發(fā)表于 05-16 15:04 ?40次下載

    C語言中指針的介紹非常詳細

    C語言中指針的介紹非常詳細 C語言中指針的介紹非常詳細
    發(fā)表于 12-25 10:39 ?57次下載

    C語言和匯編語言混合編程方法和C語言中斷處理方法

    C語言和匯編語言混合編程方法和C語言中斷處理方法,new
    發(fā)表于 01-06 14:36 ?36次下載

    C語言中的關鍵字

    C語言中的入門教程
    發(fā)表于 10-14 16:24 ?3次下載

    總結那么幾個C語言中的“坑”

    總結幾個C語言中的“坑”
    的頭像 發(fā)表于 01-16 10:52 ?2477次閱讀

    c#語言中怎么使用HTTP代理

    c#語言中怎么使用HTTP代理。
    的頭像 發(fā)表于 09-01 14:46 ?1990次閱讀

    c語言int超出范圍溢出處理

    C語言中,int類型的范圍是由編譯器和操作系統(tǒng)決定的。通常情況下,int類型的范圍為-2147483648到2147483647。當我們在程序中使用int類型的變量時,如果超出了這個范圍,就會
    的頭像 發(fā)表于 11-30 11:38 ?4088次閱讀

    c語言整型數(shù)據(jù)的溢出計算

    計算原理,介紹其風險及可能帶來的后果,并提供一些應對策略和措施,旨在幫助程序員理解溢出問題并提供有效的解決方案。 一、溢出計算原理 1.1 數(shù)據(jù)類型與范圍 C語言中的整型數(shù)據(jù)類型包括c
    的頭像 發(fā)表于 11-30 11:45 ?1839次閱讀

    c語言中數(shù)據(jù)溢出是歸0還是歸1

    C語言中,數(shù)據(jù)溢出通常不會自動歸0或歸1,而是發(fā)生未定義行為。這是因為C語言中的數(shù)據(jù)類型都有一定范圍,超出該范圍的值會導致數(shù)據(jù)
    的頭像 發(fā)表于 11-30 11:47 ?1853次閱讀