• 溢出问题:数组溢出,整数溢出,缓冲区溢出,栈溢出,指针溢出


    在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 ”, 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. ");
        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. ");
        return 0;
    }
    void cleanup_module(void)

        printk("goodbye. ");
    }
    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)字符串溢出

    我们已经知道,字符串是''结尾的。如果字符串结尾忘记带上'',那么就溢出了。注意,strlen(p)计算的是字符串中有效的字符数(不含’’)。考察下面拷贝字符串的代码,看看有什么问题没呢?

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

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

    if (NULL == strbak)

    {

    //处理内存分配失败,返回错误

    }

    strcpy(strbak, str);

    ......

    显然,由于strlen()计算的不是str的实际长度(即不包含’’字符的长度),所以strbak没有结束符’’,而在C语言中,’’是字符串的结束标志,所以是必须加上的,否则会造成字符串的溢出。所以上面的代码应该是:

    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一样遇到''才停止复制,而是等凑够n个字符才停止复制),返回指向dest的指针。要求:如果n > dest串长度,dest栈空间溢出产生崩溃异常。该函数注意的地方和strcpy类似,但是n值需特别注意 :

    1)src串长度<=dest串长度,(这里的串长度包含串尾''字符)  如果n=(0, src串长度),src的前n个字符复制到dest中。但是由于没有''字符,所以直接访问dest串会发生栈溢出的异常情况。这时,一般建议采取memset将dest的全部元素用''填充,如:memset(dest,0,7)(7为从dest起始地址开始前7个位置填充'',dest可以为字符指针和数组名)。注意:char* pc="abc"; char chs[5]; sizeof(pc)为4(包含'')(有些编译器不行),sizeof(chs)为5。 如果n = src串长度,与strcpy一致。 如果n = dest串长度,dest [0,src串长度]处存放src字串,(src串长度, dest串长度]处存放''。


    2)src串长度>dest串长度  如果n =dest串长度,则dest串没有''字符,会导致字符串溢出,输出会有乱码。如果不考虑src串复制完整性,可以将dest最后一字符置为''。所以,一般把n设为dest(含'')的长度(除非将多个src复制到dest中)。当2)中n=dest串长度时,定义dest为字符数组,因为这时没有''字符拷贝。

      

    思考题:


    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(" ");
    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);
    }

  • 相关阅读:
    nextSibling VS nextElementSibling
    线程实现连续启动停,并在某一时间段内运行
    线程:安全终止与重启
    监控知识体系
    后台服务变慢解决方案
    Java泛型类型擦除以及类型擦除带来的问题
    常见的 CSRF、XSS、sql注入、DDOS流量攻击
    Spring对象类型——单例和多例
    一次线上OOM过程的排查
    深入浅出理解基于 Kafka 和 ZooKeeper 的分布式消息队列
  • 原文地址:https://www.cnblogs.com/fengxing999/p/11101089.html
Copyright © 2020-2023  润新知