《编写高质量代码:改善c程序代码的125个建议》——建议2-6:防止无符号整数回绕

简介:

本节书摘来自华章计算机《编写高质量代码:改善c程序代码的125个建议》一书中的第1章,建议2-6,作者:马 伟 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

建议2-6:防止无符号整数回绕

C99第6.2.5节的第9条规定是:涉及无符号操作数的计算永远不会产生溢出,因为无法由最终的无符号整型表示的结果将会根据这种最终类型可以表示的最大值加1执行求模操作。也就是说,如果数值超过无符号整型数据的限定长度时就会发生回绕,即如果无符号整型变量的值超过了无符号整型的上限,就会返回0,然后又从0开始增大;如果无符号整型变量的值低于无符号整型的下限,那么就会到达无符号整型的上限,然后从上限开始减小。这就像一个人绕着跑道跑步一样,绕了一圈,又返回到出发点,因此称为回绕。
为了加深大家对无符号整数运算产生回绕的理解,我们继续来看代码清单1-9所示的一个简单例子。

代码清单1-9 无符号整数运算示例
#include <stdio.h>
int main(void)
{
    unsigned int a = 4294967295;
    unsigned int b = 2;
    unsigned int c=4;
    printf("%u\n", a + b);
    printf("%u\n", b -c);
    return 0;
}

在代码清单1-9中,我们定义了3个无符号整型变量a、b与c。其中将变量a的值初始化为4294967295(即在32位机器上存储为0xffffffff)。当程序执行语句“a+b”时,其结果超出了无符号整型的限定值(UINT_MAX :0xffffffff),于是便产生向下回绕,因此输出的结果为1(即0xffffffff+0x00000002=0x00000001);当程序执行语句“b-c”时,其结果为负数,于是便产生向上回绕,因此返回的结果为4294967294(即0x00000002-0x00000004=0xfffffffe)。具体运行结果如图1-8所示。


b860b3ba418f814bc77f1a4a848b162c30b92b17

从代码清单1-9中可以看出,无符号整数运算产生的回绕会给程序带来严重的后果,尤其是作为数组索引、指针运算、对象的长度或大小、循环计数器与内存分配函数的实参等的时候是绝对不允许产生回绕的。因此,针对无符号整数的运算,应该采用适当的方法来防止产生回绕。例如,代码清单1-10演示了如何简单地处理代码清单1-9中所产生的回绕。

代码清单1-10 代码清单1-9的解决方法
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    unsigned int a = 4294967295;
    unsigned int b = 2;
    unsigned int c=4;
    if(UINT_MAX-a<b)
    {
            /*处理错误条件*/
    }
    else
    {
            printf("%u\n", a + b);
    }
    if(b<c)
    {
            /*处理错误条件*/
    }
    else
    {
            printf("%u\n", b -c);
    }
    return 0;
}

在上面的代码中,通过一些条件对无符号操作数进行测试,从而避免了无符号操作数运算产生回绕。在实际的编程环境中,无符号整数的回绕很可能会导致缓冲区溢出,甚至导致攻击者可执行任意代码。例如,程序绕过代码中的大小判断部分的边界检测,可以导致缓冲区溢出,只要使用一般的技术就能够利用这个溢出程序。演示示例如代码清单1-11所示。

代码清单1-11 回绕导致的溢出示例
#include <stdio.h>
#include <string.h>  
int main(int argc, char *argv[])  
{      
    unsigned short s;
    int i;
    char buf[100];
    if(argc < 3)
    {
            return -1;
    }
    i = atoi(argv[1]);
    s = i;
    if(s >= 100)
    {           
            printf("拷贝字节数太大,请退出!\n");         
            return -1;
    }
    printf("s = %d\n", s);
    memcpy(buf, argv[2], i);
    buf[i] = '\0';
    printf("成功拷贝%d个字节\n", i);  
    printf("buf=%s\n", buf);
    return 0;
}

在代码清单1-11中,程序需要将argv[2]的内容复制到buf中,并由argv[1]指定复制的字节数。这里需要特别注意的语句是“if(s >= 100)”,利用该语句进行了相对严格的大小检查:如果argv[1]的值大于等于buf数组的大小(100),则不进行复制。
运行代码清单1-11,当我们执行命令“1-11 4 mawei”时,程序运行正常,并成功地复制了字符串“mawe”到buf中,运行结果如图1-9所示。


<a href=https://yqfile.alicdn.com/552e104c3e2fcd0932a749a65af2a260b9e41af9.png" >

当我们执行命令“1-11 200 mawei”时,程序同样运行正常,运行结果如图1-10所示。


565005574f0bac3469f42450520070fea3b206be

可当我们执行命令“1-11 65536 mawei”时,程序却意外地绕过了大小检查语句“if(s >= 100)”来执行相关的操作。原因很简单,程序从命令行参数中得到一个整数值并存放在整型变量i中,然后这个值被赋予了unsigned short类型的整数s,由于s在内存中是用16位进行存储的,而16位能够存储的最大十进制数是65535(即unsigned short存储的范围是0~65535),如果这个值不在unsigned short类型的存储范围内(0~65535),就会产生回绕。因此,当我们输入65536时,系统将会转换为0,从而绕过大小检查语句“if(s >= 100)”来执行余下的操作。可是这里我们将buf数组的大小初始化为100,所以在执行语句“memcpy(buf, argv[2], i)”时,程序就会产生异常而导致崩溃。其运行结果如图1-11与图1-12所示。


<a href=https://yqfile.alicdn.com/6fc4b345ebeb58a2c51cb73113f507006e541d12.png" >

其实,这类Bug很常见,而且很容易被攻击,这都是由于无符号整数发生回绕导致的。由于存在回绕,当一个有符号整数被解释成一个无符号整数时,它可能变得很大。比如,-1被当成无符号数时将会是十进制的4294967295,它是32位整数的最大值。如果我们加入的这个值被用作memcpy的参数,memcpy就会试图复制4GB数据,很明显这可能导致错误或破坏堆栈。
除此之外,无符号整数的回绕最可能被利用的情况之一就是利用计算结果来决定将要分配的缓冲区的大小。通常情况下,在程序需要为一组对象分配内存空间时,会将对象的个数乘以单个对象大小,然后用所乘结果来作为参数,从而调用malloc()或calloc()函数来分配内存。这时候,只要我们能够控制对象的个数或单个对象的大小,就有可能让程序分配错误大小的缓冲区。演示示例如代码清单1-12所示。
代码清单1-12 回绕导致的错误分配缓冲区示例
#include <stdio.h>  
#include <stdlib.h>  
int* copyarray(int *arr, int len);  
int main(int argc, char *argv[])
{      
    int arr[] = {1,2,3,4,5};          
    copyarray(arr,atoi(argv[1]));      
    return 0;  
}  
int* copyarray(int *arr, int len)  
{       
    int i=0;
    int *newarray = (int *)malloc(len*sizeof(int));          
    if(newarray == NULL)  
    {         
            /*处理newarray == NULL的情况*/      
    }  
    printf("为newarray成功分配%d字节内存\n",len*sizeof(int));
    printf("循环运行次数:%d(0x%x)\n",len,len);  
    for(i = 0; i < len; i++)  
    {         
            newarray[i] = arr[i];   
    } 
    return newarray;   
}

在代码清单1-12中,函数“int copyarray(int arr, int len)”需要将arr的内容复制到newarray中,对象的个数由len参数来指定。其中,程序使用了对象的个数乘以单个对象大小的乘积来作为malloc() 函数的参数,从而对newarray进行内存分配,即内参分配语句为“int newarray = (int )malloc(len*sizeof(int))”。
运行代码清单1-12,当我们执行命令“1-12 8”时,程序运行正常,并成功地为newarray分配了内存,并将arr的内容复制到newarray中,运行结果如图1-13所示。


<a href=https://yqfile.alicdn.com/62bb161a470a7e6efd9e701647595c6cb1f7e897.png" >

这样看来,程序貌似没有任何问题。但是当我们执行命令“1-12 1073741824”时,问题就出现了,抛出异常“Unhandled exception at 0x004010d2 in 1-12.exe: 0xC0000005: Access violation writing location 0x00387000。”。运行结果如图1-14所示。


<a href=https://yqfile.alicdn.com/ef6c130061868559815734e313890bde3cbae5f7.png" >

是什么原因导致这样的结果呢?
其实很简单,是因为函数“int copyarray(int arr, int len)”没有检查参数len而导致运算回绕失败。在通过语句“int newarray =(int ) malloc(lensizeof(int))”给newarray分配内存时,这里将参数设置为1073741824(十六进制是0x40000000),而“sizeof(int)”的返回结果为4(十六进制是0x4)。当运算表达式“0x400000000x4”时,就发生了无符号整数运算回绕,所得的结果为0x0(即0x40000000*0x4=0x0)。因此,为newarray分配的内存为0。
除此之外,在通过语句“int newarray =(int ) malloc(len*sizeof(int))”给newarray分配内存时,由于参数len 的原因而造成运算回绕,所以我们可以利用它来分配一个任意长度的缓冲区。如上面将len参数设置为1073741824,就可能出现在没有为newarray分配内存的情况下,却向其中复制了数组元素,而且循环的次数还非常多,严重时会造成系统崩溃。当然,你还可以通过选择合适的值赋给len参数以使得循环反复执行导致缓冲区溢出。同时,还可以通过覆盖malloc的控制结构来执行任意恶意代码,从而实施对堆溢出的攻击。
在本节的最后,还需要说明的是,并不是每种运算符号都会令无符号操作数运算产生回绕,表1-5给出了可能会导致回绕的操作符。


<a href=https://yqfile.alicdn.com/90187ea6f2e335797307546fc85eb1dc001613c5.png" >
相关文章