沙箱逃逸纪实:深入分析CVE-2019-0880

 

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系统仍然有效,因此我选择不公开漏洞利用代码。

(完)