说明:本文转载自http://coolshell.cn/articles/11466.html
原文作者:陈皓
整型溢出有点老生常谈了,bla, bla, bla… 但似乎没有引起多少人的重视。整型溢出会有可能导致缓冲区溢出,缓冲区溢出会导致各种黑客攻击,比如最近OpenSSL的heartbleed事件,就是一个buffer overread的事件。在这里写下这篇文章,希望大家都了解一下整型溢出,编译器的行为,以及如何防范,以写出更安全的代码。
C语言的整型问题相信大家并不陌生了。对于整型溢出,分为无符号整型溢出和有符号整型溢出。
对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说,如果一个unsigned char(1字符,8bits)溢出了,会把溢出的值与256求模。例如:
unsigned char x = 0xff;
printf("%d\n", ++x);
对于signed整型的溢出,C的规范定义是“undefined behavior”,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。比如:
signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了
printf("%d\n", ++x);
另外,千万别以为signed整型溢出就是负数,这个是不定的。比如:
signed char x = 0x7f;
signed char y = 0x05;
signed char r = x * y;
printf("%d\n", r);
上面的代码会输出:123
相信对于这些大家不会陌生了。
下面说一下,整型溢出的危害。
... ...
... ...
short len = 0;
... ...
while(len< MAX_LEN) {
len += readFromInput(fd, buf);
buf += len;
}
int copy_something(char*buf, intlen)
{
#define MAX_LEN 256
char mybuf[MAX_LEN];
... ...
... ...
if(len > MAX_LEN){// <---- [1]
return-1;
}
return memcpy(mybuf, buf, len);
}
关于整数溢出导致堆溢出的很典型的例子是,OpenSSH Challenge-Response SKEY/BSD_AUTH 远程缓冲区溢出漏洞。下面这段有问题的代码摘自OpenSSH的代码中的auth2-chall.c中的input_userauth_info_response() 函数:
nresp = packet_get_int();
if(nresp > 0) {
response = xmalloc(nresp*sizeof(char*));
for(i = 0; i < nresp; i++)
response[i] = packet_get_string(NULL);
}
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;
}
这样的例子有很多很多,这些整型溢出的问题如果在关键的地方,尤其是在搭配有用户输入的地方,如果被黑客利用了,就会导致很严重的安全问题。
在谈一下如何正确的检查整型溢出之前,我们还要来学习一下编译器的一些东西。请别怪我罗嗦。
如何检查整型溢出或是整型变量是否合法有时候是一件很麻烦的事情,就像上面的第四个例子一样,编译的优化参数-O/-O2/-O3基本上会假设你的程序不会有整形溢出。会把你的代码中检查溢出的代码给优化掉。
关于编译器的优化,在这里再举个例子,假设我们有下面的代码(又是一个相当相当常见的代码):
int len;
char* data;
if(data + len < data){
printf("invalid len\n");
exit(-1);
}
你可以写个小程序,在gcc下编译(我的版本是4.4.7,记得加上-O2和-g参数),然后用gdb调试时,用disass /m命信输出汇编,你会看到下面的结果(你可以看到整个if语句块没有任何的汇编代码——直接被编译器和谐掉了):
7 int len = 10;
8 char* data = (char *)malloc(len);
0x00000000004004d4 <+4>: mov $0xa,%edi
0x00000000004004d9 <+9>: callq 0x4003b8
9
10 if (data + len < data){
11 printf("invalid len\n");
12 exit(-1);
13 }
14
15 }
0x00000000004004de <+14>: add $0x8,%rsp
0x00000000004004e2 <+18>: retq
if((uintptr_t)data + len < (uintptr_t)data){
... ...
}
注意上面标红线的地方,说如果指针指在数组范围内没事,如果越界了就是undefined,也就是说这事交给编译器实现了,编译器想咋干咋干,那怕你想把其优化掉也可以。在这里要重点说一下,C语言中的一个大恶魔—— Undefined! 这里都是“野兽出没”的地方,你一定要小心小心再小心。
上面说了所谓的undefined行为就全权交给编译器实现,gcc在1.17版本下对于undefined的行为还玩了个彩蛋(参看Wikipedia)。
下面gcc 1.17版本下的遭遇undefined行为时,gcc在unix发行版下玩的彩蛋的源代码。我们可以看到,它会去尝试去执行一些游戏NetHack, Rogue 或是Emacs的 Towers of Hanoi,如果找不到,就输出一条NB的报错。
execl("/usr/games/hack","#pragma", 0);// try to run the game NetHack
execl("/usr/games/rogue","#pragma", 0);// try to run the game Rogue
// try to run the Tower's of Hanoi simulation in Emacs.
execl("/usr/new/emacs","-f","hanoi","9","-kill",0);
execl("/usr/local/emacs","-f","hanoi","9","-kill",0);// same as above
fatal("You are in a maze of twisty compiler features, all different");
在看过编译器的这些行为后,你应该会明白——“在整型溢出之前,一定要做检查,不然,就太晚了”。
我们来看一段代码:
void foo(int m, int n)
{
size_t s = m + n;
.......
}
比如,下面的代码是错的:
void foo(int m, int n)
{
size_t s = m + n;
if ( m>0 && n>0 && (SIZE_MAX - m < n) ){
//error handling...
}
}
但是上面的代码是错的,因为:
1)检查的太晚了,if之前编译器的undefined行为就已经出来了(你不知道什么会发生)。
2)就像前面说的一样,(SIZE_MAX – m < n) 可能会被编译器优化掉。
3)另外,SIZE_MAX是size_t的最大值,size_t在64位系统下是64位的,严谨点应该用INT_MAX或是UINT_MAX
所以,正确的代码应该是下面这样:
void foo(int m, int n)
{
size_t s = 0;
if ( m>0 && n>0 && ( UINT_MAX - m < n ) ){
//error handling...
return;
}
s = (size_t)m + (size_t)n;
}
如果n和m都是signed int,那么这段代码是错的。正确的应该像上面的那个例子一样,至少要在n*m时要把 n 和 m 给 cast 成 size_t。因为,n*m可能已经溢出了,已经undefined了,undefined的代码转成size_t已经没什么意义了。(如果m和n是unsigned int,也会溢出),上面的代码仅在m和n是size_t的时候才有效。
不管怎么说,《苹果安全编码规范》绝对值得你去读一读。
前面的代码只判断了正数的上溢出overflow,没有判断负数的下溢出underflow。让们来看看怎么判断:
对于加法,还好。
#include
void f(signed int si_a, signed int si_b) {
signed int sum;
if(((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
/* Handle error */
return;
}
sum = si_a + si_b;
}
void func(signed int si_a, signed int si_b)
{
signedint result;
if(si_a > 0) { /* si_a is positive */
if(si_b > 0) { /* si_a and si_b are positive */
if(si_a > (INT_MAX / si_b)) {
/* Handle error */
}
}else { /* si_a positive, si_b nonpositive */
if(si_b < (INT_MIN / si_a)) {
/* Handle error */
}
}/* si_a positive, si_b nonpositive */
}else { /* si_a is nonpositive */
if(si_b > 0) { /* si_a is nonpositive, si_b is positive */
if(si_a < (INT_MIN / si_b)) {
/* Handle error */
}
}else { /* si_a and si_b are nonpositive */
if( (si_a != 0) && (si_b < (INT_MAX / si_a))) {
/* Handle error */
}
}/* End if si_a and si_b are nonpositive */
}/* End if si_a is nonpositive */
result = si_a * si_b;
}
对于C++来说,你应该使用STL中的numeric_limits::max() 来检查溢出。
另外,微软的SafeInt类是一个可以帮你远理上面这些很tricky的类,下载地址:http://safeint.codeplex.com/
对于Java 来说,一种是用JDK 1.7中Math库下的safe打头的函数,如safeAdd()和safeMultiply(),另一种用更大尺寸的数据类型,最大可以到BigInteger。
可见,写一个安全的代码并不容易,尤其对于C/C++来说。对于黑客来说,他们只需要搜一下开源软件中代码有memcpy/strcpy之类的地方,然后看一看其周边的代码,是否可以通过用户的输入来影响,如果有的话,你就惨了。
参考:
INT32-C. Ensure that operations on signed integers do not result in overflow
最后, 不好意思,这篇文章可能罗嗦了一些,大家见谅。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn ,请勿用于任何商业用途)