0x00: 前言
CVE-2018-8120
是win32k
的空指针解引用内核提权漏洞 漏洞存在于函数SetImeInfoEx
中 在没有对目标窗口tagWINDOWSTATION
对象的指针成员域 spklList
指向地址进行有效性校验的情况下 对该地址直接进行读取访问
0x01:分析环境
目标系统:Windows 7 32位
调试器:WinDbg
反汇编器:IDA Pro
0x02:漏洞分析
补丁分析
使用Bindiff
对比SetImeInfoEx
函数的改动可以看到函数的改动 补丁文件中增加了判断
使用IDA
看伪代码对比 可以看到增加了对V3的判断 如果为0直接返回
使用IDA
的交叉引用功能可以看到函数NtUserSetImeInfoEx
对该函数调用
此时我们仅对影响V3
的第一个参数进行追溯 可以看到是通过_GetProcessWindowStation
获取当前进程的窗口站对象指针 然后作为参数传入函数SetImeInfoEx
GetProcessWindowStation函数用于检索当前窗口站的句柄 返回值是窗口站的句柄
窗口站
窗口站对象
1 | kd> dt win32k!tagWINDOWSTATION |
成员域spklList
是指向关联的键盘布局tagKL
对象链表首节点的指针
键盘布局tagKL
1 | kd> dt win32k!tagKL |
当某个进程被关联到指定进程时 其地址被存储在目标进程信息的tagPROCESSINFO
结构的对象成员域rpwinsta
中 进程默认关联的窗口站是由系统自动创建的交互式窗口站 其成员域spklList
指向真实的键盘布局对象节点
当用户进程创建新从窗口站时 最终在内核函数中调用函数xxxCreateWindowStation
进行创建窗口站 在函数执行期间 新的窗口站对象成员域spklList
并没有进行初始化 指向NULL
1 | kd> dt win32k!tagPROCESSINFO |
在path函数
中就增加了对成员域spklList
是否为NULL的操作 当其为NULL的时候 unpath函数
在没有判断的情况下 直接读取0页数据 当进程上下文不存在0页数据时 就会发生缺页异常
POC编写
首先我们需要使用CreateWindowStationA
函数来创建一个窗口站 然后使用SetProcessWindowStation
函数将新创建的窗口站对象与当前进程关联起来 然后调用NtUserSetImeInfoEx
函数 该函数只有一个参数 我们将此参数指向一个无效内存 然后传参给我们的SetImeInfoEx
函数
1 | signed int __stdcall NtUserSetImeInfoEx(char *a1) |
1 | signed int __stdcall SetImeInfoEx(signed int a1, _DWORD *a2) |
由于NtUserSetImeInfoEX
函数是系统调用 所以没有导出函数 我们需要使用系统调用号来实现
我们在这里找到对应版本的系统调用号
1 | "NtUserSetImeInfoEx": { |
所以我们可以根据调用号写出我们自己的调用
然后再简单的创建一下窗口站 并关联到进程上 调用我们的NtUserSetImeInfoEx
函数 而此时关联窗口站的成员域spklList
指向空地址
空地址所在的零页内存并没有映射 则当内核函数SetImeInfoEx
在访问零页内存时 即可异常
进行Crash
0x03:漏洞利用
首先需要实现任意地址读写 我们在用户进程下只能通过某些函数来有限的控制内核对象的函数数据 而当内核函数的某个成员指针指向用户地址空间时 用户进程即可利用代码分配内存空间再进行布局即可扩大控制内核函数对象的能力
首先分配0页内存 以便进行我们的内存布局
实现内核任意地址读写需要将某内核对象的函数指针成员域替换为位于用户进程空间中由利用代码实现的函数的地址 并修改代表内核或用户态的相关标志位 进而使该对象在处理相关事件时 在内核中直接执行到位于用户进程地址空间中的函数代码 并在该函数中最终实现内核提权 这次我们使用窗口对象的消息处理函数指针成员域lpfnWndProc
来实现
接下来创建窗口对象 将窗口对象的成员数据填充到源输入法扩展信息tagIMEINFOEX
对象中 置位其中的成员标志位bServerSideWindowProc
并修改指针成员域lpfnWndProc
的值 再使位于零页的伪造键盘布局tagKL
对象的成员域piiex
指向目标窗口对象的内核地址,将位于源输入法扩展信息对象中的被修改过的窗口对象成员数据覆盖到目标窗口对象的内存块中 实现对目标窗口对象的特定成员域的修改
函数SetImeInfoEx
在执行拷贝时是以tagIMEINFOEX
对象的大小作为拷贝范围的 大小为0x15
个字节 超过tagWND
对象的0xb0
个字节 则在注册窗口对象的足够的扩展区域大小
然后使用HMValidateHandle
泄露内核对象地址
接下来获取用户态映射地址的内存中拷贝tagIMEINFOEX
结构体大小的数据到利用代码定义的源输入法扩展信息对象中 获取到的窗口对象内核地址作为位于零页的伪造键盘布局对象的成员域piiex
指向的内存地址
然后修改位于源输入法扩展信息对象中的窗口对象成员数据 置位其中的成员标志位bServerSideWindowProc
并修改指针成员域lpfnWndproc
的值
将目标窗口对象的成员标志位 bServerSideWindowProc
置位 并将其窗口对象的消息处理函数成员域修改为用户进程自定义的消息处理函数地址之后 用户进程向该窗口对象发送消息时 系统将在内核中直接进入目标消息处理函数中执行消息处理的任务 从而进入自定义消息处理函数 根据参数pwnd
指向的当前窗口对象关联的线程信息tagTHEADINFO
对象 然后定位到EPROCESS
对象的地址 再根据EPROCESS
链表找到System
进程地址 获取其Token
值 在写入当前进程的Token
中即可完成提权
0x04:总结
该漏洞的触发以及原理是相对简单的 可以很好的理解并去触发该漏洞 但在利用上 相对来说就很有难度了 其实在对于零页内存和任意地址读写以及泄露内核基址这种问题 我们经常会碰到 也经常去使用 需要说的是 代代版本的缓解措施都需要去研究 现在还不具有这些能力 以及对于内存布局的问题也需要去学习