软件防御:安全解除链接与引用计数加固
对象生命周期管理漏洞是一类非常常见的内存安全漏洞。这些漏洞形式多样,通常难以通用缓解。此类漏洞通常源于引用计数(描述对象的活跃用户)的错误计算,或对某些对象状态或错误条件的处理不当。虽然通用缓解这些问题是一个持续的挑战,但微软已在Windows 8和Windows 8.1中采取措施缓解某些特定类别的这些问题。这些缓解措施通常涉及广泛的代码检测,以减少特定类别问题的影响。
引入快速失败
在进一步详细介绍本文讨论的一些缓解措施之前,简要概述这些即将推出的缓解措施报告失败的机制非常重要。
对于内存安全缓解措施,最基本(但有时被忽视)的方面之一是检测到损坏时应采取的措施。典型的内存安全缓解措施试图检测程序“偏离轨道”并严重损坏其某种内部状态的迹象;因此,检测损坏的代码应尽可能少地依赖进程状态,并在处理错误条件时尽可能少地依赖(通常导致捕获崩溃转储并终止故障程序)。
处理触发崩溃转储捕获和程序终止的机制历来非常环境特定。例如,在用户模式Windows中常用的API在Windows内核中不存在;相反,必须使用一组不同的API。此外,许多现有机制在设计时并未绝对最小化对损坏程序在错误报告时状态的依赖。
环境特定的关键故障报告机制对于编译器生成的代码或编译一次然后链接到可能在许多不同环境(如用户模式、内核模式、早期启动等)中运行的程序中的代码也是有问题的。以前,这个问题通常通过提供一小段存根代码来解决,该代码链接到程序中并提供适当的关键故障报告抽象。然而,随着依赖所述存根库的程序范围扩大,这种方法变得越来越有问题。对于错误报告设施链接到大量程序的安全功能,存根代码必须非常谨慎地选择可能依赖的API。
以/GS为例;直接链接到崩溃转储写入代码会将代码拉入几乎每个启用/GS构建的程序中,这显然是非常不可取的。有些程序可能需要在引入这些设施之前运行,即使不是这种情况,在如此广泛的程序范围内拉入额外的依赖DLL(或静态链接库代码)也会带来不可接受的性能影响。
为了满足未来基于编译器(代码生成改变)的缓解措施的需求,这些缓解措施强烈希望尽可能环境无关,以及常见的基于框架/库的缓解措施,我们在Windows 8和Visual Studio 2012中引入了一个称为快速失败(有时称为_fail fast_)的设施。快速失败代表了一种统一的机制,用于在可能损坏的进程上下文中请求立即进程终止,该机制由各种Microsoft运行时环境(启动、内核、用户、管理程序等)的支持以及一个新的编译器内在函数fastfail的组合启用。使用快速失败的代码具有可内联、紧凑(从代码生成角度)和跨多个运行时环境的二进制可移植性的优势。
在内部,快速失败通过几种特定于架构的机制实现:
架构 | 指令 | “代码”参数位置 |
---|---|---|
AMD64 | int 0x29 | rcx |
ARM | Opcode 0xDEFB* | r0 |
x86 | int 0x29 | ecx |
- ARM定义了一系列Thumb2操作码空间,这些操作码是永久未定义的,并且永远不会分配给处理器使用。这些操作码可用于平台特定目的。
单个Microsoft定义的代码参数(在winnt.h和wdm.h中分配了以FAST_FAIL_
快速失败请求是自包含的,通常只需两条指令即可执行。内核或等效物在执行快速失败请求后采取适当的操作。在用户模式代码中,当引发快速失败事件时,除了指令指针本身之外,没有内存依赖关系,即使在严重内存损坏的情况下也能最大化其可靠性。
用户模式快速失败请求作为第二次机会不可继续异常出现,异常代码为0xC0000409,并且至少有一个异常代码(第一个异常参数是作为fastfail内在函数参数提供的快速失败代码)。此异常代码以前仅用于报告/GS堆栈缓冲区溢出事件,之所以选择它,是因为Windows错误报告(WER)和调试基础设施已经知道它表示进程已损坏,并且应采取最少的进程内操作来响应故障。内核模式快速失败请求通过专用的错误检查代码KERNEL_SECURITY_CHECK_FAILURE(0x139)实现。在这两种情况下,都不会调用异常处理程序(因为程序预计处于损坏状态)。调试器(如果存在)有机会在程序终止前检查程序状态。
不支持快速失败指令本机的Windows 8之前操作系统通常会将快速失败请求视为访问冲突或UNEXPECTED_KERNEL_MODE_TRAP错误检查。在这些情况下,程序仍然终止,但不一定那么快。
紧凑的代码生成特性以及跨多个运行时环境的支持而无额外依赖,使快速失败非常适合用于涉及程序端代码检测的缓解措施,无论是基于编译器还是基于库/框架。由于故障报告逻辑可以直接以环境无关的方式嵌入应用程序代码中,在检测到损坏或不一致的具体点,故障检测时对活动程序状态的干扰最小。编译器还可以隐式地将快速失败站点视为“无返回”,因为操作系统不允许在快速失败请求后恢复程序(即使存在异常处理程序),从而启用进一步优化以最小化故障报告的代码生成影响。我们期望未来的基于编译器的缓解措施将利用快速失败来内联和上下文中报告故障(在可能的情况下)。
安全解除链接回顾
之前,我们讨论了在Windows 7内核中向执行程序池分配器有针对性地添加安全解除链接完整性检查。安全解除链接(和安全链接)是一组通用技术,用于在发生修改操作(如列表条目解除链接或链接)时验证双向链接列表的完整性。这些技术通过验证正在操作的列表条目的相邻列表链接是否仍然指向正在链接或解除链接到列表中的列表条目来操作。
安全解除链接操作历来是内存分配器簿记数据结构中包含的一个有吸引力的防御措施,作为对池溢出或堆溢出漏洞的额外防御。Windows XP Service Pack 2首次将安全解除链接引入Windows堆分配器,Windows 7将其引入内核中的执行程序池分配器。要理解为什么这是一种有价值的防御技术,检查内存分配器通常如何实现是有帮助的。
内存分配器通常包括一个可用内存区域的空闲列表,这些区域可用于满足分配请求。通常,空闲列表是通过在可用内存块中嵌入一个双向链接列表条目来实现的,该内存块逻辑上位于分配器的空闲列表上,此外还有其他关于内存块的元数据(如其大小)。这种方案允许分配器快速定位并返回合适的内存块给调用者以响应分配请求。
现在,当内存块返回给调用者以满足分配时,它从空闲列表中解除链接。这涉及更新相邻列表条目(位于空闲分配块中嵌入的列表条目内)以指向彼此,而不是刚刚释放的块。在溢出场景中,攻击者成功溢出缓冲区并覆盖相邻空闲内存块头的内容,攻击者可能有机会为next和previous指针提供任意值,这些值将在下次分配(被覆盖的)空闲内存块时被写入。
这产生了通常称为“写什么在哪里”或“随处写”的原语,让攻击者选择特定值(什么)和特定地址(哪里)来存储所述值。从利用的角度来看,这是一个强大的原语,并赋予攻击者高度的自由度[2]。
在内存分配器的上下文中,安全解除链接通过验证列表邻居是否仍然指向空闲块中嵌入的列表条目所说的元素来帮助缓解此类漏洞。如果块的列表条目已被覆盖并且攻击者控制了其列表条目,则此不变量通常会失败(前提是逻辑上的前一个和下一个列表条目也没有损坏),从而能够检测到损坏。
Windows 8中的安全解除链接
安全解除链接广泛适用于内存分配器的内部链接列表之外;许多应用程序和内核模式组件在其自己的数据结构中使用链接列表。这些数据结构也有益于插入安全解除链接(和安全链接)完整性检查;除了提供针对基于堆的溢出覆盖堆上应用程序特定数据中的列表指针的保护之外[1],应用程序级代码中的链接列表完整性检查通常提供一种更好地保护应用程序可能错误地删除包含链接列表条目的应用程序特定对象两次(由于应用程序特定对象生命周期管理问题)的情况,或者可能不正确使用或同步访问链接列表的方法。
Windows提供了一个用于操作双向链接列表的通用库,形式为一组在常见Windows头文件中提供的内联函数,这些函数既暴露给第三方驱动程序代码,也在内部大量使用。该库非常适合作为在整个Microsoft代码库以及第三方驱动程序代码中检测代码的中心位置,具有安全解除链接(和安全链接)列表完整性检查。
从Windows 8开始,“LIST_ENTRY”双向链接列表库配备了列表完整性检查,保护使用该库的代码免受列表损坏。所有写入列表条目节点的列表链接指针的列表操作将首先检查相邻列表链接是否仍然指向有问题的节点,这使得许多类别的问题在导致进一步损坏之前被捕获(例如,列表条目的双重移除通常在第二次移除时立即被捕获)。由于该库设计为操作环境无关的内联函数库,因此使用快速失败机制报告故障。
在Microsoft内部,我们的经验是安全链接(和安全解除链接)检测在识别链接列表误用方面非常有效,在Windows 8开发周期中修复了超过100个不同的错误,原因是列表完整性检查。许多Windows组件利用相同的双向链接列表库,导致整个Windows代码库的广泛覆盖[1]。
我们还使第三方代码能够利用这些列表完整性检查;使用Windows 8 WDK构建的驱动程序将默认获得完整性检查,无论构建时目标是什么操作系统。完整性检查向后兼容以前的OS;但是,以前的OS版本会对驱动程序中的列表条目完整性检查失败做出更通用的错误检查代码(如UNEXPECTED_KERNEL_MODE_TRAP)的反应,而不是Windows 8中引入的专用KERNEL_SECURITY_CHECK_FAILURE错误检查代码。
对于任何广泛的代码检测形式,一个几乎无处不在的关注点自然与检测的性能影响有关。我们的经验是,安全解除链接(和安全解除链接)的性能影响是最小的,即使在涉及大量列表条目操作操作的工作负载中也是如此。由于列表条目操作操作已经固有地涉及通过跟随指针到相邻列表条目,仅仅添加一个额外的比较(带有一个分支到公共快速失败报告标签)已被证明是非常廉价的。
引用计数加固
对于具有非平凡生命周期管理的对象,通常使用引用计数来管理保持特定对象存活的责任,并在没有对象的活跃用户时清理对象。鉴于对象生命周期管理不当是内存损坏漏洞最常见的情况之一,因此引用计数在许多这些漏洞中经常处于中心舞台也就不足为奇了。
虽然已有研究进入这一领域(例如,Mateusz “j00ru” Jurczyk 2012年11月关于引用计数漏洞的案例研究[5]),但通用缓解所有引用计数管理不当问题仍然是一个难题。引用计数相关漏洞通常可以分为几个大类:
- 对对象引用不足(例如,在获取对象的长期指针时忘记增加引用计数,或不正确地递减对象的引用计数)。这些漏洞难以廉价缓解,因为基于特定对象的生命周期模型来确定引用计数是否应在某个时间递减的可用信息通常在引用计数递减时不易获得。此类漏洞可能导致对象在另一个对象用户仍然持有他们认为有效的指针时被删除;如果攻击者可以在刚刚删除的对象相同位置分配堆内存,则对象可能被替换为潜在攻击者控制的数据。
- 对对象过度引用(例如,在错误路径中忘记递减引用计数)。此类漏洞常见于复杂代码段具有不完全清理的提前退出路径的情况。与引用不足类似,此类漏洞也可能最终导致对象过早删除,如果攻击者能够强制引用计数在重复执行获取(但然后忘记释放)对特定对象的引用的代码路径后“回绕”到零。历史上,此类漏洞最常在本地内核利用领域产生影响,其中通常有丰富的对象暴露给不受信任的用户模式代码,以及各种API来操作所述对象的状态。
从Windows 8开始,内核对象管理器已开始在其内部引用计数中强制执行防止引用计数回绕的保护。如果引用计数递增操作检测到引用计数已回绕,则立即引发REFERENCE_BY_POINTER错误检查,防止回绕的引用计数条件被利用导致后续的使用后释放情况。这使得过度引用类别的漏洞能够以稳健的方式得到强烈缓解。我们期望,有了这种加固,利用内核对象管理器对象的过度引用条件进行代码执行将不切实际,前提是所有添加引用路径都受到加固检测的保护。
此外,对象管理器还类似地保护从<= 0引用到正数引用的转换,这可能使尝试利用其他类别的引用计数漏洞的可靠性降低,如果攻击者无法轻易防止其他引用计数操作“流量”在尝试利用使用后释放条件时发生。然而,应该仍然注意到,这不是对引用不足问题的完全缓解。
在Windows 8.1中,我们通过将这种级别的加固添加到内核的某些其他部分来加强引用计数加固,这些部分维护它们自己的、对象管理器未管理的对象的“私有”引用计数。在可能的情况下,代码还已转换为使用一组通用的引用计数管理逻辑,该逻辑实现了对象管理器内部引用计数所采用的相同最佳实践,包括使用指针大小的引用计数(这进一步有助于防止引用计数回绕问题,特别是在64位平台上或攻击者必须为每个泄漏的引用分配内存的情况下)。类似于Windows 8中引入的列表条目完整性检查,在引用计数管理作为内联函数库提供的情况下,快速失败被用作方便且低开销的机制,在检测到引用计数不一致时快速中止程序。
一个具体漏洞的例子,该漏洞本可以通过Windows 8.1中更广泛采用引用计数加固得到强烈缓解,是CVE-2013-1280(MS13-017),该漏洞源于I/O管理器中的一个提前退出代码路径(响应错误条件),在该路径中代码没有正确释放先前在易受攻击函数中获取的内部I/O管理器对象的引用计数。如果攻击者能够重复执行有问题的代码路径,那么他们可能能够导致引用计数回绕,从而后来触发使用后释放条件。有了引用计数加固,尝试利用此漏洞将导致立即错误检查,而不是潜在的使用后释放情况发生。
结论
在Windows 8期间引入并在Windows 8.1期间扩展的引用计数和列表条目加固更改旨在提高某些类别的对象生命周期管理漏洞的利用成本。当受到Windows 8和Windows 8.1中部署的引用计数加固保护时,过度引用或泄漏引用等情况可以得到强烈缓解,使得实际利用它们进行代码执行极其困难。在整个Microsoft代码库(以及越来越多地通过使用Windows 8或以上WDK的第三方驱动程序)中普遍检测列表条目操作,使得利用某些生命周期管理问题的可靠性降低,并通过更接近原因捕获损坏来提高可靠性(在某些情况下,在损坏可以影响系统其他部分之前)。
话虽如此,未来仍有增加这些类别缓解措施在整个Microsoft代码库(以及第三方)中采用的机会,以及未来基于编译器或基于框架的广泛检测以捕获和检测其他类别问题的潜在机会。我们期望未来继续研究和进一步投资于基于编译器和基于框架的对象生命周期管理(和其他漏洞类别)的缓解措施。
- Ken Johnson
参考文献
[1] Ben Hawkes. Windows 8 and Safe Unlinking in NTDLL. July, 2012. [2] Kostya Kortchinsky. Real World Kernel Pool Allocation. SyScan. July, 2008. [3] Chris Valasek. Modern Heap Exploitation using the Low Fragmentation Heap. SyScan Taipei. Nov, 2011. [4] Adrian Marinescu. Windows Vista Heap Management Enhancements. Black Hat USA. August, 2006. [5] Mateusz “j00ru” Jurczyk. Windows Kernel Reference Count Vulnerabilities – Case Study. November 2012.