Windows 应用程序中的安全字符串处理
本文介绍了 Strsafe、安全的 C 运行时库 (CRT) 和安全的 STL 函数。
引言
机密性、完整性和可用性 (CIA) 是信息安全的核心原则。机密性是指限制信息访问,仅允许授权用户访问。完整性是指信息的可靠性。可用性是指计算机信息的可用性。安全原则应在整个软件开发生命周期中加以考虑。微软高度重视为客户构建安全环境,并提供 Microsoft 安全开发生命周期 (SDL) 方法来构建安全应用程序。安全性应在需求、分析、设计、实现、验证和发布阶段加以考虑。许多系统级应用程序使用 C 和 C++ 编程语言开发。C 和 C++ 的漏洞允许攻击者攻击操作系统的低级层面。微软增加了对 Microsoft 产品的安全性,并提供 Microsoft 安全开发生命周期 (SDL) 流程来使用 Microsoft 技术开发安全软件。软件开发人员应在开发过程的各个环节考虑安全性。
安全字符串处理
C/C++ 没有内置的 `string` 数据类型。C/C++ 将 `string` 视为字符数组。但是,C++ 可以使用标准模板库的 `string` 类。当应用程序同时使用标准模板库 (STL) 和 C 风格 `string` 时,应考虑安全性。C 风格 `string` 是 `NULL` 终止的。Windows C/C++ 程序员在 `string` 处理方面有许多选择。开发人员可以使用 C 运行时库 (CRT)(`strcpy`、`strcmp` 等)、Windows `string` 函数(`lstrcmp`、`lstrcpy` 等)、C++ STL 和 MFC/ATL 库 `String` 函数。微软提高了所有 `string` 函数的安全性,因为大多数应用程序安全攻击都基于缓冲区溢出。
微软发布了 `Strsafe` 函数以提高 Windows 应用程序的安全性。之后,微软增强了 CRT 库以提高安全性。这有助于开发人员构建安全应用程序,微软也在内部使用这些函数。微软重构了 ATL 和 MFC 库以利用安全的 C 运行时函数。因此,如果开发人员使用新版本的 MFC 和 ATL 来重建他们的应用程序,他们将确保应用程序使用安全版本的库。
安全 `string` 处理属于安全开发生命周期 (SDL) 实践 #9:弃用不安全函数。Microsoft SDL 建议使用 _Banned.H_、_Strsafe.h_(随 Visual Studio 提供)和安全 CRT(随 Visual Studio 提供)。国家标准与技术研究院 (NIST) 指出,在早期阶段设计安全软件的成本比发布后修复的成本低 30 倍。
单字节字符集 (SBCS)、多字节字符集 (MBCS) 和 Unicode
开发人员应了解单字节字符集 (SBCS)、多字节字符集 (MBCS) 和 Unicode 之间的区别。单字节字符为每个字符只分配一个字节的内存。单字节字符集 (SBCS) 足以表示 ASCII 字符和许多欧洲语言。但某些非欧洲字符需要一个以上的字节。因此,需要多字节字符集 (MBCS)。Unicode 字符是 2 字节的多语言字符编码。大多数现代 Microsoft 操作系统仅使用 Unicode 格式。即使您开发 ASCII 版本,Microsoft 层在调用函数之前也会将其转换为 Unicode 格式。因此,这会降低性能。因此,微软建议使用 Unicode 字符串。
Unicode 和 SBCS/MBCS 的内存分配不同。因此,微软提供了两个版本的函数。函数调用 ASCII 或 Unicode 取决于应用程序编译器设置。微软还提供了通用的文本数据类型来处理 Unicode 和 SBCS/MBCS。它将为非 Unicode 调用 SBCS/MBCS 函数,为 Unicode 编译器选项调用 Unicode 函数。微软建议将 Unicode 版本用于未来的开发。因此,可以轻松地将组件迁移到不同的区域设置,而无需付出太多努力。
Windows API 为 ASCII 和 Unicode 提供了单独的函数。Strsafe 系列函数在名称末尾提供了 A(用于 ASCII)或 W(用于 Unicode)的单独函数。例如,StrSafe StringCbCatEx 函数可用于 StringCbCatExW(Unicode)和 StringCbCatExA(ASCII)。Windows API MultiByteToWideChar 和 WideCharToMultiByte 用于在 Unicode 和 ASCII 之间转换字符串。
禁止的应用程序编程接口 (API)
Microsoft C++ 编译器在编译时提供了更多应用程序安全选项。Microsoft Visual C++ 编译器在使用被禁用的字符串 API 时会发出警告。_Strsafe.h_ 提供了 C 风格字符串的安全功能。或者,开发人员可以选择 Strsafe 或 Safe CRT 来构建安全应用程序。禁止使用的 API 列表和替代函数在安全开发生命周期 (SDL) 禁止的函数调用中进行了讨论。如果 strcpy 或 strcat 使用不当,将导致缓冲区溢出。因此,微软将这些类别的函数添加到禁止 API 列表中。禁止使用的 API 列表适用于 Microsoft 和非 Microsoft 技术和平台。N 系列函数也列在禁止 API 列表中。C 运行时“n”函数使其难以使用,这也是它们被列为 Microsoft SDL 禁止 API 列表一部分的原因之一。
根据 SDL 禁止的函数调用列表,开发人员应包含 `#include "banned.h";`。这将能够定位代码中的任何被禁止的函数。完整的禁止使用的 API 列表也包含在头文件中。或者,如果您使用的是 Visual Studio 2005 或更高版本的编译器,您有一个内置的方法来检查这些被禁止的函数。要捕获被禁止的 C 运行时函数,您可以编译 `/W4`,然后对所有 C4996 警告进行分类。
#include "c:\banned.h"
1>c:\development\c++\ban\ban\test.cpp(79) :
warning C4995: 'StrCpyN': name was marked as #pragma deprecated
1>c:\development\c++\ban\ban\test.cpp(79) :
warning C4995: 'StrCpyNW': name was marked as #pragma deprecated
Strsafe 和安全的 C 运行时库 (CRT)
微软发布了 Strsafe 库,用于开发安全的 Windows 应用程序。软件安全性应在整个软件开发生命周期 (SDLC) 中加以考虑。输入验证是系统和 Web 编程的主要问题。它可能存在缓冲区溢出、下溢等变体。应用程序在许多地方使用字符数组来比较字符串、追加字符串、追加字符串的字符、复制字符串、获取字符串输入、获取字符串长度和格式化字符串字符。Strsafe 支持的函数包含保证 `null` 终止的字符串,成功调用时返回 `HRESULT`,以及针对相应字符计数(“cch”)或字节计数(“cb”)版本的单独函数 [本文仅介绍 cch 版本]。
在 Strsafe 之后,微软希望提高 C 运行时库的安全性。因此,CRT 提供了安全的 `_s` 函数来验证其参数。它检查值是否有效、缓冲区是否有足够的内存空间等。这些函数返回 `errno_t` 来指示成功或失败。发布版本会自动终止,调试版本会显示断言对话框以进行进一步调试。本文介绍了 Strsafe、安全的 C 运行时库 (CRT) 和安全的 STL 函数。
复制字符串
安全的字符串复制支持 `wcscpy_s`(宽字符)、`_mbscpy_s`(多字节字符)和 `strcpy_s` 格式。`wcscpy_s` 的参数和返回值是宽字符字符串,而 `_mbscpy_s` 的是多字节字符字符串。否则,这三个函数行为相同。
`strcopy` 函数不接受目标缓冲区大小作为输入。因此,开发人员无法控制验证目标缓冲区的大小。`_countof` 宏用于计算静态分配数组中的元素数量。它不适用于指针类型。`wcscpy_s` 接受目标字符串、目标字符串缓冲区的大小以及 `null` 终止的源字符串。
使用安全 CRT 复制字符串
wchar_t safe_copy_str1[]=L"Hello world";
wchar_t safe_copy_str2[MAX_CHAR];
wcscpy_s( safe_copy_str2, _countof(safe_copy_str2),safe_copy_str1 );
printf ("After copy string = %S\n\n", safe_copy_str2);
`StringCchCopy` 函数接受指向目标缓冲区的指针、目标缓冲区的大小(以字符为单位)以及指向源字符串的指针。它返回 `S_OK` 表示成功;`STRSAFE_E_INVALID_PARAMETER` 表示目标缓冲区为 `0` 或最大 `STRSAFE_MAX_CCH`;`STRSAFE_E_INSUFFICIENT_BUFFER` 表示因缓冲区不足而复制失败。`StringCchCopy` 是一个通用版本,等同于 TCHAR 系列函数。它将应用于 ASCII 的 A 版本和应用于宽字符函数的 w 版本。
`StringCchCopyEx` 函数提供了更多选项。Ex 函数包含指向目标末尾指针的地址参数、指向指示目标中未使用字符数量的变量的指针以及不同的标志(`STRSAFE_FILL_BEHIND_NULL`、`STRSAFE_IGNORE_NULLS`、`STRSAFE_FILL_ON_FAILURE`、`STRSAFE_NULL_ON_FAILURE` 和 `STRSAFE_NO_TRUNCATION`)。
使用 Strsafe 复制和追加字符串
wchar_t wsString[128];
HRESULT Res;
Res= StringCchCopy(wsString, _countof(wsString), L"Hello ");
if (Res != S_OK) {
printf("StringCchCopy Failed: %S\n", wsString);
exit(-1);
}
Res= StringCchCat(wsString,sizeof(wsString),L" World!");
if (Res != S_OK) {
printf("StringCchCat Failed: %s\n", wsString);
exit(-1);
}
printf("%S\n", wsString);
获取字符串
`gets` 函数不验证字符串大小。安全的版本 `gets_s` 验证输入的字符串大小。如果缓冲区大小太小,它会在调试模式下显示“缓冲区过小”错误 [图 1]。当我运行应用程序并输入比分配的内存更多的字符时,在调试构建期间出现了以下断言错误对话框。
使用 CRT 获取用户输入字符串
#define MAX_BUF 10
// include
// do
wchar_t safe_getline[MAX_BUF];
if (gets_s(safe_getline, MAX_BUF) == NULL) {
printf("invalid input.\n");
abort();
}
printf("%S\n", safe_getline);
如果缓冲区太小,此函数将调用无效参数处理程序。如果允许执行继续,这些函数将返回 `NULL` 并将 `errno` 设置为 `EINVAL`。
`StringCchGets` 接受一个指向接收复制字符的缓冲区的指针和目标缓冲区的大小(以字符为单位)参数。它将返回 `S_OK` 表示成功获取字符串。在错误条件下,它将返回 `STRSAFE_E_END_OF_FILE`、`STRSAFE_E_INVALID_PARAMETER` 和 `STRSAFE_E_INSUFFICIENT_BUFFER`。
使用 Strsafe 获取用户输入字符串
wchar_t wsString[128];
HRESULT Res;
Res=StringCchGets(wsString, sizeof(wsString));
if (Res != S_OK) {
printf("StringCchGets Failed: %S\n", wsString);
exit(-1);
}
printf("%S \n",wsString);
字符串长度
`StringCchLength` 用于获取字符串的长度(以字符为单位),不包括终止的 `null` 字符。它还接受 `wsString` 中允许的最大字符数,并返回输入字符串的长度。它返回 `S_OK` 表示成功调用,`STRSAFE_E_INVALID_PARAMETER` 表示失败调用。
使用 Strsafe 获取字符串长度
wchar_t wsString[] = L"Hello World";
size_t length;
HRESULT Res;
Res = StringCchLength(wsString, MAX_PATH, &length);
if (Res != S_OK) {
printf("StringCchLength Failed: %S\n", wsString);
exit(-1);
}
printf("StringCchLength = %d \n",length);
格式化字符串
`StringCchPrintf` 接受参数列表并返回字符串。`StringCchPrintf` 函数接受指向缓冲区的指针和目标缓冲区的大小。它返回 `S_OK` 表示成功调用,`STRSAFE_E_INVALID_PARAMETER` 和 `STRSAFE_E_INSUFFICIENT_BUFFER` 表示调用失败。不受控制的格式字符串会引发“命令注入”攻击。
使用 Strsafe 格式化字符串
TCHAR msgBuf[BUF_SIZE];
size_t cchStringSize;
DWORD dwChars;
int val1 = 10;
int val2= 20;
HANDLE hStdout;
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
if( hStdout == INVALID_HANDLE_VALUE )
return 1;
StringCchPrintf(msgBuf, BUF_SIZE, TEXT("Parameters = %d, %d\n"), val1, val2);
StringCchLength(msgBuf, BUF_SIZE, &cchStringSize);
WriteConsole(hStdout, msgBuf, cchStringSize, &dwChars, NULL);
安全的标准模板库 (STL)
标准 C++ 库已增强,可用于安全的字符串和字符处理。微软增强了标准模板库以提高安全性。STL 添加了带有 `_s` 的新函数以提高安全性。下表包含不安全函数和更安全函数。
STL | 安全的 STL |
basic_string::copy |
basic_string::_Copy_s |
basic_streambuf::sgetn |
basic_streambuf::_Sgetn_s |
basic_streambuf::xsgetn |
basic_streambuf::_Xsgetn_s |
char_traits::copy |
char_traits::_Copy_s |
char_traits::move |
char_traits::_Move_s |
ctype::narrow |
ctype::_Narrow_s |
ctype::do_narrow |
ctype::_Do_narrow_s |
ctype::widen |
ctype::_Widen_s |
ctype::do_widen |
ctype::_Do_widen_s |
所有安全的 STL 函数都经过目标字符串大小的验证。安全的 STL 增强了调试迭代器和检查过的迭代器,以使迭代器和算法更安全。Microsoft C++ 运行时检测到迭代器使用不当,并在运行时断言并显示一个对话框。这仅适用于在调试模式下编译的程序。
检查过的迭代器不会覆盖容器的边界。它适用于发布和调试版本。如果程序定义了 `#define _SECURE_SCL 1`,则所有标准迭代器都会被检查。`_SECURE_SCL` 的默认值为 `1`。如果程序定义了 `#define _SECURE_SCL_THROWS 1`,则超出范围的迭代器使用会导致运行时异常。`_SECURE_SCL_THROWS` 的默认值为 `0`(默认情况下程序将被终止)。
以下 STL 函数在遇到越界时将生成运行时错误
- basic_string、valarray 和 bitset
- dequeue 和 vector 的 back、front 和 operator[] 操作
- list 和 queue 的 back 和 front 操作
Strsafe 和 CRT 之间的区别
否 | Strsafe | CRT |
1 | 在内核模式下运行 | 在用户模式下运行 |
2 | 始终 `NULL`-终止字符串 | 参数验证。包括传递给函数的 `NULL` 值、`Null` 终止、有效性的枚举值、积分值是否在有效范围内 |
3 | 始终返回 `HRESULT` 返回代码 | 成功时为零,否则为错误代码 |
4 | 支持 32 位和 64 位环境。 | 支持 32 位和 64 位环境。 |
5 | Windows 软件开发工具包 | Microsoft Visual Studio |
6 | 包含一个头文件 | 包含多个头文件 |
7 | 支持从 Windows XP Service Pack 2 及更高版本 | 支持从 Visual C++ 2005 及更高版本 |
8 | 所有函数都需要目标缓冲区大小参数 | 有大小的缓冲区 |
9 | 缓冲区溢出 | 缓冲区溢出和其他漏洞 |
10 | 仅字符串处理 | 包括字符串处理和 Windows 安全性、文件系统安全性、增强的错误报告等 |
结论
微软越来越重视安全应用程序开发。缓冲区溢出和其他输入验证允许黑客攻击应用程序(DoS 或 DDoS 攻击)。没有单一的文档或指南来编写安全应用程序。微软提供的指导方针使得在微软环境中攻击应用程序变得困难。安全性应在软件开发的所有阶段加以考虑。这并不意味着安全性仅在开发阶段得到考虑。本文仅介绍了安全的字符串处理。但是,微软发布了安全开发生命周期 (SDL) 指南 5.0 版(2010 年 11 月 5 日更新)用于开发安全应用程序。微软根据当前漏洞不断更新安全开发生命周期 (SDL) 指南。
摘要
- `Banned` API 是您应避免用于新应用程序和旧应用程序开发的一组函数调用。此列表可能因当前漏洞而异。
- 如果应用程序基于 Windows 构建
- 使用 Strsafe 函数并验证返回值。
- 否则,如果应用程序基于 CRT 构建,请使用安全的 CRT 函数。
- 使用最新的 Microsoft 库重建基于 MFC 和 ATL 的应用程序,这些库是使用安全版本构建的。
- 如果应用程序使用 STL,请检查安全函数是否可用。
- 遵循 Microsoft 安全开发生命周期 (SDL) 5.0 指南。
关注点
历史
- 2014 年 3 月 9 日:更新了 __countof 用于 StringCchCopy,感谢 virtualnik
- 2011 年 5 月 12 日:将 'Strafe' 和 'Starsafe' 更新为 'Strsafe',感谢 Stefan63
- 2011 年 4 月 15 日:初版