阅读(3152) (0)

C/C++ 使用错误

2021-05-28 09:33:55 更新

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

关联漏洞:

  • 中风险-信息泄露

  • 低风险-拒绝服务

  • 高风险-缓冲区溢出

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++ 代码中,使用 stringvector、智能指针(比如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

关联漏洞:

  • 中风险-逻辑漏洞