嵌入式C语言中整形溢出问题分析

news2024/11/15 20:34:59

整型溢出有点老生常谈了,bla, bla, bla… 但似乎没有引起多少人的重视。整型溢出会有可能导致缓冲区溢出,缓冲区溢出会导致各种黑客攻击。

今天分享一篇文章,希望大家都了解一下整型溢出,编译器的行为,以及如何防范,以写出更安全的代码。

什么是整型溢出

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

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

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

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

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

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

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

另外,千万别以为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;
}

上面这段代码可能是很多程序员都喜欢写的代码(我在很多代码里看到过多次),其中的MAX_LEN 可能会是个比较大的整型。

比如32767,我们知道short是16bits,取值范围是-32768 到 32767 之间。但是,上面的while循环代码有可能会造成整型溢出,而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缓冲区后面的数据被重写。

示例三:分配内存

关于整数溢出导致堆溢出的很典型的例子是,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);
}

上面这个代码中,nresp是size_t类型(size_t一般就是unsigned int/long int),这个示例是一个解数据包的示例。一般来说,数据包中都会有一个len,然后后面是data。

如果我们精心准备一个len,比如:1073741825(在32位系统上,指针占4个字节,unsigned int的最大值是0xffffffff,我们只要提供0xffffffff/4 的值——0x40000000,这里我们设置了0x4000000 + 1), nresp就会读到这个值,然后nresp * sizeof(char * )就成了 1073741825 * 4,于是溢出,结果成为了 0x100000004,然后求模,得到4。于是,malloc(4),于是后面的for循环1073741825 次,就可以干坏环事了。

经过0x40000001的循环,用户的数据早已覆盖了xmalloc原先分配的4字节的空间以及后面的数据,包括程序代码,函数指针,于是就可以改写程序逻辑。关于更多的东西,你可以看一下这篇文章《Survey of Protections from Buffer-Overflow Attacks》)。

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

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=0x104, len2 = 0xfffffffc 的情况。

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

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

关于编译器的行为

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

编译器优化

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

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

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语句块没有任何的汇编代码——直接被编译器和谐掉了):

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语句块就无法被优化了。如下所示:

if ((uintptr_t)data + len < (uintptr_t)data){
    ... ...
}

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

图片

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

正确检测整型溢出

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

我们来看一段代码:

 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)

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

 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

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

 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的时候才有效。

不管怎么说,《苹果安全编码规范》绝对值得你去读一读。

二分取中搜索算法中的溢出

我们再来看一个二分取中搜索算法(binary search),大多数人都会写成下面这个样子:

int binary_search(int a[], int len, int key)
{
    int low = 0; 
    int high = len - 1; 

    while ( low<=high ) {
        int mid = (low + high)/2;
        if (a[mid] == key) {
            return mid;
        }
        if (key < a[mid]) {
            high = mid - 1;
        }else{
            low = mid + 1;
        }
    }
    return -1;
}

上面这个代码中,你可能会有这样的想法:

1) 我们应该用size_t来做len, low, high, mid这些变量的类型。没错,应该是这样的。但是如果这样,你要小心第四行 int high = len -1; 如果len为0,那么就“high大发了”。

2) 无论你用不用size_t。我们在计算mid = (low+high)/2; 的时候,(low + high) 都可以溢出。正确的写法应该是:

int mid = low + (high - low)/2;

上溢出和下溢出的检查

前面的代码只判断了正数的上溢出overflow,没有判断负数的下溢出underflow。让们来看看怎么判断:

对于加法,还好。

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

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

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

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

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

相关文章

【安全】 Java 过滤器 解决存储型xss攻击问题

文章目录 XSS简介什么是XSS?分类反射型存储型 XSS(cross site script)跨站脚本攻击攻击场景解决方案 XSS简介 跨站脚本( cross site script )为了避免与样式css(Cascading Style Sheets层叠样式表)混淆&#xff0c;所以简称为XSS。 XSS是一种经常出现在web应用中的计算机安全…

mistyR官网教程 空转spatial

Modeling spatially resolved omics with mistyR • mistyR (saezlab.github.io) mistyR and data formats • mistyR (saezlab.github.io) Heidelberg University and Heidelberg University Hospital, Heidelberg, Germany Jožef Stefan Institute, Ljubljana, Sloveniajov…

阿里5年经验之谈 —— 浅谈自动化测试方法!

导读 在当今快节奏的软件开发环境中&#xff0c;高质量的代码交付至关重要。而针对经过多次迭代&#xff0c;主要功能趋向稳定的产品&#xff0c;大量传统的重复性手动测试方法已经无法满足高效、快速的需求。为了提高测试效率保证产品质量&#xff0c;本文通过产品实践应用&a…

Python接口自动化测试之token参数关联

前言 在做自动化接口测试时&#xff0c;有时候会遇到token的动态关联&#xff0c;例如查询余额接口&#xff0c;需要关联登录接口的token动态值&#xff0c;如何利用python脚本进行接口token关联呢?今天我们爱学习一下吧&#xff01; 一&#xff1a;获取登录接口返回的token…

研发项目管理系统对比:找到最适合的高效工具

研发部门是企业非常重要的部门&#xff0c;代表着企业未来能否在市场上拥有优秀的技术&#xff0c;站稳市场份额。很多企业的研发方式往往是瀑布式的&#xff0c;所以项目的阶段规划&#xff0c;然后每个阶段的需求分配给开发人员&#xff0c;可以随时查看每个需求的开发进度和…

Redis学习5——有序集合Zset数据类型的操作

有序集合Zset 常用命令 数据结构 跳跃表 跳跃表

移远通信EM060K系列LTE-A Cat 6模组完成全球认证覆盖

近日&#xff0c;移远通信LTE-A Cat 6模组EM060K系列顺利完成全球认证覆盖&#xff0c;将以卓越的性能和品质助力海内外客户终端大规模部署&#xff0c;为其提供畅快的高速网络连接。同时&#xff0c;凭借着有竞争力的性能和成本优势&#xff0c;EM060K系列将加速释放海外固定无…

[架构之路-235]:目标系统 - 纵向分层 - 数据库 - 数据库系统基础与概述:数据库定义、核心概念、系统组成

目录 一、核心概念 1.1 什么是数据与信息 1.2 数据与数据库的关系 1.3 什么是数据库 1.4 数据库中的数据的特点 1.5 数据库与数据结构的关系 二、数据库系统 2.1 什么是数据库管理系统 2.2 什么是数据库系统 2.3 数据库相关的人员 2.4 数据库的主要功能 2.5 Excel表…

Vuex的基础使用存值及异步

目录 一、概述 ( 1 ) 讲述 ( 2 ) 概念 ( 3 ) 作用 二、取值 1. 安装 2. 菜单栏 3. 模块 4. 引用 三、改值 四、异步&后台请求 带来的获取 一、概述 ( 1 ) 讲述 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的…

【Linux初阶】多线程1 | 页表的索引作用,线程基础(优缺点、异常、用途),线程VS进程,线程控制,C++多线程引入

文章目录 ☀️一、深入理解页表☀️二、Linux线程概念&#x1f33b;1.什么是线程&#xff08;重点&#xff09;⚡&#xff08;1&#xff09;线程的概念⚡&#xff08;2&#xff09;线程库初识 &#x1f33b;2.线程的优点&#x1f33b;3.线程的缺点&#x1f33b;4.线程异常&…

为什么设置静态代理IP后无法正常上网,怎么解决?

静态代理IP是一个固定的IP地址&#xff0c;因为其出色的稳定性和安全性而得到广泛应用&#xff0c;常用于一些对网络质量要求高、需要长期稳定和持续可靠连接的业务。设置静态代理IP后无法上网是用户常见的网络问题&#xff0c;通常有多种原因&#xff1a; 1. 静态代理IP不可用…

【Flutter学习】AppBar

App Bar 可以视为页面的标题栏&#xff0c;在 Flutter 中用AppBar组件实现。 一个简单的AppBar实现代码如下&#xff1a; import package:flutter/material.dart;void main() {runApp(const AppBarTest()); }class AppBarTest extends StatelessWidget {const AppBarTest({Key…

【AGC】云托管新建站点时间过长的问题排查方法

【问题描述】 开发者按照指导文档使用云托管服务&#xff0c;已经申请了域名&#xff0c;在创建站点时页面显示证书配置最长需要12小时&#xff0c;然而&#xff0c;在等了两天后依然是激活中的状态&#xff0c;没有如期上线。 ​ 【解决方案】 卡在上线中的状态有以下几个原…

F. Vasilije Loves Number Theory

Problem - F - Codeforces 思路&#xff1a;分析一下题意&#xff0c;对于第一种操作来说&#xff0c;每次乘以x&#xff0c;那么nn*x&#xff0c;然后问是否存在一个a使得gcd(n,a)1并且n*a的约数个数等于n&#xff0c;有最大公约数等于1我们能够知道其实这两个数是互质的&…

圆满完成重保网络防护行动,持安科技获西南兵工致信感谢

近日&#xff0c;因积极协助西南兵工有限责任公司开展重保网络防护行动中&#xff0c;提供强大零信任网络安全产品和专业技术力量配合&#xff0c;持安科技收到了来自西南兵工有限责任公司的致信感谢。 持安为西南兵工提供的应用层零信任解决方案&#xff0c;是持安科充分吸取了…

Hibiki Run 市场火爆,“Listen to Earn”赛道的现象级应用?

在 9 月 18 日&#xff0c;以“Listen to Earn”为特点的 Web3 数字音乐类项目 Hibiki Run&#xff0c;在包括 DAOStarter、Spores Network、BitMart 在内的 三个平台&#xff0c;开启了实用通证 $HUT 的 IDO / IEO 活动。据悉&#xff0c;在本轮认购开启后的短时间内所有平台均…

Python 图形化界面基础篇:更改字体、颜色和样式

Python 图形化界面基础篇&#xff1a;更改字体、颜色和样式 引言 Tkinter 库简介步骤1&#xff1a;导入 Tkinter 模块步骤2&#xff1a;创建 Tkinter 窗口步骤3&#xff1a;创建文本标签步骤4&#xff1a;更改字体步骤5&#xff1a;更改颜色步骤6&#xff1a;更改样式 完整示例…

数字图像处理实验记录一(图像基本灰度变换)

文章目录 基础知识图像是什么样的&#xff1f;1&#xff0c;空间分辨率&#xff0c;灰度分辨率2&#xff0c;灰度图和彩色图的区别3&#xff0c;什么是灰度直方图&#xff1f; 实验要求1&#xff0c;按照灰度变换曲线对图像进行灰度变换2&#xff0c;读入一幅图像&#xff0c;分…

使用python查找指定文件夹下所有xml文件中带有指定字符的xml文件

文件夹目录如下&#xff08;需要递归删除文件夹下的.DS_Store文件&#xff09;&#xff1a; labels文件夹下面是xml文件&#xff1a; import os import os.pathpath "name/labels" files os.listdir(path) # 得到文件夹下所有文件名称 s []for xmlFile in files:…

成为领导心腹:新入行的测试人员,如何快速提升自己的影响力?

作为一名新入行的测试人员&#xff0c;如何提高自己在工作中的影响力呢&#xff1f;可能有人会问了&#xff1a;“测试人员不是只要安分守己的做好自己的测试工作不就行了吗&#xff1f;又不是当管理者&#xff0c;为什么要提高影响力呢&#xff1f;”说实话&#xff0c;我刚入…