历史上,内存腐败利用一直是优秀红色团队成员工具包中最强大的附件之一。它们允许攻击者在不依赖任何用户交互的情况下执行有效负载,为攻击性安全工程师和对手带来了轻松的胜利。
对于防御者来说,幸运的是,但对于研究人员和对手来说,不幸的是,这些类型的利用变得越来越难以执行,这在很大程度上要归功于我们每天使用的系统中直接实施的广泛的操作系统缓解措施。这种巨大的减刑机制使得以前微不足道的开发在更现代的硬件和软件上变得昂贵而艰巨。
这个由两部分组成的博客系列介绍了Windows系统上的漏洞开发和漏洞研究的演变。它解决了这样的问题:“这将如何影响未来入侵的前景?”以及“开发可靠、便携和有效的二进制漏洞的代价仍然值得吗?”
从一开始,计算就引起了人们的好奇心,最终导致了“计算机错误”的发现,也就是用户交互导致的系统意外行为。这反过来又导致了恶意行为者利用这些错误,并开启了二进制利用的时代。从那时起,安全研究人员、红色团队成员和对手都再也没有回头看过。
二进制攻击的开始导致供应商,最著名的是微软和苹果(特别提到20多年前领导攻击的Linux上的Grsecurity),通过各种缓解措施挫败了这些攻击。这些漏洞缓解(其中许多是默认启用的)降低了现代漏洞的影响。
与在企业环境中大量使用Active Directory类似,这迫使Red Team研究人员将重点放在Microsoft产品上,由于Windows在公司和非公司环境中的广泛使用,对手和研究人员已将Windows作为焦点。因此,本博客将以Windows为中心,同时关注用户模式和内核模式的缓解。
当涉及到二进制攻击时,研究人员和对手总是不得不回答一个古老的问题:“在没有任何用户交互的情况下,如何在目标上执行代码?”答案以各种漏洞类别的形式出现。虽然不是详尽的列表,但一些常见的漏洞包括:
经典堆栈溢出(是的,即使在2020年也是如此):它能够覆盖堆栈上的现有内容,并使用受控写入来定位和破坏函数的返回地址以跳转到任意位置。
释放后使用:在用户模式下(或在内核模式池内存中)在堆上的内存中分配对象。虽然对该释放对象的引用/句柄仍然存在,但该对象过早地“释放”了。使用另一个原语,在释放的对象的位置创建新对象,并利用对旧对象的引用来执行或以其他方式修改新对象,该新对象取代旧对象。预期新对象的这些意外更改会以某种方式导致权限提升或其他恶意功能。
任意写入:这是将数据(例如一个或多个指针)任意写入任意位置的能力。这也可能是另一个漏洞类别的结果。根据写基元的精度,任意写基元也可以用作任意读基元。
类型混淆:对象属于一种类型,但后来该类型被引用为另一种类型。由于各种数据类型在内存中的布局,这可能会导致意外行为。
微软的马特·米勒(Matt Miller)在2019年的Bluehat IL上发表了一次演讲,概述了自2016年以来的顶级漏洞类别:越界读取、释放后使用、类型混淆和未初始化使用。这些错误类已经并仍在被对手和安全研究人员利用。
还值得注意的是,这些漏洞类别中的每一个都可以位于用户模式、内核模式以及现在的虚拟机管理程序中。用户模式漏洞历来被远程利用,或通过浏览器、办公工作套件和PDF阅读器等常见桌面应用程序利用。但是,内核模式漏洞主要在本地攻击-一旦获得系统访问权限即可提升权限。通常,这样的漏洞与用户模式漏洞结合在一起,实现了通常所说的本地远程。此外,在MS17-010(通常称为EternalBlue)、CVE-2019-0708(通常称为BlueKeep)和CVE-2020-0796(通常称为SMBGhost)等情况下,内核远程代码执行是可能的。
由于漏洞的增加,供应商不得不提供一些方法来阻止这些漏洞的执行。于是,漏洞缓解就应运而生了。
虽然安全研究人员和对手历来在通过漏洞传递有效负载的各种方法上占据上风,但供应商开始慢慢地通过实施各种缓解措施来创造公平的竞争环境,希望完全消除错误类或打破常见的利用方法。至少,人们希望缓解措施会使这项技术过于昂贵或不可靠,无法在大众市场上使用,比如在驾车攻击套件中。
从Windows的早期开始,这里定义的利用缓解已经走过了很长一段路。遗留缓解-Microsoft操作系统上发布的初始缓解-将首先讨论。当代缓解措施将是本系列概述的第二个支柱,它包含了更流行的、有文档记录的利用障碍工具。最后,文档较少且未被广泛采用的缓解措施-这里称为“现代”或尖端缓解措施-将结束本系列。
数据执行保护(DEP),称为不执行(NX),是迫使研究人员和对手采用其他攻击方法的首批缓解措施之一。DEP可防止在内存的非可执行部分执行任意代码。Windows XP SP2在用户模式和内核模式下都引入了它,尽管只针对用户模式堆和堆栈,以及内核模式堆栈加上可分页内核内存(分页池)。大多数内核模式堆内存(包括常驻内存(非分页池))在更多版本(包括Windows8)之后才变为不可执行。尽管它被认为是一种“较老的”缓解措施,但它仍然是所有漏洞研究人员和对手都必须考虑的问题。
DEP在内核模式和用户模式中的实现非常相似,因为DEP是通过页表条目在每页内存的基础上强制执行的。页表条目或PTE指的是用于虚拟内存转换的分页结构中的最低级别条目。在非常高的级别上,PTE包含负责对给定范围的虚拟地址实施各种权限和属性的位。每个虚拟内存块(称为页(通常为4KB))通过其在内核中的页表条目被标记为可执行或可写,但不能同时标记为可执行或可写。
在WinDbg中使用!address和!pte命令可以更深入地了解DEP的实现。
在内核模式DEP扩展到覆盖Windows操作系统上的驻留内核堆之前,此类分配的PTE被标记为RWX(指的是NonPagedPool),这意味着这种内核模式内存是可执行和可写的。驻留内存是指此分配类型拥有的内存永远不会从内存中“调出”,这意味着此类型的虚拟内存将始终映射到有效的物理地址。
随着Windows8的发布,NonPagedPoolNx池成为常驻内存分配的默认内核模式堆。这将捕获NonPagedPool的所有属性,但使其不可执行。就像用户模式地址一样,可执行位由内核模式虚拟地址的页表条目强制执行。
Usermode DEP可以通过常见的开发技术(如面向返回的编程、面向调用的编程和面向跳转的编程)绕过。这些“代码重用”技术用于动态调用Windows API函数(如VirtualProtect()或WriteProcessMemory()),以将内存页的权限更改为RWX,或者使用在运行时加载的不同模块的指针将外壳代码写入现有的可执行内存区域。除了更改内存的权限之外,还可以利用Virtualalloc()或类似的例程来分配可执行内存。
可以使用任意读/写原语绕过内核模式DEP,以提取存储器中特定页的页表条目控制位,并修改它们以允许写入和执行访问。也可以通过将执行流重定向到已经标记为RWX的用户模式内存来绕过它,因为默认情况下,内核模式代码可以随意调入用户模式代码。
随着DEP的加入,漏洞研究人员和对手迅速采用了代码重用技术。地址空间布局随机化(ASLR)和内核地址空间布局随机化(KASLR)的实现使得利用漏洞变得不那么直接。
ASLR及其内核模式实现KASLR将各种DLL、模块和结构的基址随机化。例如,此特定版本的Windows10在重新引导之前将内核加载到虚拟内存地址fffff800`0fe00000。
在历史上,在ASLR实现之前,击败DEP就像将应用程序或DLL反汇编成其原始汇编指令并利用指向这些指令的指针(在ASLR之前是静态的)来绕过DEP一样琐碎。但是,随着ASLR的实施,通常需要采取以下三种操作之一:
在当今的现代利用环境中,信息泄漏漏洞是绕过ASLR的标准。根据不同的情况,信息泄漏通常可以归类为除内存损坏原语之外的另一个零日漏洞。这意味着现代攻击可能需要两个零日。
由于Windows仅在每次引导的基础上执行ASLR,因此一旦系统启动,所有进程都共享相同的地址空间布局。因此,ASLR对已经实现代码执行的本地攻击者无效。类似地,因为内核向非特权用户提供自省API,而非特权用户提供内核内存地址,所以KASLR也不能有效地抵御这类攻击。因此,Windows上的ASLR和KASLR只是针对远程攻击载体的有效缓解。
然而,随着本地远程的兴起,人们认识到KASLR对首先实现用户RCE的远程攻击者无效,因为如前所述,某些Windows API函数(如EnumDeviceDrivers()或NtQuerySystemInformation())可用于枚举所有加载的内核模块的基地址。
由于本地远程攻击者首先会以针对浏览器等用户模式RCE开始,因此Microsoft开始严格强制此类应用程序在沙箱环境中运行,并引入了强制完整性控制(MIC),后来又引入了AppContainer,作为降低这些应用程序权限的一种方式,其中包括以较低的完整性级别运行这些应用程序。然后,在Windows8.1中,它阻止对中等或更高完整性级别进程的此类自省API函数的访问。
因此,低完整性级别的进程(如浏览器沙箱)将需要信息泄漏漏洞来绕过KASLR。
在Windows 10的各种版本中,其他几个泄漏内核基址的原语已得到缓解。值得注意的是,包含多个指向内核的指针的硬件抽象层(HAL)堆也位于固定位置。这是因为在引导过程中很早就需要HAL堆,甚至在实际的Windows内存管理器初始化之前也是如此。当时,最好的解决方案是在一个完全固定的位置为HAL堆保留内存。Windows10创建者更新(RS2)版本缓解了这一问题。
尽管ASLR几乎和DEP一样古老,而且它们都是最早实施的一些缓解措施之一,但在现代开发过程中必须考虑到它们。
控制流保护(CFG)及其在内核中的实现称为kCFG,是Microsoft版本的控制流完整性(CFI)。Cfg的工作方式是对使用cfg编译的模块和应用程序内部进行的间接函数调用执行检查。此外,从Windows 10 1703(RS2)发行版开始,Windows内核已经使用kCFG进行了编译。但是,请注意,为了启用kCFG,需要启用VBS(基于虚拟化的安全性)。本博客系列的第2部分将更详细地讨论VBS。
考虑到用户的效率,受CFG保护的间接调用将使用位图进行验证,其中有一组位指示目标是否“有效”或目标是否“无效”。如果目标表示进程中加载的模块中函数的起始位置,则该目标被认为是“有效的”。这意味着位图表示整个进程地址空间。使用CFG编译的每个模块在位图中都有自己的位集,具体取决于它在内存中的加载位置。如ASLR部分所述,Windows仅在每次引导时随机化地址空间,因此此位图通常主要在所有进程之间共享,从而节省了大量内存。
通常,在非常高的级别上,间接的用户模式函数调用被传递给Guard_check_icall函数(或者在其他情况下传递给Guard_Dispatch_icall)。然后,此函数取消引用函数_Guard_check_icall_fptr,并跳转到指针,该指针是指向函数LdrpValidateUserCallTargetES(在其他情况下为LdrpValidateUserCallTarget)的指针。
执行一系列逐位操作和汇编函数,这导致检查位图以确定间接函数调用内的函数是否是位图内的有效函数。无效函数将导致进程终止。
KCFG有一个非常类似的实现,即由kCFG检查间接函数调用。最值得注意的是,这“破坏”了攻击者和研究人员通过调用nt!KeQueryIntervalProfile在内核上下文中执行代码时使用的[nt!HalDispatchTable+0x8]原语,后者在64位系统上执行对[NT!HalDispatchTable+0x8]的间接函数调用。
KCFG使用对cfg稍有不同的修改,因为位图存储在变量nt!Guard_icall_bitmap中。此外,NT!_Guard_Dispatch_icall会启动测试例程来验证目标,不需要其他函数调用。