《程序员》9月文章
申明。文章仅代表个人观点,与所在公司无任何联系。
在前面的安全编码实践的文章里,我们讨论了GS编译选项,数据执行保护DEP功能,以及静态代码分析工具Prefast。这里,我们讨论在C/C++代码中禁用危险的API,其主要目的是为了减少代码中引入安全漏洞的可能性。
2.1历史
在微软产品的安全漏洞中,有很大一部分是由于不正确的使用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年前的事情了。当时,安全方面的考虑并不是设计上需要太多注意的地方。
2.2 危险API的列表
有关完整的危险API的禁用列表,大家可以参见http://msdn.microsoft.com/en-us/library/bb288454.aspx.
在这里我们列出其中的一部分,以便大家对那些API被禁用有所体会。
禁用的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 |
表2:禁用API的列表(部分)
其它被禁用的API还有scanf, strtok, gets, itoa等等。 ”n”系列的字符串处理函数,例如strncpy等,也在被禁用之列。
从上面的介绍可以看出绝大多数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的话,就可以有效降低由于误用字符串拷贝而导致缓存溢出的可能。
3.1使用StrSafe
使用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;
}
3.2使用Safe CRT
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"...");
}
3.3 StrSafe 和Safe CRT的对比
我们看到,StrSafe和Safe CRT存在功能重叠的地方。那么什么时候使用StrSafe,什么时候使用SafeCRT呢?
下面的表格【1,p246】里列出了两者之间的差异。采用何种方式应该根据具体情况而定。有时候也许只能采取其中一种方式:例如如果你的开发系统是Visual Studio 2003的话,就只能使用StrSafe。或者你的代码中有许多itoa的话,就考虑使用Safe CRT,因为StrSafe中没有提供简单的替代方式。有时候也许两者都可以。这种情况下,我个人是更喜欢采用StrSafe这种方式,因为它不依赖具体的动态库支持。如果是编写Win32上的程序的话,StrSafe的HRESULT的返回代码,也和Win32 API的代码类似,这样代码的整体风格可能会更加一致。
StrSafe | Safe CRT | |
发布方式 | Web | Microsoft Visual Studio 2005 |
头文件 | 一个 (StrSafe.h) | 多个 (不同的 C runtime 头文件) |
是否提供链接库的版本 | 是 | 是 |
是否提供内嵌(Inline)版本 | 是 | 否 |
是否是业界标准 | 否 | 正在评估过程 |
Kernel Mode支持 | 是 | 否 |
返回类型 | HRESULT (user mode) NTSTATUS (kernel mode) |
随函数变化,errno_t |
是否需要修改代码 | 是 | 是 |
主要针对 | 缓存溢出 | 缓存溢出,和其它安全方面的考虑 |
表3:StrSafe和Safe CRT对比
在开发过程中,代码中全面禁用危险的API的编码实践,存在着一定的争议性。其中最具有代表性的观点可以参见Danny Kalev的Visual C++ 8.0 Hijacks the C++ Standard一文【4】。争论主要集中在以下几点。
4.1对于程序性能的影响
以StrSafe举例,由于增加了更多的动态校验,其速度较C动态库的函数相比,是有所下降的。在【2】一文中,给出了对StrSafe速度方面的测试数据如下:
测试例子:1千万次字符串合并调用。结果:
C动态库:7.3秒
StrSafe:8.3秒
我们看到,如果开发的系统不是完全以字符串处理为工作核心的话,使用StrSafe对系统性能的影响是可以控制的。
4.2开发人员可以决定在合适的地方使用危险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:注册表字符数目的限制
于是:
但实际上,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;
}
攻击代码之所以有效,是因为:
通过这个例子我们看出,在开发复杂的系统中,即便是有经验的开发人员,加上严格的代码复查过程,还是有可能由于使用危险的API而导致安全漏洞的引入。这也是微软在Windows Vista的开发过程中全面禁用危险API的原因。
4.3程序可移植性的影响
这一点的考虑是非常值得重视的。不管是StrSafe,还是Safe CRT,都不是工业界标准。因此,如果开发的系统需要移植到其它平台的话,采用Safe CRT是肯定不合适的。StrSafe的Inline方式,因为不依赖特定库,对可移植性的影响相对较小。
在C/C++程序中禁用危险的API,可以有效降低在代码中引入安全漏洞的可能。在考虑了性能和可移植性的因素下,强烈建议在开发过程中,使用StrSafe或Safe CRT中对应的安全函数来替代被禁用的危险的API调用。