0x00 绪论
本文分析splwow64.exe中的一个任意指针解引用漏洞,该漏洞可从IE的渲染进程触发,导致沙箱逃逸。通过精心构造LPC消息发送给splwow64.exe进程,可以实现任意写splwow64.exe的地址空间。
本漏洞已经在64位Windows 7和Windows 10上测试通过。
本文会讲解漏洞所在的位置,以及如何利用漏洞逃逸IE沙箱。
该漏洞在Windows 7上似乎还没修复,因此本文不会公布利用代码。
漏洞概述
漏洞源于splwow64.exe进程对一个特定LPC调用的处理不当,导致在splwow64地址空间内以任意参数调用memcpy函数。这给攻击者提供了非常强大的原语,允许攻击者写入更高完整性进程的内存,从而无需借助内核漏洞实现浏览器沙箱逃逸。
splwow64.exe概述
splwow64.exe是微软的可执行文件,每当32位程序访问系统上安装的打印机时就会运行。这个进程很有意思,因为它是IE提升策略的白名单进程之一,也就是说,任何低完整性的IE渲染进程试图运行splwow64.exe,都会导致其以中完整性级别运行。
在深入分析漏洞之前,我们先仔细看看splwow64进程是如何运作的。
0x01 LPC端口创建
开始运行后,splwow64.exe将调用ZwCreatePort API创建LPC端口,开始等待传入连接。
让我们看看ZwCreatePort函数。
NTSTATUS NTAPI ZwCreatePort(PHANDLE,POBJECT_ATTRIBUTES,ULONG,ULONG,ULONG);
如你所见,第二个参数是指向Object Attributes结构体的指针,该结构体又将指向UNICODE_STRING结构体,里面包含进程要创建的LPC端口的名称。
为了连接到该LPC端口,首先要了解如何生成LPC端口名称。试着观察ZwCreatePort的Object Attributes指针参数多次(每次重启之后观察),就会注意到LPC端口名称里的一部分会在每次重启后变化。
LPC端口名称如下所示:
Windows 10:
RPC ControlUmpdProxy_1_VARIABLEPART_0_2000
Windows 7:
RPC ControlUmpdProxy_1_VARIABLEPART_0_0
其中VARIABLEPART这部分每次重启都会变。
这意味着想从IE沙箱进程连接到该LPC端口,我们需要知道LPC端口名称是怎么生成的。
幸运的是,生成LPC端口名的可变部分的算法非常简单,看起来像这样:
- 调用OpenProcessToken API,将当前进程的句柄作为参数传递。
- 调用GetTokenInformation API,将TokenStatistics作为TOKEN_INFORMATION_CLASS传递
- 访问新获得的TOKEN_STATISTICS结构的AuthenticationId.LowPart字段,并将其转换为十六进制字符串。
恭喜!现在你可以连接到LPC端口了!
0x02 LPC消息处理
现在,我们要知道splwow64进程如何解析LPC消息。 由于对splwow64进程的内部工作原理的完整解释不在本文的讨论范围之内,因此我们将着眼于大略,只需知道有关本次漏洞利用的知识就够了。
概括来讲,splwow64用以下方式解析传入的LPC消息:
- 它仅接受长度为0x20字节的传入消息。
- 它将把位于LPC消息的偏移0x30、0x38和0x40处的三个指针作为参数传递给GdiPrinterThunk函数。
这意味着只要发送的消息为0x20字节长,我们就可以使用任意参数调用GdiPrinterThunk函数!
听起来不错!现在,我们要了解GdiPrinterThunk是怎么工作的,看看能否通过控制函数参数来触发一些有趣的事情。
0x03 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处指定的地址
要构造任意写原语,我们可以创建一个共享section,并在LPC消息的偏移0x30处指定此共享section的地址。
创建共享section后,我们可以在所需的偏移处设置要写入到的地址和要从中读取的地址,然后发送LPC消息!
解析LPC消息时,GdiPrinterThunk将访问在消息的偏移0x30处指定的共享内存地址,如果从共享内存地址开始的第四个字节为0x76(Windows 7上为0x75),则将以攻击者所控制的、在共享内存中指定的参数调用memcpy!
0x04 利用漏洞
到这步了!我们有了非常强大的任意写原语了!
可惜,我们还要解决一些问题才能真正逃逸IE沙箱:
- W^X内存:可执行页的内存不可写。换句话说,我们不能将简单payload写入可执行内存页就完事了。
- ASLR:我们有任意写的能力,但问题是我们没有信息泄漏,它使我们可以知道目标进程中函数指针的地址,从而通过覆盖它来执行代码。
- 任意执行:我们可以在splwow64进程的内存中任意写,但是我们仍然不知道如何在需要时触发payload。
W^X内存
我们无法写入可执行内存页,也无法调用VirtualProtect来使内存页可写,所以必须另谋他法。首先想到的是把现有的某个函数指针覆盖成LoadLibraryA或者WinExec的地址,然后只要能以任意参数调用这个函数指针,就搞定了。
我们来看看OpenPrinterW函数:
如截图所示,该函数把winspool.drv的.data节中的一个地址传送到RAX寄存器,并调用LdrpValidateUserCallTarget(CFG,控制流保护)来验证该地址。
因为winspool.drv这个DLL的.data节是可写的,所以我们可以用任意写原语来覆盖其中储存的地址!
如图所示,地址已被覆盖为我们想要的地址!
ASLR
为了覆盖地址,我们先要知道splwow64进程中winspool.drv的.data节的地址。对我们来说幸运的是,Windows系统上的ASLR是系统启动时进行的:换句话说,只要没重启,所有系统DLL的基地址在每个进程中都是相同的,无论其完整性级别如何。
也就是说我们在沙箱进程的地址空间中加载winspool.drv,找到其data节,再找到OpenPrinter2W函数指针,用任意写原语在远程进程里覆盖之,就搞定了。
有了这些还不够,在Windows 7上IE渲染进程是32位的,而splwow64进程是64位的,因此我们无法获得漏洞利用所需的64位地址。
要解决这个问题,我们有两种选择:
- 启动一个64位进程以泄漏所需的地址。
- 利用Heaven’s gate技术在IE Wow64进程中加载64位DLL,并泄漏地址。
启动64位进程
这是解决问题的最简单,最稳定的方法。由于IE允许从低完整性渲染进程写入LocalLow文件夹,因此要泄漏所需的地址,我们只需这么做:
- 创建一个LeakAddresses.exe 64位可执行文件,该文件负责加载winspool.drv DLL,获取所需的地址,并将结果保存在LocalLow文件夹中的文件里。
- 将LeakAddresses.exe放到LocalLow文件夹中,然后调用CreateProcess函数运行它。由于没有提权,因此用户不知道它运行了,该文件将作为低完整性进程执行。
- 读取LeakAddresses.exe创建的文件来获取所需的地址。
- 使用获得的地址来编写LPC消息,以实现任意写原语。
Heaven’s Gate技术
此技术原理的完整描述超出了本文的范围。我推荐一篇有关该主题的出色文章(原文无链接)。简而言之,Heaven’s Gate是一种技术,利用Windows在64位系统上实现32位代码仿真的机制来加载64位DLL。
使用此技术可以在IE进程的地址空间中加载64位DLL,从而可以泄漏所需的地址,而无需在磁盘上写入任何文件。
任意执行
其实,选择覆盖OpenPrinterW函数中所调用的OpenPrinter2W函数的指针是有原因的。
在花了一些时间逆向GdiPrinterThunk函数之后,我注意到,如果我们将偏移0x4处的字节设置为0x6A(在Windows 7上为0x69),则会发出对OpenPrinterW函数的调用,第一个参数可由我们控制,又因为指向OpenPrinter2W的指针已被我们覆盖,将会调用的是我们想要的函数!
但是,我们只能控制第一个参数,因此,应该选择仅包含一个参数的函数。我的选择就落在了两个函数上:
- LoadLibraryA:此函数将在调用它的进程的地址空间中加载一个库。由于此函数仅使用一个参数,因此我们可以将DLL放到LocalLow文件夹中,并在splwow64.exe进程中触发对LoadLibraryA的调用。这样,我们的DLL将由中等完整性级别的进程加载,从而逃逸IE沙箱。
- system:由于WinExec函数有两个参数,而我们只能控制一个参数,我们不如调用system,因为msvcrt.dll已加载到splwow64的地址空间中(即使没加载,也可以调用LoadLibraryA加载之)。system函数仅使用一个参数,即命令行,并将其作为中完整性进程来执行。例如,攻击者可以用中完整性用户身份运行Powershell命令。
0x05 总结
该漏洞虽然简单,却允许攻击者完全逃逸IE沙箱,方法简易而又可行!我觉得旧Windows组件中的这类漏洞很有意思,将来应该会发现更多类似漏洞。
我的测试结果显示,该漏洞对打全补丁的Windows 7系统仍然有效,因此我选择不公开漏洞利用代码。