整数安全实验
实验目的
了解整数的定义
了解整数安全背景
掌握安全的整数操作
实验原理
1.整数
整数由包括0的自然数(0,1,2,3,…)和非零自然数的负数(-1,-2,-3,…)构成。从实数的一个子集来看,整数是能够用不带分数或小数部分写出的数,并且落在集合{…-2,-1,0,1,2…}之中。例如,65、7和-756都是整数,而1.6不是整数。
2.整数安全
整数已日益成为C程序中被低估的漏洞来源。这主要是因为不同于软件工程中的其他边界情形,整数的边界溢出问题通常被有意地忽略了。大部分从高等院校出来的程序员都明白整数是有定长限制的,可他们要么以为整数的表示范围已经够用,要么以为若对每个算术操作的结果都进行检测的话,代价太大,无法接受。因此,商业软件中的整数溢出情形基本上完全没有得到检测。
当开发对安全要求严格的系统时,如果一个程序仅对于一个给定范围内的期望输入运行良好,那我们就不能奢望当攻击者找到可以由其产生非正常结果的输入值时,该程序仍能正常工作。当然,整数的数字化表示并非无瑕。当程序对一个整数求出了一个非期望中的值(也就是说,结果并不是你用纸笔演算时所得到的),并进而将其用于数组索引、大小或者是循环计数器时,软件漏洞就出现了。
由于在大多数C软件系统的开发中都没有进行系统的整数范围检查,因此,因整数导致的安全缺陷问题肯定存在,其中一些缺陷很可能会成为软件系统的漏洞。
3.整数数据类型
整数类型提供了整数数学集合的一个有限子集的模型。一个具有整数类型的对象的值是附着在这个对象上的数学值。一个具有整数类型的对象的值的表示方式(representation)是在为该对象分配的存储空间中该值的特定位模式编码。
C提供了丰富的整数类型(带有用关键字指定的名称),并且允许实现定义其他扩展整数类型(带有非关键字的保留标识名称),两者都可以包含在标准头文件的类型定义中。
标准的整数类型包括所有从早期的Kernighan和Ritchie C(K&R C) 就已经存在的广为人知的那些整数类型台这些整数类型允许与底层机器的架构紧密对应。扩展整数类型是在C标准中定义的带有定长约束的整数类型。
在C中每个整数类型的对象需要一个固定的存储字节数。<limits.h>头文件中的常量表达式CHAR_BIT,给出了一个字节中的位数,它必须至少为8,但可能会更大,这取决于具体的实现。除unsigned char型外,不是所有的位都必须用来表示值,未使用的位被称为填充(padding)。允许填充,以便实现可适应硬件的怪癖,如跳过在多字表示中间的一个符号位。
用于表示一个给定类型的值的非填充位数被称为该类型的宽度(width) ,我们用w(type)来表示,或有时只用N表示。一个整数类型的精度(precision)指它用来表示值的位数,不包插任何标志位和填充位。
例如,在诸如x86-32的架构中没有用填充位,有符号类型的精度为w(type)-l,而对于无符号的类型,精度等于w(type)。
还有其他表示整数的方法,如任意精度或大数(bignum) 算术。这些方法在需要时动态分配存储空间,以适应正确表示值所需的宽度。然而,C标准不指定任何此类模式,并且,不像C++,内置的运算符,如"+"和"/"不能被重载和用于含有这些抽象数据类型的表达式。比如公共密钥加密的应用程序通常使用这样的模式来规避C的固定大小限制。
标准的整数类型由一组有符号的整数类型和相应的无符号整数类型组成。
实验步骤
步骤1:确保无符号整数运算时不会出现反转
反转是指无法用无符号整数表示的运算结果将会根据该类型可以表示的最大值加1执行求模操作。将运算结果用于以下之一的用途,应防止反转:
作为数组索引
指针运算
作为对象的长度或者大小
作为数组的边界
作为内存分配函数的实参
1.1 错误示例:下列代码可能导致相加操作产生无符号数反转现象。
INT32 NoCompliant(UINT32 ui1, UINT32 ui2, UINT32 * ret)
{
if( NULL == ret )
{
return ERROR;
}
*ret = ui1 + ui2;
/*上面的代码可能会导致ui1加ui2产生无符号数反转现象,譬如ui1 = UINT_MAX且ui2 = 2;这可能会导致后面的内存分配数量不足或者产生易被利用的潜在风险;*/
return (OK);
}
推荐做法:
INT32 Compliant(UINT32 ui1, UINT32 ui2, UINT32 * ret)
{
if( NULL == ret )
{
return ERROR;
}
if((UINT_MAX - ui1) < ui2) //【修改】确保无符号整数运算时不会出现反转
{
return ERROR;
}
else
{
*ret = ui1+ ui2;
}
return OK;
}
步骤2:确保有符号整数运算时不会出现溢出
2.1 整数溢出是是一种未定义的行为,意味着编译器在处理有符号整数溢出时具有很多选择。将运算结果用于以下之一的用途,应防止溢出:
作为数组索引
指针运算
作为对象的长度或者大小
作为数组的边界
作为内存分配函数的实参。
错误示例:下列代码中两个有符号整数相乘可能会产生溢出。
INT32 NoCompliant(INT32 si1, INT32 si2, INT32 *ret)
{
if ( NULL == ret )
{
return ERROR;
}
*ret = si1 * si2;
/* 上面的代码可能会产生两个有符号整数相乘可能会产生溢出,譬如si1 = INT_MAX且si2 非0;*/
return OK;
}
推荐做法:
INT32 Compliant(INT32 si1, INT32 si2, INT32 *ret)
{
if ( NULL == ret )
{
return ERROR;
}
INT64 tmp = (INT64)si1 *(INT64)si2; /*【修改】确保有符号整数运算时不会出现溢出 */
if((INT_MAX < tmp) || (INT_MIN > tmp))
{
return ERROR;
}
*ret = si1 * si2;
return OK;
}
步骤3:确保整型转换时不会出现截断错误
3.1 将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失,甚至可能引发安全问题。特别是将运算结果用于以下用途:
作为数组索引
指针运算
作为对象的长度或者大小
作为数组的边界(如作为循环计数器)
错误示例:数据类型强制转化导致数据被截断。
INT32 NoCompliant(UINT32 ui, INT8 *ret)
{
if( NULL == ret )
{
return ERROR;
}
*ret = (INT8)ui;
/*上面的代码会导致数据被截断,譬如ui = UINT_MAX场景下*/
return (OK);
}
推荐做法:
INT32 Compliant(UINT32 ui, INT8 *ret)
{
if(NULL == ret)
{
return ERROR;
}
if(SCHAR_MAX >= ui) //【修改】确保整型转换时不会出现截断
{
*ret = (INT8)ui;
}
else
{
return ERROR;
}
return OK;
}
步骤4:确保整型转换时不会出现符号错误
4.1 有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。
带符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。
错误示例:符号错误绕过长度检查
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]); //【错误】atoi返回值可能为负数
if (length < BUF_SIZE) // len为负数,长度检查无效
{
memcpy(buf, argv[2], length); /* 带符号的len被转换为size_t类型的无符号整数,负值被解释为一个极大的正整数。memcpy()调用时引发buf缓冲区溢出*/
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
}
推荐做法1:将length声明为无符号整型,这样符号错误后产生的极大正整数可以在与BUF_SIZE比较时检查出来;
推荐做法2:在长度检查时,除了要保证长度小于BUF_SIZE,还要保证长度大于0。
步骤5:把整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值
5.1 若一个整型表达式与一个很大长度的整数类型进行比较或者赋值为这种类型的变量,需要对该整型表达式的其中一个操作数类型显示转换为更大长度的整数类型,用这种更大的进行求值。这里所说的更大整数类型是相对整型表达式的操作数类型而言,譬如整型表达式的操作数类型是unsigned int ,则该规则所说的更大类型是指 unsigned long long。
错误示例:数据类型不一致导致整型表达式赋值错误。
void *NoCompliant(UINT32 blockNum)
{
if(0 == blockNum )
{
return NULL;
}
UINT64 alloc = blockNum * 16;
/*blockNum为32位的无符号数,两个32位的数相乘仍为32位的数,这会导致
alloc <= UNIT_MAX始终为TRUE.*/
return (alloc <= UINT_MAX)?malloc(blockNum*16):NULL;
}
/*...申请的内存使用后free...*/
推荐做法:
void *Compliant(UINT32 blockNum)
{
if(0 == blockNum )
{
return NULL;
}
UINT64 alloc = (UINT64)blockNum * 16; /*【修改】确保整型表达式转换时不出现数值错误 */
return (alloc <= UINT_MAX)?malloc(blockNum*16):NULL;
}
/*...申请的内存使用后free...*/
步骤6:避免对有符号整数进行位操作符运算
6.1 位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数,因为有符号整数上的有些位操作的结果是由编译器所决定的,可能会出现出乎意料的行为或编译器定义的行为。
错误示例:对有符号数作位操作运算。
#define BUF_LEN (4)
INT32 NoCompliant(void)
{
INT32 ret = 0;
INT32 i = 0x8000000; //【不推荐】避免使用有符号数作位操作符运算
INT8 buf[BUF_LEN];
memset(buf,0,BUF_LEN);
ret = snprintf(buf, BUF_LEN, "%u", i >> 24);
/* i >> 24的结果是0xFFFFFFF8(10进制4294967288),导致转换为
一个字符串时,长度超过BUF_LEN,无法存储在buf中,因此被snprintf截
断;若是采用sprintf,这个例子就会产生缓冲区溢出*/
if(-1 == ret || BUF_LEN <= ret)
{
return ERROR;
}
return OK;
}
推荐做法:
#define BUF_LEN (4)
INT32 Compliant(void)
{
INT32 ret = 0;
UINT32 i = 0x8000000;//【修改】使用无符号代替有符号数作位操作符运算
INT8 buf[BUF_LEN];
memset(buf, 0, BUF_LEN);
ret = snprintf(buf, 4, "%u", i >> 24);
if(-1 == ret || BUF_LEN <= ret)
{
return ERROR;
}
return OK;
}