C/C++ 使用错误
1.1 【必须】不得直接使用无长度限制的字符拷贝函数
不应直接使用legacy的字符串拷贝、输入函数,如strcpy、strcat、sprintf、wcscpy、mbscpy等,这些函数的特征是:可以输出一长串字符串,而不限制长度。如果环境允许,应当使用其_s安全版本替代,或者使用n版本函数(如:snprintf,vsnprintf)。
若使用形如sscanf之类的函数时,在处理字符串输入时应当通过%10s这样的方式来严格限制字符串长度,同时确保字符串末尾有\0。如果环境允许,应当使用_s安全版本。
但是注意,虽然MSVC 2015时默认引入结尾为0版本的snprintf
(行为等同于C99定义的snprintf
)。但更早期的版本中,MSVC的snprintf
可能是_snprintf
的宏。而_snprintf
是不保证\0结尾的(见本节后半部分)。
(MSVC)
Beginning with the UCRT in Visual Studio 2015 and Windows 10, snprintf is no longer identical to _snprintf. The snprintf function behavior is now C99 standard compliant.
从Visual Studio 2015和Windows 10中的UCRT开始,snprintf不再与_snprintf相同。snprintf函数行为现在符合C99标准。
请参考:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?redirectedfrom=MSDN&view=vs-2019
因此,在使用n系列拷贝函数时,要确保正确计算缓冲区长度,同时,如果你不确定是否代码在各个编译器下都能确保末尾有0时,建议可以适当增加1字节输入缓冲区,并将其置为\0,以保证输出的字符串结尾一定有\0。
// Good
char buf[101] = {0};
snprintf(buf, sizeof(buf) - 1, "foobar ...", ...);
一些需要注意的函数,例如strncpy
和_snprintf
是不安全的。 strncpy
不应当被视为strcpy
的n系列函数,它只是恰巧与其他n系列函数名字很像而已。strncpy
在复制时,如果复制的长度超过n,不会在结尾补\0。
同样,MSVC _snprintf
系列函数在超过或等于n时也不会以0结尾。如果后续使用非0结尾的字符串,可能泄露相邻的内容或者导致程序崩溃。
// Bad
char a[4] = {0};
_snprintf(a, 4, "%s", "AAAA");
foo = strlen(a);
上述代码在MSVC中执行后, a[4] == 'A',因此字符串未以0结尾。a的内容是"AAAA",调用strlen(a)
则会越界访问。因此,正确的操作举例如下:
// Good
char a[4] = {0};
_snprintf(a, sizeof(a), "%s", "AAAA");
a[sizeof(a) - 1] = '\0';
foo = strlen(a);
在 C++ 中,强烈建议用 string
、vector
等更高封装层次的基础组件代替原始指针和动态数组,对提高代码的可读性和安全性都有很大的帮助。
关联漏洞:
中风险-信息泄露
低风险-拒绝服务
高风险-缓冲区溢出
1.2 【必须】创建进程类的函数的安全规范
system、WinExec、CreateProcess、ShellExecute等启动进程类的函数,需要严格检查其参数。
启动进程需要加上双引号,错误例子:
// Bad
WinExec("D:\\program files\\my folder\\foobar.exe", SW_SHOW);
当存在D:\program files\my.exe
的时候,my.exe会被启动。而foobar.exe不会启动。
// Good
WinExec("\"D:\\program files\\my folder\\foobar.exe\"", SW_SHOW);
另外,如果启动时从用户输入、环境变量读取组合命令行时,还需要注意是否可能存在命令注入。
// Bad
std::string cmdline = "calc ";
cmdline += user_input;
system(cmdline.c_str());
比如,当用户输入1+1 && ls
时,执行的实际上是calc 1+1和ls 两个命令,导致命令注入。
需要检查用户输入是否含有非法数据。
// Good
std::string cmdline = "ls ";
cmdline += user_input;
if(cmdline.find_first_not_of("1234567890.+-*/e ") == std::string::npos)
system(cmdline.c_str());
else
warning(...);
关联漏洞:
高风险-代码执行
高风险-权限提升
1.3 【必须】尽量减少使用 _alloca 和可变长度数组
_alloca 和可变长度数组使用的内存量在编译期间不可知。尤其是在循环中使用时,根据编译器的实现不同,可能会导致:(1)栈溢出,即拒绝服务; (2)缺少栈内存测试的编译器实现可能导致申请到非栈内存,并导致内存损坏。这在栈比较小的程序上,例如IoT设备固件上影响尤为大。对于 C++,可变长度数组也属于非标准扩展,在代码规范中禁止使用。
错误示例:
// Bad
for (int i = 0; i < 100000; i++) {
char* foo = (char *)_alloca(0x10000);
..do something with foo ..;
}
void Foo(int size) {
char msg[size]; // 不可控的栈溢出风险!
}
正确示例:
// Good
// 改用动态分配的堆内存
for (int i = 0; i < 100000; i++) {
char * foo = (char *)malloc(0x10000);
..do something with foo ..;
if (foo_is_no_longer_needed) {
free(foo);
foo = NULL;
}
}
void Foo(int size) {
std::string msg(size, '\0'); // C++
char* msg = malloc(size); // C
}
关联漏洞:
低风险-拒绝服务
高风险-内存破坏
1.4 【必须】printf系列参数必须对应
所有printf系列函数,如sprintf,snprintf,vprintf等必须对应控制符号和参数。
错误示例:
// Bad
const int buf_size = 1000;
char buffer_send_to_remote_client[buf_size] = {0};
snprintf(buffer_send_to_remote_client, buf_size, "%d: %p", id, some_string); // %p 应为 %s
buffer_send_to_remote_client[buf_size - 1] = '\0';
send_to_remote(buffer_send_to_remote_client);
正确示例:
// Good
const int buf_size = 1000;
char buffer_send_to_remote_client[buf_size] = {0};
snprintf(buffer_send_to_remote_client, buf_size, "%d: %s", id, some_string);
buffer_send_to_remote_client[buf_size - 1] = '\0';
send_to_remote(buffer_send_to_remote_client);
前者可能会让client的攻击者获取部分服务器的原始指针地址,可以用于破坏ASLR保护。
关联漏洞:
中风险-信息泄露
1.5 【必须】防止泄露指针(包括%p)的值
所有printf系列函数,要防止格式化完的字符串泄露程序布局信息。例如,如果将带有%p的字符串泄露给程序,则可能会破坏ASLR的防护效果。使得攻击者更容易攻破程序。
%p的值只应当在程序内使用,而不应当输出到外部或被外部以某种方式获取。
错误示例:
// Bad
// 如果这是暴露给客户的一个API:
uint64_t GetUniqueObjectId(const Foo* pobject) {
return (uint64_t)pobject;
}
正确示例:
// Good
uint64_t g_object_id = 0;
void Foo::Foo() {
this->object_id_ = g_object_id++;
}
// 如果这是暴露给客户的一个API:
uint64_t GetUniqueObjectId(const Foo* object) {
if (object)
return object->object_id_;
else
error(...);
}
关联漏洞:
中风险-信息泄露
1.6 【必须】不应当把用户可修改的字符串作为printf系列函数的“format”参数
如果用户可以控制字符串,则通过 %n %p 等内容,最坏情况下可以直接执行任意恶意代码。
在以下情况尤其需要注意: WIFI名,设备名……
错误:
snprintf(buf, sizeof(buf), wifi_name);
正确:
snprinf(buf, sizeof(buf), "%s", wifi_name);
关联漏洞:
高风险-代码执行
高风险-内存破坏
中风险-信息泄露
低风险-拒绝服务
1.7 【必须】对数组delete时需要使用delete[]
delete []操作符用于删除数组。delete操作符用于删除非数组对象。它们分别调用operator delete[]和operator delete。
// Bad
Foo* b = new Foo[5];
delete b; // trigger assert in DEBUG mode
在new[]返回的指针上调用delete将是取决于编译器的未定义行为。代码中存在对未定义行为的依赖是错误的。
// Good
Foo* b = new Foo[5];
delete[] b;
在 C++ 代码中,使用 string
、vector
、智能指针(比如std::unique_ptr<T[]>)等可以消除绝大多数 delete[]
的使用场景,并且代码更清晰。
关联漏洞:
高风险-内存破坏
中风险-逻辑漏洞
低风险-内存泄漏
低风险-拒绝服务
1.8【必须】注意隐式符号转换
两个无符号数相减为负数时,结果应当为一个很大的无符号数,但是小于int的无符号数在运算时可能会有预期外的隐式符号转换。
// 1
unsigned char a = 1;
unsigned char b = 2;
if (a - b < 0) // a - b = -1 (signed int)
a = 6;
else
a = 8;
// 2
unsigned char a = 1;
unsigned short b = 2;
if (a - b < 0) // a - b = -1 (signed int)
a = 6;
else
a = 8;
上述结果均为a=6
// 3
unsigned int a = 1;
unsigned short b = 2;
if (a - b < 0) // a - b = 0xffffffff (unsigned int)
a = 6;
else
a = 8;
// 4
unsigned int a = 1;
unsigned int b = 2;
if (a - b < 0) // a - b = 0xffffffff (unsigned int)
a = 6;
else
a = 8;
上述结果均为a=8
如果预期为8,则错误代码:
// Bad
unsigned short a = 1;
unsigned short b = 2;
if (a - b < 0) // a - b = -1 (signed int)
a = 6;
else
a = 8;
正确代码:
// Good
unsigned short a = 1;
unsigned short b = 2;
if ((unsigned int)a - (unsigned int)b < 0) // a - b = 0xffff (unsigned short)
a = 6;
else
a = 8;
关联漏洞:
中风险-逻辑漏洞
1.9【必须】注意八进制问题
代码对齐时应当使用空格或者编辑器自带的对齐功能,谨慎在数字前使用0来对齐代码,以免不当将某些内容转换为八进制。
例如,如果预期为20字节长度的缓冲区,则下列代码存在错误。buf2为020(OCT)长度,实际只有16(DEC)长度,在memcpy后越界:
// Bad
char buf1[1024] = {0};
char buf2[0020] = {0};
memcpy(buf2, somebuf, 19);
应当在使用8进制时明确注明这是八进制。
// Good
int access_mask = 0777; // oct, rwxrwxrwx
关联漏洞:
中风险-逻辑漏洞
更多建议: