沙盒逃逸编年史:对CVE-2019-0880漏洞的深入分析
导语:这篇文章描述了splwow64.exe中的一个可利用的任意指针取消引用漏洞。
0x01 漏洞分析
这篇文章描述了splwow64.exe中的一个可利用的任意指针取消引用漏洞。此漏洞可以从Internet Explorer渲染器进程触发,并允许沙盒逃逸。
该漏洞允许通过将特定的LPC消息制作到splwow64.exe进程,从而从低完整性进程中任意写入splwow64.exe的地址空间。
该漏洞已经在Windows 7 x64和Windows 10 x64计算机上进行了测试。
在本文中,我将描述该漏洞的位置以及如何利用该漏洞从Internet Explorer沙盒中实现完整的沙盒逃逸。
该漏洞似乎未在Windows 7上进行修补,因此,我不会发布用于利用此漏洞的源代码。
漏洞描述
该漏洞在于处理对splwow64.exe进程的特定LPC调用,从而可以使用任意参数在splwow64地址空间中调用memcpy函数。这为攻击者提供了极为强大的原语,因为它允许在不依赖内核漏洞的情况下,在更高完整性进程的内存中进行写操作,并逃逸浏览器沙箱。
splwow64.exe进程
Splwow64.exe是一个Microsoft可执行文件,每当32位应用程序访问你已安装的打印机之一时,该文件便会执行。此进程特别有趣,因为它是Internet Explorer策略列入白名单的进程之一。换句话说,任何来自低完整性IE渲染器进程的调用都将生成此进程,这将导致splwow64.exe作为中等完整性进程被加载。
在深入分析漏洞本身之前,让我们尝试深入了解Splwow64流程的实际工作方式。
LPC端口创建
开始执行后,splwow64.exe将通过调用ZwCreatePort API创建LPC端口,并将开始等待传入连接。
让我们看一下ZwCreatePort函数。
NTSTATUS NTAPI ZwCreatePort(PHANDLE,POBJECT_ATTRIBUTES,ULONG,ULONG,ULONG);
如你所见,第二个参数是指向“对象属性”结构的指针,该结构将指向UNICODE_STRING函数,该函数包含由进程创建的LPC端口的名称。
为了连接到该LPC端口,我们需要了解的第一件事是如何生成LPC端口名称!如果你将检查ZwCreatePort的Object Attributes指针参数多次(每次重新引导计算机后),你会注意到LPC端口名的一部分将在每次重新引导后更改。
LPC端口名称如下所示:
在Windows 10上
\\RPC Control\\UmpdProxy_1_VARIABLEPART_0_2000
在Windows 7上
\\RPC Control\\UmpdProxy_1_VARIABLEPART_0_0
每次重新启动计算机后,VARIABLEPART都会更改。
这意味着要能够从IE Sandboxed进程实际连接到该LPC端口,我们将需要了解如何生成LPC端口名称。
对我们来说幸运的是,生成LPC端口名的可变部分的算法非常简单,看起来像这样:
· 调用OpenProcessToken API,将当前进程的句柄作为参数传递。
· 调用GetTokenInformation API,将TokenStatistics作为TOKEN_INFORMATION_CLASS传递
· 访问新获得的TOKEN_STATISTICS结构的AuthenticationId.LowPart字段,并将其转换为十六进制字符串。
现在,你可以连接到LPC端口了!
LPC消息处理
现在,我们将需要了解splwow64进程如何解析LPC消息。由于对splwow64进程的内部工作原理的完整解释不在本文的讨论范围之内,因此我们将只关注漏洞分析,以获取有关漏洞本身及其利用的足够知识。
简而言之,splwow64将以以下方式解析传入的LPC消息:
· 它仅接受长度为0x20字节的传入消息。
· 它将把位于LPC消息的偏移量0x30、0x38和0x40处的三个指针作为参数传递给GdiPrinterThunk函数。
这意味着只要发送的消息为0x20字节长,我们就可以使用任意参数调用GdiPrinterThunk函数!
现在,我们将需要了解GdiPrinterThunk实际如何工作,以查看是否可以通过控制函数参数来触发一些有趣的东西。
GdiPrinterThunk函数
GdiPrinterThunk是一个非常复杂的函数,其工作流程将由位于第一个参数中指定地址的偏移量0x4处的字节确定。如前所述,我们实际上可以通过编写特定的LPC消息来控制GdiPrinterThunk函数传递的三个参数。换句话说,这意味着我们能够控制GdiPrinterThunk工作流程!
此函数是实际的任意解除引用漏洞所在的位置:如果作为第一个参数传递的地址的偏移量0x4处的字节为0x76(在Windows 7上为0x75),则将使用由攻击者完全控制的参数来调用memcpy函数。 !
看一下伪C代码:
void GdiPrinterThunk(LPVOID firstAddress, LPVOID secondAddress, LPVOID thirdAddress) { ... if(*((BYTE*)(firstAddress + 0x4)) == 0x75){ ULONG64 memcpyDestinationAddress = *((ULONG64*)(firstAddress + 0x20)); if(memcpyDestinationAddress != NULL){ ULONG64 sourceAddress = *((ULONG64*)(firstAddress + 0x18)); DWORD copySize = *((DWORD*)(firstAddress + 0x28)); memcpy(memcpyDestinationAddress,sourceAddress,copySize); } } ... }
这是一个任意指针取消引用,使我们可以从低完整性进程中有意地写入splwow64.exe地址空间!
但是我们如何触发它呢?
如前所述,将使用以下参数调用GdiPrinterThunk函数:
· RCX设置为LPC消息的偏移量0x30中指定的地址
· RDX设置为在LPC消息的偏移量0x40中指定的地址
· R8设置为在LPC消息的偏移量0x38中指定的地址
要构造我们的“任意地址写”原语,我们可以创建一个共享节,并在LPC消息的偏移量0x30处指定此共享节的地址。
创建共享部分后,我们可以在所需的偏移量处设置要写入的地址和要从中读取的地址,然后发送LPC消息!
解析LPC消息时,GdiPrinterThunk将访问在消息的偏移量0x30处指定的共享内存地址,如果从共享内存地址功能开始的第四个字节为0x76(在Windows 7上为0x75),则将调用具有在共享内存地址中指定由攻击者控制的参数的memcpy函数!
0x02 漏洞利用
我们可以构建功能非常强大的“ Write What Where Primitive”!不幸的是,我们仍然需要解决一些问题才能真正逃逸Internet Explorer沙箱:
· W ^ X内存:可执行页的内存不可写。换句话说,我们不能只是将有效负载写入可执行内存页面。
· ASLR:我们有能力在所需的地方写我们想要的东西。问题是我们没有信息泄漏,该信息泄漏将使我们能够知道目标进程中函数指针的地址,从而通过覆盖它来执行代码。
· 任意执行:我们可以任意写入splwow64进程的内存,但是我们仍然不知道如何在需要时触发有效负载。
W ^ X内存
由于我们无法写入可执行内存页面,也无法通过调用VirtualProtect使内存页面可写,因此我们需要考虑其他事项。首先想到的是用LoadLibraryA或WinExec之类的函数的地址覆盖现有的函数指针,只要我们能够使用任意参数触发对此函数指针的调用,我们就可以完成!
看一下OpenPrinterW函数:
如你在上面的截图中看到的,该函数将移至RAX寄存器中位于winspool.drv函数.data部分中的地址,并通过调用LdrpValidateUserCallTarget(控制流保护)来验证该地址。
在Windows 7上,它将仅跳转到.data节中保存的地址,如下图所示。
由于winspool.drv DLL的.data部分是可写的,因此我们可以使用“任意地址写”原语来覆盖存储的地址!
如上图所示,该地址已被我们选择的地址覆盖!
逆向分析
为了实现上述目的,我们将需要知道splwow64进程中winspool.drv .data节的地址!幸运的是,Windows系统上的地址空间布局随机化是基于引导的。换句话说,每个系统DLL的基地址在每个进程中都是相同的,直到下一个系统重新启动为止,无论其完整性级别如何。
这意味着将winspool.drv DLL加载到沙盒进程的地址空间中,查找其数据部分,然后从该处找到指向OpenPrinter2W函数的指针,并使用获得的地址在远程进程中通过调用将其覆盖就足够了。
尽管有上述说明,但这还不足以实现完整覆盖:在Windows 7系统上,Internet Explorer渲染器进程是32位的,而splwow64进程是64位的进程。换句话说,我们将无法获得成功利用此漏洞所需的64位地址。
为了解决这个问题,我们有两种选择:
· 生成一个64位进程以泄漏所需的地址。
· 使用Heaven’s gate 技巧在我们的Internet Explorer Wow64进程中加载64位DLL,并泄漏地址。
生成64位进程
这是解决问题的最简单,最稳定的方法。由于Internet Explorer允许从Low Integrity渲染器进程写入LocalLow文件夹,因此要泄漏所需的地址,我们只需要以下各项:
· 创建一个LeakAddresses.exe 64位可执行文件,该文件将加载winspool.drv DLL,获取所需的地址,并将结果保存在LocalLow文件夹中的文件中。
· 将LeakAddresses可执行文件拖放到LocalLow文件夹中,然后通过调用CreateProcess函数运行它。由于不会发生特权提升,因此该执行对用户不可见:该文件将作为低完整性进程执行。
· 通过读取我们的LeakAddresses可执行文件创建的文件来获取所需的地址。
· 使用获得的地址来制作LPC消息,以实现Write What Where原语。
使用 Heaven’s gate 技巧
有关此技术实际上如何工作的完整描述超出了本文的范围。简而言之,Heaven’s gate是利用Windows在64位系统上实现32位代码仿真以加载64位DLL。
通过使用此技术,你将能够在Internet Explorer进程的地址空间中加载64位DLL,从而可以泄漏所需的地址,而无需在磁盘上写入任何文件。
任意代码执行
实际上,为什么选择在OpenPrinterW函数中覆盖指向OpenPrinter2W函数的指针是有原因的。 在花了一些时间逆向 GdiPrinterThunk函数之后,我注意到,如果我们将byte参数设置为0x6A(在Windows 7上为0x69),则会发出对OpenPrinterW函数的调用,第一个参数可由我们控制:因为指向OpenPrinter2W的指针已被我们覆盖,将发出对我们选择的函数的调用!
不幸的是,我们只能控制第一个参数,因此,你应该选择仅包含一个参数的函数。我选择了以下两个函数:
· LoadLibraryA:此函数将在从其调用的进程的地址空间中加载一个库。由于此函数仅使用一个参数,因此我们可以将DLL放到Local Low文件夹中,并在splwow64.exe进程中触发对LoadLibraryA的调用。这样,我们的DLL将由中等完整性级别的进程加载,从而使Internet Explorer沙箱逃逸。
· system:由于WinExec函数具有两个参数,而我们只能控制一个参数,由于msvcrt.dll DLL已加载到splwow64地址空间中,因此我们可以调用此函数,系统函数将仅使用一个参数,并作为命令行将其作为完整性进程来执行。例如,攻击者可以以Medium Integrity用户身份运行Powershell命令。
0x03 分析总结
尽管它很简单,但此漏洞仍使攻击者能够以极其简单和确定性的方式完全逃逸Internet Explorer沙箱的攻击!我发现旧版Windows组件中的这些类型的漏洞非常神奇,并且我认为将来会发现更多此类漏洞。
根据我的测试,此漏洞利用代码仍然可以在功能完善的Windows 7系统上运行,因此,我选择不发布漏洞利用代码。
发表评论