MS-0198爬坑记录(pool fengshui)

前言

Hello, 欢迎来到windows内核漏洞第四篇, 这篇文章主要讲述在对MS-16-0198的利用当中进行的一次爬坑, 以及在内核利用当中一种相当重要的技术, pool fengshui.

Anyway, 希望能对您有一点点小小的帮助 :)

一点小小的吐槽

这篇漏洞有另外两篇详细的分析. 在先知另外一个网站上. 所以在我一开始的计划当中, 我只是调一下写一下利用就好. 没打算放在这个系列里面的. 但是在写这个利用的时候, 发生了一点点事, 让我一度怀疑我是一个孤儿.

我一开始copy了代码和原文件尝试运行失败了. 于是在读文章的过程中, 修复了一些代码. 我在pool fengshui那里折腾了将近半天的时间, 因为原文的exp的数据大小在我这里是不适用的(我的环境也是windows 8.1 x64). 先知和原作者都成功的运行了exp, 于是就给了我一种为毛你们都可以, 就我不可以的孤儿感 :(

另外一个方面, 在我计划的第五篇和第六篇文章里面, 会牵扯到这里面的知识. pool fengshui, 所以最后决定写一下自己的爬坑之旅.

exp的运行

查找错误原因

于是我在源代码的触发漏洞的地方插入了两个__debugbreak()语句.

在进行漏洞函数xxx分配pool的地方下了断点, 然后得到如下的结果. 观察其分配的pool. 得到如下的结果:

我们看到在他原来的文章当中理想的风水布局的结果如图:

于是我们可以判断出原作者在我的环境上面fengshui出错了.

pool feng shui.

在查找到了我们的错误点之后, 就到了我们的pool feng shui隆重出场.

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的大概思路了. 让我们来看一下更多的细节.

pool feng shui原则

而相应的, 我们来总结一下feng shui布局的比较关键性的原则.

0x1000的划分

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.(我以前在做第二篇博客的时候由于这个点的失误, 导致我浪费了整整一天的时间 :).

分配的对象的size计算

如果你申请的pool大小为0x20, 那么在windows x64平台下的实际pool size应该是0x30, 因为还要加上pool header部分.

需要注意的是, 这一部分来源于这里. 我只是做了一点小小的改动 :)

pool feng shui的数据选择.

既然知道了我们的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的闪亮登场

CreateBitmap会分配一个pool, 其大小和上面的参数cx, cy相关. 他们与pool size的关系是, 我不知道 :(

嗯, 在阅读了大量的文章之后. 我对于这个关系越来越迷惑. 于是我开始决定自己总结关系. 一开始的时候我写了这个语句.

HBITMAP hBitmap = CreateBitmap(0x10, 2, 1, 8);

现在, 我需要知道其大小. 这篇文章里面有给出使用!poolfind指令的方法, 但是我尝试多次失败了(后面我会介绍我为什么会失败). 但是anyway. 笨人也有笨人的方法. 我总觉得我一定可以找到解决方案 :). 因为我知道在windows 8.1上如何泄露我刚刚分配的bitmap的地址.

泄露bitmap地址

在windows 8.1上泄露bitmap的地址我们可以使用GdiSharedHandleTable. 我们后面再来阐述GdiSharedHadnleTable是啥. 在这一部分让我们先用代码和调试器来找到它.

寻找GdiSharedHandleTable。

调试器寻找:

我们可以看到我们的GdiShreadHandleTablePEB相关, 且在PEB偏移为0x0f8的地方. 下面让我们用代码来找到它.

代码寻找:

我们都知道寻找PEB就需要先找TEB. 让我们先来看看一张图.

我们可以看到PEBTEB偏移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 :)

依据handle寻找其地址

找到了GdiSharedHandleTable的地址之后, 是时候让它发挥点作用了. 自己对GdiShreadHandleTable的理解如下:

[+] GDIShreadHandletable是一个数组, 其中的Entry为一个叫做GDICELL64的结构体.
[+] GDICELL64存放一些与GDI句柄相关的信息

现在, 让我们来看一下GDICELL64的分析.

可以看到它在其中泄露了有关GDI handle的内核地址. 那么, handle如何对应GdiShreadHandleTable的数组的GDICELL64的项呢.

[+] handle类似于一个数组下标. 不过index = handle & 0xFFFF = LOWROD(handle).

让我们先通过调试器验证他. 验证的截图如下.

需要注意的是, 0x18是GDICELL64的大小. 聪明的你看了前面的PPT一定可以算出来的:)

依据前面的原理代码实现如下:

验证


需要注意的是, 那个地方我打印是赋值粘贴的, 实在不想改了 :)

总结数据关系

现在我们可以使用光明正大的开始观察我们的BITMAP了. 于是我整理了下面的几张截图. 和您分享一起总结数据关系:

传入参数为0x10:

传入参数为0x70:

传入参数为0x80:

传入参数为0x90:

传入参数为0xA0:

基于此. 写出下表.

[+] 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>=0x370paged pool session对象. 让我们开始下一小节.

lpszMenuName

我们可以清楚的看到. 大于等于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

泄露tagWND可以利用HMValidateHandle函数. 此函数我测试过支持到windows RS3版本. 在sambgithub上面你可以找到对应的源码: 而另外一个方面小刀师傅的博客这里也给出了相应的介绍. 所以我只给出粗糙的介绍. 详细的可以在这里找到介绍.

先来看一张图.

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

源链接

Hacking more

...