Windows漏洞利用开发现状Part1

阅读量    127786 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

简介

从历史上来看,内存破坏漏洞一直是红队工具箱中最强大的配件之一。允许攻击者在不依赖任何用户交互的情况下执行Payload,对于攻击者来说,取得胜利就变得非常轻松。对于防御者而言,这类攻击已经变得越来越难以执行,这在很大程度上要归功于我们每天使用的系统中直接实现的缓解机制。这些缓解机制使得以前的利用技术在现代的硬件和软件上变得非常困难。

本系列文章有2部分组成,主要介绍Windows系统上漏洞利用和漏洞研究的发展历程。

 

起因

从一开始,计算机技术就吸引了人们的好奇心,这最终导致发现“计算机漏洞”,或者由于用户交互而导致系统出现意外行为。这反过来又导致了恶意者使用这些漏洞,并开启了二进制漏洞利用的时代。

二进制漏洞利用的爆发影响了许多厂商,尤其是微软和苹果公司,以各种缓解机制阻止了这些攻击,这些漏洞利用缓解机制已经降低了现代漏洞利用的影响。

类似于Active Directory在企业环境中的大规模使用,迫使红队将研究重点放在Microsoft产品上,由于Windows在企业和非企业环境中的广泛使用,对手和研究人员都将其作为重点。因此,本文将以windows为核心,介绍用户模式和内核模式的缓解机制。

 

漏洞类型

对于二进制漏洞利用研究人员不得不回答一个问题:如何在没有任何用户交互的情况下在目标上执行代码?常见的答案总是以各种漏洞类型出现。虽然不是详细的列表,但包括一些常见的漏洞:

  • 1.栈溢出( stack overflow ):这是一种覆盖堆上现有内容并使用可控数据来破坏函数的返回地址以跳转到任意地址的能力。
  • 2.Use-after-free:以用户模式(或内核模式内存池)在堆的内存中分配对象。虽然这个被释放的对象的引用/句柄仍然存在,但是这个对象已经“free(释放)”了,使用另一个原语,在释放对象的位置创建一个新对象,并使用对旧对象的引用来执行或修改新对象,该新对象在旧对象的地址上运行。这些对新对象的意外更改可能会导致权限提升或其他恶意功能。
  • 3.任意写(Arbitrary writes):这是任意写入数据的功能,比如一个或多个指针,指向任意地址,任意写原语也可以用作任意读原语,这取决于它对写原语的精确。
  • 4.类型混淆(Type-confusion):一个对象是一种类型,但后来该类型被引用为另一个类型。由于内存中各种数据类型的布局,这可能导致意外行为。

微软的Matt Miller在2019年的BlueHat IL上的演讲,总结了自2016年以来最严重的漏洞类型:越界读取、use-after-free、类型混乱和未初始化使用(uninitialized use)。这类漏洞仍然被对手和安全研究人员利用。

还值得注意的是,这类漏洞中的每一个都可以存在于用户模式、内核模式以及现在的hypervisor中。用户模式漏洞历来被远程或通过常见的桌面应用程序(如浏览器、office套件和PDF阅读器)进行利用。然而,内核模式的漏洞主要是在本地被利用——一旦获得了对系统的访问,就可以提升权限。通常,这些漏洞与用户模式漏洞结合在一起。此外,例如MS17-010(EternalBlue),CVE-2019-0708(BlueKeep)和CVE-2020-0796(SMBGhost),在其内核执行远程代码是可行的。

由于漏洞利用的上升,厂商不得不提供一些方法来阻止这些漏洞的执行。因此,缓解机制就诞生了。

 

漏洞利用缓解机制

虽然安全研究员和攻击者在通过漏洞提供Payload的各种方法中占据上风,但厂商通过实施各种缓解机制,希望完全消除这类漏洞或打破常见的利用方法,渐渐开始公平竞争。至少,大家希望这种缓解机制会使这这种利用技术过于困难或不可靠,不适合大规模市场中使用,比如在驱动利用工具包中使用。

从早期的Windows开始,漏洞机制就已经发展了很长一段时间。将首先介绍传统缓解机制,现代的缓解机制包含更多流行的漏洞利用防御手段,将在本系列文章的第二篇文章中介绍。

传统缓解机制1:DEP a.k.a No-eXecute (NX)

数据执行保护(DEP)被称为No-eXecute(NX),是早期的缓解机制之一,DEP防止在内存的非可执行部分中执行任意代码,它迫使研究人员采取其他的利用方法。在Windows XP SP2的用户模式和内核模式下都引入了该功能,只针对用户模式堆和栈,以及内核模式栈和分页的内核内存(paged pool)。在很多版本中,直到包括Windows 8,大多数内核模式堆内存,包括常驻内存(nonpaged pool)都变成了不可执行内存。尽管被认为是一种“较老的”缓解机制,但它仍然是所有漏洞研究员必须考虑的一种机制。

DEP在内核模式和用户模式下的实现非常相似,因为DEP是通过页表条目在分页内存的基础上强制执行的。页表条目或PTE是指用于虚拟内存转换的分页结构中的最低级别条目。PTE在非常高的级别上包含一些位,这些位负责为指定范围的虚拟地址强制执行各种权限和属性。虚拟内存的每个块称为页(通常为4KB),通过其在内核中的页表条目被标记为可执行或可写,但不能同时标记为可执行或可写。

利用WinDbg中的!address和!pte命令可以更深入地了解DEP的实现。

在内核模式DEP扩展到Windows操作系统上的常驻内核堆之前,用于这种分配的PTEs被标记为RWX,用于这种分配的PTEs被标记为RWX,它指的是非pagedpool,也就是说这种类型的内核模式内存是可执行和可写的。常驻内存是指:这种分配类型所拥有的内存永远不会从内存中“paged-out”,也就是说这种类型的虚拟内存将始终映射到有效的物理地址。

随着Windows 8的发布,NonPagedPoolNx池成为了常驻内存分配的默认内核模式堆。这将捕获NonPagedPool的所有属性,但使其不可执行。与用户模式地址一样,可执行位是由内核模式虚拟地址的页表条目强制执行的。

用户模式的DEP可以通过常见的利用技术来绕过,如ROP、JOP等。这些“代码重用”技术用于动态调用Windows API函数,如VirtualProtect()或WriteProcessMemory(),以便将内存页的权限更改为RWX,或使用运行时加载的不同模块的指针将shellcode写到一个已经存在的可执行内存区域。除了更改内存的权限,也可以利用VirtualAlloc()或类似的例子来分配可执行内存。

内核模式的DEP可以通过使用任意的读/写原语来绕过,以提取内存中特定页面的页表条目控制位,并修改它们以允许写和执行访问。通过将执行流重定向到已经标记为RWX的用户模式内存中,也可以绕过它,因为默认情况下,内核模式代码可以随意调用用户模式代码。

传统缓解机制2:ASLR / kASLR

随着DEP的加入,漏洞研究人员迅速采用了代码重用技术。地址随机化(ASLR)和内核地址随机化(KASLR)的实现使得利用变得不是那么轻松。

ASLR及其内核模式实现的KASLR将各种dll、模块的基地址随机化。例如,此版本的Windows 10在重新启动之前会在虚拟内存地址fffff800`0fe00000处加载内核。

重新启动后,内核将加载到其他虚拟地址fffff800`07000000。

从历史上看,在实施ASLR之前,攻破DEP就像将应用程序或DLL分解为原始汇编指令并利用指向这些指令的指针(在ASLR之前是静态的)绕过DEP一样简单。但是,在实施ASLR后,通常需要执行以下三个操作之一:

  • 1.利用dll和不使用ASLR编译的应用程序
  • 2.利用越界漏洞或其他类型的信息/内存泄漏
  • 3.暴力破解地址(在64位系统上不可行)

在现代利用环境中,信息泄漏漏洞是绕过ASLR的标准。根据不同的情况,除了内存破坏原语外,信息泄漏通常可以被归类为另一个零日漏洞。也就是说现代的利用可能需要两个漏洞。

因为Windows仅在每次引导时执行ASLR,所以一旦系统启动,所有进程都共享相同的地址空间,因此,ASLR对已经实现代码执行的本地攻击者无效。同样,由于内核为普通用户提供了自检API,并提供了内核内存地址,因此KASLR也不会对此类攻击有效。因此,Windows上的ASLR和KASLR只能效缓解对远程漏洞利用的影响。

然而,大家意识到KASLR对于首次实现用户RCE的远程攻击者是无效的,因为,某些Windows API函数,如EnumDeviceDrivers()或NtQuerySystemInformation()可以用来枚举所有加载的内核模块的基地址。

因为本地远程攻击者首先会从用户模式RCE开始,目标是浏览器等等。因此Microsoft开始要求这些应用程序在沙箱环境中运行,并引入了强制的完整性控制(MIC),后来又引入了AppContainer,作为降低这些应用程序权限的一种方式,通过在较低的完整性级别运行这些应用程序。然后,在Windows 8.1中,它阻止中等及以上完整性级别的进程访问这种API函数。

因此,一个低完整性级别的进程,比如浏览器沙箱,将需要一个信息泄漏漏洞来绕过KASLR。

在不同的windows10版本中,泄漏内核基址的其他几个原语已经得到缓解。值得注意的是,硬件抽象层(Hardware Abstraction Layer,HAL)堆也位于固定位置,其中包含多个指向内核的指针。这是因为即使在实际的Windows内存管理器尚未初始化的情况下,早期的boot进程也需要HAL堆,当时,最好的解决方案是在一个完全固定的位置为HAL堆保留内存。Windows10(RS2)版本缓解了这一问题。

尽管ASLR几乎和DEP一样古老,而且它们都是早期实施的一些缓解机制之一,但在现代漏洞利用中必须考虑到它们。

现代缓解机制1:CFG / kCFG

控制流保护(CFG)及其在内核中的实现称为kCFG,CFG的工作原理是对使用CFG编译的模块和应用程序内部产生的间接函数调用执行检查。此外,从Windows 10 1703 (RS2)发行版开始,Windows内核已经用kCFG进行了编译。但是请注意,为了启用kCFG,需要启用VBS(基于虚拟化的安全性),VBS将在本系列文章的第2篇中详细介绍。

为了提高用户的效率,CFG保护的间接调用使用bitmap进行验证,用一组bits表示目标是“有效的”还是“无效的”。如果目标表示进程中加载的模块中函数的起始位置,则认为该目标是“有效的”。也就是说bitmap表示整个进程地址空间。使用CFG编译的每个模块在bitmap中都有自己的一组bits,这取决于它在内存中的加载位置。如ASLR部分所述,Windows仅在每次启动时随机分配地址空间,因此该bitmap通常在所有进程之间共享,从而节省了大量内存。

通常,在非常高的级别上,间接的用户模式函数调用被传递给guard_check_icall函数(或者在其他情况下为guard_dispatch_icall)。然后,该函数解除对函数_guard_check_icall_fptr的引用,并执行对指针的跳转,该指针是指向函数LdrpValidateUserCallTargetES的指针(在其他情况下为LdrpValidateUserCallTarget)。

执行一系列按位操作和汇编函数,结果是检查bitmap,以确定间接函数调用中的函数是否是bitmap中的有效函数。无效的函数将导致进程终止。

kCFG有一个非常类似的实现,即由kCFG检查间接的函数调用。最值得注意的是,[nt!HalDispatchTable+0x8]原语,安全研究员使用它在内核上下文中通过调用nt!KeQueryIntervalProfile来执行代码。在64位系统上它执行对[nt!HalDispatchTable+0x8]的调用。

kCFG对CFG进行了轻微的修改,因为bitmap存储在变量nt!guard_icall_bitmap中。此外,nt !_guard_dispatch_icall来验证目标,不需要调用其他函数。

CFG缓解了这样一个事实:在exploit开发期间,一个函数指针可能需要被覆盖,以指向一个不同的函数指针(比如VirtualProtect)。

CFG是CFI缓解的forward-edge,也就是说它不考虑ret指令,这是一种backward-edge情况。由于CFG不会检查返回地址,因此可以通过利用信息泄漏来绕过CFG,这可能允许TEB之类的操作来泄漏栈。利用这点,有可能重写栈上函数的返回地址。

随着发展,后来大家发现CFG有一些缺陷。例如,模块使用导入地址表(IAT)进行导入,如Windows API函数,这些IAT表本质上是指向Windows API函数的特定模块中的虚拟地址。

IAT在默认情况下是只读的,通常不能修改。微软认为这些函数是“安全的”,因为它们处于只读状态,CFG/kCFG不保护这些函数。如果可以修改或向IAT添加恶意条目,则可以调用用户定义的指针。

此外,可以利用其他的OS函数执行代码。根据设计,CFG/kCFG只验证一个函数是否从bitmap所指示的位置开始,而不是声明函数那样。如果安全研究员可以在CFG / kCFG bitmap中找到标记为有效的其他函数,则它可能会覆盖一个函数的指针“代理”另一个函数指针来执行代码。例如,这可能导致类型混淆攻击,即使用原来预期函数的参数/对象运行一个不同的函数。

如前所述,kCFG仅在启用VBS时启用。kCFG的一个特性是,即使没有启用VBS, kCFG派遣函数仍然存在,函数调用仍然通过它们传递。无论是否启用VBS, kCFG都会对虚拟地址的“upper”位执行逐位检查,以确定该地址是否为符号扩展(也称为内核模式地址)。如果检测到用户模式地址,无论是否启用HVCI, kCFG都将导致KERNEL_SECURITY_CHECK_FAILURE错误检查。这是一种缓解内核模式代码强制调用用户模式代码的方法,我们看到这是一种绕过DEP的潜在技术,在下一节中,我们将介绍Supervisor Mode Execution Prevention (SMEP),这也是针对这种攻击的一种现代缓解方法。

值得注意的是,kCFG bitmap是由HVCI (Hypervisor-Protected Code Integrity)保护的。

现代缓解机制2:SMEP

Supervisor Mode Execution Prevention 是一种基于硬件的CPU缓解机制,专门针对内核漏洞攻击而实现的。

当引入NonPagedPoolNx时,就不能再直接将shellcode写入内核模式并执行它了。这就产生了这样一种想法,即可以使用Windows API函数(如VirtualAlloc())在用户模式下分配shellcode,然后将返回shellcode的指针传回内核模式。然后内核将在内核的“上下文中”执行用户模式代码,也就是说shellcode将以内核权限运行。

通过禁止从内核执行用户模式代码,SMEP可以缓解这种攻击。更具体地说,基于x86的CPU有一个内部状态,称为代码权限级别(Code Privilege Level, CPL)。这些CPU有四个不同的CPLs,称为环(rings)。Windows仅使用了ring 0和ring 3,SMEP不允许在CPL 0的上下文中执行属于CPL 3的代码。

SMEP是通过CR4控制寄存器的20位来启用的。控制寄存器是用来改变或启用CPU某些特性的寄存器,比如通过分页实现虚拟内存等。虽然SMEP是通过CR4寄存器的第20位启用的,但它之后是通过内存地址的PTE强制执行的。对于任何分页结构,通过检查PTE的用户位与管理员位(U/S)来执行SMEP。如果该位设置为U(ser),则该页面将被视为用户模式页面。如果该位被清除,该位被表示为S(upervisor),则该页面被视为一个supervisor(内核模式)页面。

正如Alex Ionescu在Infiltrate 2015上所讲,如果仅将一个分页条目设置为“ S”,则SMEP不会导致崩溃。这种实现很重要,因为可以通过任意写绕过SMEP,通过“欺骗”CPU从用户模式执行shellcode。

首先,定位用户模式下分配的PTE,然后清除U/S位,使其设置为S。当这种情况发生时,CPU将把这个用户模式地址作为内核模式页面处理——允许执行。

一种较老的绕过SMEP的技术是利用ROP在系统范围内禁用它。攻击者可以在内核模式下利用ROP gadget,方法是找到一个允许覆盖CR4寄存器的值并清除20位的gadget,从而使SMEP返回到CR4寄存器。

该方法的缺陷是必须使用内核模式ROP gadget在内核中执行,才能遵循SMEP的规则。此外,与所有代码重用攻击一样,gadget之间的偏移可能会在Windows版本之间发生变化,而ROP要想工作,必须对栈进行控制。还有一种称为HyperGuard的保护,这超出了本文的范围,它还可以防止现代系统上的CR4修改。

kd> bp nt!part_2

在本文中,我们回顾了传统的缓解机制,以及当代的缓解机制,如CFG和SMEP,这些缓解机制主要是为了给漏洞研究员提高漏洞利用的门槛。这为更现代的缓解机制(如ACG、XFG、CET和VBS)奠定了基础,增加了漏洞利用复杂性。在下篇文章,主要介绍更现代的缓解机制。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多