本节书摘来自华章计算机《编写高质量代码:改善c程序代码的125个建议》一书中的第1章,建议2-7,作者:马 伟 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
建议2-7:防止有符号整数溢出
整数溢出是一种常见、难预测且严重的软件漏洞,由它引发的程序Bug可能比格式化字符串与缓冲区溢出等缺陷更难于发现。C99标准中规定,当两个操作数都是有符号整数时,就有可能发生整数溢出,它将会导致“不能确定的行为”。也就是说整数溢出是一种未定义的行为,这也就意味着编译器在处理有符号整数的溢出时具有很多的选择,遵循标准的编译器可以做它们想做的任何事,比如完全忽略该溢出或终止进程。大多数编译器都会忽略这种溢出,这可能会导致不确定的值或错误的值保存在整数变量中。
整数溢出有时候是很难发现的,一般情况下在整数溢出发生之前,你都无法知道它是否会发生溢出,即使你的代码经过仔细审查,有时候溢出也是不可避免的。因此,程序很难区分先前计算出的结果是否正确,而且如果计算结果将作为一个缓冲区的大小、数组的下标、循环计数器与内存分配函数的实参等时将会非常危险。当然,因为无法直接改写内存单元,所以大多数整数溢出是没有办法利用的。但是,有时候整数溢出将会导致其他类型的缺陷发生,比如很容易发生的缓冲区溢出等。代码清单1-13是一个简单的整数溢出示例。
代码清单1-13 整数溢出示例
#include <stdio.h>
int main(void)
{
int s1 = 2147483647;
int s2 = 1073741824;
int s3 = -1879048193;
int s4=1;
int s5=4;
printf("%d(0x%x)+%d(0x%x)=%d(0x%x)\n", s1, s1, s4, s4,
s1+s4, s1+s4);
printf("%d(0x%x)-%d(0x%x)=%d(0x%x)\n", s2, s2, s3, s3,
s2-s3, s2-s3);
printf("%d(0x%x)*%d(0x%x)=%d(0x%x)\n", s2, s2, s5, s5,
s2*s5, s2*s5);
return 0;
}
在32位操作系统中,类型int 的取值范围为“-2147483647~2147483647”,限制是由INT_MIN与INT_MAX宏指定的,如下面的代码所示:
#define INT_MIN (-2147483647 - 1)
#define INT_MAX 2147483647
而在代码清单1-13中,当程序执行语句“s1+s4 、s2-s3 与s2*s5”时,其结果都超过类型int 的取值范围,因此发生溢出行为,运行结果如图1-15所示。
当然,面对这些简单的有符号整数运算溢出,简单地通过对操作数进行预测的方法就能够避免发生有符号整数运算溢出。比如,代码清单1-14就采用了补码的表示形式来对操作数进行预测。
代码清单1-14 采用补码的表示形式来对操作数进行预测
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int s1 = 2147483647;
int s2 = 1073741824;
int s3 = -1879048193;
int s4=1;
if(((s1^s4)|(((s1^(~(s1^s4)
&(1<<(sizeof(int)*CHAR_BIT-1))))+s4)^s4))>=0)
{
/*处理溢出条件*/
}
else
{
printf("%d(0x%x)+%d(0x%x)=%d(0x%x)\n", s1, s1,s4,s4,
s1+s4, s1+s4);
}
if(((s2^s3)&(((s2^((s2^s3)
&(1<<(sizeof(int)*CHAR_BIT-1))))-s3)^s3))<0)
{
/*处理溢出条件*/
}
else
{
printf("%d(0x%x)-%d(0x%x)=%d(0x%x)\n", s2, s2, s3, s3,
s2-s3, s2-s3);
}
return 0;
}
如上面的代码所示,这种方式可以有效地避免发生简单的有符号整数运算溢出,有兴趣的朋友可以自己测试。其实,不只算术运算可能造成溢出,任何企图改变该有符号整型变量值的操作都可能造成溢出。示例如代码清单1-15所示。
代码清单1-15 溢出示例
#include <stdio.h>
int main(void)
{
int si1= 1073741824;
int si2=0;
int si3= -1073741824;
int si4=4;
int si5=-1;
printf("si1 = %d (0x%x)\n", si1, si1);
si2 = si1 + si3;
printf("si1 + %d(0x%x) = %d (0x%x)\n",si3,si3, si2, si2);
si2 = si1 * si4;
printf("si1 * %d(0x%x) = %d (0x%x)\n",si4,si4, si2, si2);
si2 = si1 - si5;
printf("si1 - %d(0x%x) = %d (0x%x)\n",si5,si5, si2, si2);
return 0;
}
代码清单1-15的运行结果如图1-16所示。
与无符号整数的回绕相似,并不是每种运算符号都会令有符号操作数运算产生溢出,
与前面所讲的无符号整数回绕一样,有符号整数的这种溢出也很容易导致缓冲区溢出,同时也很容易让攻击者可执行任意代码,演示示例如代码清单1-16所示。
代码清单1-16 溢出导致的结果示例
#include <stdio.h>
#include <stdlib.h>
int copychar(char *c1,int len1, char *c2, int len2);
int main(int argc, char *argv[])
{
copychar(argv[1],atoi(argv[2]),argv[3],atoi(argv[4]));
return 0;
}
int copychar(char *c1,int len1, char *c2, int len2)
{
char buf[100];
if((len1 + len2) > 100)
{
printf("超出buf容纳范围(100)!\n");
return -1;
}
memcpy(buf, c1, len1);
memcpy(buf+len1, c2, len2);
printf("复制%d+%d=%d个字节到buf!\n",len1,len2,len1+len2);
printf("buf=%s\n", buf);
return 0;
}
在代码清单1-16中,程序需要将c1与c2的内容复制到buf中,并分别由len1与len2来指定复制的字节数。这里需要特别注意的语句是“if((len1 + len2) > 100)”,我们利用该语句进行了相对严格的大小检查:如果len1 + len2的值大于buf数组的大小(100),则不进行复制。
运行代码清单1-16,当我们执行命令“1-16 Hello! 6 C 2”时,程序运行正常,并成功地将字符串复制到buf中,运行结果如图1-17所示。
https://yqfile.alicdn.com/8c08fa3021127403f0c03083b05544ce95285b22.png" >
当我们执行命令“1-16 Hello! 50 C 51”时,程序同样运行正常,运行结果如图1-18所示。
可当我们执行命令“1-16 Hello! 2147483647 C 2”时,程序却意外地绕过大小检查语句“if((len1 + len2) > 100)”来执行相关的操作。是什么原因导致这种情况发生的呢?
其实很简单,就是由于整数溢出而导致的。从执行的命令“1-16 Hello! 2147483647 C 2”可以得出,len1的值为2147483647(即十六进制为0x7fffffff),len2值为2(即十六进制为0x00000002),当执行语句“len1 + len2(即0x7fffffff+0x00000002)”时会发生溢出,所得结果为-2147483647(即十六进制为0x80000001)。因为-2147483647远远小于100,从而使程序绕过大小检查语句“if((len1 + len2) > 100)”来执行余下的操作。也正因为如此,在执行语句“memcpy(buf, c1, len1)”时便导致异常“Unhandled exception at 0x65726f66 in 1-16.exe: 0xC0000005: Access violation reading location 0x65726f66”的发生。