菜鸡调试学习了一下 CVE-2018-8120 两种利用方式, 分享一下, 如有错误的地方, 还请指正.
该漏洞是由于 win32k!SetImeInfoEx 在复制数据时, 由于检查不够, 造成空指针解引用漏洞. 下面是补丁前的代码:
可以看到, 函数在使用 tagWINDOWSTATION 中的 spklList 成员时, 没有判断其是否为 NULL 就在下面开始使用. 下面是补丁后的代码(这里可以用 expand -F:* 命令从更新程序中提取补丁后的文件):
补丁后在 if 语句中增加了判断 spklList 成员是否为 NULL 的条件.
通过查看上层函数可以知道, SetImeInfoEx 的第一个参数来自函数 GetProcessWindowStation, 是获取的当前进程的窗口站. 查看其实现可以知道, 它获取的是 EPROCESS 中的 Win32Process 成员, 该成员指向 win32k!tagPROCESSINFO 结构.
一个程序运行后, 会默认关联到一个叫 "WinSta0" 的窗口站, 该窗口站结构中的 spklList 成员并不为空, 运行一个简单的命令行程序并查看 spklList:
这里我们可以调用 CreateWidnowStation 创建一个窗口站, 创建好后打印句柄值, 用 windbg 查看该窗口站结构中的 spklList 成员:
可以看到, 创建的窗口站默认情况下 spklList 成员为 NULL. 接下来, 我们可以调用函数 SetProcessWindowStation 来将窗口站和我们的程序关联.
最后就是要执行到 SetImeInfoEx 了, 它的上级函数是 NtUserSetImeInfoEx, 该函数就一个参数, 指向结构 tagIMEINFOEX 的指针, 它在应用层无任何导出, 这里我们可以在 Shadow SSDT 中找到该函数并计算其 Index, 然后自己调用该函数:
最后将上述代码整合编译, 运行后, 即可看到蓝屏.
通过上面我们可以知道, 当发生问题时, keyboard_layout 指向 NULL, 只要通过以下两个条件
就可以执行到 memcpy, 这里可以造成向任意地址写入 sizeof(tagIMEINFOEX) 个字节的数据. 我们只要在用户层分配零页内存当作 keyboard_layout, imeinfoex 来自用户层调用时的参数, 我们可以控制. 这样, 我们只要将 keyboard_layout 中的 hkl 和 imeinfoex 的 hkl 设置为相同的值, 并把要写入的地址设置到 piiex, 要写入的数据作为用户层参数下传即可. 这里注意 piiex->fLoadFlag 要为 0. 下面学习下两种漏洞利用方法.
该方法是野外样本所用的方法, 其主要是构造一个应用层可以调用的调用门描述符, 并利用该漏洞将调用门描述符写入 GDT 中. 然后应用层通过调用门进入特权代码段后返回时, 并没有调用 retf 指令而是调用 ret 指令返回, 这导致在返回后 CS 还是特权代码段, 然后就可以用 System 的 Token 替换当前进程 Token 完成提权.
这里首先调用 _sgdt 获取 GDTR 寄存器的内容:
然后分配 sizeof(tagIMEINFOEX) 大小的内存, 在其中布局要覆盖到 GDT 中的数据:
这里的代码从 GDT 偏移 0x144 的位置开始覆盖, 用 Windbg 查看 GDT 可以看到, 没有用到的项如下方式填充:
所以上面代码也如此填充. 这里之所以选择 0x144 从一个内容为 0 的地方开始, 是因为我们下传的 tagIMEINFOEX 的第一个成员 hkl 设置为了 0, 还有就是让 piiex 偏移 0x48 的位置为 0(也就是 fLoadFlag). 这里主要就是构造了一个代码段和该代码段要执行的代码以及一个调用门, 调用门里的代码段选择子和偏移就是我们自己构造的代码段的选择子和填充代码的位置, 按照 Intel 手册中调用门个格式构造就可以了. 这里我把调用门构造在 GDT 偏移 0x168 的位置, 也就是 buff 偏移 0x24 的位置(覆盖时从 GDT 偏移 0x144 开始覆盖). 代码段紧随其后, 要执行的代码我放在了 GDT 偏移 0x148 的位置, 这里就写了一个 ret.
布局好后设置触发漏洞的条件, 触发漏洞覆盖 GDT:
覆盖 GDT 以后, 就可以调用我们的调用门了:
调试时在调用调用门的地方步进后中断在内核调试器中, 可以看到, 执行了我们的 ret 返回到 call 下面的的代码后, CS 寄存器为我们构造的代码段选择子 , 这里是 0x170:
这里注意一下, 此时虽然在特权代码段, 可以访问 R0 的数据, 但这里不能用 fs 寄存器访问 KPCR, 因为 fs 寄存器并没有被切换, fs 还是应用层的, 指向 TEB. 所以这里提前获取了内核导出的保存 System EPROCESS 的变量 PsInitialSystemProcess 和当前进程的 EPROCESS, 以供 Shellcode 使用. 这里通过以下方法获取:
Shellcode 执行完返回后, 进程的 Token 被替换, 提权完成.
这个方法就是通过漏洞产生的任意地址写数据能力, 来覆盖窗口对象的 lpfnWndProc 窗口消息处理函数, 最终实现任意代码执行.
窗口对象在内核的结构:
这里主要关注下 state 的 bServerSideWindowProc 位和 lpfnWndProc 成员. 使用 SendMessage 发送一个消息时会判断该位是否置位, 当 bServerSideWindowProc 位被置位时, 消息处理函数 lpfnWndProc 会在内核上下文中被执行. 而这个窗口对象的内核地址在用户层可以通过 HMValidateHandle 来获得, 这个函数返回映射在用户层的 tagWND, 这块内存是个只读内存. 下面说下主要的利用步骤.
首先我们在 user32 中寻找 HMValidateHandle 函数, 该函数在 IsMenu 函数中有被调用:
然后我们创建一个窗口并通过 HMValidateHandle 来泄露内核地址
这里注意下, 因为 tagWND 的大小只有 0xb0, 而我们在复制内存时, 一次要复制 0x15c 大小的数据, 所以这里通过增加窗口扩展的大小来增加内核分配内存大大小, 通过 2000 的源代码和 IDA 查看 xxxCreateWindowEx 函数, 分配 tagWND 的代码如下:
可以看到, 分配时会用 tagWND 的大小加上我们设置的窗口扩展大小. 还有就是调用 CreateWidnow 时的 style 参数也要注意, 我们知道, 在漏洞复制内存时需要判断 piiex 偏移 0x48 的位置, 也就是 fLoadFlag 成员要为 0, 该成员对应到 tagWND 结构, 是 rcWindow 的 right 成员, 在使用一些 style 参数时, 该位置不为 0, 这里使用 WS_POPUP, 使用这个值在创建后 right 为 0.
接下来设置要覆盖的地址:
从复制 tagWND 复制 0x15c 字节, 然后修改其 bServerSideWindowProc 和 lpfnWndProc 成员, 然后修改零页内存符合漏洞触发条件, 并将要写入数据的地址设置为 tagWND 的内核地址, 然后触发漏洞即可. 这里的 Shellcode 使用简单的搜索 System 进程, 然后读取 Token 替换当前进程的 Token 即可.
通过对比 5 月补丁分析 win32k 空指针解引用漏洞
windows_kernel_address_leaks
CVE-2018-8120 Analysis and Exploit
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2018-7-23 20:56
被污师编辑
,原因: