1、注意:strncpy、strncat等带n版本的字符串操作函数在源字符串长度超出n标识的长度时,会将包括’\0’结束符在内的超长字符串截断,导致’\0’结束符丢失。这时需要手动为目标字符串设置’\0’结束符。
char dst[11]; // 【注意】最好每次定义时初始化为0: dst[11] = {0};
char src[] = "0123456789";
char *tmp = NULL;
memset(dst, ‘@‘, sizeof(dst));
memcpy(dst, src, strlen(src));
dst[sizeof(dst) - 1] = ’\0’; //【修改】dst以’\0’结尾
2、避免字符串/内存操作函数的源指针和目标指针指向内存重叠区
在使用像memcpy、strcpy、strncpy、sscanf()、sprintf()、snprintf()和wcstombs()这样的函数时,复制重叠对象会存在未定义的行为,这种行为可能破坏数据的完整性。
memcpy与memmove的目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中。
但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增加了一点点开销。
memmove的处理措施:
?当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
?当源内存的首地址大于目标内存的首地址时,实行正向拷贝
?当源内存的首地址小于目标内存的首地址时,实行反向拷贝
3、使用格式化函数时推荐使用精度说明符
#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、确保无符号整数运算时不会出现反转
无符号数u1 u2,在计算u1+u2时,需要判断u1+u2是否大于UINT_MAX
if((UINT_MAX - ui1) < ui2) //【修改】确保无符号整数运算时不会出现反转
{
return ERROR;
}
else
{
*ret = ui1+ ui2;
}
5、确保有符号整数运算时不会出现溢出
INT32 si1, INT32 si2;
INT64 tmp = (INT64)si1 *(INT64)si2; /*【修改】确保有符号整数运算时不会出现溢出 */
//++ 将32位有符号数转换成64位,并且计算完成后需要比较结果是否有符号数的范围内
if((INT_MAX < tmp) || (INT_MIN > tmp))
{
return ERROR;
}
6、确保整型转换时不会出现截断错误、符号错误
【截断错误】将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。
【符号错误】从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。带符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。
//++[符号错误绕过长度检查]
int length; //++ 声明为无符号数
char buf[BUF_SIZE];
length = atoi(argv[1]); //【错误】atoi返回值可能为负数
if (length < BUF_SIZE) // len为负数,长度检查无效
{
memcpy(buf, argv[2], length); /* 带符号的len被转换为size_t类型的无符号整数,负值被解释为一个极大的正整数。memcpy()调用时引发buf缓冲区溢出*/
printf("Data copied\n");
}
7、把整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值
UINT32 blockNum;
UINT64 alloc = (UINT64)blockNum * 16; /*【修改】确保整型表达式转换时不出现数值错误 */
//++ 先将两个32位的数之积转换为64位再赋值给64位的变量。
8、避免对有符号整数进行位操作符运算
说明:位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数,因为有符号整数上的有些位操作的结果是由编译器所决定的,可能会出现出乎意料的行为或编译器定义的行为。
9、内存管理
申请内存后初始化(memset )
禁止内存指针移动后(非malloc分配后的起始值),通过该指针释放内存,会出现未知错误。因为malloc一块内存后,它的前一个字节存放了分配的内存大小,free时会根据该字节所代表的大小去free内存。
10、禁止调用OS命令解析器执行命令或运行程序,防止命令注入
禁止使用system()和popen()。替代方案是POSIX的exec系列函数或Win32 API CreateProcess()等与命令解释器无关的进程创建函数来替代。
错误示例:
system(sprintf("any_exe %s", input)); //【错误】参数不是硬编码,禁止使用system
这行代码是需要执行一个名为any_exe的程序,程序参数来自用户的输入input。这种情况下,恶意用户输入参数:
happy; useradd attacker
最终shell将字符串”any_exe happy; useradd attacker”解释为两条独立的命令连续执行:
any_exe happy
useradd attacker
这样攻击者通过注入了一条命令”useradd attacker”创建了一个新用户。这明显不是程序所希望的。
改用:
if (execve("/usr/bin/any_exe", args, envs) == -1) /*【修改】使用execve代替system */
11、禁止使用std::ostrstream,推荐使用std::ostringstream
说明: std::ostrstream的使用上需要特别注意几点:
(1)str() 会调用成员函数freeze(),它会冻结字符序列,当缓冲区不够大以至于需要分配新缓冲区时,这么做可以避免事情变得复杂。
(2)str()不会附加字符串终止符号(’\0’)。
(3)data()返回所有字符串,没有附带’\0’结尾字符(目前有些编译器自动调用c_str方法了)。
上面如果不注意,就可能会导致内存访问越界、缓冲区溢出等问题,所以建议不要使用ostrstream。[C++03]标准将std::strstream标明为deprecated,替代方案是std::stringstream。ostringstream没有上述问题。
错误示例:下列代码使用了std::ostrstream,可能会导致内存访问越界等问题。
void NoCompliant()
{
std::ostrstream mystr; //【错误】不要使用std::ostrstream
mystr << “hello world”;
// ostream.str方法返回的指针,没有空结束符,容易造成问题
char *p = mystr.str();
std::cout << mystr.str() << std::endl;
}
12、C++中,必须使用C++标准库替代C的字符串操作函数
C标准的系列字符串处理函数strcpy/strcat/sprintf/scanf/gets,不检查目标缓冲区的大小,容易引入缓冲区溢出的安全漏洞。
C++标准库提供了字符串类抽象的一个公共实现std::string,支持字符串的常规操作
13、必须使用int类型来接收字符输入/输出函数的返回值,不要使用char型
说明:字符输入/输出函数fgetc()、getc()和getchar()都从一个流读取一个字符,并把它以int值的形式返回。如果这个流到达了文件尾或者发生读取错误,函数返回EOF。fputc()、putc()、putchar()和ungetc()也返回一个字符或EOF。
如果这些I/O函数的返回值需要与EOF进行比较,不要将返回值转换为char类型。
因为char是有符号8位的值,int是32位的值。如果getchar()返回的字符的ASCII值为0xFF,转换为char类型后将被解释为EOF。0xFF这个值被有符号扩展后是0xFFFFFFFF,刚好等于EOF的值。
注意:对于sizeof(int) == sizeof(char)的平台,用int接收返回值也可能无法与EOF区分,这时要用feof()和ferror()检测文件尾和文件错误。
14、文件路径验证前,必须对其进行标准化
说明:当文件路径来自非信任域时,需要先将文件路径规范化再做校验。路径在验证时会有很多干扰因素,如相对路径与绝对路径,如文件的符号链接、硬链接、快捷路径、别名等。
所以在验证路径时需要对路径进行标准化,使得路径表达唯一化、无歧义。
如果没有作标准化处理,攻击者就有机会:
推荐做法:
Linux下对文件进行标准化,可以防止黑客通过构造指向系统关键文件的链接文件。realpath() 函数返回绝对路径,删除了所有符号链接:
void Compliant(char *lpInputPath)
{
char realpath[MAX_PATH];
if ( realpath(inputPath, realpath) == NULL)
/* handle error */;
/*... do something ...*/
}
Windows下可以使用PathCanonicalize函数对文件路径进行标准化:
void Compliant(char *lpInputPath)
{
char realpath[MAX_PATH];
char *lpRealPath = realpath;
if ( PathCanonicalize(lpRealPath,lpInputPath) == NULL)
/* handle error */;
/*... do something ...*/
}
15、访问文件时尽量使用文件描述符代替文件名作为输入,以避免竞争条件问题
说明:该建议应用场景如下,当对文件的元信息进行操作时(比如修改它的所有者、对文件进行统计,或者修改它的权限位),首先要打开该文件,然后对打开的文件进行操作。只要有可能,应尽量避免使用获取文件名的操作,而是使用获取文件描述符的操作。这样做将避免文件在程序运行时被替换(一种可能的竞争条件)。
例如,当access()和open()两者都利用一个字符串参数而不是一个文件句柄来进行相关操作时,攻击者就可以通过在access()和open()之间的间隙替换掉原来的文件,如下所示:
错误示例:下列代码使用access()函数,可能引发竞争条件问题。
void Noncompliant(char * file)
{
if(!access(file, W_OK)) //【不推荐】不要使用函数access(),易引发条件竞争
{
f = fopen(file, "w+");
/*...*/
/* close f after operate(f)*/
}
else
{
fprintf(stderr, "Unable to open file %s.\n", file);
}
}
16、正确处理容器的erase()方法与迭代子的关系
说明:调用容器的erase(iter)方法后,迭代子指向的对象被析构,迭代子已经失效,如果再对迭代子执行递增递减或者引用操作会导致程序崩溃。
//++ 错误用法:
m_mapID2NE.erase(iter);
iter++; //【错误】erase后,iter指向的对象可能已失效
//++ 正确用法:
m_mapID2NE.erase(iter++); //【修改】将迭代子后置递增作为erase参数
也可以使用earse方法的返回值来保存迭代子,因为返回的是被删除元素迭代子指向的下一个元素位置:iter = erase(iter)
。
注意这种用法可以用于list和vector的erase(),但不适用于map。因为std::map::erase()的返回值在不同STL实现版本是有差异的,有的有返回值,有的没有返回值,所以对map只能使用推荐做法。