不應(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é)后半部分)。
(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ù)時(shí),要確保正確計(jì)算緩沖區(qū)長度,同時(shí),如果你不確定是否代碼在各個(gè)編譯器下都能確保末尾有0時(shí),建議可以適當(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ù)制時(shí),如果復(fù)制的長度超過n,不會在結(jié)尾補(bǔ)\0。
同樣,MSVC _snprintf
系列函數(shù)在超過或等于n時(shí)也不會以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)險(xiǎn)-信息泄露
低風(fēng)險(xiǎn)-拒絕服務(wù)
高風(fēng)險(xiǎn)-緩沖區(qū)溢出
system、WinExec、CreateProcess、ShellExecute等啟動進(jìn)程類的函數(shù),需要嚴(yán)格檢查其參數(shù)。
啟動進(jìn)程需要加上雙引號,錯(cuò)誤例子:
// Bad
WinExec("D:\\program files\\my folder\\foobar.exe", SW_SHOW);
當(dāng)存在D:\program files\my.exe
的時(shí)候,my.exe會被啟動。而foobar.exe不會啟動。
// Good
WinExec("\"D:\\program files\\my folder\\foobar.exe\"", SW_SHOW);
另外,如果啟動時(shí)從用戶輸入、環(huán)境變量讀取組合命令行時(shí),還需要注意是否可能存在命令注入。
// Bad
std::string cmdline = "calc ";
cmdline += user_input;
system(cmdline.c_str());
比如,當(dāng)用戶輸入1+1 && ls
時(shí),執(zhí)行的實(shí)際上是calc 1+1和ls 兩個(gè)命令,導(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)險(xiǎn)-代碼執(zhí)行
高風(fēng)險(xiǎn)-權(quán)限提升
_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ī)范中禁止使用。
錯(cuò)誤示例:
// 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)險(xiǎn)!
}
正確示例:
// 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)險(xiǎn)-拒絕服務(wù)
高風(fēng)險(xiǎn)-內(nèi)存破壞
所有printf系列函數(shù),如sprintf,snprintf,vprintf等必須對應(yīng)控制符號和參數(shù)。
錯(cuò)誤示例:
// 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)險(xiǎn)-信息泄露
所有printf系列函數(shù),要防止格式化完的字符串泄露程序布局信息。例如,如果將帶有%p的字符串泄露給程序,則可能會破壞ASLR的防護(hù)效果。使得攻擊者更容易攻破程序。
%p的值只應(yīng)當(dāng)在程序內(nèi)使用,而不應(yīng)當(dāng)輸出到外部或被外部以某種方式獲取。
錯(cuò)誤示例:
// Bad
// 如果這是暴露給客戶的一個(gè)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++;
}
// 如果這是暴露給客戶的一個(gè)API:
uint64_t GetUniqueObjectId(const Foo* object) {
if (object)
return object->object_id_;
else
error(...);
}
關(guān)聯(lián)漏洞:
中風(fēng)險(xiǎn)-信息泄露
如果用戶可以控制字符串,則通過 %n %p 等內(nèi)容,最壞情況下可以直接執(zhí)行任意惡意代碼。
在以下情況尤其需要注意: WIFI名,設(shè)備名……
錯(cuò)誤:
snprintf(buf, sizeof(buf), wifi_name);
正確:
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ù)
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將是取決于編譯器的未定義行為。代碼中存在對未定義行為的依賴是錯(cuò)誤的。
// Good
Foo* b = new Foo[5];
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ù)
兩個(gè)無符號數(shù)相減為負(fù)數(shù)時(shí),結(jié)果應(yīng)當(dāng)為一個(gè)很大的無符號數(shù),但是小于int的無符號數(shù)在運(yùn)算時(shí)可能會有預(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,則錯(cuò)誤代碼:
// 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)險(xiǎn)-邏輯漏洞
代碼對齊時(shí)應(yīng)當(dāng)使用空格或者編輯器自帶的對齊功能,謹(jǐn)慎在數(shù)字前使用0來對齊代碼,以免不當(dāng)將某些內(nèi)容轉(zhuǎn)換為八進(jìn)制。
例如,如果預(yù)期為20字節(jié)長度的緩沖區(qū),則下列代碼存在錯(cuò)誤。buf2為020(OCT)長度,實(shí)際只有16(DEC)長度,在memcpy后越界:
// Bad
char buf1[1024] = {0};
char buf2[0020] = {0};
memcpy(buf2, somebuf, 19);
應(yīng)當(dāng)在使用8進(jìn)制時(shí)明確注明這是八進(jìn)制。
// Good
int access_mask = 0777; // oct, rwxrwxrwx
關(guān)聯(lián)漏洞:
中風(fēng)險(xiǎn)-邏輯漏洞
更多建議: