CVE-2018-8120
相关信息:
An elevation of privilege vulnerability exists in Windows when the Win32k component fails to properly handle objects in memory, aka “Win32k Elevation of Privilege Vulnerability.” This affects Windows Server 2008, Windows 7, Windows Server 2008 R2. (当win32k组件无法正确处理内存中的对象时,windows中就存在一个空指针解引用漏洞,可以用来做特权提升,即”Win32k特权漏洞提升”,这会影响Windows Server2008,windows 7和windows server 2008 r2)
补丁链接:https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8120
测试环境:
OS 名称: Microsoft Windows 7 专业版->X64
OS 版本: 6.1.7600 Build 7600
漏洞成因:
在win32k.sys的NtUserSetImeInfoEx函数中,因为未对窗口站rpwinsta变量的spklList元素做是否为空的检测,导致在使用spklList的时候会存在一个空指针解引用的问题,如果当前进程创建一个spklList成员为null的窗口站,然后将这个新创建的窗口站与当前进程关联,在调用NtUserSetImeInfoEx函数对输入法设置扩展IME信息时,将会获取spklList成员指向的内存中的数据,因为此时该成员为0,所以就会访问位于用户地址空间的零号内存。因为默认零号内存是未映射的所以此操作会导致页面错误,导致系统蓝屏发生。
常见的利用思路可以事先把零号内存申请出来,然后在用户层构造假的tagKL内核对象,导致内核会误认为是正确的键盘布局tagKL节点对象,从而实现任意地址写任意值的目的,当我们能够实现任意地址任意写后我们可以把特定的内核对象的函数指针字段修改为我们shellcode的地址或修改内核模式或用户模式的执行的相关标志位,然后就有了任意代码执行的能力。
漏洞细节:
首先我们来熟悉一下NtUserSetImeInfoEx函数,该函数主要是用于将用户进程定义的输入法扩展信息IME对象设置成当前进程所关联的窗口站中的键盘布局节点对象中的IME对象。
该函数存放于win32k.sys文件中,可以获取测试机中的win32k.sys文件,让ida进行反编译,以下是我测试环境中的NtUserSetImeInfoEx函数,
首先熟悉一下该函数的执行流程。
[1]-获取当前进程的窗口站对象rpwinsta,什么是窗口站?
窗口站是一个内核对象,包含剪切板,原子表和一个或多个桌面对象,每个窗口站对象作为结构体tagWINDOWSTATION的实例存在于内核中,以下为窗口站结构体
[2]-判断当前窗口站rpwinsta是否为空,如果不为空就执行[3]。
[3]-根据当前窗口站rpwinsta获取spklList成员,spklList指向的是关联的键盘布局对象链表的第一个节点指针,键盘布局tagKL结构体定义如下:
[4]-遍历关联键盘布局对象链表,该循环存在两个出口
1. 当遍历到的键盘布局对象的hkl成员等于传入的IME对象时结束循环,这种情况表示找到了hkl等于传入的IME对象的键盘布局对象,然后结束循环,执行[5]
2. 当遍历到的键盘布局对象再次等于第一个键盘布局对象节点时,这种情况表示链表遍历完毕后还未找到目标对象,所以直接跳到函数的末尾。
[5]-获取目标键盘布局对象中的piiex成员,piiex成员指向需要关联的输入法扩展IME信息对象,结构如下:
[6]-最终的拷贝操作,如果目标键盘布局对象不为空,并且fLoadFlag成员为false,那么就把传入的IME对象拷贝到目标键盘布局对象的piiex成员指向的IME信息对象处。
通过补丁对比可以知道补丁其实就是在[2]的位置对当前窗口站rpwinsta变量的spklList成员做是否为空的检测,这样我们就知道了修补的漏洞可能是因为spklList为空导致的,假设spklList为空,那么在接下来的访问操作中就会触发访问异常问题,从而导致BSOD(Blue Screen of Death)。
漏洞验证:
poc:
当用户进程调用CreateWindowStation函数创建一个新的窗口站时,新的窗口站对象的spklList成员是没有被初始化的,默认指向NULL地址,然后使用SetProcessWindowsStation将新窗口站与当前进程关联起来,再通过调用NtUserSetImeInfoEx函数来触发该漏洞,从而验证漏洞的存在。
CVE-2018-8120-poc.cpp
asm.asm
因为我的虚拟机是X64,而vs2015编译x64程序不支持内联汇编,所以需要新创建一个.asm文件来写汇编代码,编译生成的方式可以参考:vs2015下汇编代码与c/c++代码混合编程
poc编译完成后,甩到虚拟机中遛一遛,会发现直接把虚拟机给打蓝屏了,ok,就是要的这个效果
但是这还没有达到我们最终的目的,我们的目的是特权提升,继续我们的利用之路
漏洞利用:
构造数据:
通过前面的分析我们知道,该漏洞主要是访问零地址的时候因为零地址未被映射而导致访问异常,如果我们能够事先把零页给映射出来,然后再精心构造一下这块内存的数据,这样不但不会导致内核出现访问异常,还可以控制内核的执行逻辑,从而实现提权操作。
首先我们需要把0地址内存给申请出来,这里关于虚拟空间分布的知识需要了解。
- 32位windows系统中,可使用的虚拟地址空间大小共计2^32字节(4GB)。通常低地址的2GB用于用户空间,而高地址的2GB用于内核空间。
- 64位windows系统中,虚拟地址空间理论上的大小为2^64字节,但实际只使用了一部分,范围从0x000’0000’0000至0x7FF’FFFF’FFFF的8TB用于用户空间,范围从0xFFFF’0800’0000’0000至0xFFFF’FFFF’FFFF’FFFF的248TB的部分用于内核空间。
而空指针赋值分区是进程地址空间中从0x0000’0000到0x0000’FFFF的闭区间,保留该分区的目的是为了帮助程序员捕获对空指针的赋值,而如果进程中的线程试图读取或写入位于这一分区的内存地址,则会引发访问异常。
虚拟地址分区
x86普通模式
x86 3G用户模式
x64 64位Windows
空指针赋值分区
0x00000000
0x0000FFFF
0x00000000
0x0000FFFF
0x00000000 00000000
0x00000000 0000FFFF
用户模式分区
0x00010000
0x7FFEFFFF
0x0001000
0xBFFEFFFF
0x00000000 00010000
0x000007FF FFFEFFFF
64KB禁入分区
0x7FFF0000
0x7FFFFFFF
0xBFFF0000
0xBFFFFFFF
0x000007FF FFFF0000
0x000007FF FFFFFFFF
内核模式分区
0x80000000
0xFFFFFFFF
0xC0000000
0xFFFFFFFF
0x00000800 00000000
0xFFFFFFFF FFFFFFFF
虚拟地址空间分布表
1. 映射零页地址
2. 构造零页内存
Bitmap GDI函数实现内核任意地址读/写
通过修改Bitmap GDI函数关键对象的方式可以将有限的任意地址写漏洞转化为内核任意地址读/写。
当创建一个bitmap时,一个结构体被附加在进程PEB的GdiSharedHandleTable成员中。GdiSharedHandleTable是一个GDICELL64结构体数组的指针
通过以下的方式获取Bitmap的内核地址:
触发漏洞:
首先通过ba e 1 win32k!NtUserSetImeInfoEx对漏洞函数下断
通过syscall调用漏洞函数NtUserSetImeInfoEx,并把我们构建好的内存布局当做参数传入,
紧接着内核就会断在win32k!NtUserSetImeInfo函数调用处,来看下我们传给内核的应用层的数据,通过r 输出当前寄存器信息可以看到rcx指向的就是我们应用层构造的数据,单步运行
接着内核会通过win32k!memmove函数把应用层的内存数据(0x1c3980)拷贝到内核内存(0xfffff88002943a60)中
接着内核调用PsGetCurrentProcessWin32Process函数获取当前进程的窗口站对象rpwinsta,并索引rpwinsta中的spklList元素,由于存在漏洞,并没有对获取到的pkl元素做合法检查,导致存在0地址解引用的问题,可以看到此时的rax等于0,紧接着索引pkl->hkl[rax+0x28]的数据,并与ime_info_ex进行比较,如果不等就进入循环,因为我们在触发漏洞之前对0号内存做映射了,所以pkl->hk和ime_info_ex都是我们可控的,这里的循环我们可以直接绕过。
循环完毕后会索引pkl->piiex的数据,如果获取的数据不为NULL,并且数据偏移0x4c中的值为0,那么就调用win32k!memmove函数直接把ime_info_ex指向的内存拷贝到pkl->piiex指向的位置,因为我们在最开始把pkl->piiex设置成了mpv,所以这里的拷贝就相当于对mpv做写操作,写入的内容是ime_info_ex中的内容,而ime_info_ex前8个字节存放的是wpv的地址,这就相当于把wpv写入到mpv中,我们的GDI内核任意读写就构造好了,接下来使用Gdi32的GetBitmapBits/SetBitmapBits两个函数来实现任意读写的功能,这样就成功完成了把有限的任意地址写转换成内核任意地址读/写。
有了任意读写的功能之后,我们可以替换HalDispatchTable里面的HalQuerySystemInformation函数指针为我们的提权shellcode,替换完成后调用NtQueryIntervalProfile来触发shellcode进行提权。
GDI任意读/写
函数指针修改
提权shellcode
参考
https://paper.seebug.org/602/ 文章
https://github.com/bigric3/cve-2018-8120 32位源码
https://github.com/unamer/CVE-2018-8120 64位源码
https://www.f-secure.com/weblog/archives/kasslin_AVAR2006_KernelMalware_paper.pdf
http://vexillium.org/dl.php?call_gate_exploitation.pdf
https://ti.360.net/blog/articles/analysis-of-cve-2018-8120-in-win-7-x64/
http://www.freebuf.com/vuls/173798.html
Author: Let_go
Link: http://github.com/2018/08/26/CVE-2018-8120/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.