轉自:
http://bbs.lvy8.cn/ShowPost_PR5339.html
程序員》9月文章
申明。文章僅代表個人觀點,與所在公司無任何聯系。
- 概述
在前面的安全編碼實踐的文章里,我們討論了GS編譯選項,數據執行保護DEP功能,以及靜態代碼分析工具Prefast。這里,我們討論在C/C++代碼中禁用危險的API,其主要目的是為了減少代碼中引入安全漏洞的可能性。
- 那些是危險的API
在微軟產品的安全漏洞中,有很大一部分是由于不正確的使用C動態庫(C Runtime Library) 的函數,特別是有關字符串處理的函數導致的。表一給出了微軟若干由于不當使用C動態庫函數而導致的安全漏洞【1,p242】。
微軟安全公告 |
涉及產品 |
涉及的函數 |
MS02-039 |
Microsoft SQL Server 2000 |
sprint |
MS05-010 |
Microsoft License Server |
lstrcpy |
MS04-011 |
Microsoft Windows (DCPromo) |
wvsprintf |
MS04-011 |
Microsoft Windows (MSGina) |
lstrcpy |
MS04-031 |
Microsoft Windows (NetDDE) |
wcscat |
MS03-045 |
Microsoft Windows (USER) |
wcscpy |
表1:不當使用C動態庫函數而導致的安全漏洞
不當使用C動態庫函數容易引入安全漏洞,這一點并不奇怪。C動態庫函數的設計大約是30年前的事情了。當時,安全方面的考慮并不是設計上需要太多注意的地方。
有關完整的危險API的禁用列表,大家可以參見http://msdn.microsoft.com/en-us/library/bb288454.aspx.
在這里我們列出其中的一部分,以便大家對那些API被禁用有所體會。
<DIV align=left>
禁用的API |
替代的StrSafe函數 |
替代的Safe CRT函數 |
有關字符串拷貝的API |
strcpy, wcscpy, _tcscpy, _mbscpy, StrCpy, StrCpyA, StrCpyW, lstrcpy, lstrcpyA, lstrcpyW, strcpyA, strcpyW, _tccpy, _mbccpy |
StringCchCopy, StringCbCopy,
StringCchCopyEx, StringCbCopyEx
|
strcpy_s |
有關字符串合并的API |
strcat, wcscat, _tcscat, _mbscat, StrCat, StrCatA, StrCatW, lstrcat, lstrcatA, lstrcatW, StrCatBuffW, StrCatBuff, StrCatBuffA, StrCatChainW, strcatA, strcatW, _tccat, _mbccat |
StringCchCat, StringCbCat,
StringCchCatEx, StringCbCatEx
|
strcat_s |
有關sprintf的API |
wnsprintf, wnsprintfA, wnsprintfW, sprintfW, sprintfA, wsprintf, wsprintfW, wsprintfA, sprintf, swprintf, _stprintf |
StringCchPrintf, StringCbPrintf,
StringCchPrintfEx, StringCbPrintfEx
|
_snprintf_s
_snwprintf_s
|
</DIV>
表2:禁用API的列表(部分)
其它被禁用的API還有scanf, strtok, gets, itoa等等。 ”n”系列的字符串處理函數,例如strncpy等,也在被禁用之列。
- 如何替代被禁用的危險API
從上面的介紹可以看出絕大多數C動態庫中的字符串處理函數都被禁用。那么,如何在代碼中替代這些危險的API呢?在表2里,我們看到有兩種替代方案:
后面我們會討論這兩種方案的不同之處。這里我們先說它們的共同點:提供更安全的字符串處理功能。特別在以下幾個方面:
- 目標緩存區的大小被顯式指明。
- 動態校驗。
- 返回代碼。
以StringCchCopy舉例。它的定義如下:
HRESULT StringCchCopy(
LPTSTR pszDest,
size_t cchDest,
LPCTSTR pszSrc
);
cchDest指明目標緩存區pszDest最多能容納字符的數目,其值必須在1和STRSAFE_MAX_CCH之間。StringCchCopy總是確保pszDest被拷貝的字符串是以NULL結尾。并且提供以下的返回代碼: S_OK,STRSAFE_E_INVALID_PARAMETER,和STRSAFE_E_INSUFFICIENT_BUFFER。這樣,采用StringCchCopy來替代被禁用的strcpy的話,就可以有效降低由于誤用字符串拷貝而導致緩存溢出的可能。
使用StrSafe非常簡單。在C/C++代碼中加入以下的頭文件即可。
#include "strsafe.h"
StrSafe.h包含在Windows Platform SDK中。用戶可以通過在微軟的網站直接下載。
下面給出一個使用StrSafe的代碼示例【2】。
不安全的代碼:
void UnsafeFunc(LPTSTR szPath,DWORD cchPath) {
TCHAR szCWD[MAX_PATH];
GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD);
strncpy(szPath, szCWD, cchPath);
strncat(szPath, TEXT("\\"), cchPath);
strncat(szPath, TEXT("desktop.ini"),cchPath);
}
在以上代碼里存在著幾個問題:首先,沒有錯誤代碼的校驗。更嚴重的是,在strncat中,cchPath是目標緩存區可以存放字符的最大數目,而正確傳遞的參數應該是目標緩存區剩余的字符數目。
使用StrSafe后的代碼是
bool SaferFunc(LPTSTR szPath,DWORD cchPath) {
TCHAR szCWD[MAX_PATH];
if (GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD) &&
SUCCEEDED(StringCchCopy(szPath, cchPath, szCWD)) &&
SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("\\"))) &&
SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("desktop.ini")))) {
return true;
}
return false;
}
SafeCRT自Visual Studio 2005起開始支持。當代碼中使用了禁用的危險的CRT函數,Visual Studio 2005編譯時會報告相應警告信息,以提醒開發人員考慮將其替代為Safe CRT中更為安全的函數。
下面給出一個使用Safe CRT的代碼示例【3】。
不安全的代碼:
void UnsafeFunc (const wchar_t * src)
{
// Original
wchar_t dest[20];
wcscpy(dest, src); // 編譯警告
wcscat(dest, L"..."); // 編譯警告
}
以上這段代碼里存在著明顯緩存溢出的問題。
使用Safe CRT后的代碼是
errno_t SaferFunc(const wchar_t * src)
{
wchar_t dest[20];
errno_t err = wcscpy_s(dest, _countof(dest), src);
if (!err)
return err;
return wcscat_s(dest, _countof(dest), L"...");
}
我們看到,StrSafe和Safe CRT存在功能重疊的地方。那么什么時候使用StrSafe,什么時候使用SafeCRT呢?
下面的表格【1,p246】里列出了兩者之間的差異。采用何種方式應該根據具體情況而定。有時候也許只能采取其中一種方式:例如如果你的開發系統是Visual Studio 2003的話,就只能使用StrSafe。或者你的代碼中有許多itoa的話,就考慮使用Safe CRT,因為StrSafe中沒有提供簡單的替代方式。有時候也許兩者都可以。這種情況下,我個人是更喜歡采用StrSafe這種方式,因為它不依賴具體的動態庫支持。如果是編寫Win32上的程序的話,StrSafe的HRESULT的返回代碼,也和Win32 API的代碼類似,這樣代碼的整體風格可能會更加一致。
<DIV align=left>
|
StrSafe |
Safe CRT |
發布方式 |
Web |
Microsoft Visual Studio 2005 |
頭文件 |
一個 (StrSafe.h) |
多個 (不同的 C runtime 頭文件) |
是否提供鏈接庫的版本 |
是 |
是 |
是否提供內嵌(Inline)版本 |
是 |
否 |
是否是業界標準 |
否 |
正在評估過程 |
Kernel Mode支持 |
是 |
否 |
返回類型 |
HRESULT (user mode)
NTSTATUS (kernel mode)
|
隨函數變化,errno_t |
是否需要修改代碼 |
是 |
是 |
主要針對 |
緩存溢出 |
緩存溢出,和其它安全方面的考慮 |
</DIV>
表3:StrSafe和Safe CRT對比
- 爭論
在開發過程中,代碼中全面禁用危險的API的編碼實踐,存在著一定的爭議性。其中最具有代表性的觀點可以參見Danny Kalev的Visual C++ 8.0 Hijacks the C++ Standard一文【4】。爭論主要集中在以下幾點。
以StrSafe舉例,由于增加了更多的動態校驗,其速度較C動態庫的函數相比,是有所下降的。在【2】一文中,給出了對StrSafe速度方面的測試數據如下:
測試例子:1千萬次字符串合并調用。結果:
C動態庫:7.3秒
StrSafe:8.3秒
我們看到,如果開發的系統不是完全以字符串處理為工作核心的話,使用StrSafe對系統性能的影響是可以控制的。
首先,同意如果代碼中正確使用危險API的話,也是可以避免安全漏洞的引入。但是,在具體的開發實踐中,存在著以下問題:
- 開發人員的素質和培訓
- 有時候即使執行嚴格的代碼復查,仍然可能由于使用危險的API而引入安全漏洞。
第二點尤其關鍵。大家看到這里可能會有疑問,使用危險的API有這么容易出問題嗎?即便代碼復查(code review)也沒能看出來?【5】中給出了一個微軟安全漏洞的具體實例。
微軟 05-047 Plug-n-Play RPC:即插即用中的漏洞,允許遠程執行代碼和特權提升。經過身份驗證的攻擊者可以通過創建特制的網絡消息并將該消息發送到受影響的系統來嘗試利用此漏洞。導致這個嚴重的安全漏洞的代碼如下:
#define MAX_CM_PATH 360
GetInstanceList(
IN LPCWSTR pszDevice, IN OUT LPWSTR *pBuffer, IN OUT PULONG pulLength)
{
WCHAR RegStr[MAX_CM_PATH], szInstance[MAX_DEVICE_ID_LEN];
...
// Validate that passed in pszDevice is an actual registry entry
// If lookup for the key fails, reject call and cleanup.
// ghEnumKey points to HKLM\System\CurrentControlSet\Enum
if (RegOpenKeyEx(ghEnumKey, pszDevice, 0,
KEY_ENUMERATE_SUB_KEYS, &hKey) != ERROR_SUCCESS) {
Status = CR_REGISTRY_ERROR;
goto Clean0;
}
...
ulLen = MAX_DEVICE_ID_LEN; // size in chars
...
// Query szInstance from registry
RegStatus = RegEnumKeyEx(hKey, ulIndex, szInstance, &ulLen, ...);
if (RegStatus == ERROR_SUCCESS) {
// Build lookup string given a valid registry root key and valid instance ID
wsprintf(RegStr, TEXT("%s\\%s"), pszDevice, szInstance);}
復查這段代碼時,我們看到,雖然使用了危險的API:wsprintf,但應該是不會發生緩存溢出的問題。這是因為根據MSDN,
圖1:注冊表字符數目的限制
于是:
- pszDevice 應該少于255 characters
- pszDevice 是一個 有效的 key 在HKLM\System\CurrentControlSet\Enum
- szInstance 是一個有效的subkey在pszDevice下
- RegStr is 360 characters
- 攻擊者并不能控制注冊表內容
但實際上,wspringf還是導致了緩存溢出的安全漏洞。到底是怎么回事?我們來看一下攻擊代碼:
errno_t SaferFunc(const wchar_t * src)
int main()
{
PWCHAR pszFilter = (PWCHAR)malloc(sizeof(WCHAR)*1000);
PWCHAR Buffer = (PWCHAR)malloc(86);
wsprintf(pszFilter,L"ISAPNP\\ReadDataPort\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\0");
CM_Get_Device_ID_List((PCWSTR)pszFilter,Buffer,86,1);
return 0;
}
攻擊代碼之所以有效,是因為:
- pszFilter(除去末位的0) 作為pszDevice 傳遞給GetInstanceList
- RegOpenKeyEx 接收了這個長字符串,忽略那些“\”,返回 ERROR_SUCCESS。
通過這個例子我們看出,在開發復雜的系統中,即便是有經驗的開發人員,加上嚴格的代碼復查過程,還是有可能由于使用危險的API而導致安全漏洞的引入。這也是微軟在Windows Vista的開發過程中全面禁用危險API的原因。
這一點的考慮是非常值得重視的。不管是StrSafe,還是Safe CRT,都不是工業界標準。因此,如果開發的系統需要移植到其它平臺的話,采用Safe CRT是肯定不合適的。StrSafe的Inline方式,因為不依賴特定庫,對可移植性的影響相對較小。
- 總結
在C/C++程序中禁用危險的API,可以有效降低在代碼中引入安全漏洞的可能。在考慮了性能和可移植性的因素下,強烈建議在開發過程中,使用StrSafe或Safe CRT中對應的安全函數來替代被禁用的危險的API調用。
- 參考文獻
- The Security Development Lifecycle: SDL, Michael Howard; Steve Lipner, Microsoft
- Strsafe.h: Safer String Handling in C, http://msdn.microsoft.com/en-us/library/ms995353.aspx, Michael Howard, Microsoft
- Repel Attacks on Your Code with the Visual Studio 2005 Safe C and C++ Libraries, http://msdn.microsoft.com/en-us/magazine/cc163794.aspx, Martyn Lovell, Microsoft
- Visual C++ 8.0 Hijacks the C++ Standard, http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=259, Danny Kalev
- School of hard knocks: things you can learn from working with MSRC, PhNeutral 0x7d7, Damian Hasse, Microsoft