1 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结尾的(见本节后半部分)。

  1. MSVC
  2. 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.
  3. Visual Studio 2015Windows 10中的UCRT开始,snprintf不再与_snprintf相同。snprintf函数行为现在符合C99标准。
  4. 请参考: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。

  1. // Good
  2. char buf[101] = {0};
  3. snprintf(buf, sizeof(buf) - 1, "foobar ...", ...);

一些需要注意的函数,例如strncpy_snprintf是不安全的。 strncpy不应当被视为strcpy的n系列函数,它只是恰巧与其他n系列函数名字很像而已。strncpy在复制时,如果复制的长度超过n,不会在结尾补\0。

同样,MSVC _snprintf系列函数在超过或等于n时也不会以0结尾。如果后续使用非0结尾的字符串,可能泄露相邻的内容或者导致程序崩溃。

  1. // Bad
  2. char a[4] = {0};
  3. _snprintf(a, 4, "%s", "AAAA");
  4. foo = strlen(a);

上述代码在MSVC中执行后, a[4] == ‘A’,因此字符串未以0结尾。a的内容是”AAAA”,调用strlen(a)则会越界访问。因此,正确的操作举例如下:

  1. // Good
  2. char a[4] = {0};
  3. _snprintf(a, sizeof(a), "%s", "AAAA");
  4. a[sizeof(a) - 1] = '\0';
  5. foo = strlen(a);

在 C++ 中,强烈建议用 stringvector 等更高封装层次的基础组件代替原始指针和动态数组,对提高代码的可读性和安全性都有很大的帮助。

关联漏洞:

中风险-信息泄露

低风险-拒绝服务

高风险-缓冲区溢出

1.2 【必须】创建进程类的函数的安全规范

system、WinExec、CreateProcess、ShellExecute等启动进程类的函数,需要严格检查其参数。

启动进程需要加上双引号,错误例子:

  1. // Bad
  2. WinExec("D:\\program files\\my folder\\foobar.exe", SW_SHOW);

当存在D:\program files\my.exe的时候,my.exe会被启动。而foobar.exe不会启动。

  1. // Good
  2. WinExec("\"D:\\program files\\my folder\\foobar.exe\"", SW_SHOW);

另外,如果启动时从用户输入、环境变量读取组合命令行时,还需要注意是否可能存在命令注入。

  1. // Bad
  2. std::string cmdline = "calc ";
  3. cmdline += user_input;
  4. system(cmdline.c_str());

比如,当用户输入1+1 && ls时,执行的实际上是calc 1+1和ls 两个命令,导致命令注入。

需要检查用户输入是否含有非法数据。

  1. // Good
  2. std::string cmdline = "ls ";
  3. cmdline += user_input;
  4. if(cmdline.find_first_not_of("1234567890.+-*/e ") == std::string::npos)
  5. system(cmdline.c_str());
  6. else
  7. warning(...);

关联漏洞:

高风险-代码执行

高风险-权限提升

1.3 【必须】尽量减少使用 _alloca 和可变长度数组

_alloca 和可变长度数组使用的内存量在编译期间不可知。尤其是在循环中使用时,根据编译器的实现不同,可能会导致:(1)栈溢出,即拒绝服务; (2)缺少栈内存测试的编译器实现可能导致申请到非栈内存,并导致内存损坏。这在栈比较小的程序上,例如IoT设备固件上影响尤为大。对于 C++,可变长度数组也属于非标准扩展,在代码规范中禁止使用。

错误示例:

  1. // Bad
  2. for (int i = 0; i < 100000; i++) {
  3. char* foo = (char *)_alloca(0x10000);
  4. ..do something with foo ..;
  5. }
  6. void Foo(int size) {
  7. char msg[size]; // 不可控的栈溢出风险!
  8. }

正确示例:

  1. // Good
  2. // 改用动态分配的堆内存
  3. for (int i = 0; i < 100000; i++) {
  4. char * foo = (char *)malloc(0x10000);
  5. ..do something with foo ..;
  6. if (foo_is_no_longer_needed) {
  7. free(foo);
  8. foo = NULL;
  9. }
  10. }
  11. void Foo(int size) {
  12. std::string msg(size, '\0'); // C++
  13. char* msg = malloc(size); // C
  14. }

关联漏洞:

低风险-拒绝服务

高风险-内存破坏

1.4 【必须】printf系列参数必须对应

所有printf系列函数,如sprintf,snprintf,vprintf等必须对应控制符号和参数。

错误示例:

  1. // Bad
  2. const int buf_size = 1000;
  3. char buffer_send_to_remote_client[buf_size] = {0};
  4. snprintf(buffer_send_to_remote_client, buf_size, "%d: %p", id, some_string); // %p 应为 %s
  5. buffer_send_to_remote_client[buf_size - 1] = '\0';
  6. send_to_remote(buffer_send_to_remote_client);

正确示例:

  1. // Good
  2. const int buf_size = 1000;
  3. char buffer_send_to_remote_client[buf_size] = {0};
  4. snprintf(buffer_send_to_remote_client, buf_size, "%d: %s", id, some_string);
  5. buffer_send_to_remote_client[buf_size - 1] = '\0';
  6. send_to_remote(buffer_send_to_remote_client);

前者可能会让client的攻击者获取部分服务器的原始指针地址,可以用于破坏ASLR保护。

关联漏洞:

中风险-信息泄露

1.5 【必须】防止泄露指针(包括%p)的值

所有printf系列函数,要防止格式化完的字符串泄露程序布局信息。例如,如果将带有%p的字符串泄露给程序,则可能会破坏ASLR的防护效果。使得攻击者更容易攻破程序。

%p的值只应当在程序内使用,而不应当输出到外部或被外部以某种方式获取。

错误示例:

  1. // Bad
  2. // 如果这是暴露给客户的一个API:
  3. uint64_t GetUniqueObjectId(const Foo* pobject) {
  4. return (uint64_t)pobject;
  5. }

正确示例:

  1. // Good
  2. uint64_t g_object_id = 0;
  3. void Foo::Foo() {
  4. this->object_id_ = g_object_id++;
  5. }
  6. // 如果这是暴露给客户的一个API:
  7. uint64_t GetUniqueObjectId(const Foo* object) {
  8. if (object)
  9. return object->object_id_;
  10. else
  11. error(...);
  12. }

关联漏洞:

中风险-信息泄露

1.6 【必须】不应当把用户可修改的字符串作为printf系列函数的“format”参数

如果用户可以控制字符串,则通过 %n %p 等内容,最坏情况下可以直接执行任意恶意代码。

在以下情况尤其需要注意: WIFI名,设备名……

错误:

  1. snprintf(buf, sizeof(buf), wifi_name);

正确:

  1. snprinf(buf, sizeof(buf), "%s", wifi_name);

关联漏洞:

高风险-代码执行

高风险-内存破坏

中风险-信息泄露

低风险-拒绝服务

1.7 【必须】对数组delete时需要使用delete[]

delete []操作符用于删除数组。delete操作符用于删除非数组对象。它们分别调用operator delete[]和operator delete。

  1. // Bad
  2. Foo* b = new Foo[5];
  3. delete b; // trigger assert in DEBUG mode

在new[]返回的指针上调用delete将是取决于编译器的未定义行为。代码中存在对未定义行为的依赖是错误的。

  1. // Good
  2. Foo* b = new Foo[5];
  3. delete[] b;

在 C++ 代码中,使用 stringvector、智能指针(比如std::unique_ptr)等可以消除绝大多数 delete[] 的使用场景,并且代码更清晰。

关联漏洞:

高风险-内存破坏

中风险-逻辑漏洞

低风险-内存泄漏

低风险-拒绝服务

1.8【必须】注意隐式符号转换

两个无符号数相减为负数时,结果应当为一个很大的无符号数,但是小于int的无符号数在运算时可能会有预期外的隐式符号转换。

  1. // 1
  2. unsigned char a = 1;
  3. unsigned char b = 2;
  4. if (a - b < 0) // a - b = -1 (signed int)
  5. a = 6;
  6. else
  7. a = 8;
  8. // 2
  9. unsigned char a = 1;
  10. unsigned short b = 2;
  11. if (a - b < 0) // a - b = -1 (signed int)
  12. a = 6;
  13. else
  14. a = 8;

上述结果均为a=6

  1. // 3
  2. unsigned int a = 1;
  3. unsigned short b = 2;
  4. if (a - b < 0) // a - b = 0xffffffff (unsigned int)
  5. a = 6;
  6. else
  7. a = 8;
  8. // 4
  9. unsigned int a = 1;
  10. unsigned int b = 2;
  11. if (a - b < 0) // a - b = 0xffffffff (unsigned int)
  12. a = 6;
  13. else
  14. a = 8;

上述结果均为a=8

如果预期为8,则错误代码:

  1. // Bad
  2. unsigned short a = 1;
  3. unsigned short b = 2;
  4. if (a - b < 0) // a - b = -1 (signed int)
  5. a = 6;
  6. else
  7. a = 8;

正确代码:

  1. // Good
  2. unsigned short a = 1;
  3. unsigned short b = 2;
  4. if ((unsigned int)a - (unsigned int)b < 0) // a - b = 0xffff (unsigned short)
  5. a = 6;
  6. else
  7. a = 8;

关联漏洞:

中风险-逻辑漏洞

1.9【必须】注意八进制问题

代码对齐时应当使用空格或者编辑器自带的对齐功能,谨慎在数字前使用0来对齐代码,以免不当将某些内容转换为八进制。

例如,如果预期为20字节长度的缓冲区,则下列代码存在错误。buf2为020(OCT)长度,实际只有16(DEC)长度,在memcpy后越界:

  1. // Bad
  2. char buf1[1024] = {0};
  3. char buf2[0020] = {0};
  4. memcpy(buf2, somebuf, 19);

应当在使用8进制时明确注明这是八进制。

  1. // Good
  2. int access_mask = 0777; // oct, rwxrwxrwx

关联漏洞:

中风险-逻辑漏洞