通过阅读《华为C&C++语言安全规范》1,我了解到了我在编程中很多缺失的部分。现在记录下几个要点:
编码人员往往由于粗心,将指针当做数组进行sizeof操作,导致实际的执行结果与预期不符。 下面的代码,buffer和path分别是指针和数组,编码人员想对这2个内存进行清0操作,但由于编码人员的疏忽,第5行代码,将内存大小误写成了sizeof,与预期不符。
如果要判断当前的指针类型大小,请使用sizeof(char *)的方式。char *buffer = (char *)malloc(size); char path[MAX_PATH] = {0}; ... memset(path, 0, sizeof(path)); memset(buffer, 0, sizeof(buffer));
相关指南:
CERT.ARR01-C. Do not apply the sizeof operator to a pointer when taking the size of an array
指针与数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。但是数组和指针存在差异。指针,是一个变量,存储的数据是地址。数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组,其外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量2。数组名只是大多数时候隐式转换成指向首元素的指针类型右值。这些时候不会转换:1)对其用 &;2)对其用 sizeof;3)C++中取引用3。
我们先来一段代码作为演示:
#include
using namespace std;
void func(int C[])
{
cout<<"In function, C sizeof(C):"<<sizeof(C)<<endl;
cout<<"C point:"<<C<<endl;
cout<<"C &point:"<<&C<<endl;
C++;
}
int main()
{
int A[10];
int* B=new int[10];
cout<<"A sizeof(A):"<<sizeof(A)<<endl;
cout<<"B sizeof(B):"<<sizeof(B)<<endl;
// 取引用地址
cout<<"A point:"<<A<<endl;
cout<<"A &point:"<<&A<<endl;
cout<<"B point:"<<B<<endl;
cout<<"B &point:"<<&B<<endl;
//调用函数
func(A);
//A++;//Error
return 0;
}
在X86的编译环境下,输出的结果为:
A sizeof(A):40
B sizeof(B):4
A point:0x6dfec8
A &point:0x6dfec8
B point:0x1fa838
B &point:0x6dfec4
In function, C sizeof(C):4
C point:0x6dfec8
C &point:0x6dfeb0
显然第14行输出的是数组长度,第15行输出的是指针长度(在X86下为4字节,在x64环境下为8字节)。
第17-第20行,对数组取引用,其地址和本身的地址是一样,而指针则不一样。
第23行,当调用函数的时候,数组会转换为指针,因此长度为4,并且可以做自加运算。
断言只能在调试版使用,断言被触发后,程序会立即退出,因此严禁在正式发布版本使用断言,请通过编译选项进行控制。 错误用法如:
int Foo(int *array, int size) { assert(array != NULL); ... }
ASSERT()是MFC的宏4,ASSERT只有在Debug版本中才有效,如果编译为Release版本则被忽略。
assert()的功能类似,它是ANSI C标准中规定的函数,它与ASSERT的一个重要区别是可以用在Release版本中5。
在msvc16里面是这样定义的:
#undef assert
#ifdef NDEBUG
#define assert(expression) ((void)0)
#else
_ACRTIMP void __cdecl _wassert(
_In_z_ wchar_t const* _Message,
_In_z_ wchar_t const* _File,
_In_ unsigned _Line
);
#define assert(expression) (void)( \
(!!(expression)) || \
(_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
)
#endif
在MinGW里面
/* According to C99 standard (section 7.2) the assert
macro shall be redefined each time assert.h gets
included depending on the status of NDEBUG macro. */
#undef assert
...
#ifdef NDEBUG
#define assert(_Expression) ((void)0)
#else /* !defined (NDEBUG) */
#if defined(_UNICODE) || defined(UNICODE)
#define assert(_Expression) \
(void) \
((!!(_Expression)) || \
(_wassert(_CRT_WIDE(#_Expression),_CRT_WIDE(__FILE__),__LINE__),0))
#else /* not unicode */
#define assert(_Expression) \
(void) \
((!!(_Expression)) || \
(_assert(#_Expression,__FILE__,__LINE__),0))
#endif /* _UNICODE||UNICODE */
#endif /* !defined (NDEBUG) */
综上所述,使用语言自带的assert即可,也没有其他的选择。当在Release环境下,assert也自动编译为((void)0)
。对于华为的这条安全规范表示不解,希望有人能解答一下。
另外:这里还发现很有意思的东西:
/* According to C99 standard (section 7.2) the assert
macro shall be redefined each time assert.h gets
included depending on the status of NDEBUG macro. */
意思就是每次包含
在程序正式发布阶段,断言不会被编译进去,为了确保调试版和正式版的功能一致性,严禁在断言中使用任何赋值、修改变量、资源操作、内存申请等操作。 例如,以下的断言方式是错误的:
ASSERT(p1 = p2); //p1被修改 ASSERT(i++ > 1000); //i被修改 ASSERT(close(fd) == 0);//fd被关闭
为了更加准确地发现错误的位置,每一条断言只校验一个条件。 下面的断言同时校验多个条件,在断言触发的时
候,无法判断到底是哪一个条件导致的错误:int Foo(int *array, int size) { ASSERT(array != NULL && size > 0 && size < MAX_SIZE); ... }
应该将每个条件分开:
int Foo(int *array, int size) { ASSERT(array != NULL); ASSERT(size > 0); ASSERT(size < MAX_SIZE); ... }
对于设计成API的函数,必须对参数进行合法性判断,严禁在API实现过程中产生RASH。对API函数的参数进行ASSERT操作是没有意义的。 例如,对于提供应用服务器IP的平台公共API接口这样实现是错误的:
int GetServerIP(char *ip, size_t ipSize) { ASSERT(ip != NULL); ... }
公共接口API应当对输入参数进行代码检查:
int GetServerIP(char *ip, size_t ipSize) { if (ip == NULL) { ... } ... }
不可重入函数在多线程环境下其执行结果不能达到预期效果,需谨慎使用。常见的不可重入函数包括:
rand, srand
getenv, getenv_s
strtok
strerror
asctime, ctime, localtime, gmtime
setlocale
atomic_init
tmpnam
mbrtoc16, c16rtomb, mbrtoc32, c32rtomb
gethostbyaddr
gethostbyname
inet_ntoa
如果字符串或者指针作为函数参数,为了防止空指针引用错误,在引用前必须确保该参数不为NULL,如果上层调用者已经保证了该参数不可能为NULL,在调用本函数时,在函数开始处可以加ASSERT进行校验。 例如下面的代码,因为BYTE *p有可能为NULL,因此在使用前需要进行判断。
int Foo(int *p, int count) { if (p != NULL && count > 0) { int c = p[0]; } ... } int Foo2() { int *arr = ... int count = ... Foo(arr, count); ... }
下面的代码,由于p的合法性由调用者保证,对于Foo函数,不可能出现p为NULL的情况,因此加上ASSERT进行校验。
int Foo(int *p, int count) { ASSERT(p != NULL); //ASSERT is added to verify p. ASSERT(count > 0); int c = p[0]; ... } int Foo2() { int *arr = ... int count = ... ... if (arr != NULL && count > 0) { Foo(arr, count); } ... }
严禁使用C++的异常机制,所有的错误都应该通过错误值在函数之间传递并做相应的判断, 而不应该通过异常机制进行错误处理。 编码人员必须完全掌控整个编码过程,建立攻击者思维,增强安全编码意识,主动把握有可能出错的环节。而使用C++异常机制进行错误处理,会削弱编码人员的安全意识。 异常机制会打乱程序的正常执行流程,使程序结构更加复杂,原先申请的资源可能会得不到有效清理。 异常机制导致代码的复用性降低,使用了异常机制的代码,不能直接给不使用异常机制的代码复用。 异常机制在实现上依赖于编译器、操作系统、处理器,使用异常机制,导致程序执行性能降低。 在二进制层面,程序被加载后,异常处理函数增加了程序的被攻击面,攻击者可以通过覆盖异常处理函数地址,达到攻击的效果。 例外: 在接管C++语言本身抛出的异常(例如new失败、STL)、第三方库(例如IDL)抛出的异常时,可以使用异常机制,例如:
int len = ...; char *p = NULL; try { p = new char[len]; } catch (bad_alloc) { ... abort(); }
相关指南:
Google C++ Style Guide.Exceptions: We do not use C++ exceptions.
这点在《游戏引擎架构8》中也提到过。
构造函数内仅作成员变量的初始化工作,其他的操作通过成员函数完成。
型
实例:
class CMsg { public: CMsg(); ~CMsg(); Const unsigned char *GetMsg(); protected: int size; unsigned char *msg; }; CMsg::CMsg() { size = 0; msg = NULL; } const unsigned char *CMsg::GetMsg() { return msg; }
严禁在线程内主动终止自身线程,线程函数在执行完毕后会自动、安全地退出。主动终止自身线程的操作,不仅导致代码复用性变差,同时容易导致资源泄漏错误。
程序应该安全退出,除了main函数以外,禁止任何地方调用exit、ExitProcess函数退出进程。直接退出进程会导致代码的复用性降低,资源得不到有效地清理。 程序应该通过错误值传递的机制进行错误处理。 以下代码加载文件,加载过程中如果出错,直接调用exit退出:
void LoadFile(const char *filePath) { FILE* fp = fopen(filePath, "rt"); if (fp == NULL) { exit(0); } ... }
正确的做法应该通过错误值传递机制,例如:
BOOL LoadFile(const char *filePath) { BOOL ret = FALSE; FILE* fp = fopen(filePath, "rt"); if (fp != NULL) { ... } ... return ret; }
abort会导致程序立即退出,资源得不到清理。 例外: 只有发生致命错误,程序无法继续执行的时候,在错误处理函数中使用abort退出程序,例如:
void FatalError(int sig) { abort(); } int main(int argc, char *argv[]) { signal(SIGSEGV, FatalError); ... }
调用格式化函数时,如果format参数由外部可控,会造成字符串格式化漏洞。 这些格式化函数有: 格式化输出函数:xxxprintf 格式化输入函数:xxxscanf 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line(); 格式化日志函数:syslog(),vsyslog()。
错误示例:char *msg = GetMsg(); ... printf(msg);
推荐做法:
char *msg = GetMsg(); ... printf("%s\n", msg);
相关指南:
CERT.FIO47-C. Use valid format strings
MITRE.CWE-134: Use of Externally-Controlled Format String
由于整数在运算过程中可能溢出,当运算结果赋值给比他更大类型,或者和比他更大类型进行比较时,会导致实际结果与预期结果不符。 请观察以下二个代码及其输出:
int main(int argc, char *argv[]) { unsigned int a = 0x10000000; unsigned long long b = a * 0xab; printf("b = %llX\n", b); return 0; }
输出: b = B0000000
int main(int argc, char *argv[]) { unsigned int a = 0x10000000; unsigned long long b = (unsigned long long )a * 0xab; printf("b = %llX\n", b); return 0; }
输出: b = AB0000000
位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数。 错误示例:
int data = ReadByte(); int a = data >> 24;
推荐做法:(为简化示例代码,此处假设ReadByte函数实际不存在返回值小于0的情况)
unsigned int data = (unsigned int)ReadByte(); unsigned int a = data >> 24;
相关指南:
CERT.INT13-C. Use bitwise operators only on unsigned operands
MISRA.C.2004.Rule 12.7 (required): Bitwise operators shall not be applied to operands whose underlyingtype is signed.
在C语言中,如果在未知的有符号上执行位操作,很可能会导致缓冲区溢出,从而在某些情况下导致攻击者执行任意代码,同时,还可能会出现出乎意料的行为或编译器定义的行为。
代码如下:
#include
int main()
{
int y=0x80000000;
printf("%d\n",y>>24);//以十进制有符号形式输出。
printf("%u\n",y>>24);//以十进制无符号形式输出。
return 0;
}
输出为:
-128
4294967168
由于int类型的最高位是符号位,剩下的31位才用来存储数值,y>>24的结果应该是0xffffff80(负数右移,左补1),当我们以无符号整型输出时,为正数的0xffffff80,以有符号整型输出时,应减一再取反,结果为-1289。
然而,在右移运算中,空出的位用 0 还是符号位进行填充是由于具体的 C 语言编译器实现来决定。在通常情况下,如果要进行移位的操作数是无符号类型的,那么空出的位将用 0 进行填充;如果要进行移位的操作数是有符号类型的,则 C 语言编译器实现既可选择 0 来进行填充,也可选择符号位进行填充。
因此,如果很关心一个右移运算中的空位,那么可以使用 unsigned 修饰符来声明变量,这样空位都会被设置为 0。同时,如果一个程序采用了有符号数的右移位操作,那么它就是不可移植的10。
指针的大小随着平台的不同而不同,强行进行整数与指针间的互相转化,降低了程序的兼容性,在转换过程中可能引起指针高位信息的丢失。
错误示例:char *ptr = ...; unsigned int number = (unsigned int)ptr;
推荐做法:
char *ptr = ...; uintptr_t number = (uintptr_t)ptr;
相关指南:
CERT.INT36-C. Converting a pointer to integer or integer to pointer
MISRA.C.2004.Rule 11.3 (advisory): A cast should not be performed between a pointer type and an integral type.
对指针进行逻辑运算,会导致指针的性质改变,可能产生内存非法访问的问题。 下面是错误的用法:
BOOL dealName(const char *nameA, const char *nameB) { ... if (nameA) ... if (!nameB) ... }
下面是正确的用法:
BOOL dealName(const char *nameA, const char *nameB) { ... if (nameA != NULL) ... if (nameB == NULL) ... }
例外: 为检查地址对齐而对地址指针进行的位运算可以作为例外。
相关指南:
MISRA.C.2004.Rule 12.6 (advisory): The operands of logical operators (&&, || and !) should be effectively Boolean. Expressions that are effectively Boolean should not be used as operands to operators other than (&&, ||, !, =, ==, != and ?.
内存申请的大小可能来自于外部数据,必须检查其合法性,防止过多地、非法地申请内存。不能申请0长度的内存。 例如:
int Foo(int size) { if (size <= 0) { //error ... } ... char *msg = (char *)malloc(size); ... }
相关指南:
CERT.MEM04-C. Beware of zero-length allocations
MITRE.CWE-789: Uncontrolled Memory Allocation
char *msg = (char *)malloc(size); if (msg != NULL) { ... }
相关指南:
CERT.MEM11-C. Do not assume infinite heap space
CERT.ERR33-C. Detect and handle standard library errors
CERT.MEM52-CPP. Detect and handle memory allocation errors
MITRE.CWE 252, Unchecked Return Value
MITRE.CWE 391, Unchecked Error Condition
MITRE.CWE 476, NULL Pointer Dereference
MITRE.CWE 690, Unchecked Return Value to NULL Pointer Dereference
MITRE.CWE 703, Improper Check or Handling of Exceptional Conditions
MITRE.CWE 754, Improper Check for Unusual or Exceptional Conditions
malloc、new分配出来的内存没有被初始化为0,要确保内存被引用前是被初始化的。 以下代码使用malloc申请内存,在使用前没有初始化:
int *CalcMetrixColomn( int **metrix ,int *param, size_t size ) { int *result = NULL; ... size_t bufSize = size * sizeof(int); ... result = (int *)malloc(bufSize); ... result[0] += metrix[0][0] * param[0]; ... return result; }
以下代码使用memset_s()对分配出来的内存清零。
int *CalcMetrixColomn(int **metrix ,int *param, size_t size) { int *result = NULL; ... size_t bufSize = size * sizeof(int); ... result = (int *)malloc(bufSize); ... int ret = memset_s(result, bufSize, 0, bufSize); //【修改】确保内存被初始化后才被引用 ... result[0] += metrix[0][0] * param[0]; ... return result; }
相关指南:
CERT.EXP33-C. Do not read uninitialized memory
CERT.EXP53-CPP. Do not read uninitialized memory
悬挂指针可能会导致双重释放(double-free)以及访问已释放内存的危险。消除悬挂指针以及消除众多与内存相关危险的一个最为有效地方法就是当指针使用完后将其置新值。 如果一个指针释放后能够马上离开作用域,因为它已经不能被再次访问,因此可以无需对其赋予新值。
示例:char *message = NULL; ... message = (char *)malloc(len); ... if (...) { free(message); //在这个分支内对内存进行了释放 message = NULL; //释放后将指针赋值为NULL } ... if (message != NULL) { free(message); message = NULL; }
相关指南:
CERT.MEM01-C. Store a new value in pointers immediately after free()
CERT.MEM50-CPP. Do not access freed memory
realloc()原型如下:
void *realloc(void *ptr, size_t size);
随着参数的不同,其行为也是不同。 1) 当ptr不为NULL,且size不为0时,该函数会重新调整内存大小,并将新的内存指针返回,并保证最小的size的内容不变; 2) 参数ptr为NULL,但size不为0,那么行为等同于malloc(size); 3) 参数size为0,则realloc的行为等同于free(ptr)。 由此可见,一个简单的C函数,却被赋予了3种行为,这不是一个设计良好的函数。虽然在编码中提供了一些便利性,但是却极易引发各种bug。
相关指南:
CERT.MEM36-C. Do not modify the alignment of objects by calling realloc()
POSIX和C99均未定义alloca()的行为,在有些平台下不支持该函数,使用alloca会降低程序的兼容性和可移植性,该函数在栈帧里申请内存,申请的大小很可能超过栈的边界,影响后续的代码执行。 请使用malloc或new,从堆中动态分配内存。
创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件。 下列代码没有显式配置文件的访问权限。
int fd = open(fileName, O_CREAT | O_WRONLY); //【错误】缺少访问权限设置
推荐做法:
int fd = open(fileName, O_CREAT | O_WRONLY, S_IRUSR|S_IWUSR);
string类是C++内部定义的字符串管理类,如果口令等敏感信息通过string进行操作,在程序运行过程中,敏感信息可能会散落到内存的各个地方,并且无法清0。 以下代码,Foo函数中获取密码,保存到string变量password中,随后传递给VerifyPassword函数,在这个过程中,password实际上在内存中出现了2份。
int VerifyPassword(string password) { //... } int Foo() { string password = GetPassword(); VerifyPassword(password); ... }
应该使用char或unsigned char保存敏感信息,如下代码:
int VerifyPassword(const char *password) { //... } int Foo() { char password[MAX_PASSWORD] = {0}; GetPassword(password, sizeof(password)); VerifyPassword(password); ... }
《华为C&C++语言安全规范》 https://www.jb51.net/books/720904.html ↩︎
C/C++数组名与指针区别深入探索_ljob2006的博客-CSDN博客 https://blog.csdn.net/ljob2006/article/details/4872167 ↩︎
c中,数组名跟指针有区别吗? - 知乎 https://www.zhihu.com/question/41805285 ↩︎
大小写 ASSERT 有什么区别_百度知道 https://zhidao.baidu.com/question/110720542.html ↩︎
问题: 什么是ASSERT()? ASSERT()和assert()的区别是什么?_流风的专栏-CSDN博客 https://blog.csdn.net/procedurecode/article/details/1942115 ↩︎
第一章assert.h_暮秋小屋-CSDN博客 https://blog.csdn.net/qq_49150256/article/details/112753240 ↩︎
可重入函数对于线程安全的意义(附函数表) - 云+社区 - 腾讯云 https://cloud.tencent.com/developer/article/1685935 ↩︎
游戏引擎架构 (豆瓣) https://book.douban.com/subject/25815142/ ↩︎
有符号执行位操作导致的BUG_Tonson_的博客-CSDN博客 https://blog.csdn.net/weixin_44444450/article/details/109668780 ↩︎
位操作及其使用注意事项,C语言位操作及其使用方法详解 http://c.biancheng.net/view/362.html ↩︎