- 概述
在前面的安全编码实践的文章里,我们讨论了GS编译选项,
- 那些是危险的API
2.1历史
在微软产品的安全漏洞中,有很大一部分是由于不正确的使用
微软安全公告 | 涉及产品 | 涉及的函数 |
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动态库函数容易引入安全漏洞,这一点并不奇怪。
2.2 危险API的列表
有关完整的危险API的禁用列表,大家可以参见http:
在这里我们列出其中的一部分,以便大家对那些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等,
- 如何替代被禁用的危险API
从上面的介绍可以看出绝大多数C动态库中的字符串处理函数
- StrSafe
- Safe CRT
后面我们会讨论这两种方案的不同之处。
- 目标缓存区的大小被显式指明。
- 动态校验。
- 返回代码。
以StringCchCopy举例。它的定义如下:
HRESULT StringCchCopy(
LPTSTR pszDest,
size_t cchDest,
LPCTSTR pszSrc
);
cchDest指明目标缓存区pszDest最多能容纳字
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(
strncpy(szPath, szCWD, cchPath);
strncat(szPath, TEXT("//"), cchPath);
strncat(szPath, TEXT("desktop.ini"),cchPath);
}
在以上代码里存在着几个问题:首先,没有错误代码的校验。
使用StrSafe后的代码是
bool SaferFunc(LPTSTR szPath,DWORD cchPath) {
TCHAR szCWD[MAX_PATH];
if (GetCurrentDirectory(
SUCCEEDED(StringCchCopy(
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函数,V
下面给出一个使用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,
下面的表格【1,p246】里列出了两者之间的差异。
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的编码实践,
4.1对于程序性能的影响
以StrSafe举例,由于增加了更多的动态校验,
测试例子:1千万次字符串合并调用。结果:
C动态库:7.3秒
StrSafe:8.3秒
我们看到,
4.2开发人员可以决定在合适的地方使用危险API
首先,同意如果代码中正确使用危险API的话,
- 开发人员的素质和培训
- 有时候即使执行严格的代码复查,仍然可能由于使用危险的API而
引入安全漏洞。
第二点尤其关键。大家看到这里可能会有疑问,使用危险的
微软 05-047 Plug-n-Play RPC:即插即用中的漏洞,允许远程执行代码和特权提升。
#define MAX_CM_PATH
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/
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:w
图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)*
PWCHAR Buffer = (PWCHAR)malloc(86);
wsprintf(pszFilter,L"
CM_Get_Device_ID_List((
return 0;
}
攻击代码之所以有效,是因为:
- pszFilter(除去末位的0) 作为pszDevice 传递给GetInstanceList
- RegOpenKeyEx 接收了这个长字符串,忽略那些“/”,返回 ERROR_SUCCESS。
通过这个例子我们看出,在开发复杂的系统中,
4.3程序可移植性的影响
这一点的考虑是非常值得重视的。不管是StrSafe,
- 总结
在C/C++程序中禁用危险的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