C/C++整形变量溢出问题
参考
概述
整形溢出分为无符号(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),一个正的最大数。导致程序越界访问。