使用 Visual C++ 防御功能保护您的代码

来源:互联网 发布:大数据信息平台 编辑:程序博客网 时间:2024/06/06 02:51
使用 Visual C++ 防御功能保护您的代码
Michael Howard
许 多代码都是使用 C 和 C++ 编写的,但遗憾的是,其中很多此类代码都有一些甚至连开发人员都不知道的安全漏洞。以任何语言编写的程序都存在可能会使其用户受到攻击的漏洞,但 C 和 C++ 语言在 Internet 历史上占有特殊的地位,原因是它们的许多安全漏洞都源自使其大受欢迎的功能:对计算机硬件无限制的访问以及随之而来的性能优势。在阅读有关安全和 C 或 C++ 方面的资料时,通常很容易看到“缓冲”和“溢出”等术语,因为缓冲通常就是直接访问内存的一个示例。这种类型的直接访问虽然功能非常强大,但也非常非常危险。
对于在生成 C 和 C++ 代码过程中出现的许多缓冲区溢出问题,其原因有多种。第一个原因我在上面已提到过:编程语言提供对易受攻击内存的直接访问。第二个原因是开发人员出错。第三个原因是编译器通常不提供防御功能。对于第一个问题很容易提供补救措施,但自此 C 和 C++ 开始成为不同的语言。
开发人员出错问题可通过培训来部分加以解决,但是我还没有真正看到在这方面教育机构的兴起。行业中也确实有一些进行安全培训的机构,但是我们只能提供部分解决方案或解决部分问题,我非常希望看到各所大学能对学生进行更多有关软件安全方面的教育。您可能要问“为什么教育机构不尝试培训这一非常重要的主题?”说实话,我也不知道为什么。不过这确实非常让人郁闷。
最后,即使有一流的培训,但有些安全问题真的非常复杂,就算是受过良好教育的工程师也不能完全解决。我们人类并不是完美的。
在编译器中建立更多防御措施是 Microsoft Visual C++ 团队多年以来一直试图解决的问题,在我们安全团队的协助下目前正逐渐得到改进。本专栏将概述一些在 Visual C++® 2005 中可用的缓冲区溢出防御措施及其他内容。请注意,一些其他编译器也提供防御措施,但 Visual C++ 有两个主要优势超越了 gcc 等编译器。首先,所有这些防御措施都默认集成在工具集中,无需下载某些稀奇古怪的加载项。其次,这些选项易于使用。
Visual C++ 工具集提供的防御措施包括(不分先后顺序):
  • 基于堆栈的缓冲区溢出检测 (/GS)
  • 安全异常处理 (/SafeSEH)
  • 数据执行保护 (DEP) 兼容性 (/NXCompat)
  • 映像随机化 (/DynamicBase)
  • 自动使用更安全的函数调用
  • C++ operator::new
在详细讨论每一项之前,我要指出的是这些防御措施并不能弥补不安全的代码。您应该始终尽最大努力编写最安全的代码,如果不知道该怎样做,可以先阅读一些有关这一主题的专业书籍。
Typical Stack Compared to One Compiled with /GS  (单击该图像获得较大视图)
另外我还要指出,这些都是 Microsoft 的“安全开发生命周期”(SDL) 的要求,这意味着编写 C 和 C++ 代码时必须使用这些选项,否则不能交付。偶尔会有些例外,但是非常少,因此在这里不对其做详细讨论。
最后必须要牢记的要点是:这些精心设计的防御措施也是可以被绕过的,具体视代码问题而定。代码使用的防御措施越多,解决起来就越困难,但是没有任何防御措施是尽善尽美的。它们都是为了减少被侵入的机会。您一定对此已经有所了解!唯一的应对方案就是使用更安全的函数调用,这是真正能够堵上漏洞的防御措施。让我们详细了解一下各个防御措施。
基于堆栈的缓冲区溢出检测 (/GS)
基于堆栈的缓冲区溢出检测是 Visual C++ 中提供的最陈旧最常见的防御措施。/GS 编译器标志的目标非常简单:减少恶意代码正确执行的机会。默认情况下,/GS 选项在 Visual C++ 2003 及更高版本中处于启用状态,它在运行时检测特定种类的堆栈溢出。为了进行检测,它会在函数堆栈中包括一个随机数(在堆栈的返回地址之前),当函数返回时,函数收尾代码将检查此值以确保它未进行过更改。如果它调用的 cookie 发生了变化,则执行过程将被中止。
用来设置 cookie 的函数导引代码如下所示:
复制代码
sub    esp, 8mov    eax, DWORD PTR ___security_cookiexor    eax, espmov    DWORD PTR __$ArrayPad$[esp+8], eaxmov    eax, DWORD PTR _input$[esp+4]
此处显示的是用来检查 cookie 的函数收尾代码:
复制代码
mov    ecx, DWORD PTR __$ArrayPad$[esp+12]add    esp, 4xor    ecx, espcall    @__security_check_cookie@4add    esp, 8
Visual C++ 2005 还会在堆栈上移动数据,使该数据更不容易被破坏。示例包括将缓冲区移动到高于非缓冲区的内存中。例如,此步骤有助于保护位于堆栈上的函数指针。此外,运行时将指针和缓冲区参数移至较低内存可减轻各种缓冲区溢出攻击。请参阅典型堆栈和 /GS 堆栈的比较图。
/GS 编译器选项不能应用于下列任何情况:
  • 函数不包含缓冲区。
  • 优化未启用。
  • 函数被定义为具有变量参数列表。
  • 函数使用无保护的关键字标记 (C++)。
  • 函数在第一个语句中包含内嵌汇编代码。
  • 缓存区不是 8 字节类型且大小小于 4 个字节。
在 Visual C++ 2005 SP1 中新增了一个选项,可使 /GS 试探法更积极主动,从而可以保护更多的函数。Microsoft 添加此选项是因为在具有基于堆栈的缓冲区溢出代码中发布了少量的安全性快报并且代码(即使是使用 /GS 进行编译的)不受 cookie 的保护。这一新增的选项大大增加了受保护的函数数量。
要使用此选项,可将以下代码行放到需要添加保护的模块中,例如用来处理 Internet 数据的代码:
复制代码
#pragma strict_gs_check(on)
这只是一个用来说明 Microsoft 如何不断发展 /GS 功能的示例。Visual C++ 2003 中的初始版本非常简单,随后在 Visual C++ 2005 SP1 中有所更新,在了解了一些新攻击以及一些绕过现有攻击的新方法后,我们又在 Visual C++ 2008 中对其再次进行了更新。在分析中我们发现,/GS 会导致不真实的兼容性或性能问题。
安全异常处理 (/SafeSEH)
会对 Internet Information Server (IIS) 4.0 造成影响的 CodeRed 蠕虫是由基于堆栈的缓冲区溢出导致的。有趣的是,/GS 并不能发现蠕虫侵入问题,因为代码并不溢出受影响的函数的返回地址,而是破坏堆栈上的异常处理程序。这个示例就很好地说明了为什么必须要始终强调编写安全代码,而不能完全依赖于基于编译器的各类防御措施。
异常处理程序是指出现异常情况(如除零)时执行的代码。处理程序的地址保存在函数堆栈帧中,因此会受到破坏。Visual Studio® 2003 及更高版本中的链接程序包含一个选项,可以在编译时将有效异常处理程序的列表存储在映像的 PE 标头中。如果在运行时引发异常,操作系统会检查映像标头以确定异常处理程序的地址是否正确;如果不正确,应用程序将被终止。如果在链接代码时使用此技术,即可预防 CodeRed。除了出现异常外,/SafeSEH 选项不会导致性能降低,因此链接时应始终使用此选项。
DEP 兼容性 (/NXCompat)
目前几乎生产的每个 CPU 都支持 no execute (NX) 功能,这意味着 CPU 不会执行非代码页。想一下此功能的意义:几乎每个缓冲区溢出漏洞都属于数据错误;攻击者利用缓冲区溢出将数据注入进程,然后在恶意数据缓冲区中继续执行。CPU 究竟为什么运行数据?
链接 /NXCompat 选项意味着您的可执行文件将受到 CPU 的 no execute 功能的保护。根据我们的经验,使用此选项后,Microsoft 安全团队很少再出现兼容性问题,而且性能也没有降低。
Windows Vista® SP1 还新增了一个新的 API,它可以为您的运行进程启用 DEP,它一经设置别无法再取消:
复制代码
SetProcessDEPPolicy(PROCESS_DEP_ENABLE);
映像随机化 (/DynamicBase)
Windows Vista 和 Windows Server® 2008 支持映像随机化,这意味着当系统启动时,它会在内存中混排操作系统映像。此功能的目的只是消除一些来自攻击者的预见性。它也称为“地址空间布局随机化”(ASLR)。请注意,对于要使用的任何 ASLR,都必须同时启用 DEP。
默认情况下,Windows® 将只调整相关的系统组件。如果希望由操作系统来移动映像(强烈建议),则应链接 /DynamicBase 选项。(此选项可在 Visual Studio 2005 SP1 及更高版本的工具集中获得。)链接 /DynamicBase 后有一个有趣的副作用 — 操作系统还会对您的堆栈进行随机化,这有助于降低可预见性,使攻击者破坏系统的企图更难以得逞。还要注意,在 Windows Vista 和 Windows Server 2008 中堆也会被随机化,但这是默认设置,无需编译或链接任何特殊选项。
更安全的函数调用
请看以下几行代码:
复制代码
void func(char *p) {    char d[20];    strcpy(d,p);    // etc}
假定 *p 包含不受信任的数据,则此代码代表一个安全漏洞。此代码的不当之处在于编译器可能会将对 strcpy 的调用提升为更安全函数调用(此更安全的函数会将复制操作绑定到目标缓冲区的大小)。为什么呢?因为缓冲区大小是静态的,并且在编译时已知!
利用 Visual C++,您可以将下列代码行添加到 stdafx.h 头文件中:
复制代码
#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1
编译器随后将继续运行并生成以下代码(这些代码来自初始的、不安全的函数):
复制代码
void func(char *p) {    char d[20];    strcpy_s(d,__countof(d), p);    // etc}
如您所见,代码现在是安全的,开发人员只添加了 #define,此外未进行任何处理。这是我最喜欢的 Visual C++ 新增功能之一,因为大约 50% 的不安全函数调用都可以被自动提升为安全调用。
C++ Operator::new
最后,Visual C++ 2005 及更高版本还新增了一种防御功能,它可以在调用 operator::new 时检测整数溢出。您可以从以下代码开始:
复制代码
CFoo *p = new CFoo[count];
Visual C++ 将其编译为:
复制代码
00004    33 c9           xor    ecx, ecx00006    8b c6           mov    eax, esi00008    ba 04 00 00 00  mov    edx, 40000d    f7 e2           mul    edx0000f    0f 90 c1       seto    cl00012    f7 d9           neg    ecx00014    0b c8            or    ecx, eax00016    51             push    ecx00017    e8 00 00 00 00 call    ??2@YAPAXI@Z    ; operator new
在计算了要分配的内存量后 (mul edx),将根据相乘后溢出标志的值来决定是否设置 CL 寄存器,因此,ECX 将为 0x00000000 或 0xFFFFFFFF。由于下一个操作(或 ecx)的原因,ECX 寄存器将是 0xFFFFFFFF 或者是在 EAX 中包含的值,即最初相乘的结果。它随后被传递到 operator::new,在进行 2^N-1 次分配时它会失败。
此防御功能无限制。没有编译器开关,它只是编译器所做的工作。
如果失败会怎样?
噢!这是一个棘手的问题!如果触发了上述列出的任何防御功能,都会产生非常令人讨厌的结果:应用程序会退出。这不是最理想的解决方案,但总要比运行攻击者的恶意代码好得多。
SDL 强制要求新代码使用所有这些防御功能,这完全是因为外部有太多的攻击,而且您永远不能确保代码 100% 没有漏洞。SDL 的一个流行语是“代码失败该怎么办?”在现实生活中“该怎么办”意味着不屈不挠!不要让攻击者的代码肆意横行,要让攻击者寸步难行。决不要放弃!
因此要用最新版本的 C++ 编译器进行编译以获得更好的/GS,并且使用最新链接器进行链接以利用 CPU 和操作系统提供的防御功能。