C/C++整形变量溢出问题

参考

C语言的整型溢出问题 | 酷 壳 - CoolShell

 

 


 

概述

整形溢出分为无符号(unsigned)整型溢出和有符号(signed)整型溢出

无符号整型溢出

对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2 ^ ( 8 * sizeof ( type ) )作模运算”

比如,两个unsigned int类型求和溢出

unsigned int a = 4294967295;  // 最大值,2^32-1
unsigned int b = 2;
unsigned int sum = a + b;  // (2^32 - 1 + 2) % (2^(8*4))
std::cout << "Sum: " << sum << std::endl;  // 输出结果会是 1,发生了溢出

 

有符号整形溢出

对于signed整型的溢出,C的规范定义是“undefined behavior”

也就是说,有符号整型的溢出问题取决于编译器

 

 

整型溢出的危害

示例一:整型溢出导致死循环

int len = 0;

while(len< MAX_LEN) {
    len += readFromInput(fd, buf);         // <---- [1]
    buf += len; 
}

  

在这段代码的[1]处,存在整型溢出的风险。具体来说,当 len 的值接近 MAX_LEN 时,如果 readFromInput 函数返回一个超出 int 类型表示范围的正值,那么累加操作 len += readFromInput(fd, buf) 可能会导致整型溢出。这种情况下,len 可能无法正确地表示实际读取的数据长度,从而导致程序行为出现异常,比如len溢出变为负数,不断地死循环。

解决方案

int len = 0;
while(len < MAX_LEN) {
    int bytesRead = readFromInput(fd, buf);
    if (bytesRead <= 0) {
        break;  // 读取出错或者已经读取到末尾,退出循环
    }
    if (len > MAX_LEN - byteRead) {
        bytesRead = MAX_LEN - len;  // 限制本次读取的字节数,以免超过 MAX_LEN
    }
    len += bytesRead;
    buf += bytesRead;
}

对 readFromInput 函数的返回值进行了检查。这样可以确保在累加 len 的过程中不会发生整型溢出,并且在读取失败的情况下可以正确地终止循环。

 

示例二:整型转型时的溢出

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

[1]处的if语句存在整型溢出的问题,具体来说,len是个signed int,而memcpy则需一个size_t的len,也就是一个unsigned 类型。于是,len会被类型转换为unsigned,此时,如果我们给len传一个负数,会通过了if的检查,但在memcpy里会被提升为一个正数,于是我们的mybuf就是overflow了。这个会导致mybuf缓冲区后面的数据被重写。

 

解决方案

unsigned int copy_something(char *buf, unsigned int len)
{
    #define MAX_LEN 256u  // 将 MAX_LEN 修改为无符号整型常量
    char mybuf[MAX_LEN];
    if(len > MAX_LEN){
        return -1;  // 应该返回错误值,确保逻辑正确
    }
    memcpy(mybuf, buf, len);  // 这里应该直接调用 memcpy 函数进行内存拷贝
    return len;
}

 涉及到内存大小的变量,一般设置成unsigned int或者size_t类型,避免溢出问题

 

示例三: 分配内存

关于整数溢出导致堆溢出的很典型的例子是,OpenSSH Challenge-Response SKEY/BSD_AUTH 远程缓冲区溢出漏洞。

下面这段有问题的代码摘自OpenSSH的代码中的auth2-chall.c中的input_userauth_info_response() 函数:

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

 

这里以32位系统为例

nresp是size_t类型,大小为0 ~ 2^32-1,(size_t在32位系统上定义为 unsigned int,也就是32位无符号整型。在64位系统上定义为 unsigned long ,也就是64位无符号整形)

sizeof(char*) == 4,因为32位系统上,指针占四个字节

这个示例是一个解数据包的示例
packet_get_int()通常会返回一个包的长度len
假如说“精心设计”一个len,比如0x40000001,(unsigned int 最大值是0xffffffff,除以4就是0x40000000,再加1就是”精心设计“的len)
然后nresp读到这个0x40000001后 , 通过计算 nresp*sizeof(char*) , 0x40000001 * 4 , 结果是0x100000004, 就会溢出,结果取模(无符号整数溢出取模),得到最终的结果4
因此malloc申请的内存大小的大小就是4
但是此时nresp的大小为1073741825(对应十六进制0x40000001)
因此for循环会循环nresp次,此前申请的4字节空间很快就会被覆盖,后面的循环中很可能会被恶意利用

当后续的数据被拷贝到这个过小的内存空间时,就会导致堆缓冲区溢出,覆盖了原本的内存内容,包括后面的数据和代码。

这种类型的漏洞可能会被恶意攻击者利用来执行任意的恶意代码,导致拒绝服务、远程代码执行,或者其他潜在的安全问题。

 

示例四:缓冲区溢出导致安全问题

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;
}

在注释标记为 [1] 的位置,程序会检查 len1 和 len2 的总和是否超过了 mybuf 的大小。如果总和超过 256,则会返回 -1,这可以防止 mybuf 超出其分配的内存范围。

然而,这段代码中并没有考虑到 len1 和 len2 是无符号整数,因此它们不会被检查是否小于0,同时也没有检查它们是否大于或等于无符号整数的最大值。如果 len1 和 len2 非常大,甚至超过了无符号整数的最大值,那么 (len1 + len2) 的结果会小于256(无符号整数溢出),但仍可能导致缓冲区溢出。

(注:通常来说,在这种情况下,如果你开启-O代码优化选项,那个if语句块就全部被和谐掉了——被编译器给删除了)

 

示例五: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),一个正的最大数。导致程序越界访问。

 

posted @ 2024-03-26 22:12  云北海  阅读(44)  评论(0编辑  收藏  举报