windows kernel exploit part 4

前言

Hello, 欢迎来到windows kernel exploit第四篇, 这篇文章主要讲述在对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的时候, 内存块会往后面挤

free list

分配的对象需要属于同一种对象

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分配出了错).

相关链接

[+] 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

后记

这个漏洞我还没有调试完成, 还有个比较大的坑没有爬完. 后续爬完之后, 我会把这个漏洞的修改的exp放到我的github上面, 同时更新此博客.

其实我更希望您能在此文当中看到的不只是pool fengshui的技巧, 而是在内核当中调试器下见真章的那种感觉, 这一个思想帮助我(我是一个很笨很笨的人)解决了很多的困惑.

Anyway, 谢谢您阅读这篇又丑又长的博客 :)

最后, wjllz是人间大笨蛋.

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 前言
    1. 1.1. 一点小小的吐槽
  2. 2. exp的运行
  3. 3. 查找错误原因
  4. 4. pool feng shui.
    1. 4.1. pool feng shui概述
    2. 4.2. pool feng shui原则
      1. 4.2.1. 0x1000的划分
      2. 4.2.2. 分配的对象需要属于同一种对象
      3. 4.2.3. 分配的对象的size计算
    3. 4.3. pool feng shui的数据选择.
    4. 4.4. CreateBitmap的闪亮登场
    5. 4.5. 泄露bitmap地址
      1. 4.5.1. 寻找GdiSharedHandleTable。
        1. 4.5.1.1. 调试器寻找:
        2. 4.5.1.2. 代码寻找:
        3. 4.5.1.3. 验证截图
      2. 4.5.2. 依据handle寻找其地址
      3. 4.5.3. 验证
      4. 4.5.4. 总结数据关系
        1. 4.5.4.1. 传入参数为0x10:
        2. 4.5.4.2. 传入参数为0x70:
        3. 4.5.4.3. 传入参数为0x80:
        4. 4.5.4.4. 传入参数为0x90:
        5. 4.5.4.5. 传入参数为0xA0:
      5. 4.5.5. 验证
  5. 5. lpszMenuName
    1. 5.1. 泄露tagWND
      1. 5.1.0.1. 总结数据关系
      2. 5.1.0.2. 验证数学关系:
  • 6. 释放内存块
    1. 6.0.1. 实验验证
  • 7. 相关链接
  • 8. 后记
  • ,