C语言的整型溢出问题

整型溢出有点老生常谈了,bla, bla, bla… 但似乎没有引起多少人的重视。整型溢出会有可能导致缓冲区溢出,缓冲区溢出会导致各种黑客攻击,比如最近OpenSSL的heartbleed事件,就是一个buffer overread的事件。在这里写下这篇文章,希望大家都了解一下整型溢出,编译器的行为,以及如何防范,以写出更安全的代码。

什么是整型溢出

C语言的整型问题相信大家并不陌生了。对于整型溢出,分为无符号整型溢出和有符号整型溢出。

对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说,如果一个unsigned char(1字符,8bits)溢出了,会把溢出的值与256求模。例如:

1
2
unsigned char x = 0xff;
printf("%d\n", ++x);

上面的代码会输出:0 (因为0xff + 1是256,与2^8求模后就是0)

对于signed整型的溢出,C的规范定义是“undefined behavior”,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。比如:

1
2
signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了
printf("%d\n", ++x);

上面的代码会输出:-128,因为0x7f + 0×01得到0×80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。

另外,千万别以为signed整型溢出就是负数,这个是不定的。比如:

1
2
3
4
signed char x = 0x7f;
signed char y = 0x05;
signed char r = x * y;
printf("%d\n", r);

上面的代码会输出:123

相信对于这些大家不会陌生了。

整型溢出的危害

下面说一下,整型溢出的危害。

示例一:整形溢出导致死循环
1
2
3
4
5
6
7
8
... ...
... ...
short len = 0;
... ...
while(len< MAX_LEN) {
    len += readFromInput(fd, buf);
    buf += len;
}

上面这段代码可能是很多程序员都喜欢写的代码(我在很多代码里看到过多次),其中的MAX_LEN 可能会是个比较大的整型,比如32767,我们知道short是16bits,取值范围是-32768 到 32767 之间。但是,上面的while循环代码有可能会造成整型溢出,而len又是个有符号的整型,所以可能会成负数,导致不断地死循环。

示例二:整形转型时的溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
int copy_something(char *buf, int len)
{
    #define MAX_LEN 256
    char mybuf[MAX_LEN];</pre>
<pre>     ... ...
     ... ...
     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缓冲区后面的数据被重写。

示例三:分配内存

关于整数溢出导致堆溢出的很典型的例子是,OpenSSH Challenge-Response SKEY/BSD_AUTH 远程缓冲区溢出漏洞。下面这段有问题的代码摘自OpenSSH的代码中的auth2-chall.c中的input_userauth_info_response() 函数:

1
2
3
4
5
6
nresp = packet_get_int();
if (nresp > 0) {
    response = xmalloc(nresp*sizeof(char*));
    for (i = 0; i < nresp; i++)
        response[i] = packet_get_string(NULL);
}

上面这个代码中,nresp是size_t类型(size_t一般就是unsigned int/long int),这个示例是一个解数据包的示例,一般来说,数据包中都会有一个len,然后后面是data。如果我们精心准备一个len,比如:1073741825(在32位系统上,指针占4个字节,unsigned int的最大值是0xffffffff,我们只要提供0xffffffff/4 的值——0×40000000,这里我们设置了0×4000000 + 1), nresp就会读到这个值,然后nresp*sizeof(char*)就成了 1073741825 * 4,于是溢出,结果成为了 0×100000004,然后求模,得到4。于是,malloc(4),于是后面的for循环1073741825 次,就可以干环事了(经过0×40000001的循环,用户的数据早已覆盖了xmalloc原先分配的4字节的空间以及后面的数据,包括程序代码,函数指针,于是就可以改写程序逻辑。关于更多的东西,你可以看一下这篇文章《Survey of Protections from Buffer-Overflow Attacks》)。

示例四:缓冲区溢出导致安全问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

上面这个例子本来是想把buf1和buf2的内容copy到mybuf里,其中怕len1 + len2超过256 还做了判断,但是,如果len1+len2溢出了,根据unsigned的特性,其会与2^32求模,所以,基本上来说,上面代码中的[1]处有可能为假的。(注:通常来说,在这种情况下,如果你开启-O代码优化选项,那个if语句块就全部被和谐掉了——被编译器给删除了)比如,你可以测试一下 len1=0×104, len2 = 0xfffffffc 的情况。

这样的例子有很多很多,这些整型溢出的问题如果在关键的地方,尤其是在搭配有用户输入的地方,如果被黑客利用了,就会导致很严重的安全问题。

关于编译器的行为

在谈一下如何正确的检查整型溢出之前,我们还要来学习一下编译器的一些东西。请别怪我罗嗦。

编译器优化

如何检查整型溢出或是整型变量是否合法有时候是一件很麻烦的事情,就像上面的第四个例子一样,编译的优化参数-O/-O2/-O3基本上会假设你的程序不会有整形溢出。会把你的代码中检查溢出的代码给优化掉。

关于编译器的优化,在这里再举个例子,假设我们有下面的代码(又是一个相当相当常见的代码):

1
2
3
4
5
6
7
int len;
char* data;
if (data + len < data){
    printf("invalid len\n");
    exit(-1);
}

上面这段代码中,len 和 data 配套使用,我们害怕len的值是非法的,或是len溢出了,于是我们写下了if语句来检查。这段代码在-O的参数下正常。但是在-O2的编译选项下,整个if语句块被优化掉了。

你可以写个小程序,在gcc下编译(我的版本是4.4.7,记得加上-O2和-g参数),然后用gdb调试时,用disass /m命信输出汇编,你会看到下面的结果(你可以看到整个if语句块没有任何的汇编代码——直接被编译器和谐掉了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
7           int len = 10;
8           char* data = (char *)malloc(len);
   0x00000000004004d4 <+4>:     mov    $0xa,%edi
   0x00000000004004d9 <+9>:     callq  0x4003b8 <malloc@plt>
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

对此,你需要把上面 char* 转型成 uintptr_t 或是 size_t,说白了也就是把char*转成unsigned的数据结构,if语句块就无法被优化了。如下所示:

1
2
3
if ((uintptr_t)data + len < (uintptr_t)data){
    ... ...
}

关于这个事,你可以看一下C99的规范说明《 ISO/IEC 9899:1999 C specification 》第 §6.5.6 页,第8点,我截个图如下:(这段话的意思是定义了指针+/-一个整型的行为,如果越界了,则行为是undefined)

注意上面标红线的地方,说如果指针指在数组范围内没事,如果越界了就是undefined,也就是说这事交给编译器实现了,编译器想咋干咋干,那怕你想把其优化掉也可以。在这里要重点说一下,C语言中的一个大恶魔—— Undefined! 这里都是“野兽出没”的地方,你一定要小心小心再小心

花絮:编译器的彩蛋

上面说了所谓的undefined行为就全权交给编译器实现,gcc在1.17版本下对于undefined的行为还玩了个彩蛋(参看Wikipedia)。

下面gcc 1.17版本下的遭遇undefined行为时,gcc在unix发行版下玩的彩蛋的源代码。我们可以看到,它会去尝试去执行一些游戏NetHack, Rogue 或是Emacs的 Towers of Hanoi,如果找不到,就输出一条NB的报错。

1
2
3
4
5
6
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");

正确检测整型溢出

在看过编译器的这些行为后,你应该会明白——“在整型溢出之前,一定要做检查,不然,就太晚了”。

我们来看一段代码:

1
2
3
4
5
void foo(int m, int n)
{
    size_t s = m + n;
    .......
}

上面这段代码有两个风险:1)有符号转无符号2)整型溢出。这两个情况在前面的那些示例中你都应该看到了。所以,你千万不要把任何检查的代码写在 s = m + n 这条语名后面,不然就太晚了。undefined行为就会出现了——用句纯正的英文表达就是——“Dragon is here”——你什么也控制不住了。(注意:有些初学者也许会以为size_t是无符号的,而根据优先级 m 和 n 会被提升到unsigned int。其实不是这样的,m 和 n 还是signed int,m + n 的结果也是signed int,然后再把这个结果转成unsigned int 赋值给s)

比如,下面的代码是错的:

1
2
3
4
5
6
7
void foo(int m, int n)
{
    size_t s = m + n;
    if ( m>0 && n>0 && (SIZE_MAX - m < n) ){
        //error handling...
    }
}

上面的代码中,大家要注意 (SIZE_MAX – m < n) 这个判断,为什么不用m + n > SIZE_MAX呢?因为,如果 m + n 溢出后,就被截断了,所以表达式恒真,也就检测不出来了。另外,这个表达式中,m和n分别会被提升为unsigned。

但是上面的代码是错的,因为:

1)检查的太晚了,if之前编译器的undefined行为就已经出来了(你不知道什么会发生)。

2)就像前面说的一样,(SIZE_MAX – m < n) 可能会被编译器优化掉。

3)另外,SIZE_MAX是size_t的最大值,size_t在64位系统下是64位的,严谨点应该用INT_MAX或是UINT_MAX

 所以,正确的代码应该是下面这样:

1
2
3
4
5
6
7
8
9
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;
}

在《苹果安全编码规范》(PDF)中,第28页的代码中:

如果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。让们来看看怎么判断:

对于加法,还好。

1
2
3
4
5
6
7
8
9
10
11
#include <limits.h>
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;
}

对于乘法,就会很复杂(下面的代码太夸张了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void func(signed int si_a, signed int si_b)
{
  signed int 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;
}

更多的防止在操作中整型溢出的安全代码可以参看《INT32-C. Ensure that operations on signed integers do not result in overflow》

其它

对于C++来说,你应该使用STL中的numeric_limits::max() 来检查溢出。

另外,微软的SafeInt类是一个可以帮你远理上面这些很tricky的类,下载地址:http://safeint.codeplex.com/

对于Java 来说,一种是用JDK 1.7中Math库下的safe打头的函数,如safeAdd()和safeMultiply(),另一种用更大尺寸的数据类型,最大可以到BigInteger。

可见,写一个安全的代码并不容易,尤其对于C/C++来说。对于黑客来说,他们只需要搜一下开源软件中代码有memcpy/strcpy之类的地方,然后看一看其周边的代码,是否可以通过用户的输入来影响,如果有的话,你就惨了。

参考

  • Basic Integer Overflow
  • OWASP:Integer overflow
  • C compilers may silently discard some wraparound checks
  • Apple Secure Coding Guide
  • Wikipedia: Undefined Behavior
  • INT32-C. Ensure that operations on signed integers do not result in overflow

最后, 不好意思,这篇文章可能罗嗦了一些,大家见谅。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/447917.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Thymeleaf 简介、教程

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Thymeleaf是一个适用于Web和独立环境的现代服务器端Java模板引擎。 Thymeleaf的主要目标是为您的开发工作流程带来优雅的自然模板 - 可…

如期而至,GCC 4.9.0正式版发布!

摘要&#xff1a;GCC是一套由GNU开发的编程语言编译器。近日&#xff0c;GCC 4.9.0发布&#xff0c;主要新特性包括&#xff1a;提升了C11和C14特性&#xff1b;诊断信息支持彩色显示&#xff1b;移除mudflap运行时检查器等。 如期而至&#xff0c;GCC 4.9.0发布&#xff0c;该…

《 追风筝的人 》:“ 为你,千千万万遍 ” ...

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 刚来研发中心的时候&#xff0c;在我的新位置上发现了一本书&#xff0c;问后得知是前同事留下的&#xff0c;已无主 。 我就收下了。一…

广州科目三考试 不得不看的十条提醒(图)

导读&#xff1a; 考试科目三时考试常会有点小紧张。经常会有考生因为紧张犯了些小错误而被pass掉。如何来应对呢&#xff1f;首先是放松心态&#xff0c;这点其实大家都明白&#xff0c;只是做不到。有人一到考试的时候就紧张&#xff0c;完全思维混乱&#xff0c;动作僵硬。建…

HDU 6706 huntian oy

题意 求以下式子的值&#xff0c;T组数据各个字母满足1 ≤ n , a , b ≤109 &#xff0c;a,b互质 思路&#xff1a; 卡常毒瘤题&#xff0c;出题人时限卡的非常紧&#xff0c;考场上推出来又T又WA 1 #include<bits/stdc.h>2 using namespace std;3 typedef long long ll;…

linux 查看空间(内存、磁盘、文件目录、分区)的几个命令

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. free free命令用于显示内存状态。 free指令会显示内存的使用情况&#xff0c;包括实体内存&#xff0c;虚拟的交换文件内存&#x…

广州学车科目三路考操作步骤要领

广州学车&#xff0c;科目三路考操作步骤是关键&#xff0c;许多朋友明明会开车&#xff0c;却因为一些步骤上的小疏忽而不得到不补考&#xff0c;今天总结出这个广州学车科目三路考操作步骤要领&#xff0c;希望对大家有帮助&#xff1a; 广州学车&#xff0c;科目三路考操作步…

如何和何时使用 CSS 的权重设置 !important (建议:永不使用!)

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 特别声明&#xff1a;此篇文章由David根据Louis Lazaris的英文文章原名《!important CSS Declarations: How and When to Use Them》进行…

广州科目三电子考需注意哪些问题?

广州驾考科目三从4月1日起开始试行电子评判与人工评判相结合的新制度&#xff0c;即电子路考&#xff0c;多数学员对新制度表示不适应&#xff0c;那么&#xff0c;科目三电子路考需要注意哪些问题? 从4月1日开始&#xff0c;科目三考试将试行计算机辅助与人工评判相结合的制度…

解决 VUE: 本地运行和服务器上运行样式不一致,run、build 运行时样式有出入

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 我的情况&#xff1a; 我遇到 2 种情况&#xff0c;一个是表格的分页样式有变化。另一个是导航菜单样式有变化。 2. 解决&#xff…

开发中的“软”与“硬”:高画质移动游戏开发之道

摘要&#xff1a;游戏的效果不仅与游戏引擎的渲染相关&#xff0c;与硬件优化也有千丝万缕的联系。一款基于芯片优化的移动游戏界面&#xff0c;甚至可以堪比视频游戏的视觉效果。高通半导体事业部资深经理刘晓光从软硬件两个层面分享了移动游戏开发之道。 在今年的Unity亚洲开…

解决 VUE: [Vue warn]: Do not use built-in or reserved HTML elements as component id: xx

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 新增一个组件&#xff0c;运行无问题&#xff0c;但F12报错&#xff1a; vue.esm.js?efeb:591 [Vue warn]: Do not use built-in o…

Linux系统重置和修改root密码

Linux系统经常会出现忘记root密码的情况&#xff0c;写下此随笔&#xff0c;以便记忆和学习。 一、重置root密码的步骤如下&#xff1a; 1.如果系统是开机状态&#xff0c;重启一下。进到下面这个界面按字母“e”键。 2.找到 linux16这一行&#xff0c;将下图红框中的内容修改为…

KETTLE 使用教程

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Kettle的建立数据库连接、使用kettle进行简单的全量对比插入更新&#xff1a;kettle会自动对比用户设置的对比字段&#xff0c;若目标表…

为什么你应该参与到开源项目中

试图描述开源并不是一件容易的事——很多图书作家&#xff0c;社区领袖和主持人对于开源社区的工作原理以及它是否对新人程序员有帮助持不同意见试图描述开源并不是一件容易的事——很多图书作家&#xff0c;社区领袖和主持人对于开源社区的工作原理以及它是否对新人程序员有帮…

历史上最知名的15位计算机科学家

基于维基百科上超过11,000位历史人物的数据&#xff0c;麻省理工学院媒体实验室创建出了一种名为“历史人气指数&#xff08;HPI&#xff09;”的参数。以下列出了15个历史上最知名的计算机科学家&#xff0c;我们来看一下他们的“HPI”分数。麻省理工学院媒体实验室推出了一个…

想要转人工智能,程序员该如何学习?(学习路线、知识体系)

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 对于程序员来说&#xff0c;码农之路并不是一帆风顺。对于每一个入行IT业的社会青年来说&#xff0c;谁不是抱着想要成为最高峰的技术大…

【转载】矩阵求导、几种重要的矩阵及常用的矩阵求导公式

版权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。本文链接&#xff1a;https://blog.csdn.net/daaikuaichuan/article/details/80620518 一、矩阵求导 一般来讲&#xff0c;我们约定x(x1,x2,...xN)…

数据结构与算法绪论

基本概念和术语 数据数据是信息的载体&#xff0c;是描述客观事物属性的数&#xff0c;字符以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。 数据元素数据元素是数据的基本单位&#xff0c;通常作为一个整体进行考虑和处理。一个数据元素可由若干个数据项组成…

Windows10 网络图标消失 连接不上网络 的解决方法

【背景】电脑win10的&#xff0c;下载一个软件重启之后网络图标消失&#xff0c;并且无法联网。 参照此解决方法&#xff1a; 原因&#xff1a; 【Windows Event Log】服务对应的注册表出现问题&#xff0c;导致无法正常启动&#xff0c;进而导致一些依赖于它的联网服务无法正常…