<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          全面總結(jié)C語言中整形溢出問題

          共 12976字,需瀏覽 26分鐘

           ·

          2023-11-08 08:30


                     
                     

          來源:https://coolshell.cn/articles/11466.html

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

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

          什么是整型溢出

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

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

          unsigned char x = 0xff;
          printf("%d\n", ++x);

          上面的代碼會(huì)輸出:0 (因?yàn)?xff + 1是256,與2^8求模后就是0)

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

          signed char x =0x7f//注:0xff就是-1了,因?yàn)樽罡呶皇?也就是負(fù)數(shù)了
          printf("%d\n", ++x);

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

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

          signed char x = 0x7f;
          signed char y = 0x05;
          signed char r = x * y;
          printf("%d\n", r);

          上面的代碼會(huì)輸出:123

          相信對于這些大家不會(huì)陌生了。

          整型溢出的危害

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

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

           ... ...
          ... ...
          short len = 0;
          ... ...
          while(len< MAX_LEN) {
              len += readFromInput(fd, buf);
              buf += len;
          }

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

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

          示例二:整形轉(zhuǎn)型時(shí)的溢出

          int copy_something(char *buf, int len)
          {
              #define MAX_LEN 256
              char mybuf[MAX_LEN];
               ... ...
               ... ...

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

               return memcpy(mybuf, buf, len);
          }

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

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

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

          關(guān)于整數(shù)溢出導(dǎo)致堆溢出的很典型的例子是,OpenSSH Challenge-Response SKEY/BSD_AUTH 遠(yuǎn)程緩沖區(qū)溢出漏洞。下面這段有問題的代碼摘自O(shè)penSSH的代碼中的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; i++)
                  response[i] = packet_get_string(NULL);
          }

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

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

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

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

          int func(char *buf1, unsigned int len1,
                   char *buf2, unsigned int len2 )

          {
             char mybuf[256]; 

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

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

             do_some_stuff(mybuf); 

             return 0;
          }

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

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

          示例五:size_t 的溢出

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

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

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

          這樣的例子有很多很多,這些整型溢出的問題如果在關(guān)鍵的地方,尤其是在搭配有用戶輸入的地方,如果被黑客利用了,就會(huì)導(dǎo)致很嚴(yán)重的安全問題。

          關(guān)于編譯器的行為

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

          編譯器優(yōu)化

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

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

          int len;
          char* data;

          if (data + len < data){
              printf("invalid len\n");
              exit(-1);
          }

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

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

          7 int len = 10;
          8 char* data = (char *)malloc(len);
          0x00000000004004d4 <+4>: mov $0xa,%edi
          0x00000000004004d9 <+9>: callq 0x4003b8 <malloc@plt>

          9
          10 if (data + len < data){
          11 printf("invalid len\n");
          12 exit(-1);
          13 }
          14
          15 }
          0x00000000004004de <+14>: add $0x8,%rsp
          0x00000000004004e2 <+18>: retq

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

          if ((uintptr_t)data + len < (uintptr_t)data){
              ... ...
          }

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

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

          正確檢測整型溢出

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

          我們來看一段代碼:

           void foo(int m, int n)
          {
              size_t s = m + n;
              .......
          }

          上面這段代碼有兩個(gè)風(fēng)險(xiǎn):1)有符號(hào)轉(zhuǎn)無符號(hào),2)整型溢出。

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

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

          比如,下面的代碼是錯(cuò)的:

           void foo(int m, int n)
          {
              size_t s = m + n;
              if ( m>0 && n>0 && (SIZE_MAX - m < n) ){
                  //error handling...
              }
          }

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

          但是上面的代碼是錯(cuò)的,因?yàn)椋?/p>

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

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

          3)另外,SIZE_MAX是size_t的最大值,size_t在64位系統(tǒng)下是64位的,嚴(yán)謹(jǐn)點(diǎn)應(yīng)該用INT_MAX或是UINT_MAX

          所以,正確的代碼應(yīng)該是下面這樣:

           void foo(int m, int n)
          {
              size_t s = 0;
              if ( m>0 && n>0 && ( UINT_MAX - m < n ) ){
                  //error handling...
                  return;
              }
              s = (size_t)m + (size_t)n;
          }

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

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

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

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

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

          int binary_search(int a[], int len, int key)
          {
              int low = 0
              int high = len - 1

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

          上面這個(gè)代碼中,你可能會(huì)有這樣的想法:

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

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

          int mid = low + (high - low)/2;

          上溢出和下溢出的檢查

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

          對于加法,還好。

          #include <limits.h>

          void f(signed int si_a, signed int si_b) {
              signed int sum;
              if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
                  ((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
                  /* Handle error */
                  return;
              }
              sum = si_a + si_b;
          }

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

          void func(signed int si_a, signed int si_b)
          {
            signed int result;
            if (si_a > 0) {  /* si_a is positive */
              if (si_b > 0) {  /* si_a and si_b are positive */
                if (si_a > (INT_MAX / si_b)) {
                  /* Handle error */
                }
              } else { /* si_a positive, si_b nonpositive */
                if (si_b < (INT_MIN / si_a)) {
                  /* Handle error */
                }
              } /* si_a positive, si_b nonpositive */
            } else { /* si_a is nonpositive */
              if (si_b > 0) { /* si_a is nonpositive, si_b is positive */
                if (si_a < (INT_MIN / si_b)) {
                  /* Handle error */
                }
              } else { /* si_a and si_b are nonpositive */
                if ( (si_a != 0) && (si_b < (INT_MAX / si_a))) {
                  /* Handle error */
                }
              } /* End if si_a and si_b are nonpositive */
            } /* End if si_a is nonpositive */

            result = si_a * si_b;
          }

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



          最近很多小伙伴找我要一些程序員必備資料,于是我翻出了壓箱底的寶藏,免費(fèi)分享給大家!


          掃描海報(bào)二維碼免費(fèi)獲取。


          瀏覽 1377
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  精品久久久久久蜜桃 | 日韩精品a∨中文在线播放 | 亚洲人成人片77777 | 黄色电影在线观看国内免费 | 尻屄尻美女屄屄网 |