C/C++ 使用錯誤

2021-05-28 10:07 更新

1.1 【必須】不得直接使用無長度限制的字符拷貝函數(shù)

不應(yīng)直接使用legacy的字符串拷貝、輸入函數(shù),如strcpy、strcat、sprintf、wcscpy、mbscpy等,這些函數(shù)的特征是:可以輸出一長串字符串,而不限制長度。如果環(huán)境允許,應(yīng)當(dāng)使用其_s安全版本替代,或者使用n版本函數(shù)(如:snprintf,vsnprintf)。

若使用形如sscanf之類的函數(shù)時,在處理字符串輸入時應(yīng)當(dāng)通過%10s這樣的方式來嚴(yán)格限制字符串長度,同時確保字符串末尾有\(zhòng)0。如果環(huán)境允許,應(yīng)當(dāng)使用_s安全版本。

但是注意,雖然MSVC 2015時默認(rèn)引入結(jié)尾為0版本的snprintf(行為等同于C99定義的snprintf)。但更早期的版本中,MSVC的snprintf可能是_snprintf的宏。而_snprintf是不保證\0結(jié)尾的(見本節(jié)后半部分)。

(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函數(shù)行為現(xiàn)在符合C99標(biāo)準(zhǔn)。


請參考:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?redirectedfrom=MSDN&view=vs-2019

因此,在使用n系列拷貝函數(shù)時,要確保正確計算緩沖區(qū)長度,同時,如果你不確定是否代碼在各個編譯器下都能確保末尾有0時,建議可以適當(dāng)增加1字節(jié)輸入緩沖區(qū),并將其置為\0,以保證輸出的字符串結(jié)尾一定有\(zhòng)0。

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

一些需要注意的函數(shù),例如strncpy_snprintf是不安全的。 strncpy不應(yīng)當(dāng)被視為strcpy的n系列函數(shù),它只是恰巧與其他n系列函數(shù)名字很像而已。strncpy在復(fù)制時,如果復(fù)制的長度超過n,不會在結(jié)尾補(bǔ)\0。

同樣,MSVC _snprintf系列函數(shù)在超過或等于n時也不會以0結(jié)尾。如果后續(xù)使用非0結(jié)尾的字符串,可能泄露相鄰的內(nèi)容或者導(dǎo)致程序崩潰。

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

上述代碼在MSVC中執(zhí)行后, a[4] == 'A',因此字符串未以0結(jié)尾。a的內(nèi)容是"AAAA",調(diào)用strlen(a)則會越界訪問。因此,正確的操作舉例如下:

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

在 C++ 中,強(qiáng)烈建議用 string、vector 等更高封裝層次的基礎(chǔ)組件代替原始指針和動態(tài)數(shù)組,對提高代碼的可讀性和安全性都有很大的幫助。

關(guān)聯(lián)漏洞:

  • 中風(fēng)險-信息泄露

  • 低風(fēng)險-拒絕服務(wù)

  • 高風(fēng)險-緩沖區(qū)溢出

1.2 【必須】創(chuàng)建進(jìn)程類的函數(shù)的安全規(guī)范

system、WinExec、CreateProcess、ShellExecute等啟動進(jìn)程類的函數(shù),需要嚴(yán)格檢查其參數(shù)。

啟動進(jìn)程需要加上雙引號,錯誤例子:

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

當(dāng)存在D:\program files\my.exe的時候,my.exe會被啟動。而foobar.exe不會啟動。

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

另外,如果啟動時從用戶輸入、環(huán)境變量讀取組合命令行時,還需要注意是否可能存在命令注入。

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

比如,當(dāng)用戶輸入1+1 && ls時,執(zhí)行的實際上是calc 1+1和ls 兩個命令,導(dǎo)致命令注入。

需要檢查用戶輸入是否含有非法數(shù)據(jù)。

// 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(...);

關(guān)聯(lián)漏洞:

  • 高風(fēng)險-代碼執(zhí)行

  • 高風(fēng)險-權(quán)限提升

1.3 【必須】盡量減少使用 _alloca 和可變長度數(shù)組

_alloca 和可變長度數(shù)組使用的內(nèi)存量在編譯期間不可知。尤其是在循環(huán)中使用時,根據(jù)編譯器的實現(xiàn)不同,可能會導(dǎo)致:(1)棧溢出,即拒絕服務(wù); (2)缺少棧內(nèi)存測試的編譯器實現(xiàn)可能導(dǎo)致申請到非棧內(nèi)存,并導(dǎo)致內(nèi)存損壞。這在棧比較小的程序上,例如IoT設(shè)備固件上影響尤為大。對于 C++,可變長度數(shù)組也屬于非標(biāo)準(zhǔn)擴(kuò)展,在代碼規(guī)范中禁止使用。

錯誤示例:

// Bad
for (int i = 0; i < 100000; i++) {
  char* foo = (char *)_alloca(0x10000);
  ..do something with foo ..;
}


void Foo(int size) {
  char msg[size]; // 不可控的棧溢出風(fēng)險!
}

正確示例:

// Good
// 改用動態(tài)分配的堆內(nèi)存
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
}

關(guān)聯(lián)漏洞:

  • 低風(fēng)險-拒絕服務(wù)

  • 高風(fēng)險-內(nèi)存破壞

1.4 【必須】printf系列參數(shù)必須對應(yīng)

所有printf系列函數(shù),如sprintf,snprintf,vprintf等必須對應(yīng)控制符號和參數(shù)。

錯誤示例:

// 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 應(yīng)為 %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的攻擊者獲取部分服務(wù)器的原始指針地址,可以用于破壞ASLR保護(hù)。

關(guān)聯(lián)漏洞:

  • 中風(fēng)險-信息泄露

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

所有printf系列函數(shù),要防止格式化完的字符串泄露程序布局信息。例如,如果將帶有%p的字符串泄露給程序,則可能會破壞ASLR的防護(hù)效果。使得攻擊者更容易攻破程序。

%p的值只應(yīng)當(dāng)在程序內(nèi)使用,而不應(yīng)當(dāng)輸出到外部或被外部以某種方式獲取。

錯誤示例:

// 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(...);
}

關(guān)聯(lián)漏洞:

  • 中風(fēng)險-信息泄露

1.6 【必須】不應(yīng)當(dāng)把用戶可修改的字符串作為printf系列函數(shù)的“format”參數(shù)

如果用戶可以控制字符串,則通過 %n %p 等內(nèi)容,最壞情況下可以直接執(zhí)行任意惡意代碼。

在以下情況尤其需要注意: WIFI名,設(shè)備名……

錯誤:

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

正確:

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

關(guān)聯(lián)漏洞:

  • 高風(fēng)險-代碼執(zhí)行

  • 高風(fēng)險-內(nèi)存破壞

  • 中風(fēng)險-信息泄露

  • 低風(fēng)險-拒絕服務(wù)

1.7 【必須】對數(shù)組delete時需要使用delete[]

delete []操作符用于刪除數(shù)組。delete操作符用于刪除非數(shù)組對象。它們分別調(diào)用operator delete[]和operator delete。

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

在new[]返回的指針上調(diào)用delete將是取決于編譯器的未定義行為。代碼中存在對未定義行為的依賴是錯誤的。

// Good
Foo* b = new Foo[5];
delete[] b;

在 C++ 代碼中,使用 string、vector、智能指針(比如std::unique_ptr<T[]>)等可以消除絕大多數(shù) delete[] 的使用場景,并且代碼更清晰。

關(guān)聯(lián)漏洞:

  • 高風(fēng)險-內(nèi)存破壞

  • 中風(fēng)險-邏輯漏洞

  • 低風(fēng)險-內(nèi)存泄漏

  • 低風(fēng)險-拒絕服務(wù)

1.8【必須】注意隱式符號轉(zhuǎn)換

兩個無符號數(shù)相減為負(fù)數(shù)時,結(jié)果應(yīng)當(dāng)為一個很大的無符號數(shù),但是小于int的無符號數(shù)在運(yùn)算時可能會有預(yù)期外的隱式符號轉(zhuǎn)換。

// 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;

上述結(jié)果均為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;

上述結(jié)果均為a=8

如果預(yù)期為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;

關(guān)聯(lián)漏洞:

  • 中風(fēng)險-邏輯漏洞

1.9【必須】注意八進(jìn)制問題

代碼對齊時應(yīng)當(dāng)使用空格或者編輯器自帶的對齊功能,謹(jǐn)慎在數(shù)字前使用0來對齊代碼,以免不當(dāng)將某些內(nèi)容轉(zhuǎn)換為八進(jìn)制。

例如,如果預(yù)期為20字節(jié)長度的緩沖區(qū),則下列代碼存在錯誤。buf2為020(OCT)長度,實際只有16(DEC)長度,在memcpy后越界:

// Bad
char buf1[1024] = {0};
char buf2[0020] = {0};


memcpy(buf2, somebuf, 19);

應(yīng)當(dāng)在使用8進(jìn)制時明確注明這是八進(jìn)制。

// Good
int access_mask = 0777;  // oct, rwxrwxrwx

關(guān)聯(lián)漏洞:

  • 中風(fēng)險-邏輯漏洞
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號