Hello, 欢迎来到windows内核漏洞第四篇, 这篇文章主要讲述在对MS-16-0198
的利用当中进行的一次爬坑, 以及在内核利用当中一种相当重要的技术, pool fengshui
.
Anyway, 希望能对您有一点点小小的帮助 :)
这篇漏洞有另外两篇详细的分析. 在先知和另外一个网站上. 所以在我一开始的计划当中, 我只是调一下写一下利用就好. 没打算放在这个系列里面的. 但是在写这个利用的时候, 发生了一点点事, 让我一度怀疑我是一个孤儿.
我一开始copy了代码和原文件尝试运行失败了. 于是在读文章的过程中, 修复了一些代码. 我在pool fengshui那里折腾了将近半天的时间, 因为原文的exp的数据大小在我这里是不适用的(我的环境也是windows 8.1 x64). 先知和原作者都成功的运行了exp, 于是就给了我一种为毛你们都可以, 就我不可以的孤儿感 :(
另外一个方面, 在我计划的第五篇和第六篇文章里面, 会牵扯到这里面的知识. pool fengshui, 所以最后决定写一下自己的爬坑之旅.
于是我在源代码的触发漏洞的地方插入了两个__debugbreak()
语句.
在进行漏洞函数xxx
分配pool
的地方下了断点, 然后得到如下的结果. 观察其分配的pool
. 得到如下的结果:
我们看到在他原来的文章当中理想的风水布局的结果如图:
于是我们可以判断出原作者在我的环境上面fengshui
出错了.
在查找到了我们的错误点之后, 就到了我们的pool feng shui
隆重出场.
依然, 我们尽量少做重复性质的工作. 所以这里我会对pool feng shui做一个大概的总结. 相关性的详细讨论你可以在这里找到.
我们先来看一下这张图(图片来源blackhat):
这是我们所期待的布局. 为什么让我们的vul buffer
落入此地址呢. 在一些利用当中. 实现利用要对vul buffer
相连的对象的关键数据结构进行操作(如bitmap). 具体的你可以在我的第三篇博客里面找到实际样例.
于是, 为了使这个理想的布局情况能够出现, 我们需要借用pool fengshui
的技术. 链接里面已经给了pool fengshui
的相关链接. 你可以查看他了解更多细节.
我们来看blackhat上面的作者是如何实现的.
[+] 第一步: 填充大小为0x808对象
[+] 第二步: 填充大小为0x5f8对象(留下0x200的空隙)
[+] 第三步: 填充大小为0x200的对象
[+] 第四步: 释放大小为0x5f8的对象
[+] 第五步: 填充大小为0x538的对象(留下0xc0的空隙)
[+] 第六步: 填充大小为0xc0的对象
[+] 第七步: 释放部分0x200对象(留下0x200的对象, vul buffer能够填充进去)
在漏洞代码
进行vul buffer
(大小也为0x200)分配的时候, 能够落入到我们预先安排的0x200的空隙当中. 上面的就是pool fengshui
的大概思路了. 让我们来看一下更多的细节.
而相应的, 我们来总结一下feng shui布局的比较关键性的原则.
0x1000在pool
的分配当中, 与freelist挂钩. 分为两个情况
[+] 当分配的pool size大于0x808的时候, 内存块会往前面挤
[+] 当分配的pool size小于0x808的时候, 内存块会往后面挤
pool 分为几个类型. 我查阅的windows 7
的资料. 不过对于windows 10
应该是同样适用的
[+] Nnonpaged pool
[+] paged pool
[+] session pool
也就是, 上面的0x200
的数据和0xc0
的数据想挨在一起. 那么他们必须是同样的pool type. 此处为Paged Session Pool
.(我以前在做第二篇博客的时候由于这个点的失误, 导致我浪费了整整一天的时间 :).
如果你申请的pool
大小为0x20
, 那么在windows x64平台下的实际pool size
应该是0x30
, 因为还要加上pool header
部分.
需要注意的是, 这一部分来源于这里. 我只是做了一点小小的改动 :)
既然知道了我们的pool feng shui
的思路, 那么我们就需要分配nSize
的对象了. 如何寻找nSize
的对象呢. 我目前知道的是有两个思路.
[+] 寻找某对象可以分配任意的size
[+] 寻找某对象刚好满足size的n/1
==> 如果你想分配的size是0x80. A(20)可以分配0x20大小的对象. B(80)可以分配0x80的对象. 那么
for(int i = 0; i < 0x1000; i++)
B(80)
for(int i = 0; i < 0x1000; i++)
for(int j = 0; j < 0x4; j++) //4 * 0x20 = 0x80
A(20)
第二种方式的局限性比较大, 可能在某种情况下你找不到刚好能够分配0x20大小的对象, 比如我就没有找到 :), 于是我们开始选取任意大小的对象.
CreateBitmap会分配一个pool
, 其大小和上面的参数cx
, cy
相关. 他们与pool size
的关系是, 我不知道 :(
嗯, 在阅读了大量的文章之后. 我对于这个关系越来越迷惑. 于是我开始决定自己总结关系. 一开始的时候我写了这个语句.
HBITMAP hBitmap = CreateBitmap(0x10, 2, 1, 8);
现在, 我需要知道其大小. 这篇文章里面有给出使用!poolfind
指令的方法, 但是我尝试多次失败了(后面我会介绍我为什么会失败). 但是anyway. 笨人也有笨人的方法. 我总觉得我一定可以找到解决方案 :). 因为我知道在windows 8.1
上如何泄露我刚刚分配的bitmap
的地址.
在windows 8.1上泄露bitmap的地址我们可以使用GdiSharedHandleTable
. 我们后面再来阐述GdiSharedHadnleTable
是啥. 在这一部分让我们先用代码和调试器来找到它.
我们可以看到我们的GdiShreadHandleTable
和PEB
相关, 且在PEB
偏移为0x0f8
的地方. 下面让我们用代码来找到它.
我们都知道寻找PEB
就需要先找TEB. 让我们先来看看一张图.
我们可以看到PEB
在TEB
偏移0x60
处. 接着, 我们从TEB
一步一步找着就好.
幸运的是微软提供了NtCurrentTeb()
函数能够帮助我们方便的寻找到TEB
.
DWORD64 tebAddr = NtCurrentTeb();
然后我们再使用第一张图找到PEB
的地址.
DWORD64 pebAddr = *(PDWORD64)((PUCHAR)tebAddr + 0x60); // 0x60是PEB的偏移
接着使用我们的最开始的图来找到我们的GdiSharedHandleTable
的地址.
DWORD64 gdiSharedHandleTableAddr = *(PDWORD64)((PUCHAR)pebAddr + 0xf8);
Too easy :)
找到了GdiSharedHandleTable
的地址之后, 是时候让它发挥点作用了. 自己对GdiShreadHandleTable
的理解如下:
[+] GDIShreadHandletable是一个数组, 其中的Entry为一个叫做GDICELL64的结构体.
[+] GDICELL64存放一些与GDI句柄相关的信息
现在, 让我们来看一下GDICELL64
的分析.
可以看到它在其中泄露了有关GDI handle
的内核地址. 那么, handle如何对应GdiShreadHandleTable
的数组的GDICELL64
的项呢.
[+] handle类似于一个数组下标. 不过index = handle & 0xFFFF = LOWROD(handle).
让我们先通过调试器验证他. 验证的截图如下.
需要注意的是, 0x18是GDICELL64
的大小. 聪明的你看了前面的PPT一定可以算出来的:)
依据前面的原理代码实现如下:
需要注意的是, 那个地方我打印是赋值粘贴的, 实在不想改了 :)
现在我们可以使用光明正大的开始观察我们的BITMAP
了. 于是我整理了下面的几张截图. 和您分享一起总结数据关系:
基于此. 写出下表.
[+] 0x10 ==> 0x370
[+] 0x20 ==> 0x370
[+] 0x70 ==> 0x370
[+] 0x80 ==> 0x370
[+] 0x90 ==> 0x390
[+] 0xA0 ==> 0x3B0
之后随着我二把刀的数学水平, 我总结出了如下的关系式(她可能不太准确, 但应付风水布局应该足够了. :)
if(nWidth >= 0x80)
nSize = (nWidth - 0x80) * 2 + 0x370(这一部分还有内存对齐之类的我就不做计算了, 你可以由上面的自己实验)
else
nSize = 0x370
再来随便找个数值验证一下.
BinGo, 我们找到了能帮我们分配nSize>=0x370
的paged pool session
对象. 让我们开始下一小节.
我们可以清楚的看到. 大于等于0x370的对象我们很愉快的找到了相应的分配. 但是小于0x370的呢. 比如上面的0x200和0xc0. 于是我们想到了lpszMenuName
.
按照惯例. 我们先用调试器找到lpszMenuName.
首先我们得知道lpszMenuName(menu是菜单的意思)关联一个window的windows窗口对象
, 其在内核当中对应结构体对象为tagWND
, 于是我们来看下面的图(需要注意的是, 下面的截图我都是在windows 7 x64的环境下截的图, 因为从8开始微软去掉了很多的导出符号, 不过大多数时候windows 7的数据在后续的操作系统上还是成立的, 这算是一个自己调试内核的一个小技巧...)
kd> dt win32k!tagWND
[...]
+0x098 pcls : Ptr64 tagCLS
[...]
其中tagCLS
对应的是windows窗口
对应的类, 在tagCLS
当中我们能够记录找到lpszManuName
. 记录一下我们等下写代码需要的数据.
[+] 0x98 ==> tagCLS相对于tagWND的偏移.
[+] 0x88 ==> lpszMenuName相对于tagCLS的偏移.
聪明的你一定猜到了, 如果我们能够泄露窗口的地址. 那么我们就能根据前面的思路泄露出lpszMenuName
的地址, 从而通过传给wndclass.lpszMenuName不同大小的字符串(我的实验使用UNICODE做的).来观察出其大小关系.
泄露tagWND可以利用HMValidateHandle
函数. 此函数我测试过支持到windows RS3
版本. 在samb
的github上面你可以找到对应的源码: 而另外一个方面小刀师傅的博客这里也给出了相应的介绍. 所以我只给出粗糙的介绍. 详细的可以在这里找到介绍.
先来看一张图.
tagWND
对应一个桌面堆
. 内核的桌面堆会映射到用户态去. HMValidateHandle
能够获取这个映射的地址. 在这个映射(head.pSelf)当中存储着当前tagWND
对象的内核地址. 而HMValidateHandle
函数的地址未导出, 不过在导出的IsMenu
函数有使用, 所以可以通过硬编码的形式找到它.
再次感谢小刀师傅的博客. 小刀师傅拥有着我所有想要的优点.
借助于此, 我创建了如下的代码来帮我观察lpszMenuName
的大小关系.
而实验的验证结果如下(需要注意的是, 这里我们的A系列函数会扩充为W系列函数, 这一部分在windows核心编程当中有提到).
anyway, 你也知道, 截图十分的痛苦. 所以我直接给出数据的表, 具体的你可以自己依据上面的思路来观察. :)
[+] 0x01 ==> 0x20
[+] 0x03 ==> 0x20
[+] 0x05 ==> 0x20
[+] 0x06 ==> 0x20
[+] 0x10 ==> 0x40
[+] 0x20 ==> 0x60
[+] 0x30 ==> 0x80
[+] 0x40 ==> 0xa0
关系式:
if(nMalloc >= 0x10)
nSize = nMalloc * 2 + 0x20(这一部分还有内存对齐之类的我就不做计算了, 你可以由上面的自己实验)
else
nSize = 0x20
BingGO!
我们已经有了合适的用于分配内存块的函数, 接着就是其对应的释放了.
释放BitMap:
DeleteObject(hBitmap)
释放lpszMenuName:
UnregisterClass(&wns, NULL);
依赖于此, 我们很轻松的实现了blackhat
演讲上面提到的布局. 验证如下(由于内存对齐, 我更改了一点点布局):
而MS-16-098
的风水部分我会在爬完坑之后放到我的github上(据我的推测, 它的0x60分配出了错).
这个漏洞我还没有调试完成, 还有个比较大的坑没有爬完. 后续爬完之后, 我会把这个漏洞的修改的exp放到我的github上面, 同时更新此博客.
其实我更希望您能在此文当中看到的不只是pool fengshui
的技巧, 而是在内核当中调试器下见真章
的那种感觉, 这一个思想帮助我(我是一个很笨很笨的人)解决了很多的困惑.
Anyway, 谢谢您阅读这篇又丑又长的博客 :)
最后, wjllz是人间大笨蛋.
[+] sakura师父的博客: http://eternalsakura13.com/
[+] 小刀师父的博客: https://xiaodaozhi.com/
[+] MS 16-098的分析: https://sensepost.com/blog/2017/exploiting-ms16-098-rgnobj-integer-overflow-on-windows-8.1-x64-bit-by-abusing-gdi-objects/
[+] 写完文章之后发现的一篇很好的博客: http://trackwatch.com/windows-kernel-pool-spraying/
[+] 本文的样例代码地址: https://github.com/redogwu/blog_exp_win_kernel/tree/master/pool-fengshui/pool-fengshui
[+] 自己维护的一个库: https://github.com/redogwu/windows_kernel_exploit
[+] 我的github地址: https://github.com/redogwu/
[+] 我的个人博客地址: http://www.redog.me