前言

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

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

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

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

Let's Go

POC 分析

环境准备

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

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

逆向POC文件

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

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

#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的文档:

[+] 留意:

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

崩溃堆栈

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

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代码对应如下:

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

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

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

调试器部分的分析

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

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


运行到崩溃指令处.

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

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

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

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

由于不小心失误多操作了一次, 所以我们的值多减了一次一, 在后面我把它加回来了. 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, 而不是堆栈. 于是我运行了如下命令.

其中需要留意的信息有:

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

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

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

第二步

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

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

可能你会比较疑惑的是各个变量的意义是什么, 与前面的思路一样, 我采用了动态调试验证.(定位到函数流, 跟着走一遍, 观察相应变量的值). 给出结论之前先看我们POC代码中的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, 让我们来算一下我们的结论是否正确:

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

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

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);
}

验证:

而另外一个0x18的分配我采用了k0shi师傅的Class进行了分配, 这几天发现了lpszMenuName也适合(请期待我的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函数:


其中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被覆盖.

fix header

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

提权.

提权与我的第二篇博客类似, 都是替换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
[+] 我的个人博客: redogwu.github.io
[+] 我的github地址: https://github.com/redogwu
[+] 第一篇: https://redogwu.github.io/2018/11/02/windows-kernel-exploit-part-1/
[+] 第二篇: https://redogwu.github.io/2018/11/02/windows-kernel-exploit-part-2/

源链接

Hacking more

...