1. 程式人生 > >C語言的整型溢位問題

C語言的整型溢位問題

整型溢位有點老生常談了,bla, bla, bla… 但似乎沒有引起多少人的重視。整型溢位會有可能導致緩衝區溢位,緩衝區溢位會導致各種黑客攻擊,比如最近OpenSSL的heartbleed事件,就是一個buffer overread的事件。在這裡寫下這篇文章,希望大家都瞭解一下整型溢位,編譯器的行為,以及如何防範,以寫出更安全的程式碼。

什麼是整型溢位

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

對於unsigned整型溢位,C的規範是有定義的——“溢位後的數會以2^(8*sizeof(type))作模運算”,也就是說,如果一個unsigned char(1字元,8bits)溢位了,會把溢位的值與256求模。例如:

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


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

對於signed整型的溢位,C的規範定義是“undefined behavior”,也就是說,編譯器愛怎麼實現就怎麼實現。對於大多數編譯器來說,算得啥就是啥。比如:

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

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

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

 signed char x = 0x7f; 

 signed char y = 0x05;

 signed char r = x * y;

 printf("%d\n", r);

上面的程式碼會輸出:123

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

整型溢位的危害

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

示例一:整形溢位導致死迴圈
  

 short len = 0;

 while(len< MAX_LEN)

 {

    len += readFromInput(fd, buf);

    buf += len;

 }

上面這段程式碼可能是很多程式設計師都喜歡寫的程式碼(我在很多程式碼裡看到過多次),其中的MAX_LEN 可能會是個比較大的整型,比如32767,我們知道short是16bits,取值範圍是-32768 到 32767 之間。但是,上面的while迴圈程式碼有可能會造成整型溢位,而len又是個有符號的整型,所以可能會成負數,導致不斷地死迴圈。

示例二:整形轉型時的溢位
 int copy_something(char *buf, int len)

 {     

     #define MAX_LEN 256   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傳一個負數,會通過了if的檢查,但在memcpy裡會被提升為一個正數,於是我們的mybuf就是overflow了。這個會導致mybuf緩衝區後面的資料被重寫。

示例三:分配記憶體

關於整數溢位導致堆溢位的很典型的例子是,OpenSSH Challenge-Response SKEY/BSD_AUTH 遠端緩衝區溢位漏洞。下面這段有問題的程式碼摘自OpenSSH的程式碼中的auth2-chall.c中的input_userauth_info_response() 函式:

 nresp = packet_get_int(); 

 if (nresp > 0) 
 {     
    response = xmalloc(nresp*sizeof(char*));    
    
    for (i = 0; i < nresp; i++)    
    {     
        response[i] = packet_get_string(NULL); 
    }
    
 }

上面這個程式碼中,nresp是size_t型別(size_t一般就是unsigned int/long int),這個示例是一個解資料包的示例,一般來說,資料包中都會有一個len,然後後面是data。如果我們精心準備一個len,比如:1073741825(在32位系統上,指標佔4個位元組,unsigned int的最大值是0xffffffff,我們只要提供0xffffffff/4 的值——0x40000000,這裡我們設定了0x4000000 + 1), nresp就會讀到這個值,然後nresp*sizeof(char*)就成了 1073741825 * 4,於是溢位,結果成為了 0x100000004,然後求模,得到4。於是,malloc(4),於是後面的for迴圈1073741825 次,就可以幹環事了(經過0x40000001的迴圈,使用者的資料早已覆蓋了xmalloc原先分配的4位元組的空間以及後面的資料,包括程式程式碼,函式指標,於是就可以改寫程式邏輯。關於更多的東西,你可以看一下這篇文章《Survey of Protections from Buffer-Overflow Attacks》)。

示例四:緩衝區溢位導致安全問題
 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); 


   return0;

 }

上面這個例子本來是想把buf1和buf2的內容copy到mybuf裡,其中怕len1 + len2超過256 還做了判斷,但是,如果len1+len2溢位了,根據unsigned的特性,其會與2^32求模,所以,基本上來說,上面程式碼中的[1]處有可能為假的。(注:通常來說,在這種情況下,如果你開啟-O程式碼優化選項,那個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--)  { ... } 

上面這兩個示例是我們經常用的從尾部遍歷一個數組的for迴圈。第一個是字串,第二個是C++中的vector容器。strlen()和vector::size()返回的都是 size_t,size_t在32位系統下就是一個unsigned int。你想想,如果strlen(s)和v.size() 都是0呢?這個迴圈會成為個什麼情況?於是strlen(s) – 1 和 v.size() – 1 都不會成為 -1,而是成為了 (unsigned int)(-1),一個正的最大數。導致你的程式越界訪問。

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

關於編譯器的行為

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

編譯器優化

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

關於編譯器的優化,在這裡再舉個例子,假設我們有下面的程式碼(又是一個相當相當常見的程式碼):

 int len; char* data;

 if(data + len < data)

 {

    printf("invalid len\n");

    exit(-1);

 }

上面這段程式碼中,len 和 data 配套使用,我們害怕len的值是非法的,或是len溢位了,於是我們寫下了if語句來檢查。這段程式碼在-O的引數下正常。但是在-O2的編譯選項下,整個if語句塊被優化掉了。

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

          int len = 10;

          char* data = (char *)malloc(len);

   0x00000000004004d4 <+4>:     mov    $0xa,%edi

   0x00000000004004d9 <+9>:     callq  <a target=_blank href="mailto:[email protected]">[email protected]</a>

 

          if(data + len < data)

          {

              printf("invalid len\n");

              exit(-1); 

          } 

 
      } 

   0x00000000004004de <+14>:    add    $0x8,%rsp

   0x00000000004004e2 <+18>:    retq

對此,你需要把上面 char* 轉型成 uintptr_t 或是 size_t,說白了也就是把char*轉成unsigned的資料結構,if語句塊就無法被優化了。如下所示:

 if ((uintptr_t)data + len < (uintptr_t)data)

 {

    ... ...

 }

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

注意上面標紅線的地方,說如果指標指在陣列範圍內沒事,如果越界了就是undefined,也就是說這事交給編譯器實現了,編譯器想咋幹咋幹,那怕你想把其優化掉也可以。在這裡要重點說一下,C語言中的一個大惡魔—— Undefined! 這裡都是“野獸出沒”的地方,你一定要小心小心再小心

花絮:編譯器的彩蛋

上面說了所謂的undefined行為就全權交給編譯器實現,gcc在1.17版本下對於undefined的行為還玩了個彩蛋(參看Wikipedia)。

下面gcc 1.17版本下的遭遇undefined行為時,gcc在unix發行版下玩的彩蛋的原始碼。我們可以看到,它會去嘗試去執行一些遊戲NetHackRogue 或是Emacs的 Towers of Hanoi,如果找不到,就輸出一條NB的報錯。

 execl("/usr/games/hack","#pragma", 0);// try to run the game NetHack execl("/usr/games/rogue","#pragma", 0);// try to run the  game Rogue 

 // try to run the Tower's of Hanoi simulation in Emacs.execl("/usr/new/emacs","-f","hanoi","9","-kill",0);

 execl("/usr/local/emacs","-f","hanoi","9","-kill",0);// same as above fatal("You are in a maze of twisty compiler features,   all different");

正確檢測整型溢位

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

我們來看一段程式碼:

void foo(int m, int n) 

{ 

    size_t s = m + n;

    .......

}

上面這段程式碼有兩個風險:1)有符號轉無符號2)整型溢位。這兩個情況在前面的那些示例中你都應該看到了。所以,你千萬不要把任何檢查的程式碼寫在 s = m + n 這條語名後面,不然就太晚了。undefined行為就會出現了——用句純正的英文表達就是——“Dragon is here”——你什麼也控制不住了。(注意:有些初學者也許會以為size_t是無符號的,而根據優先順序 m 和 n 會被提升到unsigned int。其實不是這樣的,m 和 n 還是signed int,m + n 的結果也是signed int,然後再把這個結果轉成unsigned int 賦值給s)

比如,下面的程式碼是錯的:

 void foo(intm, int n) 

 { 

    size_t s = m + n;

    if( m>0 && n>0 && (SIZE_MAX - m < n) ){

        //error handling...   }

 }

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

但是上面的程式碼是錯的,因為:

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

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

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

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

 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;

 }

在《蘋果安全編碼規範》(PDF)中,第28頁的程式碼中:

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

不管怎麼說,《蘋果安全編碼規範》絕對值得你去讀一讀。

二分取中搜索演算法中的溢位

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

 int binary_search(int a[], int len,intkey)

 {  

    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;

 }

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

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

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

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

上溢位和下溢位的檢查

前面的程式碼只判斷了正數的上溢位overflow,沒有判斷負數的下溢位underflow。讓們來看看怎麼判斷:

對於加法,還好。

 #include <limits.h>voidf(signedintsi_a,signedintsi_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;

 }

對於乘法,就會很複雜(下面的程式碼太誇張了):

 voidfunc(signedint si_a, signedintsi_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;

 } 

其它

對於C++來說,你應該使用STL中的numeric_limits::max() 來檢查溢位。

另外,微軟的SafeInt類是一個可以幫你遠理上面這些很tricky的類,下載地址:http://safeint.codeplex.com/

對於Java 來說,一種是用JDK 1.7中Math庫下的safe打頭的函式,如safeAdd()和safeMultiply(),另一種用更大尺寸的資料型別,最大可以到BigInteger。

可見,寫一個安全的程式碼並不容易,尤其對於C/C++來說。對於黑客來說,他們只需要搜一下開源軟體中程式碼有memcpy/strcpy之類的地方,然後看一看其周邊的程式碼,是否可以通過使用者的輸入來影響,如果有的話,你就慘了。

參考

最後, 不好意思,這篇文章可能羅嗦了一些,大家見諒。

(全文完)

相關推薦

c語言和字元的自動型別轉換

char a = -1; //機器碼為0xff unsigned char b = 254; //機器碼0xfe if (a <= b){ printf("a <= b\n"); } else{ printf("a > b\n"); }   上述程式碼輸出結果:

c語言和字符的自動類轉換

size \n 變量賦值 類型 結果 決定 特殊 code computer char a = -1; //機器碼為0xff unsigned char b = 254; //機器碼0xfe if (a <= b){ printf("a <= b\n");

第三章c語言和浮點數

1。c語言可移植型別:stdint.h 和 inttypes.h #include<stdio.h> #include<inttypes.h> int main(int argc, char const *argv[]) { int32_t me32; s

C語言---字串轉換

C語言提供了幾個標準庫函式,可以將任意型別(整型、長整型、浮點型等)的數字轉換為字串。以下是用itoa()函式將整數轉 換為字串的一個例子:     # include <stdio.h>    # include <stdlib.h>     vo

C語言溢位問題 int、long、long long取值範圍 最大最小值

《C和指標》中寫過:long與int:標準只規定long不小於int的長度,int不小於short的長度。 double與int型別的儲存機制不同,long int的8個位元組全部都是資料位,而double是以尾數,底數,指數的形式表示的,類似科學計數法,因此double比i

C語言溢位問題

整型溢位有點老生常談了,bla, bla, bla… 但似乎沒有引起多少人的重視。整型溢位會有可能導致緩衝區溢位,緩衝區溢位會導致各種黑客攻擊,比如最近OpenSSL的heartbleed事件,就是一個buffer overread的事件。在這裡寫下這篇文章,希望大家都瞭解

C語言中的溢位和移位溢位

1 整型溢位 原文連結:https://coolshell.cn/articles/11466.html     1.1 無符號整型溢位和有符號整型溢位    對於unsigned整型溢位,C的規範是有定義的——“溢位後的數會以2^(8*sizeof(type))作模運

匯編語言-處理,利用堆棧原樣輸出

tsp dsw cto pps asq log pos 結果 jks 要求:輸入任意一個整型數字字符串,並將整型原樣輸出。 這個子程序主要用於格式化排版,比output直接輸出字符串有很大美觀性。 1 ; Example assembly language progr

C語言int數據範圍

操作 求反 是把 int 超過 logs span 表示 color 在32位及以上操作系統上,int型數據的十進制表示範圍是:-231 到 231-1。原因:因為int是帶符號類型,所以最高位為符號位,於是最大表示的正數的原碼(正數的原碼和補碼相同):01111111

C#語言總結

數字類型 中大 字母 方式 取值 書寫 類型 c#語言 種類 C#語言類型在目前的學習中大致有四種類型是比較常見,分別是char、string、int、double,下面總結一下這4種語言的知識點。 1、定義 char字符類型:單個文字(漢字、字

C 語言編程--quickSort實現

ring log enum endif ide swa none ret sta 1 #ifndef _GENERICQUICKSORT_H_ 2 #define _GENERICQUICKSORT_H_ 3 void generic_swap(void * pa, v

C#實現數據字任意編碼任意進制的轉換和逆轉換

har eve blog ons rst each adapter AC CA 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.

C++學習--大數加法

無論整型還是長整型,都有它的極限,但有時需要運算很大的數,超過整型的極限。那就需要用字串儲存加數,再進行運算。 在C++學習的環境下,我把寫的函式都封裝在一個類中,提供一個函式介面供外部使用。 我的思想是:找出兩個數較長的,來確定結果字串的長度,預留一位進位,所以結果字串的長度會比兩個加數的

C語言陣列函式指標,替代switch case結構

#include <stdio.h> typedef void (*KeyEvent)(void); KeyEvent pKey_Func[100]; void pFunc0(void) { printf(“Hello Kitty\n”); } void pFun

Linux中create_elf_tables函式溢位漏洞分析(CVE-2018-14634)

在這篇文章中,我們將跟大家分析Linux平臺中create_elf_tables函式的一個整型溢位漏洞(CVE-2018-14634)。 概述 在近期的一次安全分析過程中,我們在64位Linux系統核心裡的create_elf_tables()函式中發現了一個整型溢位漏洞,本地攻擊者將

C#長時間與java長時間轉換

        最近在有一個解析並轉發病毒軟體日誌的活,這個軟體用的是SQLite嵌入式資料庫儲存病毒日誌。查詢病毒記錄後,我發現它用長整型儲存攻擊時間這個欄位,而且是一個10位的值。而我的解析系統是用C#寫的,C#的用來表示時間刻度的長整型一般都是18位的值,這讓我很是鬱悶

mysql無符號溢位問題及解決辦法

mysql環境下出現了無符號整型溢位的問題,即一個表中一個自定義的無符號整型欄位,然後程式碼通過update遞減,當低於0的時候,會溢位到最大的整型值42949967295, 解決辦法 1:更改程式碼,update內容, 原update table set a=a-1

c語言中整數溢位的概念

在編寫程式時,如果整數的值太大,超出了所定義的整數型別的範圍會怎麼樣? 下面分別將有符號型別好無符號型別整數設定為最大允許值加略大一些的值,看一看結果是是什麼。    //printf函式使用%u說明符顯示unsigned  int型別的值 程式段 #include &l

C++11——

C ++基本資料型別的大小 1Byte=8bit 1bit 儲存 2^1=2個值 #include <iostream> int main() { std::cout << "bool:\t

C++中的超範圍賦值問題

在C++的標準中,是規定了每一個算術型別的最小儲存空間的,但是該標準並不阻止編譯器來使用更大的儲存空間,而且事實上也正好如此,對於C++ 的內建型別,幾乎所有的編譯器都使用了更大的儲存空間來儲存資料。---yyc 在C++中可以理解物件的型別決定了物件的取值範圍,但是當我們