赞
踩
变量声明赋予初值,可以避免由于编程人员的疏忽导致的变量未初始化引用。
示例:
SOCKET s = INVALID_SOCKET;
unsigned char *msg = NULL;
BOOL success = FALSE;
int fd = -1;
以下代码,由于变量声明未赋予初值,在最后free的时候出错。
char *message; // 错误!必须声明为 char *message = NULL;
...
if (condition) {
message = (char *)malloc(len);
...
}
...
if (message != NULL) {
free(message); //如果condition未满足,会造成free未初始化的内存。
}
资源释放后,对应的变量应该立即赋予新值,防止后续又被重新引用。如果释放语句刚好在变量作用域的最后一句,可以不进行赋值。
示例:
SOCKET s = INVALID_SOCKET;
unsigned char *msg = NULL;
int fd = -1;
...
closesocket(s);
s = INVALID_SOCKET;
...
free(msg);
msg = (unsigned char *)malloc(...); //msg变量又被赋予新值
...
close(fd);
fd = -1;
...
变量声明赋予初值,可以避免由于编程人员的疏忽导致的变量未初始化引用。
示例:
class CMsg {
public:
CMsg();
~CMsg();
protected:
int size;
unsigned char *msg;
};
CMsg::CMsg()
{
size = 0;
msg = NULL;
}
编码人员往往由于粗心,将指针当做数组进行sizeof操作,导致实际的执行结果与预期不符。 下面的代码,buffer和path分别是指针和数组,编码人员想对这2个内存进行清0操作,但由于编码人员的疏忽,第5行代码,将内存大小误写成了sizeof,与预期不符。
char *buffer = (char *)malloc(size);
char path[MAX_PATH] = {0};
...
memset(path, 0, sizeof(path));
memset(buffer, 0, sizeof(buffer));
如果要判断当前的指针类型大小,请使用sizeof(char *)的方式。
在变量声明前加const关键字,表示该变量不可被修改,这样就可以利用编译器进行类型检查,将代码的权限降到更低。
例如下面是不好的定义:
float pi = 3.14159f;
应当这样定义:
const float PI = 3.14159f;
应该尽可能减少全局变量的使用,如果多个线程会访问到该全局变量,则访问过程必须加锁。 以下代码中,g_list是全局变量,对链表进行搜索操作时,在while循环语句的前后加锁。
ItemList *g_list = NULL; ItemList *SearchList(const char *name) { Lock(); ItemList *p = g_list; while (p != NULL) { if (strcmp(p->name, name) == 0) { break; } p = p->next; } UnLock(); return p; }
性能敏感的代码,请考虑采用原子操作或者无锁算法。
程序在运行期间,函数内的局部变量保存在栈中,栈的大小是有限的。如果申请过大的静态数组,可能导致出现运行出错。 建议在申请静态数组的时候,大小不超过0x1000。 下面的代码,buff申请过大,导致栈空间不够,程序发生stackoverflow异常。
#define MAX_BUFF 0x1000000
int Foo()
{
char buff[MAX_BUFF] = {0};
...
}
断言是一种除错机制,用于验证代码是否符合编码人员的预期。编码人员在开发期间应该对函数的参数、代码中间执行结果合理地使用断言机制,确保程序的缺陷尽量在测试阶段被发现。 断言被触发后,说明程序出现了不应该出现的严重错误,程序会立即提示错误,并终止执行。 断言必须用宏进行定义,只在调试版本有效,最终发布版本不允许出现assert函数,例如:
#include <assert.h>
#ifdef DEBUG
#define ASSERT(f) assert(f)
#else
#define ASSERT(f) ((void)0)
#endif
下面的函数VerifyUser,上层调用者会保证传进来的参数是合法的字符串,不可能出现传递非法参数的情况。因
此,在该函数的开头,加上4个ASSERT进行校验。
BOOL VerifyUser(const char *userName, const char *password)
{
ASSERT(userName != NULL);
ASSERT(strlen(userName) > 0);
ASSERT(password != NULL);
ASSERT(strlen(password) > 0);
...
}
以下的switch,由于不可能出现default的情况,所以在default处直接调用ASSERT:
enum { COLOR_RED = 1, COLOR_GREEN, COLOR_BLUE }; ... switch (color) { case COLOR_RED: ... case COLOR_GREEN: ... case COLOR_BLUE: ... default: { ASSERT(0); } }
以下代码,SendMsg是CMsg类的成员函数,socketID是成员变量,在调用SendMsg的时候必须保证socketID已经
被初始化,因此在此处用ASSERT判断socketID的合法性。
CMsg::CMsg()
{
socketID = INVALID_SOCKET;
}
int CMsg::SendMsg(const char *msg, int len)
{
ASSERT(socketID != INVALID_SOCKET);
...
ret = send(socketID, msg, len, 0);
...
}
在linux内核中定义ASSERT宏,可以采用如下方式:
#ifdef DEBUG
#define ASSERT(f) BUG_ON(!(f))
#else
#define ASSERT(f) ((void)0)
#endif
断言只能在调试版使用,断言被触发后,程序会立即退出,因此严禁在正式发布版本使用断言,请通过编译选项进行控制。 错误用法如:
int Foo(int *array, int size)
{
assert(array != NULL);
...
}
断言不能用于校验程序在运行期间可能导致的错误。 以下代码的所有ASSERT的用法是错误的。
FILE *fp = fopen(path, "r");
ASSERT(fp != NULL); //文件有可能打开失败
char *str = (char *)malloc(MAX_LINE
ASSERT(str != NULL); //内存有可能分配失败
ReadLine(fp, str);
char *p = strstr(str, 'age=');
ASSERT(p != NULL); //文件中不一定存在该字符串
int age = atoi(p+4);
ASSERT(age > 0); //文件内容不一定符合预期
为了更加准确地发现错误的位置,每一条断言只校验一个条件。 下面的断言同时校验多个条件,在断言触发的时候,无法判断到底是哪一个条件导致的错误:
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);
...
}
通过函数参数传递数组或一块内存进行写操作时,函数参数必须同时传递数组元素个数或所传递的内存块大小,否则函数在使用数组下标或访问内存偏移时,无法判断下标或偏移的合法范围,产生越界访问的漏洞。 以下代码中,函数ParseMsg不知道msg的范围,容易产生内存越界访问漏洞。
int ParseMsg(BYTE *msg)
{
...
}
...
size_t len = ...
BYTE *msg = (BYTE *)malloc(len); //此处分配的内存块等同于字节数组
...
ParseMsg(msg);
...
正确的做法是将msg的大小作为参数传递到ParseMsg中,如下代码:
int ParseMsg(BYTE *msg, size_t msgLen)
{
ASSERT(msg != NULL);
ASSERT(msgLen != 0);
...
}
...
size_t len = ...
BYTE *msg = (BYTE *)malloc(len);
...
ParseMsg(msg, len);
...
如果参数是指针型参数,且内容不会被修改,请定义为const类型。
int Foo(const char *filePath)
{
...
int fd = open(filePath, ...);
...
}
如果字符串或者指针作为函数参数,为了防止空指针引用错误,在引用前必须确保该参数不为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();
}
构造函数没有返回值,不能做错误判断,因此在构造函数内,不能做任何有可能失败的操作。 下面的代码中,
open、new、ConnectServer都有可能失败,这些操作不应该放在构造函数内。
CFoo::CFoo()
{
int fd = open(...);
char *str = new char[...];
BOOL b = ConnectServer(...);
...
}
构造函数内仅作成员变量的初始化工作,其他的操作通过成员函数完成。
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; }
类成员进行定义的时候,需要考虑类的功能,尽量减少对外接口的暴露。
atexit函数注册若干个有限的函数,当exit被调用后,自动调用由atexit事先注册的函数。 当资源不再使用后,编码人员应该立即主动地进行清理,而不应该在最终程序退出后通过事先注册的例程被动地清理。
例外: 作为服务维测监控功能,为定位程序异常退出原因的模块,可以作为例外使用atexit()函数。
调用kill、TerminateProcess等函数强行终止其他进程(如kill -9),会导致其他进程的资源得不到清理。 对于进程间通信,应该主动发送一个停止命令,通知对方进程安全退出。 当发送给对方进程退出信号后,在等待一定时间内如果对方进程仍然未退出,可以调用kill、TerminateProcess函数。
if (WaitForRemoteProcessExit(...) == TIME_OUT) {
kill(...); //目标进程在限定时间内仍然未退出,强行结束目标进程
}
严禁在线程内主动终止自身线程,线程函数在执行完毕后会自动、安全地退出。主动终止自身线程的操作,不仅导致代码复用性变差,同时容易导致资源泄漏错误。
程序应该安全退出,除了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);
...
}
部分字符串处理函数由于设计时安全考虑不足,或者存在一些隐含的目的缓冲区长度要求,容易被误用,导致缓冲区写溢出。典型函数如itoa,realpath。 以下的代码,试图将数字转为字符串,但是目标存储空间的长度不足。
int num = ... char str[8] = {0}; itoa(num, str, 10); // 10进制整数的最大存储长度是12个字节 以下的代码,试图将路径标准化,但是目标存储空间的长度不足。 char resolvedPath[100] = {0}; realpath(path, resolvedPath); //realpath函数的存储缓冲区长度是由PATH_MAX常量定义,或是由 _PC_PATH_MAX系统值配置的,通常都大于100字节 以下的代码,在对外部数据进行解析并将内容保存到name中,考虑了name的大小,是正确的做法。 char *msg = GetMsg(); ... char name[MAX_NAME] = {0}; int i=0; //必须考虑msg不包含预期的字符’\n’ while (*msg != '\0' && *msg != '\n' && i < sizeof(name) - 1) { name[i++] = *msg++; } name[i] = '\0'; //保证最后有’\0’
对字符串进行存储操作,必须确保字符串有’\0’结束符,否则在后续的调用strlen等操作中,可能会导致内存越界访问漏洞。
外部数据作为数组索引对内存进行访问时,必须对数据的大小进行严格的校验,否则为导致严重的错误。 下面的代码,通过if语句判断offset的合法性:
int Foo(BYTE *buffer, int size)
{
...
int offset = ReadIntFromMsg();
if (offset >= 0 && offset < size) {
BYTE c = buffer[offset];
...
}
...
}
在调用内存操作相关的函数时(例如memcpy、memmove、memcpy_s、memmove_s等),如果复制长度外部可控,则必须校验其合法性,否则容易导致内存溢出。 下例中,循环长度来自设备外部报文,由于没有校验大小,可造成缓冲区溢出:
typedef struct BigIntType {
unsigned int length;
char val[MAX_INT_DIGITS];
}BigInt;
BigInt *AsnOctsToBigInt(const AsnOcts *asnOcts)
{
BigInt *bigNumber = NULL;
...
for (i = 0; i < asnOcts->octetLen; i++) {
bigNumber->val[i] = asnOcts->octs[i];
}
...
}
调用格式化函数时,如果format参数由外部可控,会造成字符串格式化漏洞。 这些格式化函数有:
char *msg = GetMsg();
...
printf(msg);
推荐做法:
char *msg = GetMsg();
...
printf("%s\n", msg);
在计算机中,整数存储的长度是固定的(例如32位或64位),当整数之间进行运算时,可能会超过这个最大固定长度,导致整数溢出或反转,使得实际计算结果与预期结果不符。 如果涉及到除法或者求余操作,必须确保除数不为0。
错误示例1:
size_t width = ReadByte();
size_t height = ReadByte();
size_t total = width * height; //可能整数溢出
void *bitmaps = malloc(total);
推荐做法1:
size_t width = ReadByte();
size_t height = ReadByte();
if (width == 0 || height == 0 || width > MAX_WIDTH || height > MAX_HEIGHT) {
//error
...
}
size_t total = width * height; // MAX_WIDTH * MAX_HEIGHT 不会溢出
void *bitmaps = malloc(total);
错误示例2:
size_t a = ReadByte();
size_t b = 1000 / a; //a可能是0
size_t c = 1000 % a; //a可能是0
...
推荐做法2:
size_t a = ReadByte();
if (a == 0) {
//error
...
}
size_t b = 1000 / a; //a不可能是0
size_t c = 1000 % a; //a不可能是0
...
由于整数在运算过程中可能溢出,当运算结果赋值给比他更大类型,或者和比他更大类型进行比较时,会导致实际结果与预期结果不符。 请观察以下二个代码及其输出:
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 data = ReadByte();
int a = data >> 24;
推荐做法:(为简化示例代码,此处假设ReadByte函数实际不存在返回值小于0的情况)
unsigned int data = (unsigned int)ReadByte();
unsigned int a = data >> 24;
指针的大小随着平台的不同而不同,强行进行整数与指针间的互相转化,降低了程序的兼容性,在转换过程中可能引起指针高位信息的丢失。
错误示例:
char *ptr = ...;
unsigned int number = (unsigned int)ptr;
推荐做法:
char *ptr = ...;
uintptr_t number = (uintptr_t)ptr;
对指针进行逻辑运算,会导致指针的性质改变,可能产生内存非法访问的问题。 下面是错误的用法:
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)
...
}
例外: 为检查地址对齐而对地址指针进行的位运算可以作为例外。
如下示例中,由于循环条件受外部输入的报文内容控制,可进入死循环:
unsigned char *FindAttr(unsigned char type, unsigned char *msg, size_t inputMsgLen)
{
...
msgLength = ntohs(*(unsigned short *)&msg[RD_LEA_PKT_LENGTH]);
...
while (msgLength != 0) {
attrType = msg[0];
attrLength = msg[RD_LEA_PKT_LENGTH];
...
msgLength -= attrLength;
msg += attrLength;
}
...
}
此例中,需要检查报文的实际可读长度,报文内容提供的循环增量(避免为0),以防止缓冲区溢出。
内存申请的大小可能来自于外部数据,必须检查其合法性,防止过多地、非法地申请内存。不能申请0长度的内
存。 例如:
int Foo(int size)
{
if (size <= 0) {
//error
...
}
...
char *msg = (char *)malloc(size);
...
}
char *msg = (char *)malloc(size);
if (msg != NULL) {
...
}
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;
}
悬挂指针可能会导致双重释放(double-free)以及访问已释放内存的危险。消除悬挂指针以及消除众多与内存相
关危险的一个最为有效地方法就是当指针使用完后将其置新值。 如果一个指针释放后能够马上离开作用域,因为它
已经不能被再次访问,因此可以无需对其赋予新值。
示例:
char *message = NULL;
...
message = (char *)malloc(len);
...
if (...) {
free(message); //在这个分支内对内存进行了释放
message = NULL; //释放后将指针赋值为NULL
}
...
if (message != NULL) {
free(message);
message = NULL;
}
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。
创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件。 下列代码没有显式配置文件的访问权限。
int fd = open(fileName, O_CREAT | O_WRONLY); //【错误】缺少访问权限设置
推荐做法:
int fd = open(fileName, O_CREAT | O_WRONLY, S_IRUSR|S_IWUSR);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。