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

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

  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函數(shù)行為現(xiàn)在符合C99標(biāo)準(zhǔn)。
  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系列拷貝函數(shù)時(shí),要確保正確計(jì)算緩沖區(qū)長度,同時(shí),如果你不確定是否代碼在各個(gè)編譯器下都能確保末尾有0時(shí),建議可以適當(dāng)增加1字節(jié)輸入緩沖區(qū),并將其置為\0,以保證輸出的字符串結(jié)尾一定有\(zhòng)0。

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

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

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

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

上述代碼在MSVC中執(zhí)行后, a[4] == 'A',因此字符串未以0結(jié)尾。a的內(nèi)容是"AAAA",調(diào)用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++ 中,強(qiáng)烈建議用 stringvector 等更高封裝層次的基礎(chǔ)組件代替原始指針和動態(tài)數(shù)組,對提高代碼的可讀性和安全性都有很大的幫助。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

錯誤示例:

  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]; // 不可控的棧溢出風(fēng)險(xiǎn)!
  8. }

正確示例:

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

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

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

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

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

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

錯誤示例:

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

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

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

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

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

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

錯誤示例:

  1. // Bad
  2. // 如果這是暴露給客戶的一個(gè)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. // 如果這是暴露給客戶的一個(gè)API:
  7. uint64_t GetUniqueObjectId(const Foo* object) {
  8. if (object)
  9. return object->object_id_;
  10. else
  11. error(...);
  12. }

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

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

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

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

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

錯誤:

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

正確:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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;

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

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

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

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

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

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

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

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

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

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

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號