赞
踩
通过阅读《华为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));
- 1
- 2
- 3
- 4
- 5
相关指南:
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<iostream> 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); ... }
- 1
- 2
- 3
- 4
- 5
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.h>时,都会根据NDEBUG的当前状态重新定义assert宏6。
在程序正式发布阶段,断言不会被编译进去,为了确保调试版和正式版的功能一致性,严禁在断言中使用任何赋值、修改变量、资源操作、内存申请等操作。 例如,以下的断言方式是错误的:
ASSERT(p1 = p2); //p1被修改 ASSERT(i++ > 1000); //i被修改 ASSERT(close(fd) == 0);//fd被关闭
- 1
- 2
- 3
为了更加准确地发现错误的位置,每一条断言只校验一个条件。 下面的断言同时校验多个条件,在断言触发的时
候,无法判断到底是哪一个条件导致的错误:int Foo(int *array, int size) { ASSERT(array != NULL && size > 0 && size < MAX_SIZE); ... }
- 1
- 2
- 3
- 4
- 5
应该将每个条件分开:
int Foo(int *array, int size) { ASSERT(array != NULL); ASSERT(size > 0); ASSERT(size < MAX_SIZE); ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
对于设计成API的函数,必须对参数进行合法性判断,严禁在API实现过程中产生RASH。对API函数的参数进行ASSERT操作是没有意义的。 例如,对于提供应用服务器IP的平台公共API接口这样实现是错误的:
int GetServerIP(char *ip, size_t ipSize) { ASSERT(ip != NULL); ... }
- 1
- 2
- 3
- 4
- 5
公共接口API应当对输入参数进行代码检查:
int GetServerIP(char *ip, size_t ipSize) { if (ip == NULL) { ... } ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
不可重入函数在多线程环境下其执行结果不能达到预期效果,需谨慎使用。常见的不可重入函数包括:
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); ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
下面的代码,由于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); } ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
严禁使用C++的异常机制,所有的错误都应该通过错误值在函数之间传递并做相应的判断, 而不应该通过异常机制进行错误处理。 编码人员必须完全掌控整个编码过程,建立攻击者思维,增强安全编码意识,主动把握有可能出错的环节。而使用C++异常机制进行错误处理,会削弱编码人员的安全意识。 异常机制会打乱程序的正常执行流程,使程序结构更加复杂,原先申请的资源可能会得不到有效清理。 异常机制导致代码的复用性降低,使用了异常机制的代码,不能直接给不使用异常机制的代码复用。 异常机制在实现上依赖于编译器、操作系统、处理器,使用异常机制,导致程序执行性能降低。 在二进制层面,程序被加载后,异常处理函数增加了程序的被攻击面,攻击者可以通过覆盖异常处理函数地址,达到攻击的效果。 例外: 在接管C++语言本身抛出的异常(例如new失败、STL)、第三方库(例如IDL)抛出的异常时,可以使用异常机制,例如:
int len = ...; char *p = NULL; try { p = new char[len]; } catch (bad_alloc) { ... abort(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
相关指南:
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; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
严禁在线程内主动终止自身线程,线程函数在执行完毕后会自动、安全地退出。主动终止自身线程的操作,不仅导致代码复用性变差,同时容易导致资源泄漏错误。
程序应该安全退出,除了main函数以外,禁止任何地方调用exit、ExitProcess函数退出进程。直接退出进程会导致代码的复用性降低,资源得不到有效地清理。 程序应该通过错误值传递的机制进行错误处理。 以下代码加载文件,加载过程中如果出错,直接调用exit退出:
void LoadFile(const char *filePath) { FILE* fp = fopen(filePath, "rt"); if (fp == NULL) { exit(0); } ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
正确的做法应该通过错误值传递机制,例如:
BOOL LoadFile(const char *filePath) { BOOL ret = FALSE; FILE* fp = fopen(filePath, "rt"); if (fp != NULL) { ... } ... return ret; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
abort会导致程序立即退出,资源得不到清理。 例外: 只有发生致命错误,程序无法继续执行的时候,在错误处理函数中使用abort退出程序,例如:
void FatalError(int sig) { abort(); } int main(int argc, char *argv[]) { signal(SIGSEGV, FatalError); ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
调用格式化函数时,如果format参数由外部可控,会造成字符串格式化漏洞。 这些格式化函数有: 格式化输出函数:xxxprintf 格式化输入函数:xxxscanf 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line(); 格式化日志函数:syslog(),vsyslog()。
错误示例:char *msg = GetMsg(); ... printf(msg);
- 1
- 2
- 3
推荐做法:
char *msg = GetMsg(); ... printf("%s\n", msg);
- 1
- 2
- 3
相关指南:
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; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
输出: 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; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
输出: b = AB0000000
位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数。 错误示例:
int data = ReadByte(); int a = data >> 24;
- 1
- 2
推荐做法:(为简化示例代码,此处假设ReadByte函数实际不存在返回值小于0的情况)
unsigned int data = (unsigned int)ReadByte(); unsigned int a = data >> 24;
- 1
- 2
相关指南:
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<stdio.h>
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;
- 1
- 2
推荐做法:
char *ptr = ...; uintptr_t number = (uintptr_t)ptr;
- 1
- 2
相关指南:
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) ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
下面是正确的用法:
BOOL dealName(const char *nameA, const char *nameB) { ... if (nameA != NULL) ... if (nameB == NULL) ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
例外: 为检查地址对齐而对地址指针进行的位运算可以作为例外。
相关指南:
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 ?声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/240905
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。