溢出問題:數組溢出,整數溢出,緩沖區溢出,棧溢出,指針溢出


在C/C++程序里有一類非常典型的問題,那就是:溢出問題。一般在筆試題里,這類問題會以程序改錯或者安全問題出現。現在分別來分析一下常見的數組溢出,整數溢出,緩沖區溢出,棧溢出和指針溢出等。

(1)數組溢出


在C語言中,數組的元素下標是從0開始計算的,所以,對於n個元素的數組a[n], 遍歷它的時候是a[0],a[1],...,a[n-1],如果遍歷到a[n],數組就溢出了。 
void print_array(int a[], int n)
{
    for (int i = 0; i < n; i++) 
    {
        a[i] = a[i+1];//當i = n-1時,就發生了數組越界
        printf(“%d\n”, a[i]);
    }
}
上面的循環判斷應該改為:
for (int i = 0; i < n-1; i++)

(2)整數溢出


整數的溢出分為下溢出和上溢出。比如,對於有符號的char(signed char)類型來說,它能表示的范圍為:[-128,127]之間;而對於無符號的char(unsigned char)來說, 它能表示的范圍為:[0,255]。
那么,對於下面的代碼:
signed char c1 = 127;
c1 = c1+1;//發生上溢出,c1的值將變為-128
signed char c2 = -128;
c2 = c2-1;//發生下溢出,c2的值將變為127
unsigned char c3 = 255;
c3 = c3+1;//發生上溢出,c3的值將變為0
unsigned char c4 = 0;
c4 = c4-1;//發生下溢出,c4的值將變為255
從上面的例子可以看出,當一個整數向上溢出,將會變為最小值,而向下溢出,將會變為最大值。

來看下面的溢出代碼,該代碼負責提供一個小寫字母轉換表,但存在一個整數溢出問題:
void BuildToLowerTable( void ) /* ASCII版本*/
{
    unsigned char ch;
    /* 首先將每個字符置為它自己 */
    /*ch為unsigned char,無符號數,當ch值為UCHAR_MAX, ch++將會發生向上溢出,變為0,導致循環無法退出。*/
    for (ch=0; ch <= UCHAR_MAX;ch++)
        chToLower[ch] = ch;
    /* 將大寫字母改為小寫字母 */
    for( ch = ‘A’; ch <= ‘Z’; ch++ )
        chToLower[ch] = ch +’a’ – ‘A’;
}
該代碼負責在內存中查找指定的字符ch,但也存在一個溢出問題
void * memchr( void *pv, unsigned char ch, size_t size )
{
    unsigned char *pch = (unsigned char *) pv;
    /*當size的值為0的時候,由於size是無符號整數,因此會發生下溢出,變為一個最大的整數 循環也將無法退出*/ 
    while( -- size >=0 )
    {
        if( *pch == ch )
            return (pch );
        pch++;
    }
    return( NULL );
}

整數溢出也會帶來安全問題,甚至會造成權限提升到最高級別,比如Linux系統中的root權限。曾經的黑客通過對gid和uid的溢出,將用戶id的gid和uid權限設置為了0,從而成為了超級管理員賬戶。

(3)緩沖區溢出


緩沖區溢出一般是調用了一些不安全的字符串操作函數比如:strcpy,strcat等(這些字符串操作函數在拷貝或者修改目標位置的時候,並不判斷長度是否會超過目標緩存),或者設置參數超過了目標緩存能容納的大小而造成的溢出問題。
void func1(char* s)
{
    char buf[10];
    /*此時,buf只有10個字節,如果傳入的s超過10個字節,就會造成溢出*/
    strcpy(buf, s);
}
void func2(void)
{
    printf("Hacked by me.\n");
    exit(0);
}
int main(int argc, char* argv[])
{
    char badCode[] = "aaaabbbb2222cccc4444ffff";
    DWORD* pEIP = (DWORD*)&badCode[16];
    *pEIP = (DWORD)func2;
    /*badCode字符串超過了10個字節,傳遞給func1會造成棧上緩沖區溢出
    而且,由於badCode經過精心構造,在溢出的時候,根據函數的調用約定規則,會覆蓋棧上的返回地址,
    指向了func2。所以,在func1退出的時候,會直接調用func2
    */
    func1(badCode);
    return 0;
}

(4)棧溢出


無論是內核棧,還是應用層的棧,都是有一定大小限制的。如果在棧上分配的空間大於了這個限制,就會造成棧大小溢出,破壞棧上的數據。比如局部變量過多,或者遞歸調度嵌套太深都會造成棧溢出。比如:
int init_module(void)
{
    char buf[10000]; //buf[]分配在棧上,但10000的空間超過了棧的默認大小8KB。
    //所以發生溢出
    memset(buf,0,10000);
    printk("kernel stack.\n");
    return 0;
}
void cleanup_module(void)

    printk("goodbye.\n");
}
MODULE_LICENSE("GPL");
//應用棧的大小多少?內核棧的大小多少?什么時候容易棧溢出?

(5)指針溢出


一塊長度為size大小的內存buffer,buffer的首地址為p,那么buffer最后一個字節的地址:
p+size-1,而不是p+size。如果寫成了p+size,就會造成溢出,比如下面的代碼:
void* memchr( void *pv, unsigned char ch, size_t size )
{
    unsigned char *pch = ( unsigned char * )pv;
    unsigned char *pchEnd = pch + size;
    while( pch < pchEnd )
    {
        if( *pch == ch )
            return ( pch );
        pch ++ ;
    }
    return( NULL );
}

上面的代碼用於查找內存中特定的字符位置。對於其中的while()循環,平時執行似乎都沒有任何問題。但是,考慮一種特別情況,即pv所指的內存位置為末尾若干字節,那么因為pchEnd = pch+size,所以pchEnd指向最后一個字符的下一個字節,將會超出內存的范圍,即pchEnd所指的位置已經不存在。
知道了問題所在,那么可以將內存的結尾計算方式改為: 
pchEnd = pv + size – 1; 
while ( pch <= pchEnd ) 
{
        if( *pch == ch )
            return ( pch );
        pch ++ ;
}
…… 
pchEnd指向了最后一個字節。但是,檢查循環內部的執行情況可知,由於pch每增加到pchEnd+1時,都會發生上溢。因此,循環將無法退出。 於是,可以將程序修改為下面的代碼。將用size變量來控制循環的退出。這樣就不會存在任何問題了。
void *memchr( void *pv, unsigned char ch, size_t size )
{
    unsigned char *pch = ( unsigned char * )pv;
    while( size -- > 0 )
    {
        if( *pch == ch )
            return( pch );
        pch ++;
    }
    return( NULL );
}

大家知道,--size的效率一般比size--的效率高。那么是否可以將循環的判斷條件改為下面的語句呢? 
while( --size >= 0 ) 
…… 

實際上這是不行的。因為當size=0時,由於size是無符號數,那么它將發生下溢,變成了size所能表示的最大正數,循環也將無法退出。 

(6)字符串溢出

我們已經知道,字符串是'\0'結尾的。如果字符串結尾忘記帶上'\0',那么就溢出了。注意,strlen(p)計算的是字符串中有效的字符數(不含’\0’)。考察下面拷貝字符串的代碼,看看有什么問題沒呢?

 

char *str = “Hello, how are you!”;

char *strbak = (char *)malloc(strlen(str));

if (NULL == strbak)

{

//處理內存分配失敗,返回錯誤

}

strcpy(strbak, str);

......

 

顯然,由於strlen()計算的不是str的實際長度(即不包含’\0’字符的長度),所以strbak沒有結束符’\0’,而在C語言中,’\0’是字符串的結束標志,所以是必須加上的,否則會造成字符串的溢出。所以上面的代碼應該是:

 

char *str = “Hello, how are you!”;

char *strbak = (char *)malloc(strlen(str)+1);

if (NULL == strbak)

{

    //內存分配失敗,返回錯誤

}

strcpy(strbak, str);

 

同樣對於strncpy也可能會造成字符串溢出。strncpy函數原型:

char * strncpy(char *dest, char *src, size_t n); 

功能:將字符串src中最多n個字符復制到字符數組dest中(它並不像strcpy一樣遇到'\0'才停止復制,而是等湊夠n個字符才停止復制),返回指向dest的指針。要求:如果n > dest串長度,dest棧空間溢出產生崩潰異常。該函數注意的地方和strcpy類似,但是n值需特別注意 :

 

1)src串長度<=dest串長度,(這里的串長度包含串尾'\0'字符)  如果n=(0, src串長度),src的前n個字符復制到dest中。但是由於沒有'\0'字符,所以直接訪問dest串會發生棧溢出的異常情況。這時,一般建議采取memset將dest的全部元素用'\0'填充,如:memset(dest,0,7)(7為從dest起始地址開始前7個位置填充'\0',dest可以為字符指針和數組名)。注意:char* pc="abc"; char chs[5]; sizeof(pc)為4(包含'\0')(有些編譯器不行),sizeof(chs)為5。 如果n = src串長度,與strcpy一致。 如果n = dest串長度,dest [0,src串長度]處存放src字串,(src串長度, dest串長度]處存放'\0'。

 


2)src串長度>dest串長度  如果n =dest串長度,則dest串沒有'\0'字符,會導致字符串溢出,輸出會有亂碼。如果不考慮src串復制完整性,可以將dest最后一字符置為'\0'。所以,一般把n設為dest(含'\0')的長度(除非將多個src復制到dest中)。當2)中n=dest串長度時,定義dest為字符數組,因為這時沒有'\0'字符拷貝。

  

思考題:


1,分析下面程序運行情況,有什么問題呢?請深入分析
1 void main(void)
2 {
3     char x,y,z;
4     int i;
5     int a[16];
6     for(i=0;i<=16;i++)
7     {
8         a[i]=0;
9        printf("\n");
10   }
11   return 0;
12 }

2,下面算法將一個字符串逆置,如:"hello world"-->"dlrow olleh"。試分析存在的問題。
void ReverseString(char * str)
{
    int n;
    char c;
    n = strlen(str);
    for (int i = 0; i < n/2; i++)
    {
        c = str[i];
        str[i] = str[n-i];
        str[n-i] = c;
    }
}
3,下面代碼用於將一個char類型的數求反。試分析下面代碼問題。
signed char func(signed char c)
{
    return c*(-1);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM