CC语言安全编程规范V精编版

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

C C语言安全编程规范
V
公司内部编号:(GOOD-TMMT-MMUT-UUPTY-UUYY-DTTI-
华为技术有限公司内部技术规范
DKBA 6914-2013.05 C&C++语言安全编程规范
2013年05月07日发布 2013
年05月07日实施
华为技术有限公司
Huawei Technologies Co., Ltd.
版权所有侵权必究
修订声明
本规范拟制与解释部门:
网络安全技术能力中心
本规范的相关系列规范或文件:
《Java语言安全编程规范》《Web应用安全开发规范》
相关国际规范或文件一致性:

替代或作废的其它规范或文件:

相关规范或文件的相互关系:
本规范作为《C语言编程规范》和《C++语言编程规范》安全性要求的补充和扩展。

目录
C&C++语言安全编程规范
0规范制定说明
0.1前言
随着公司业务发展,越来越多的产品被公众、互联网所熟知,并成为安全研究组织的研究对象、黑客的漏洞挖掘目标,容易引起安全问题。

安全问题影响的不只是单个产品,甚至有可能影响到公司整体声誉。

产品安全涉及需求、设计、实现、部署多个环节,实现的安全是产品安全的重要一环。

为了帮助产品开发团队编写安全的代码,减少甚至规避由于编码错误引入安全风险,特制定本规范。

《C&C++语言安全编程规范》参考业界安全编码的研究成果,并结合产品编码实践的经验总结,针对C/C++语言编程中的字符串操作、整数操作、内存管理、文件操作、STL库使用等方面,描述可能导致安全漏洞或潜在风险的常见错误。

以期减少缓冲区溢出、整数溢出、格式化字符串攻击、命令注入攻击、目录遍历等典型安全问题。

0.2使用对象
本规范的读者及使用对象主要为使用C和C++语言的开发人员、测试人员等。

0.3适用范围
本规范适合于公司基于C或C++语言开发的产品。

0.4术语定义
原则: 编程时必须遵守的指导思想。

规则:编程时必须遵守的约定。

建议:编程时必须加以考虑的约定。

说明:对此原则/规则/建议进行必要的解释。

错误示例:对此原则/规则/建议从反面给出例子。

推荐做法:对此原则/规则/建议从正面给出例子。

延伸阅读材料:建议进一步阅读的参考材料。

1通用原则
原则1.1:对外部输入进行校验
说明:对于外部输入(包括用户输入、外部接口输入、配置文件、网络数据和环境变量等)可能用于以下场景的情况下,需要检验入参的合法性:
输入会改变系统状态
输入作为循环条件
输入作为数组下标
输入作为内存分配的尺寸参数
输入作为格式化字符串
输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
输入影响代码逻辑
这些情况下如果不对用户数据作合法性验证,很可能导致DoS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。

对外部输入验证常见有如下几种方式:
(1)校验输入数据长度:
如果输入数据是字符串,通过校验输入数据的长度可以加大攻击者实施攻击的难度,从而防止缓冲区溢出、恶意代码注入等漏洞。

(2)校验输入数据的范围:
如果输入数据是数值,必须校验数值的范围是否正确,是否合法、在有效值域内,例如在涉及到内存分配、数组操作、循环条件、计算等安全操作时,若没有进行输入数值有效值域的校验,则可能会造成内存分配失败、数组越界、循环异常、计算错误等问题,这可能会被攻击者利用并进行进一步的攻击。

(3)输入验证前,对数据进行归一化处理以防止字符转义绕过校验:通过对输入数据进行归一化处理(规范化,按照常用字符进行编码),彻底去除元字符,可以防止字符转义绕过相应的校验而引起的安全漏洞。

(4)输入校验应当采用“白名单”形式:
“黑名单”和“白名单”是进行数据净化的两种途径。

“黑名单”尝试排斥无效的输入,而“白名单”则通过定义一个可接受的字符列表,并移除任何不接受的字符来仅仅接受有效的输入。

有效输入值列表通常是一个可预知的、定义良好的集合,并且其大小易于管理。

“白名单”的好处在于,程序员可以确定一个字符串中仅仅包含他认为安全的字符。

“白名单”比“黑名单”更受推荐的原因是,程序员不必花力气去捕捉所有不可接受的字符,只需确保识别了可接受的字符就可以了。

这样一来,程序员就不用绞尽脑汁去考虑攻击者可能尝试哪些字符来绕过检查。

原则1.2:禁止在日志中保存口令、密钥
说明:在日志中不能保存口令和密钥,其中的口令包括明文口令和密文口令。

对于敏感信息建议采取以下方法,
不打印在日志中;
若因为特殊原因必须要打印日志,则用“*”代替。

原则1.3:及时清除存储在可复用资源中的敏感信息
说明:存储在可复用资源中的敏感信息如果没有正确的清除则很有可能被低权限用户或者攻击者所获取和利用。

因此敏感信息在可复用资源中保存应该遵循存储时间最短原则。

可复用资源包括以下几个方面:堆(heap)
栈(stack)
数据段(data segment)
数据库的映射缓存
存储口令、密钥的变量使用完后必须显式覆盖或清空。

原则1.4:正确使用经过验证的安全的标准加密算法
说明:禁用私有算法或者弱加密算法(如DES,SHA1等),应该使用经过验证的、安全的、公开的加密算法。

加密算法分为对称加密算法和非对称加密算法。

推荐使用的常用对称加密算法有:
AES
推荐使用的常用非对称算法有:
RSA
数字签名算法(DSA)
此外还有验证消息完整性的安全哈希算法(SHA256)等。

基于哈希算法
的口令安全存储必须加入盐值(salt)。

密钥长度符合最低安全要求:
AES: 128位
RSA: 2048位
DSA: 1024位
SHA: 256位
原则1.5:遵循最小权限原则
说明:程序在运行时可能需要不同的权限,但对于某一种权限不需要始
终保留。

例如,一个网络程序可能需要超级用户权限来捕获原始网络数
据包,但是在执行数据报分析等其它任务时,则可能不需要相同的权
限。

因此程序在运行时只分配能完成其任务的最小权限。

过高的权限可
能会被攻击者利用并进行进一步的攻击。

(1)撤销权限时应遵循正确的撤销顺序:
在涉及到set-user-ID和set-group-ID程序中,当有效的用户ID(user ID)和组ID(group ID)与真实的用户不同时,不但要撤销用户层面(user level)的权限而且要撤销组层面(group level)的权限。

在进行这样的操作时,要保证撤销顺序的正确性。

权限撤销顺序的不正确操作,可能会被攻击者获得过高的权限而进行进一步的攻击。

(2)完成权限撤销操作后,应确保权限撤销成功:
不同平台下所谓的“适当的权限”的意义是不相同的。

例如在Solaris 中,setuid()的适当的权限指的是PRIV_PROC_SETID权限在进程的有效权限集中。

在BSD中意味着有效地用户ID(EUID)为0或者
uid=geteuid()。

而在Linux中,则是指进程具有CAP_SETUID能力并且当EUID不等于0、真正的用户ID(RUID)或者已保存的set-user
ID(SSUID)中任何一个时,setuid(geteuid())是失败的。

原则1.6:删除或修改没有效果的代码
说明:删除或修改一些即使执行后、也不会有任何效果的代码。

一些存在的代码(声明或表达式),即使它被执行后,也不会对代码的结果或数据的状态产生任何的影响,或者产生不是所预期的效果,这样的代码在可能是由于编码错误引起的,往往隐藏着逻辑上的错误。

原则1.7:删除或修改没有使用到的变量或值
说明:删除或修改没有使用到的变量或值。

一些变量或值存在于代码里,但并没有被使用到,这可能隐含着逻辑上的错误,需要被识别出来,删除这类语句或做相应的修改。

2字符串操作安全
规则2.1:确保有足够的空间存储字符串的字符数据和’\0’结束符
说明:在分配内存或者在执行字符串复制操作时,除了要保证足够的空间可以容纳字符数据,还要预留’\0’结束符的空间,否则会造成缓冲区溢出。

错误示例1:拷贝字符串时,源字符串长度可能大于目标数组空间。

void main(int argc, char *argv[])
{
char dst[128];
if( argc > 1 )
{
strcpy(dst, argv[1]); // 源字符串长度可能大于目标数组空间,造成缓冲区溢出
}
/*…*/
}
推荐做法:根据源字符串长度来为目标字符串分配空间。

void main(int argc, char *argv[])
{
char *dst = NULL;
if( argc > 1 )
{
dst = (char *)malloc(strlen(argv[1]) + 1); /* 【修改】确保字符串空间足够容纳argv[1] */
if( dst != NULL )
{
strncpy(dst, argv[1], strlen(argv[1]));
dst[strlen(argv[1])] = ’\0’; //【修改】dst 以’\0’结尾
}
}
/*...dst使用后free...*/
}
错误示例2:典型的差一错误,未考虑’\0’结束符写入数组的位置,造成缓冲区溢出和内存改写。

void NoCompliant()
{
char dst[ARRAY_SIZE + 1];
char src[ARRAY_SIZE + 1];
unsigned int i = 0;
memset(src, '@', sizeof(dst));
for(i=0; src[i] != ’\0’ && (i < sizeof(dst)); ++i ) dst[i] = src[i];
dst[i] = ’\0’;
/*…*/
}
推荐做法:
void Compliant()
{
char dst[ARRAY_SIZE + 1];
char src[ARRAY_SIZE + 1];
unsigned int i = 0;
memset(src, '@', sizeof(dst));
for(i=0; src[i]!=’\0’ && (i < sizeof(dst) - 1 ); ++i) /*【修改】考虑’\0’结束符 */
dst[i] = src[i];
dst[i] = ’\0’;
/*…*/
}
规则2.2:字符串操作过程中确保字符串有’\0’结束符
说明:字符串结束与否是以’\0’作为标志的。

没有正确地使用’\0’结束字符串可能导致字符串操作时发生缓冲区溢出。

因此对于字符串或字符数组的定义、设置、复制等操作,要给’\0’预留空间,并保证字符串有’\0’结束符。

注意:strncpy、strncat等带n版本的字符串操作函数在源字符串长度超出n标识的长度时,会将包括’\0’结束符在内的超长字符串截断,导致’\0’结束符丢失。

这时需要手动为目标字符串设置’\0’结束符。

错误示例1:strlen()不会将’\0’结束符算入长度,配合memcpy使用时会丢失’\0’结束符。

void Noncompliant()
{
char dst[11];
char src[] = ;
char *tmp = NULL;
memset(dst, '@', sizeof(dst));
memcpy(dst, src, strlen(src));
printf("src: %s \r\n", src);
tmp = dst; //到此,dst还没有以’\0’结尾
do
{
putchar(*tmp);
}while (*tmp++); // 访问越界
return;
}
推荐做法:为目标字符串设置’\0’结束符
void Compliant()
{
char dst[11];
char src[] = ;
char *tmp = NULL;
memset(dst, '@', sizeof(dst));
memcpy(dst, src, strlen(src));
dst[sizeof(dst) - 1] = ’\0’; //【修改】dst以’\0’结尾
printf("src: %s \r\n", src);
tmp = dst;
do
{
putchar(*tmp);
} while (*tmp++);
return;
}
错误示例2:strncpy()拷贝限长字符串,截断了’\0’结束符。

void Noncompliant()
{
char dst[5];
char src[] = ;
strncpy(dst, src, sizeof(dst));
printf(dst); //访问越界,dst没有’\0’结束符return;
}
推荐做法:
void Compliant()
{
char dst[5];
char src[] = ;
strncpy(dst, src, sizeof(dst));
dst[sizeof(dst)-1] = ’\0’; // 【修改】最后字节置
为’\0’
printf(dst);
return;
}
规则2.3:把数据复制到固定长度的内存前必须检查边界
说明:将未知长度的数据复制到固定长度的内存空间可能会造成缓冲区溢出,因此在进行复制之前应首先获取并检查数据长度。

典型的如来自gets()、getenv()、scanf()的字符串。

错误示例:输入消息长度不可预测,不加检查的复制会造成缓冲区溢出。

void Noncompliant()
{
char dst[16];
char * temp = getInputMsg();
if(temp != NULL)
{
strcpy(dst,temp); // temp长度可能超过dst的大小 }
return;
}
推荐做法:
void Compliant()
{
char dst[16];
char *temp = getInputMsg();
if(temp != NULL)
{
strncpy(dst, temp, sizeof(dst)); /* 【修改】只复制不超过数组dst大小的数据 */
}
dst[sizeof(dst) -1] = ’\0’; //【修改】copy以’\0’结尾
return;
}
规则 2.4:避免字符串/内存操作函数的源指针和目标指针指向内存重叠区
说明:内存重叠区是指一段确定大小及地址的内存区,该内存区被多个地址指针指向或引用,这些指针介于首地址和尾地址之间。

在使用像memcpy、strcpy、strncpy、sscanf()、sprintf()、
snprintf()和wcstombs()这样的函数时,复制重叠对象会存在未定义
的行为,这种行为可能破坏数据的完整性。

错误示例1:snprintf的参数使用存在问题
void Noncompliant()
{
#define MAX_LEN 1024
char cBuf[MAX_LEN + 1] = {0};
int nPid = 0;
strncpy(cBuf, ”Hello World!”,strlen(”Hello World!”));
snprintf(cBuf, MAX_LEN, "%d: %s", nPid, cBuf); /* cBuf既是源又是目标,函数使用不安全 */
return;
}
推荐做法:使用不同源和目标缓冲区来实现复制功能。

void Compliant()
{
#define MAX_LEN 1024
char cBuf[MAX_LEN + 1] = {0};
char cDesc[MAX_LEN + 1] = {0}; //【修改】另起一个缓冲区,防止缓冲区重叠出错
int nPid = 0;
strncpy(cDesc, ”Hello World!”, strlen(”Hello
World!”)); /* 【修改】防止缓冲区重叠出错 */
snprintf(cBuf, MAX_LEN, "%d: %s", nPid, cDesc); /* 【修改】防止缓冲区重叠出错 */
return;
}
错误示例2:
#define MSG_OFFSET 3
#define MSG_SIZE 6
void NoCompliant ()
{
char str[] = "test string";
char *ptr1 = str;
char *ptr2;
ptr2 = ptr1+MSG_OFFSET;
memcpy(ptr2, ptr1, MSG_SIZE);
return;
}
推荐做法:使用memmove函数,源字符串和目标字符串所指内存区域可以重叠,但复制后目标字符串内容会被更改,该函数将返回指向目标字符串的指针。

#define MSG_OFFSET 3
#define MSG_SIZE 6
void Compliant ()
{
char str[] = "test string";
char *ptr1 = str;
char *ptr2;
ptr2 = ptr1 + MSG_OFFSET;
memmove(ptr2, ptr1, MSG_SIZE); /*【修改】使用memmove代替memcpy,防止缓冲区重叠出错 */
return;
}
memcpy与memmove的目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中。

但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增加了一点点开销。

memmove的处理措施:
当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
当源内存的首地址大于目标内存的首地址时,实行正向拷贝
当源内存的首地址小于目标内存的首地址时,实行反向拷贝
3格式化输出安全
规则3.1:格式化输出函数的格式化参数和实参类型必须匹配
说明:使用格式化字符串应该小心,确保格式字符和参数在数据类型上的匹配。

格式字符和参数之间的不匹配会导致未定义的行为。

大多数情况下,不正确的格式化字符串会可能会导致格式化漏洞,使程序异常终止。

错误示例1:格式字符和参数的类型不匹配
void Noncompliant_ArgMismatch()
{
char *error_msg = "Resource not available to user.";
int error_type = 3;
/* ...do something... */
printf("Error (type %s): %d\n", error_type, error_msg); /*【错误】格式化参数类型不匹配 */
}
推荐做法:
void Noncompliant_ArgMismatch()
{
char *error_msg = "Resource not available to user.";
int error_type = 3;
/* ...do something... */
printf("Error (type %s): %d\n", error_msg, error_type); /*【修改】匹配格式化参数类型 */
}
错误示例2:将结构体作为参数
void Noncompliant_StructAsArg()
{
struct sParam
{
int num;
char msg[100];
int result;
};
struct sParam tmp = {10, "hello Baby!", 0};
char *errormsg = "Resource not available to user.";
int errortype = 3;
/* ...do something... */
if (tmp.result == 0)
{
printf("Error Param: %s \n", tmp); /*【错误】不能将整个结构体作为格式化参数 */
}
}
推荐做法:
void Noncompliant_StructAsArg()
{
struct sParam
{
int num;
char msg[100];
int result;
};
struct sParam tmp = {10, "hello Baby!", 0};
char *errormsg = "Resource not available to user.";
int errortype = 3;
/* ...do something... */
if (tmp.result == 0)
{
printf("Error Param:num=%d, msg=%s, result=%d\n", tmp.num, tmp.msg, tmp.result); //【修改】将结构体的内部变量作为格式化参数
}
}
规则3.2:格式化输出函数的格式化参数和实参个数必须匹配
说明:使用格式化字符串应该小心,确保格式字符和参数在数量上的匹配。

格式字符和参数之间的不匹配会导致未定义的行为。

大多数情况下,不正确的格式化字符串会导致程序异常终止。

错误示例:格式字符和参数的数量不匹配,格式化字符串在编码时会大量使用,如拼装SQL语句和拼装调试信息。

尤其是调试信息,量大时容易copy-paste省事,这就容易出现不匹配的错误。

void Noncompliant()
{
char *error_msg = "Resource not available to user.";
/* ...do something... */
printf("Error (type %s)\n"); //【错误】格式化参数个数不匹配
}
推荐做法:
void Compliant()
{
char *error_msg = "Resource not available to user.";
/* ...do something... */
printf("Error (type %s)\n", error_msg); //【修改】使格式化参数个数匹配
}
规则3.3:禁止以用户输入来构造格式化字符串
说明:调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。

如果攻击者对一个格式化字符串可以部分或完全控制,将导致进程崩溃、查看栈的内容、改写内存、甚至执行任意代码等风险。

错误示例:下列代码直接将用户输入作为格式字符串输出。

void Noncompliant(char *user, char *password)
{
char input[1000];
if (fgets(input, sizeof(input) - 1, stdin) == NULL) {
/* handle error */
}
input[sizeof(input)-1] = ’\0’;
printf(input); //【错误】不允许将用户输入直接作为格式字符串
}
示例代码的input直接来自用户输入,并作为格式化字符串直接传递给printf()。

当用户输入的是“%s%s%s%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。

格式字符%s显示栈上相应参数所指定的地址的内存。

这里input被当成格式化字符串,而没有提供参数,因此printf()读取栈中任意内存位置,直到格式字符耗尽或者遇到一个无效指针或未映射地址为止。

推荐做法:通过显式参数”%s”将 printf()的格式化字符串确定下
来。

void Compliant(char *user, char *password)
{
char input[1000];
if (fgets(input, sizeof(input)-1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = ’\0’;
printf(“%s”, input); //【修改】通过%s将格式字符串确定下来
}
建议3.1:使用格式化函数时推荐使用精度说明符
说明:使用格式化函数时(例如sprintf(),scanf_s()等),可能会含
有字符串参数,应尽量为格式化指示符加上精度说明符以限制拷贝字符
串的长度,防止缓冲区溢出漏洞。

错误示例:使用格式化函数sprintf,没有添加精度说明符,可能会导致缓冲区溢出。

#define BUF_SIZE 128
void NoCompliant()
{
char buffer[BUF_SIZE + 1];
sprintf( buffer, "Usage: %s argument\n", argv[0] );
/* ...do something... */
}
推荐做法:优先采用snprintf替代sprintf来防止缓冲区溢出。

若没有带n版本的snprintf函数,可参考如下示例,使用sprintf,在接收字符串时,加上精度说明符,确定接收的长度,以免造成缓冲区溢出。

#define BUF_SIZE 128
void Compliant()
{
char buffer[BUF_SIZE + 1];
sprintf(buffer, "Usage: %.100s argument\n", argv[0]); /*【修改】字符串加上精度说明符 */
/* ...do something... */
}
通过精度限制从argv[0] 中只能拷贝 100 个字节。

4整数安全
C99标准定义了整型提升(integer promotions)、整型转换级别(integer conversion rank)以及普通算术转换(usual arithmetic conversions)的整型操作。

不过这些操作实际上也带来了安全风险。

规则4.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;
}
延伸阅读材料:漏洞就是因为反转问题,导致分配内存空间不足,引发堆溢出。

规则4.2:确保有符号整数运算时不会出现溢出
说明:整数溢出是是一种未定义的行为,意味着编译器在处理有符号整数溢出时具有很多选择。

将运算结果用于以下之一的用途,应防止溢出:
作为数组索引
指针运算
作为对象的长度或者大小
作为数组的边界
作为内存分配函数的实参
错误示例:下列代码中两个有符号整数相乘可能会产生溢出。

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;
}
延伸阅读材料:整数溢出可能导致缓冲区溢出以及任意代码执行。

Apple Mac OS X 10.3及以前版本在处理GIF文件时,存在整数溢出漏
洞,可被利用执行任意代码。

攻击者可以将特定的GIF文件放在Web页面或者邮件附件中,诱使目标打开此文件从而触发利用。

具体可参考US-CERT披露的漏洞。

规则4.3:确保整型转换时不会出现截断错误
说明: 将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。

截断错误会引起数据丢失,甚至可能引发安全问题。

特别是将运算结果用于以下用途:
作为数组索引
指针运算
作为对象的长度或者大小
作为数组的边界(如作为循环计数器)
错误示例:数据类型强制转化导致数据被截断。

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:确保整型转换时不会出现符号错误
说明: 有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。

带符号整型转换到无符号整型,最高位(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。

规则4.5:把整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值
说明:若一个整型表达式与一个很大长度的整数类型进行比较或者赋值为这种类型的变量,需要对该整型表达式的其中一个操作数类型显示转换为更大长度的整数类型,用这种更大的进行求值。

这里所说的更大整数类型是相对整型表达式的操作数类型而言,譬如整型表达式的操作数类型是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...*/
建议4.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进制),导致转换为
一个字符串时,长度超过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;
}
5内存管理安全
规则5.1:禁止引用未初始化的内存
说明:有些函数如malloc分配出来的内存是没有初始化的,可以使用memset进行清零,或者使用calloc进行内存分配,calloc分配的内存是清零的。

当然,如果后面需要对申请的内存进行全部赋值,就不要清零了,但要确保内存被引用前是被初始化的。

此外,分配内存初始化,可以消除之前可能存放在内存中的敏感信息,避免敏感信息的泄露。

错误示例:如下代码没有对malloc的y内存进行初始化,所以功能不正确。

/* return y = Ax */
int * Noncompliant(int **A, int *x, int n)
{
if(n <= 0)
return NULL;
int *y = (int*)malloc (n * sizeof (int));
if(y == NULL)
return NULL;
int i, j;
for (i = 0; i < n; ++i)
{
for (j = 0; j < n; ++j)
{
y[i] += A[i][j] * x[j];
}
}
return y;
}
/*...申请的内存使用后free...*/
推荐做法:使用memset对分配出来的内存清零。

int * Compliant(int **A, int *x, int n)
{
if(n <= 0)
return NULL;
int *y = (int*)malloc(n * sizeof (int));
if(y == NULL)
return NULL;
int i, j;
memset (y, 0, n * sizeof(int)); //【修改】确保内存被初始化后才被引用
for (i = 0; i < n; ++i)
{
for (j = 0; j < n; ++j)
{
y[i] += A[i][j] * x[j];
}
}
return y;
}
/*...申请的内存使用后free...*/
延伸阅读材料:参见《C和C++安全编码》(机械工业出版社出版,作者Robert C.Seacord)第4章的tar命令的漏洞。

这个漏洞没有初始化分配的内存,导致敏感的密码泄露。

规则5.2:禁止访问已经释放的内存
说明:访问已经释放的内存,是很危险的行为,主要分为两种情况:(1)堆内存:一块内存释放了,归还内存池以后,就不应该再访问。

因为这块内存可能已经被其他部分代码申请走,内容可能已经被修改;直接修改释放的内存,可能会导致其他使用该内存的功能不正常;读也不能保证数据就是释放之前写入的值。

在一定的情况下,可以被利用执行恶意的代码。

即使是对空指针的解引用,也可能导致任意代码执行漏洞。

如果黑客事先对内存0地址内容进行恶意的构造,解引用后会指向黑客指定的地址,执行任意代码。

(2)栈内存:在函数执行时,函数内局部变量的存储单元都可以在栈上创建,函数执行完毕结束时这些存储单元自动释放。

如果返回这些已释放的存储单元的地址(栈地址),可能导致程序崩溃或恶意代码被利用。

相关文档
最新文档