CC 语言安全编程规范 V.docx
《CC 语言安全编程规范 V.docx》由会员分享,可在线阅读,更多相关《CC 语言安全编程规范 V.docx(29页珍藏版)》请在冰豆网上搜索。
CC语言安全编程规范V
华为技术有限公司内部技术规范
DKBA
C&C++语言安全编程规范
2013年05月07日发布2013年05月07日实施
华为技术有限公司
HuaweiTechnologiesCo.,Ltd.
版权所有XX
修订声明
本规范拟制与解释部门:
网络安全技术能力中心
本规范的相关系列规范或文件:
《Java语言安全编程规范》《Web应用安全开发规范》
相关国际规范或文件一致性:
无
替代或作废的其它规范或文件:
无
相关规范或文件的相互关系:
本规范作为《C语言编程规范》和《C++语言编程规范》安全性要求的补充和扩展。
规范号
主要起草部门专家
主要评审部门专家
修订情况
网络安全能力中心:
罗东67107、于鹏、苗宏、朱喜红00210657
电信软件与核心网:
陈辉军00190784
无线产品线:
肖飞龙00051938
网络产品线:
魏建雄00222905
IT产品线:
熊华梁00106214
中央软件院:
朱楚毅00217543、
林水平00109837、周强00048368、辛威00176185、
鞠章蕾00040951、谢青00101378
中央硬件院:
刘永合00222758
终端公司:
杨棋斌00060469
企业网络:
黄凯进00040281、
企业SecoSpace:
王瑾
中央软件院:
黄茂青00057072、卢峰00210300
网络产品线:
李强00203020、罗天00062283、廖永强00111217、任志清00048956、李海蛟00040826、陈璟00222879、勾国凯00048893、范佳甲00109753
中央硬件院:
刘崇山00159994、施文超00109740
企业网络:
李有永
IT产品线:
李显才00044635、何昌军00061280
能力中心:
郭曙光00121837
网络安全实验室:
林结斌00206214
电信软件与核心网:
朱刚00192988
无线产品线:
李瀛00130531、王爱成00223009、杨彬00065941、于继万00052142、解然00234688
C&C++语言安全编程规范
1规范制定说明
1.1前言
随着公司业务发展,越来越多的产品被公众、互联网所熟知,并成为安全研究组织的研究对象、黑客的漏洞挖掘目标,容易引起安全问题。
安全问题影响的不只是单个产品,甚至有可能影响到公司整体声誉。
产品安全涉及需求、设计、实现、部署多个环节,实现的安全是产品安全的重要一环。
为了帮助产品开发团队编写安全的代码,减少甚至规避由于编码错误引入安全风险,特制定本规范。
《C&C++语言安全编程规范》参考业界安全编码的研究成果,并结合产品编码实践的经验总结,针对C/C++语言编程中的字符串操作、整数操作、内存管理、文件操作、STL库使用等方面,描述可能导致安全漏洞或潜在风险的常见错误。
以期减少缓冲区溢出、整数溢出、格式化字符串攻击、命令注入攻击、目录遍历等典型安全问题。
1.2使用对象
本规范的读者及使用对象主要为使用C和C++语言的开发人员、测试人员等。
1.3适用范围
本规范适合于公司基于C或C++语言开发的产品。
1.4术语定义
原则:
编程时必须遵守的指导思想。
规则:
编程时必须遵守的约定。
建议:
编程时必须加以考虑的约定。
说明:
对此原则/规则/建议进行必要的解释。
错误示例:
对此原则/规则/建议从反面给出例子。
推荐做法:
对此原则/规则/建议从正面给出例子。
延伸阅读材料:
建议进一步阅读的参考材料。
2通用原则
原则:
对外部输入进行校验
说明:
对于外部输入(包括用户输入、外部接口输入、配置文件、网络数据和环境变量等)可能用于以下场景的情况下,需要检验入参的合法性:
●输入会改变系统状态
●输入作为循环条件
●输入作为数组下标
●输入作为内存分配的尺寸参数
●输入作为格式化字符串
●输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
●输入影响代码逻辑
这些情况下如果不对用户数据作合法性验证,很可能导致DoS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
对外部输入验证常见有如下几种方式:
(1)校验输入数据长度:
如果输入数据是字符串,通过校验输入数据的长度可以加大攻击者实施攻击的难度,从而防止缓冲区溢出、恶意代码注入等漏洞。
(2)校验输入数据的范围:
如果输入数据是数值,必须校验数值的范围是否正确,是否合法、在有效值域内,例如在涉及到内存分配、数组操作、循环条件、计算等安全操作时,若没有进行输入数值有效值域的校验,则可能会造成内存分配失败、数组越界、循环异常、计算错误等问题,这可能会被攻击者利用并进行进一步的攻击。
(3)输入验证前,对数据进行归一化处理以防止字符转义绕过校验:
通过对输入数据进行归一化处理(规范化,按照常用字符进行编码),彻底去除元字符,可以防止字符转义绕过相应的校验而引起的安全漏洞。
(4)输入校验应当采用“白名单”形式:
“黑名单”和“白名单”是进行数据净化的两种途径。
“黑名单”尝试排斥无效的输入,而“白名单”则通过定义一个可接受的字符列表,并移除任何不接受的字符来仅仅接受有效的输入。
有效输入值列表通常是一个可预知的、定义良好的集合,并且其大小易于管理。
“白名单”的好处在于,程序员可以确定一个字符串中仅仅包含他认为安全的字符。
“白名单”比“黑名单”更受推荐的原因是,程序员不必花力气去捕捉所有不可接受的字符,只需确保识别了可接受的字符就可以了。
这样一来,程序员就不用绞尽脑汁去考虑攻击者可能尝试哪些字符来绕过检查。
原则:
禁止在日志中保存口令、密钥
说明:
在日志中不能保存口令和密钥,其中的口令包括明文口令和密文口令。
对于敏感信息建议采取以下方法,
●不打印在日志中;
●若因为特殊原因必须要打印日志,则用“*”代替。
原则:
及时清除存储在可复用资源中的敏感信息
说明:
存储在可复用资源中的敏感信息如果没有正确的清除则很有可能被低权限用户或者攻击者所获取和利用。
因此敏感信息在可复用资源中保存应该遵循存储时间最短原则。
可复用资源包括以下几个方面:
●堆(heap)
●栈(stack)
●数据段(datasegment)
●数据库的映射缓存
存储口令、密钥的变量使用完后必须显式覆盖或清空。
原则:
正确使用经过验证的安全的标准加密算法
说明:
禁用私有算法或者弱加密算法(如DES,SHA1等),应该使用经过验证的、安全的、公开的加密算法。
加密算法分为对称加密算法和非对称加密算法。
推荐使用的常用对称加密算法有:
●AES
推荐使用的常用非对称算法有:
●RSA
●数字签名算法(DSA)
此外还有验证消息完整性的安全哈希算法(SHA256)等。
基于哈希算法的口令安全存储必须加入盐值(salt)。
密钥长度符合最低安全要求:
●AES:
128位
●RSA:
2048位
●DSA:
1024位
●SHA:
256位
原则:
遵循最小权限原则
说明:
程序在运行时可能需要不同的权限,但对于某一种权限不需要始终保留。
例如,一个网络程序可能需要超级用户权限来捕获原始网络数据包,但是在执行数据报分析等其它任务时,则可能不需要相同的权限。
因此程序在运行时只分配能完成其任务的最小权限。
过高的权限可能会被攻击者利用并进行进一步的攻击。
(1)撤销权限时应遵循正确的撤销顺序:
在涉及到set-user-ID和set-group-ID程序中,当有效的用户ID(userID)和组ID(groupID)与真实的用户不同时,不但要撤销用户层面(userlevel)的权限而且要撤销组层面(grouplevel)的权限。
在进行这样的操作时,要保证撤销顺序的正确性。
权限撤销顺序的不正确操作,可能会被攻击者获得过高的权限而进行进一步的攻击。
(2)完成权限撤销操作后,应确保权限撤销成功:
不同平台下所谓的“适当的权限”的意义是不相同的。
例如在Solaris中,setuid()的适当的权限指的是PRIV_PROC_SETID权限在进程的有效权限集中。
在BSD中意味着有效地用户ID(EUID)为0或者uid=geteuid()。
而在Linux中,则是指进程具有CAP_SETUID能力并且当EUID不等于0、真正的用户ID(RUID)或者已保存的set-userID(SSUID)中任何一个时,setuid(geteuid())是失败的。
正是由于权限行为的复杂性,所以所需的权限在撤销时可能会失败。
这会被攻击者利用并进行进一步的攻击。
例如Kernel版本在的Linux就有一个权限撤销漏洞,当权限功能位置为0时,setuid(getuid())没有如预期的那样撤销权限成功。
因此在进行权限撤销操作后,应该校验以保证权限撤销成功。
原则:
删除或修改没有效果的代码
说明:
删除或修改一些即使执行后、也不会有任何效果的代码。
一些存在的代码(声明或表达式),即使它被执行后,也不会对代码的结果或数据的状态产生任何的影响,或者产生不是所预期的效果,这样的代码在可能是由于编码错误引起的,往往隐藏着逻辑上的错误。
原则:
删除或修改没有使用到的变量或值
说明:
删除或修改没有使用到的变量或值。
一些变量或值存在于代码里,但并没有被使用到,这可能隐含着逻辑上的错误,需要被识别出来,删除这类语句或做相应的修改。
3字符串操作安全
规则:
确保有足够的空间存储字符串的字符数据和’\0’结束符
说明:
在分配内存或者在执行字符串复制操作时,除了要保证足够的空间可以容纳字符数据,还要预留’\0’结束符的空间,否则会造成缓冲区溢出。
错误示例1:
拷贝字符串时,源字符串长度可能大于目标数组空间。
voidmain(intargc,char*argv[])
{
chardst[128];
if(argc>1)
{
strcpy(dst,argv[1]);.dst使用后free...*/
}
错误示例2:
典型的差一错误,未考虑’\0’结束符写入数组的位置,造成缓冲区溢出和内存改写。
voidNoCompliant()
{
chardst[ARRAY_SIZE+1];
charsrc[ARRAY_SIZE+1];
unsignedinti=0;
memset(src,'@',sizeof(dst));
for(i=0;src[i]!
=’\0’&&(idst[i]=src[i];
dst[i]=’\0’;
/*…*/
}
推荐做法:
voidCompliant()
{
chardst[ARRAY_SIZE+1];
charsrc[ARRAY_SIZE+1];
unsignedinti=0;
memset(src,'@',sizeof(dst));
for(i=0;src[i]!
=’\0’&&(idst[i]=src[i];
dst[i]=’\0’;
/*…*/
}
规则:
字符串操作过程中确保字符串有’\0’结束符
说明:
字符串结束与否是以’\0’作为标志的。
没有正确地使用’\0’结束字符串可能导致字符串操作时发生缓冲区溢出。
因此对于字符串或字符数组的定义、设置、复制等操作,要给’\0’预留空间,并保证字符串有’\0’结束符。
注意:
strncpy、strncat等带n版本的字符串操作函数在源字符串长度超出n标识的长度时,会将包括’\0’结束符在内的超长字符串截断,导致’\0’结束符丢失。
这时需要手动为目标字符串设置’\0’结束符。
错误示例1:
strlen()不会将’\0’结束符算入长度,配合memcpy使用时会丢失’\0’结束符。
voidNoncompliant()
{
chardst[11];
charsrc[]="09";
char*tmp=NULL;
memset(dst,'@',sizeof(dst));
memcpy(dst,src,strlen(src));
printf("src:
%s\r\n",src);
tmp=dst;;
interror_type=3;
/*...dosomething...*/
printf("Error(type%s):
%d\n",error_type,error_msg);/*【错误】格式化参数类型不匹配*/
}
推荐做法:
voidNoncompliant_ArgMismatch()
{
char*error_msg="Resourcenotavailabletouser.";
interror_type=3;
/*...dosomething...*/
printf("Error(type%s):
%d\n",error_msg,error_type);/*【修改】匹配格式化参数类型*/
}
错误示例2:
将结构体作为参数
voidNoncompliant_StructAsArg()
{
structsParam
{
intnum;
charmsg[100];
intresult;
};
structsParamtmp={10,"helloBaby!
",0};
char*errormsg="Resourcenotavailabletouser.";
interrortype=3;
/*...dosomething...*/
if==0)
{
printf("ErrorParam:
%s\n",tmp);/*【错误】不能将整个结构体作为格式化参数*/
}
}
推荐做法:
voidNoncompliant_StructAsArg()
{
structsParam
{
intnum;
charmsg[100];
intresult;
};
structsParamtmp={10,"helloBaby!
",0};
char*errormsg="Resourcenotavailabletouser.";
interrortype=3;
/*...dosomething...*/
if==0)
{
printf("ErrorParam:
num=%d,msg=%s,result=%d\n",,,;;
/*...dosomething...*/
printf("Error(type%s)\n");;
/*...dosomething...*/
printf("Error(type%s)\n",error_msg);.dosomething...*/
}
推荐做法:
优先采用snprintf替代sprintf来防止缓冲区溢出。
若没有带n版本的snprintf函数,可参考如下示例,使用sprintf,在接收字符串时,加上精度说明符,确定接收的长度,以免造成缓冲区溢出。
#defineBUF_SIZE128
voidCompliant()
{
charbuffer[BUF_SIZE+1];
sprintf(buffer,"Usage:
%.100sargument\n",argv[0]);/*【修改】字符串加上精度说明符*/
/*...dosomething...*/
}
通过精度限制从argv[0]中只能拷贝100个字节。
4整数安全
C99标准定义了整型提升(integerpromotions)、整型转换级别(integerconversionrank)以及普通算术转换(usualarithmeticconversions)的整型操作。
不过这些操作实际上也带来了安全风险。
规则:
确保无符号整数运算时不会出现反转
说明:
反转是指无法用无符号整数表示的运算结果将会根据该类型可以表示的最大值加1执行求模操作。
将运算结果用于以下之一的用途,应防止反转:
●作为数组索引
●指针运算
●作为对象的长度或者大小
●作为数组的边界
●作为内存分配函数的实参
错误示例:
下列代码可能导致相加操作产生无符号数反转现象。
INT32NoCompliant(UINT32ui1,UINT32ui2,UINT32*ret)
{
if(NULL==ret)
{
returnERROR;
}
*ret=ui1+ui2;
/*上面的代码可能会导致ui1加ui2产生无符号数反转现象,譬如ui1=UINT_MAX且ui2=2;这可能会导致后面的内存分配数量不足或者产生易被利用的潜在风险;*/
return(OK);
}
推荐做法:
INT32Compliant(UINT32ui1,UINT32ui2,UINT32*ret)
{
if(NULL==ret)
{
returnERROR;
}
if((UINT_MAX-ui1)return(alloc<=UINT_MAX)?
malloc(blockNum*16):
NULL;
}
/*...申请的内存使用后free...*/
推荐做法:
void*Compliant(UINT32blockNum)
{
if(0==blockNum)
{
returnNULL;
}
UINT64alloc=(UINT64)blockNum*16;/*【修改】确保整型表达式转换时不出现数值错误*/
return(alloc<=UINT_MAX)?
malloc(blockNum*16):
NULL;
}
/*...申请的内存使用后free...*/
建议:
避免对有符号整数进行位操作符运算
说明:
位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数,因为有符号整数上的有些位操作的结果是由编译器所决定的,可能会出现出乎意料的行为或编译器定义的行为。
错误示例:
对有符号数作位操作运算。
#defineBUF_LEN(4)
INT32NoCompliant(void)
{
INT32ret=0;
INT32i=0x8000000;.申请的内存使用后free...*/
推荐做法:
使用memset对分配出来的内存清零。
int*Compliant(int**A,int*x,intn)
{
if(n<=0)
returnNULL;
int*y=(int*)malloc(n*sizeof(int));
if(y==NULL)
returnNULL;
inti,j;
memset(y,0,n*sizeof(int));.申请的内存使用后free...*/
延伸阅读材料:
参见《C和C++安全编码》(机械工业出版社出版,作者Robert)第4章的tar命令的漏洞。
这个漏洞没有初始化分配的内存,导致敏感的密码泄露。
规则:
禁止访问已经释放的内存
说明:
访问已经释放的内存,是很危险的行为,主要分为两种情况:
(1)堆内存:
一块内存释放了,归还内存池以后,就不应该再访问。
因为这块内存可能已经被其他部分代码申请走,内容可能已经被修改;直接修改释放的内存,可能会导致其他使用该内存的功能不正常;读也不能保证数据就是释放之前写入的值。
在一定的情况下,可以被利用执行恶意的代码。
即使是对空指针的解引用,也可能导致任意代码执行漏洞。
如果黑客事先对内存0地址内容进行恶意的构造,解引用后会指向黑客指定的地址,执行任意代码。
(2)栈内存:
在函数执行时,函数内局部变量的存储单元都可以在栈上创建,函数执行完毕结束时这些存储单元自动释放。
如果返回这些已释放的存储单元的地址(栈地址),可能导致程序崩溃或恶意代码被利用。
错误示例1:
解引用一个已经释放了内存的指针,会导致未定义的行为。
typedefstruct_tagNode
{
intvalue;
struct_tagNode*next;
}Node;
Node*Noncompliant()
{
Node*head=(Node*)malloc(Node);
if(head==NULL)
{
/*...dosomething...*/
returnNULL;
}
/*...dosomething...*/
free(head);
/*...dosomething...*/
head->next=NULL;.dosomething...*/
returnmsg;.dosomething...*/
free(ptr);
}
/*...dosomething...*/
free(ptr);.dosomething...*/
free(ptr);
ptr=NULL;
}
/*...dosomething...*/
.申请的内存使用后free...*/
推荐做法:
调用malloc之前,需要判断malloc的参数是否合法。
确保x为整数后才申请内存,否则视为参数无效,不予申请,以避免出现申请过大内存而导致拒绝服务。
int*Compliant(intx)
{
inti;
int*y;
if(x>0).申请的内存使用后free...*/
规则:
禁止释放非动态申请的内存
说明:
非动态申请的内存并不是由内存分配器管理的,如果使用free函数对这块内存进行释放,会对内存分配器产生影响,造成拒绝服务。
如果黑客能控制非动态申请的内存内容,并对其进行精心的构造,甚至导致程序执行任意代码。
错误示例:
非法释放非动态申请的内存。
voidNoncompliant()
{
charstr[]="thisisastring";
/*...dosomething...*/
free(str);.dosomething...*/
/../../etc/passwd”或“../../../”
(2)构造指向系统关键文件的链接文件,例如symlink("/etc/shadow","/tmp/log")
通过上述两种方式之一可以实现读取或修改系统重要数据文件,威胁系统安全。
推荐做法:
Linux下对文件进行标准化,可以防止黑客通过构造指向系统关键文件的链接文件。
realpath