背景
2018年5月15日,ESET披露了其捕获的PDF文档样本中的两枚0-day漏洞。其中CVE-2018-4990为Adobe PDF阅读器的代码执行漏洞,而CVE-2018-8120则是Windows操作系统Win32k的内核提权漏洞,在获取代码执行权限后通过内核提权漏洞绕过Adobe PDF阅读器的沙盒保护,实现任意代码执行。
360威胁情报中心在2018年5月25日已经发布了《CVE-2018-4990 Adobe Reader 代码执行漏洞利用分析》(详见参考资料[1]),而其中的内核提权漏洞虽然已经有公开的漏洞利用代码,但仅仅是针对Windows 32位环境下的利用。由于大部分用户机器已经是64位操作系统,所以泄露的利用代码危害有限。而在近日,有安全研究人员在GitHub上上传了针对Windows 7 64位环境下CVE-2018-8120的漏洞利用代码,经验证分析该漏洞利用代码真实可用,考虑到漏洞相关的技术细节和验证程序已经公开,所以此漏洞接下来极有可能被利用来执行大规模的攻击。
漏洞分析
在本文中我们试图通过公开的针对Windows 7 64位环境的内核提权POC对漏洞原理和利用过程进行详细分析,并记录整个分析过程。如有分析不当之处敬请谅解。
分析环境:分析和调试的过程将在 Windows 7 x64 为基础的环境中进行
样本来源:https://github.com/unamer/CVE-2018-8120
补丁比较
通过分析安全公告补丁程序可以知道,本次漏洞主要修复了系统中的win32k.sys内核模块文件,我们将64位Windows 7中的win32k.sys文件与未打补丁的文件进行对比,发现本次针对 win32k.sys 的NtUserSetImeInfoEx函数做了以下修补:
可以明显看到,补丁后的函数代码在函数中增加了对窗口站对象tagWINDOWSTATION的成员域spklList的值是否为0的校验,如果值为0则函数直接返回:
修补前的代码:
修补后的代码:
漏洞细节
根据以上对win32k.sys补丁前后改动的代码对比可知,漏洞发生在函数NtUserSetImeInfoEx中。NtUserSetImeInfoEx 是操作系统提供的接口函数,用于将用户进程定义的输入法扩展信息对象设置在与当前进程关联的窗口站中。
窗口站
窗口站是和当前进程和会话(session)相关联的一个内核对象,它包含剪贴板(clipboard)、原子表、一个或多个桌面(desktop)对象等。窗口站 tagWINDOWSTATION 结构体的定义如下:
NtUserSetImeInfoEx执行过程分析
知道了补丁代码修复的部分,我们来看下漏洞函数NtUserSetImeInfoEx的具体实现,此函数只有一个tagIMEINFOEX类型的参数:
整理后的漏洞函数执行过程分析如下:
l 函数先获取当前的窗口站rpwinsta,并从rpwinsta指向的窗口站对象中获取spklList成员
spklList 是指向关联的键盘布局 tagKL 对象链表首节点的指针。键盘布局 tagKL 结构体的定义如下:
l 然后函数从首节点开始遍历键盘布局对象链表,直到节点对象的pklNext成员指回到首节点对象为止。函数判断每个被遍历的节点对象的hkl成员是否与参数 ime_info_ex 指向的源输入法扩展信息对象的hkl成员相等
l 接下来函数判断目标键盘布局对象的piiex成员是否为空,且成员变量 fLoadFlag 值是否为 FALSE。如果是,则把参数 ime_info_ex 的数据拷贝到目标键盘布局对象的piiex成员中
函数的实现过程比较简单,在这里我们可以清楚的看到分析过程的第2步中导致漏洞产生的原因:
在遍历键盘布局对象链表 spklList 的时候并没有判断 spklList 地址是否为 NULL,假设此时 spklList 为空的话,接下来对 spklList 访问的时候将触发访问异常,导致系统 BSOD 的发生。
POC验证
我们使用PowerShell脚本来测试验证该漏洞,以下PowerShell脚本使用CreateWindowStation创建了一个窗口站,并调用函数 SetProcessWindowStation 将创建的窗口站与当前进程关联起来,然后打印出窗口站的HANDLE,最后调用 NtUserSetImeInfoEx 函数触发漏洞:
打印出窗口站HANDLE后我们使用PCHunter找到窗口站的内核地址0xfffffA801a1c4270:
在WinDbg中使用win32k!tagWINDOWSTATION结构类型查看创建的窗口站句柄0xfffffA801a1c4270地址:
这时我们可以看到spklList 在创建的窗口站中默认初始化为空的。结合上面的我们对 NtUserSetImeInfoEx 函数的分析可知,如果此时我们接着调用 NtUserSetImeInfoEx 函数的话, 就会导致系统 BSOD 的发生:
异常代码:
总结整个漏洞成因:
l 当用户进程调用CreateWindowStation 等函数创建新的窗口站时,最终在内核会执行窗口站创建的操作。在该函数执行期间,新的窗口站对象的 spklList 成员并没有被初始化,将始终指向 NULL 地址。
l 而NtUserSetImeInfoEx并没有对窗口站对象的 spklList 成员做空指针判断,由于当前进程关联的窗口站对象的 spklList 成员指向 NULL 地址,而空地址所在的零页内存此时并没有映射,因此当内核函数 NtUserSetImeInfoEx 在试图访问零页内存时,将触发访问异常,导致BSOD 的发生。
Windows 7 x64环境下的漏洞利用
2018年5月20日,@unamer 在 github 上传了一份使用CVE-2018-8120针对Windows 7、Windows 2008 32位和64位系统下的提权利用代码。此章节我们通过公开的样本来分析Windows 7 x64环境下使用该漏洞的提权利用过程。
分配零页内存
由前面的分析可知,由于代码设计不当,当内核中某个对象指针指向空地址这样的位于用户地址空间的内存地址时,用户进程中的利用代码将能够通过分配这样的内存页并进行巧妙的内存布局来实现任意内核代码执行的能力,从而实现提权操作。
而漏洞触发的时候NtUserSetImeInfoEx 函数会把可控参数拷贝到零地址上,了解虚拟地址空间分布知识的话都应该知道,在32位 Windows系统中,可用的虚拟地址空间共计为 2^32 字节(4 GB)。通常低地址的2GB用于用户空间,高地址的2GB 用于系统内核空间。而在64位 Windows系统中,虚拟地址空间的理论大小为 2^64 字节,但实际上仅使用一部分。范围从 0x000'00000000 至 0x7FF'FFFFFFFF 的 8 TB 用于用户空间,范围从 0xFFFF0800'00000000 至 0xFFFFFFFF'FFFFFFFF 的 248 TB 的部分用于系统空间。可以注意到空指针赋值分区,这一分区是进程地址空间中从0x00000000到0x0000FFFF的闭区间,保留该分区的目的是为了帮助程序员捕获对空指针的赋值。而如果进程中的线程试图读取或写入位于这一分区内的内存地址,则会引发访问违规异常:
虚拟地址空间分部
而使用ZwAllocateVirtualMemory函数可以在指定进程的虚拟空间中申请一块内存,该块内存默认将以64kb大小对齐。将BaseAdress设置为0时,系统会寻找第一个未使用的内存块来分配,并不能在零页内存中分配空间。在AllocateType参数中有一个分配类型是MEM_TOP_DOWN,该类型表示内存分配从上向下分配内存。如果指定 BaseAddress为一个低地址,例如 1,同时指定分配内存的大小大于这个值 ,例如8192(一个内存页),这样分配成功后 地址范围就是0xFFFFE001(-8191)到 1把0地址包含在内了,此时再去尝试向 NULL指针执行的地址写数据,程序就不会异常了。通过这种方式我们发现在0地址分配内存的同时,也会在高地址(内核空间)分配内存。详细请参考[11]。
Bitmap GDI函数实现内核任意地址读/写
而利用最近几年提出的通过修改Bitmap GDI函数关键对象的方式则可以将有限的任意地址写漏洞转化为内核任意地址读/写。此技术关键在当创建一个bitmap时,我们可以泄露出其在内核中的地址。这一泄露在Windows10的v1607版本之后才被打上补丁。
当创建一个bitmap时,一个结构被附加到了进程PEB的GdiSharedHandleTable成员中。GdiSharedHandleTable是一个GDICELL64结构体数组的指针:
我们可以以下方式找到Bitmap的内核地址:
使用WinDbg来验证整个过程:
l 首先用以下PowerShell脚本创建一个Bitmap对象,并打印内核对象句柄,然后用工具找到这个的对象以便后面做对比
打印出Bitmap内核对象句柄:
切换进程:
获取GdiSharedHandleTable地址:
获取Bitmap 句柄地址:
可以看到地址fffff900`c1ca6000和上面图片里用工具获取的一致,Bitmap内核地址有了,该怎么用呢?GDICELL结构的 pKernelAddress 成员指向 BASEOBJECT 结构,不过我们关心的是在这一头部之后,有一个特定的结构体,它的类型是由 wType 成员决定的:
位图结构:
pvScan0 成员就是我们需要利用的,因为GetBitmapBits 和 SetBitmapBits 这两个API能操作这个成员。GetBitmapBits 允许我们在 pvScan0 地址上读任意字节,SetBitmapBits 允许我们在 pvScan0 地址上写任意字节。如果我们有一个漏洞(例如:CVE-2018-8120)可以修改一次内核地址, 把 pvScan0 改成我们想要操作的内核地址,这样是不是就实现了可以重复利用的内核任意读写呢?
有了这些基础知识,我们整理出使用该方法并配合某个任意地址写的漏洞来将GetBitmapBits / SetBitmapBits改造成可以实现任意地址读写的利用函数,具体步骤:
l 创建2个位图(Manager/Worker)
l 使用句柄查找GDICELL64,分别计算两个位图的pvScan0 地址
l 使用漏洞将 WorkerpvScan0 偏移量地址写入 Manager 的 pvScan0 值
l 使用 Manager 上的SetBitmapBits 来选择地址
l 在 Worker 上使用GetBitmapBits/ SetBitmapBits来读取/写入上一步设置的地址
整个利用操作流程如下:
利用Bitmap GDI实现CVE-2018-8120在Windows 7 x64环境下的漏洞利用
有了上述的所有基础知识,我们来看看利用程序是如果使用该技术在Windows 7 64位环境下利用CVE-2018-8120实现提权攻击的。
第一步,首先利用ZwAllocateVirtualMemory分配基地址位于零页的内存块,以使零页完成映射:
调用NtAllocateVirtualMemory函数成功在零地址上面分配了一块空间:
第二步,创建创建2个位图(Manager/Worker)对象,并使用CVE-2018-8120漏洞将 WorkerpvScan0偏移量地址写入Manager 的 pvScan0 值,创建内核任意地址读写能力,具体操作如下:
l 创建两个bitmap对象
l 泄露出各自的内核地址,得到mpv 和 wpv 两个指针:
l 使用任意写来设置 mpv 去指向 wpv 的地址,实现可重用的内核任意读写
先要构造环境让函数运行到下面拷贝数据的地方,还记得上面我们对 NtUserSetImeInfoEx 函数漏洞原理的分析吗? NtUserSetImeInfoEx 函数从首节点开始遍历键盘布局对象链表,直到节点对象的pklNext成员指回到首节点对象为止。然后判断每个被遍历的节点对象的 hkl 成员是否与参数 ime_info_ex 指向的源输入法扩展信息对象的 hkl 成员相等。为了触发后面的拷贝操作,我们需要跳过这个循环
要跳过这个循环很简单,我们让 pkl->hkl 和 NtUserSetImeInfoEx 函数的参数地址相等就行了。下面的代码就是在零页地址位置伪造了一个和 tagIMEINFOEX 结构体 spklList 成员类型一样的 tagKL 结构体,然后把它的 hkl 字段设置为 wpv 的地址。后面我们把 wpv 的地址放在 NtUserSetImeInfoEx 函数的参数 ime_info_ex 的第一个成员里面。这样就不会进入 while ( pkl->hkl != ime_info_ex ) 这个循环了:
然后再把 mpv 的地址放在零页内存地址偏移0x50 的地方,NtUserSetImeInfoEx 函数会把它的参数( IMEINFOEX 结构)拷贝到目标键盘布局 tagKL 对象的 piiex 成员指向的输入法信息对象缓冲区中。在win7 x64下面这个字段的偏移就是 0x50:
以下是调试时候获取到的mpv 和 wpv 的值:
接着构造漏洞利用环境,调用CreateWindowStation 函数创建一个窗口站,调用 SetProcessWindowStation 函数把窗口站和当前进程关联起来,接着构造一个 tagIMEINFOEX 结构,把 wpv 的地址放到它的 hkl 成员中,调用 NtUserSetImeInfoEx 后,NtUserSetImeInfoEx 函数就会把 wpv 的地址拷贝到 mpv 里面:
在调试器里面我们可以看到 已经成功触发了NtUserSetImeInfoEx 的漏洞:
当NtUserSetImeInfoEx 执行完后,我们的GDI内核任意读写的环境就已经构造好了:
注意:NtUserSetImeInfoEx触发的拷贝数据长度是不可控的,memmove(piiex, &ime_info_ex, 0x160ui64); 代码里面可以看到是0x160的长度,也就是sizeof(tagIMEINFOEX),我们要在这个地方修改Worker Bitmap的 pvScan0的地址,就会把 SURFACE 结构里面的其它数据给覆盖掉。后面调用Gdi32 的 GetBitmapBits/SetBitmapBits 这两个函数就会不成功,因为这两个函数操作pvScan0 的方式是和 SURFOBJ 结构的 lDelta、iBitmapFormat、iType、fjBitmap 还有 SURFACE 结构的 flags 字段相关的。关于该结构的详细信息可以参考[10]。
以下代码是需要修复成员的对应值:
最后一步,使用 Gdi32 的GetBitmapBits/SetBitmapBits API调用来读写内核地址空间,实现任意代码执行:
下面的代码实现了替换HalDispatchTable 里面的 NtQueryIntervalProfile 函数为我们的提权ShellCode的地址,调用NtQueryIntervalProfile 以达到执行ShellCode的目的:
最终在ShellCode中替换SYSTEM进程的token为当前进程的token,以实现提权:
参考
[1].https://ti.360.net/blog/articles/analysis-of-cve-2018-4990/
[2].https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8120
[3].https://msdn.microsoft.com/en-us/library/windows/hardware/ff569901(v=vs.85).aspx
[4].https://github.com/unamer/CVE-2018-8120
[5].https://github.com/FuzzySecurity/HackSysTeam-PSKernelPwn
[6].https://www.reactos.org/wiki/Techwiki:Win32k/SURFACE
[7].https://www.coresecurity.com/blog/abusing-gdi-for-ring0-exploit-primitives
[9].https://xiaodaozhi.com/exploit/156.html
[10].https://xiaodaozhi.com/exploit/42.html
[11].http://blog.nsfocus.net/null-pointer-vulnerability-defense/