直播中
Michael Howard
Microsoft 安全工程師
摘要: Michael Howard 介紹了這一工作,該工作旨在使 C 運(yùn)行庫在面對惡意代碼的威脅時,更加可靠。這項(xiàng)變更適用于 Visual Studio 2005,它還將同時對 C 運(yùn)行庫和 C++ 標(biāo)準(zhǔn)模板庫產(chǎn)生影響。
注意:本文中的內(nèi)容適用于發(fā)布前的軟件版本。發(fā)布產(chǎn)品時,可能會更改本文檔中的部分內(nèi)容。
新版 CRT 的新增功能 | |
C++ 中的情況 | |
標(biāo)準(zhǔn)的變化 | |
小結(jié) | |
發(fā)現(xiàn)安全漏洞 |
無論是居室還是代碼,我們總是有必要不時對其進(jìn)行認(rèn)真的清理。不幸的是,在我們開始清理時,總會產(chǎn)生這樣的疑惑,那就是這些東西到底來自何方?為什么我們從未注意到它的存在?盡管能夠清理部分內(nèi)容,但總有一些會保留下來。如果您在某些方面與我相像,那么可能還會導(dǎo)致出現(xiàn)更明顯、更新奇的問題。
我們看一下問題出在什么地方。C 運(yùn)行庫亟待進(jìn)行有效的改進(jìn),這里的改進(jìn)不是指一般意義上的完善,而是使它具有穩(wěn)固的結(jié)構(gòu),使它完全脫胎換骨!
請認(rèn)真考慮一下此問題。請問人們在什么情況下寫出了諸如 strcpy 和 strcat 之類的函數(shù)?是很久以前 Kernighan 和 Ritchie 剛剛開發(fā)出 C 語言的那段美好時光,那時代碼面臨的威脅遠(yuǎn)沒有現(xiàn)在嚴(yán)重,網(wǎng)絡(luò)的互連也遠(yuǎn)沒有現(xiàn)在普及。而現(xiàn)在的情況則讓我摸不清頭腦,那就是您仍然能夠使用諸如 strcpy 的函數(shù)寫出安全的代碼。這就是數(shù)據(jù)在起作用。但諸如 strcpy 之類的函數(shù)本身無法幫助您寫出安全的代碼,并且在調(diào)用這類函數(shù)時可能出現(xiàn)災(zāi)難性的錯誤。正如 'Nuff 所說,是的,gets 只是一般的錯誤!
那么,針對這種情況,我們能夠采取什么措施呢?您可能聽說過 strsafe.h 文件,該文件中包含了一組一致的、更安全的字符串處理函數(shù),于 2002 年 Windows Security Push 活動期間開發(fā),適用于 Visual Studio® .NET 2003 和 Platform SDK??梢圆殚?Strsafe.h:Safer String Handling in C,了解有關(guān) strsafe 的更多信息。
也可以在許多開放源代碼的操作系統(tǒng)中使用其他函數(shù)(比如,strlcpy 和 strlcat)。可以在 David Wheeler 撰寫的 Secure Programming for Linux and Unix HOWTO 一文中查閱這兩個函數(shù)的有關(guān)信息。
盡管開發(fā) strl* 和 strsafe 是一種有效的措施,我們?nèi)匀恍枰诤诵牡?C 運(yùn)行庫中創(chuàng)建更加可靠的功能,這正是經(jīng)過更新的 CRT 發(fā)揮作用的地方。Microsoft Visual C++® 庫開發(fā)小組決定跟蹤并認(rèn)真檢查 CRT 中的每一個函數(shù),以確定其中是否存在安全性方面的缺陷,找出可能的解決方法。眾所周知,為了提高安全性,也為了有助于用戶寫出更加安全的代碼,已經(jīng)重新編寫了許多函數(shù)(大約有 400 個左右)。
首先,本文所介紹的新增加的 CRT 函數(shù)將出現(xiàn)在 Visual Studio 2005 中,但最終發(fā)行的版本可能與目前的版本有所不同。其次要指出的是,僅僅通過改變編譯器的某個參數(shù),新的庫不會奇跡般地使得不安全的代碼變?yōu)榘踩拇a,但肯定有助于增加代碼的安全性。
更安全的可供選擇的方法不會取代已有的功能。換言之, strcpy 仍將是 strcpy。它的更加安全的版本具有一個新名稱,即 strcpy_s。但是,如果在編譯時使用新的庫,那么舊版本的函數(shù)將失效。所以需要說明的是,編譯器會立即向您發(fā)出警告信息。也就是說,與修復(fù)安全性缺陷相比,改正編譯器警告信息所指出的錯誤要更加容易。請認(rèn)真考慮我就此問題提出的建議!
某些函數(shù)(比如,calloc)僅僅加強(qiáng)了檢查參數(shù)的工作,其功能與以前的版本完全相同,所以不存在 calloc_s 函數(shù)。稍后將介紹有關(guān) calloc 函數(shù)的更多信息。
我最贊同的更改是使用 strncat_s 函數(shù)代替了 strncat 函數(shù)。strncat 函數(shù)的問題在于它的最后一個參數(shù)不表示目標(biāo)緩沖區(qū)的總的大小,它指示目標(biāo)緩沖區(qū)中剩余的最小緩沖區(qū)的大小,以及需要復(fù)制的數(shù)目。這可能導(dǎo)致各種類型的 off-by-one(差 1)錯誤,甚至導(dǎo)致更嚴(yán)重的 off-by-lots(差多) 錯誤。請看下面的例子:
if (szURL != NULL) { char szTmp[MAX_PATH]; char *szExtSrc, *szExtDst; strncpy(szTmp, szURL, MAX_PATH); szExtSrc = strchr(szURL, '.'); szExtDst = strchr(szTmp, '.'); if(szExtDst) { szExtDst[0] = 0; if (fValid) strncat(szTmp, szExtSrc, MAX_PATH); } }
調(diào)用 strncat 函數(shù)時出現(xiàn)錯誤-嚴(yán)重錯誤。實(shí)際上,這時將發(fā)生緩沖區(qū)溢出。無法將 MAX_PATH 字符串安全地復(fù)制到 szTemp 中,因?yàn)樵谡{(diào)用 strncpy 函數(shù)時,已經(jīng)將 szURL 添加至該字符串,這實(shí)際上減少了 szTmp 中剩余的空間。
以下是一個較為簡單的例子:
char szTarget[12]; char *s = "Hello, World"; strncpy(szTarget,s,sizeof(szTarget)); strncat(szTarget,s,sizeof(szTarget));
如果在 Visual C++ 2003 中編譯此程序,將出現(xiàn)一個錯誤,指示 szTarget 附近的數(shù)據(jù)已被破壞。這是因?yàn)榫幾g器參數(shù) /GS 在起作用。它檢測到一個基于堆棧的緩沖區(qū)溢出,并中止了應(yīng)用程序。
可以使用以下代碼來解決這個問題:
char szTarget[12]; char *s = "Hello, World"; strncpy(szTarget,s,sizeof(szTarget)); strncat(szTarget,s,strlen(szTarget) - strlen(s));
但程序中仍然存在一個頑固的錯誤。如果目標(biāo)緩沖區(qū)的長度正好等于源緩沖區(qū)的長度,那么許多 n 版本的函數(shù)不會使目標(biāo)緩沖區(qū)以空字符結(jié)束,這使得 strlen(szTarget) 有可能返回一個大于目標(biāo)緩沖區(qū)長度的值,因?yàn)闆]有末尾的“\0”字符。這樣的話,程序會變得混亂不堪!
以下是一個以更加靈活的方式使用新運(yùn)行庫的程序:
char szTarget[12]; char *s = "Hello, World"; size_t cSource = strlen_s(s,20); strncpy_s(temp,sizeof(szTarget),s,cSource); strncat_s(temp,sizeof(szTarget),s,cSource);
其中的兩個新增加的函數(shù) strncpy_s 和 strncat_s 具有類似的特征:
• |
它們都返回錯誤代碼 (errno_t),而不返回指針。 |
• |
目標(biāo)緩沖區(qū) (char *)。 |
• |
目標(biāo)緩沖區(qū)的總的字符計(jì)數(shù) (size_t)。 |
• |
源緩沖區(qū) (const char *)。 |
• |
源緩沖區(qū)總的字符計(jì)數(shù) (size_t)。 |
記錄兩個緩沖區(qū)的計(jì)數(shù),分別用于每個緩沖區(qū)。沒有必要跟蹤處于變化狀態(tài)的目標(biāo)緩沖區(qū)計(jì)數(shù),雖然這一任務(wù)肯定較容易完成。此外還有其他引人入勝的特性。這兩個函數(shù)都是以空字符來結(jié)束字符串,但以下功能是我特別看重的。請查看一下我在前面“發(fā)現(xiàn)安全漏洞”部分中所提供的代碼示例:
void noOverflow(char *str) { char buffer[10]; strncpy(buffer,str,(sizeof(buffer)-1)); buffer[(sizeof(buffer)-1)]=0; /* 上面兩行代碼用于避免緩沖區(qū)溢出 */ }
我在 2003 年 12 月發(fā)布的一篇文檔中發(fā)現(xiàn)了這些代碼,這篇文檔來自一個大型的跨國軟件公司(但不是 Microsoft),它用來向開發(fā)人員說明編寫安全代碼的優(yōu)點(diǎn)。這段代碼的問題是,它存在一個很明顯的安全漏洞。如果 *str 指向 NULL,那么 strncpy 在復(fù)制 NULL 指針時將出現(xiàn)錯誤!在各種開放源代碼的軟件中所使用的 strlcat 存在同樣的問題,但 strncat_s 不是這樣。
strncat_s 不會出現(xiàn)錯誤的原因在于,所有更新的運(yùn)行庫函數(shù)都會對輸入的參數(shù)執(zhí)行更為嚴(yán)格的檢查。以下是 strncat_s 函數(shù)中參數(shù)有效性驗(yàn)證部分的內(nèi)容:
/* 驗(yàn)證部分 */ _VALIDATE_RETURN_ERRCODE(front != NULL, EINVAL); _VALIDATE_RETURN_ERRCODE(sizeInTChars > 0, EINVAL); _VALIDATE_RETURN_ERRCODE(back != NULL || count == 0, EINVAL);
驗(yàn)證宏語句為:
#define _VALIDATE_RETURN_ERRCODE( expr, errorcode \ { \ _ASSERTE( ( expr ) ); \ if ( !( expr ) ) \ { \ errno = errorcode; \ _INVALID_PARAMETER(expr); \ return ( errorcode ); \ } \ }
_INVALID_PARAMETER 用于在出錯后進(jìn)行的調(diào)試中提供文件的有關(guān)信息,以幫助用戶調(diào)試代碼。
在學(xué)校,老師總是教導(dǎo)我們要檢查函數(shù)參數(shù)。現(xiàn)在,這項(xiàng)工作將由 CRT 來完成。實(shí)現(xiàn)這一飛躍僅僅用了二十年。
您應(yīng)該清楚的一點(diǎn)是,strsafe 函數(shù)(比如,StringCchCopy 和 StringCchCat)所執(zhí)行的操作與 strncpy_s 和 strncat_s 函數(shù)是不同的。strncat_s 函數(shù)在檢測到錯誤之后,會將字符串設(shè)置為 NULL。但是在默認(rèn)情況下, strsafe 函數(shù)將向目標(biāo)填充盡可能多的數(shù)據(jù),然后以 NULL 結(jié)束此字符串??梢栽?strsafe 函數(shù)中加入以下代碼來模仿這一操作:
StringCchCatEx(dst,sizeof(dst)/sizeof(dst[0]),src,NULL,NULL,STRSAFE_NULL_ON_FAILURE)
其他用于操作緩沖區(qū)的函數(shù)也具有同樣的行為,這些函數(shù)包括各種 printf 和 scanf 函數(shù)、mbstowcs、strerror、_strdate 和 _strtime、asctime 以及 ctime 函數(shù)等。對于緩沖區(qū)操作函數(shù)以外的函數(shù)也進(jìn)行了更新,這些函數(shù)包括 _makepath、_splitpath、getenv、rand 以及許多其他函數(shù)。
calloc 函數(shù)也是一個有趣的函數(shù)。如果 size * num 超出了 2^32,很容易導(dǎo)致出現(xiàn)整數(shù)溢出的錯誤,更新后的 calloc 函數(shù)驗(yàn)證計(jì)算是否未溢出:
/* 確保 (size * num) 未溢出 */ if (num > 0 && (_HEAP_MAXREQ / num) <= size) { errno=ENOMEM; return NULL; }
Visual C++ 小組并未停止使用 C 運(yùn)行庫。對于標(biāo)準(zhǔn)模板庫 (STL),有許多眾所周知的事實(shí)。您知道使用迭代程序能夠使緩沖區(qū)溢出嗎?許多安全性方面的風(fēng)險都與不恰當(dāng)?shù)厥褂昧说绦蛴嘘P(guān),使用當(dāng)?shù)鲇行Х秶鷷r中止運(yùn)行(或出現(xiàn)異常)的迭代程序可以消除這一風(fēng)險。下面是一個示例:
#include <vector> vector<int> v(10); // 向量大小為 10 v[20] = 10; // 出現(xiàn)緩沖區(qū)溢出 vector<int>::iterator it = v.end(); // 超出界限后導(dǎo)致緩沖區(qū)溢出 ++it;
#define _SECURE_SCL (1) 編譯這段代碼后,可以使所有的迭代程序進(jìn)行檢查范圍的操作。
也可以不使用新添加的 #define,而使用新功能來達(dá)到相同的目的。例如,以下代碼將不會導(dǎo)致溢出:
vector<int> v(10); // 向量大小為 10 stdext::checked_iterator<vector<int> > ck_it(v, v.end()); // 超出界限后將中止程序 ++ck_it;
其他升級的類和方法有 operator[](vector、string、deque、bitset classes 等), front 和 back(vector、queue、list 類等)。此外還更新了各種算法,包括 copy、copy_backward、*_copy、transform、fill、set_* 等等。
看到這里,您可能會想,標(biāo)準(zhǔn)有什么變化嗎?C 和 C++ 不符合標(biāo)準(zhǔn)嗎?不,它們已經(jīng)標(biāo)準(zhǔn)化,并且 Microsoft 已將最新的草案提交給標(biāo)準(zhǔn)化委員會。可以轉(zhuǎn)到 http://std.dkuug.dk/jtc1/sc22/wg14/www/docs/n1031.pdf 查看此草案。
我確實(shí)很高興使用新的運(yùn)行庫。雖然它不能顯著提高代碼的安全性,但它卻是防止緩沖區(qū)溢出的又一個工具。在即將結(jié)束之際,我要感謝 Visual C++ 庫開發(fā)小組出色地完成了這一任務(wù),尤其要感謝 Martyn Lovell 對寫作本文檔提供的幫助。
“以往的 CRT 大勢已去,愿新的 CRT 煥發(fā)生機(jī)!”
許多人幫助我最后改正了錯誤。早在這篇文章之前,答案就已明確。所謂“安全的” strncpy 并不檢查參數(shù)是否為 NULL,它能夠?qū)е履膽?yīng)用程序死機(jī)或非法訪問。
好,看一下本月的問題。這段代碼有什么問題?
void ReadDataFromFile(char *szFilename, LPOVERLAPPED_COMPLETION_ROUTINE func) { HANDLE hFile = CreateFile(szFilename, FILE_ALL_ACCESS, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); OVERLAPPED io; memset(&io,0,sizeof OVERLAPPED); DWORD dwWritten=0, dwRes=0; // 執(zhí)行讀取操作 const size_t cBuff = 1024; char buff[cBuff]; if (!ReadFileEx(hFile,buff,cBuff,&io,func)){ // 哦!保證! } // 其他代碼 }
Michael Howard 是 Microsoft Secure Engineering 小組的高級安全程序經(jīng)理,是 Writing Secure Code 的作者之一,現(xiàn)在正在進(jìn)行該書新版本的寫作,他還是《Designing Secure Web-based Applications for Windows 2000》的主要作者。他的主要工作就是確保人們設(shè)計(jì)、構(gòu)建、測試和介紹無缺陷的安全系統(tǒng)。他最喜歡的話是“尺有所短,寸有所長”。