让CVE-2014-4113成功溢出Win8

1、简介

在2014年10月14日的时候,Crowdstrike和FireEye发表了一篇文章,描述了一个新的针对Windows的提权漏洞。 Crowdstrike的文章表 明:这一新漏洞是在追踪一个高度先进攻击团队飓风熊猫(HURRICANE PANDA)发现的,在这之前,飓风熊猫利用此漏洞进行攻击已至少五个月了。

在微软发布MS14-058补丁后,文章中描述的漏洞利用工具流出。在写这篇文章之前,已经有很多对该工具的分析及此漏洞的MSF利用模块,目前利用工具支持除了Win8以外的所有32位和64位版本Windows。

有趣的是,FireEye在博客中分析指出:Win8之后的版本因为更多的保护机制,所以漏洞只能攻击Win8以下的Windows版本。

所以我很好奇漏洞是否能被利用在最新版本的Windows。本文介绍了我的分析结果和演示如何成功利用在Win8和Win8.1。

2、漏洞细节

以下的分析是基于在外流传的Win64.exe和已公开的信息。

该漏洞已经在其他地方有详细描述,所以我们只关注相关的细节。

Win32k.sys是Windows子系统内核模式的一部分,它负责窗口管理及提供图形设备接口(GDI)等。其中用户模式的user32!TrackPopupMenu函数可以用来触发漏洞,而负责该函数处理的是win32k!xxxHandleMenuMessages,它调用了win32k!xxxMNFindWindowFromPoint,(一般返回win32k!tagWND结构体地址或错误代码-1,-5),调用的函数检查了第一个返回值,却没有检查-5 ,win32k!xxxSendMessage会把-5(0xfffffffb)当作一个有效地址,然后值将通过win32k!xxxSendMessage函数传入win32k!xxxSendMessageTimeout(在Win8上是win32k!xxxSendTransformableMessageTimeout)。这个公布的exploit在用户模式中在0xfffffffb用ZwAllocateVirtualMemoryAPI申请内存并且在这个地址放置一个构造的win32k!tagWND结构体。漏洞被触发后内核访问这个用户模式中伪造的结构体,这个被构造的结构体能够改变程序执行流程然后执行一个win32k!tagWND结构体里的函数指针。这个函数指针指向一个简单的内核模式shellcode:用一个指向以system权限运行的进程的主令牌的指针替换了当前EPROCESS结构体中指向主令牌的指针。

3 在Win8.1上的利用

此漏洞不能直接用于Windows8,因为Win8的SMEP (Supervisor ModeExecution Prevention)会阻止shellcode在内核模式的用户模式的内存页执行。 但是win32k!xxxSendTransformableMessageTimeout的不正确调用仍然存在,不过Win8.1中已替代为调用win32k!tagWND地址结构的索引,这是正确的边界检查。

所以Windows 8.1这个调用指令不再能被控制。然而,在下一节中我们将看到一个精心构造的win32k!tagWND地址结构,仍然可以被用来成功地利用这个安全漏洞。

3.1 精心构造的win32k!tagWND 结构体

要利用这一漏洞我们要在Win8.1用户模式下申请一个设计好的win32k!tagWND地址结构。当漏洞被触发时,win32k!xxxSendTransformableMessageTimeout函数首先在win32k!tagWND地址偏移0×10处读取一个64位的值并和win32k!gptiCurrentkernel指针对比,如果在这个偏移地址提供了一个不支持的值则产生异常。接下来程序从偏移量0处读取一个 WORD并用它作为一个内存索引. 被这个索引提到的byte会被和0×01对比。

如果我们把win32k!tagWND结构体的第一个DWORD设为0,,就能让检查失败,并让程序调用win32k!xxxInterSendMessageEx并以我们构造好的win32k!tagWND结构体地址作为第一个参数。

win32k!xxxInterSendMessageEx函数会再次读取win32k!tagWND结构体中偏移0×10处的指针,并尝试解引用这个指针来读取新的指针,这个新的指针用来读取偏移0×170处的值,并和ntoskrnl!PsGetCurrentProcessWin32Process返回的值做对比。

构造的win32k!tagWND结构体在双重解引用成功后会在用户模式内存读出值0,然后win32k!xxxInterSendMessageEx 会从0x2b0读取一个字节,这个值可以是任意的,除了0×20。

在所有条件都满足后,win32k!xxxInterSendMessageEx最终会调用win32k!IsWindowDesktopComposed通过指针传递我们构造的win32k!tagWND结构体作为一个参数。

win32k!IsWindowDesktopComposed会从构造的win32k!tagWND结构体中读取偏移0×18处的值,如果读出的值为0,函数会返回0 ,不解引用这个结构的任何成员。

如果所有条件都能达到,win32k!xxxInterSendMessageEx最终代码就会类似下面:

这段代码执行链表插入动作 试图将在RDI 寄存器中找到的值插入链表。这个寄存器的值是一个我们没法直接控制的内核地址。 这段代码一开始会读取 win32k!tagWND 结构体偏移量为0×60处的链表头并检查是否为空,如果为空则这个值被存在win32k!tagWND结构体中作为新的链表头。

如果win32k!tagWND结构体在偏移0×60处已经存储了一个链表头这段代码会开始遍历链表直到发现尾指针为空并用RDI寄存器中的内存地址覆盖这个指针。这段代码能让我们把任意地址的8个空byte用一个内核地址(RDI寄存器中的值)来覆盖。虽然我们无法控制内核地址但是这已经足够。

3.2 寻找覆盖目标

在内核内存中能覆盖的东西很多,我们需要的是一个以64位值的0开始并且覆盖后能发到提权效果的内存位置并且我们要能够在用户模式中获取这个内存位置的地址。

我选择了Cesar Cerrudo在他的论文“Easy local WindowsKernel exploitation” 中提到的技术,利用NtQuerySystemInformation(SystemHandleInformation)API来获取Windows 令牌对象的地址。这让我们能够让我们获得嵌入令牌的SEP_TOKEN_PRIVILEDGES结构体的地址 我的想法是通过覆盖这个在主令牌中的结构体。然后添加新的权限来提权。这样的好处是可以无视SMEP。

所以现在来看看SEP_TOKEN_PRIVILEDGES这个结构体。

SEP_TOKEN_PRIVILEDGES结构体中前三个区域代表位掩码,每个权限由一个位代表。我们感兴趣的是其中一些我们能被内核允许的高权限。

结构体中8个NUL可能凑不够,可以通过尽量减小权限来凑满。我们打开进程主令牌的句柄然后创建一个最小权限的令牌。

结果如下。

发现还是没办法凑齐8个NUL。于是我用带有DisableAllPrivileges flag 参数的 AdjustTokenPrivileges API 函数, 来禁用所有可能的权限。

可以看到,调用这个函数成功将64位区域清零了。

现在可以覆盖那个区域了,但是依然无法控制被写上去的内核地址。

我们只能保证这个内核地址有两个字节是0xff。但是最有用的特权在Enabled field的前几位,所以不能直接写,而是通过写PresentField位掩码来保证覆盖到。这样权限就能够得以提升。

3.3 综合所有步骤

为成功利用需要用ZwAllocateVirtualMemory (地址0xfffffffb)在用户模式伪造一个Win32k!tahWND结构体,把这结构体内容按3.1说的填好。

下一步创建一个所有权限都被从Enabled field移除的令牌,并用NtQuerySystemInformation(SystemHandleInformation)API把这个令牌的内核地址泄露给 SEP_TOKEN_PRIVILEGES 结构体,为了能指向Present field的中间我们给地址增加3,并把加后的地址(减8)存放在构造的win32k!tagWND结构体偏移0×60处。

为了获得新权限我们用ImpersonateLoggedOnUser API 来模拟这个令牌的安全环境并利用特权,然后用WriteOrocessMemory API 将shellcode插入一个以System权限运行的进程(WinLogon.exe)并用CreateRemoteThread来执行他,最终成功获取System权限。