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

前言

Hello, 歡迎來到windows kernel exploit第四篇, 這篇文章主要講述在對MS-16-0198的利用當中進行的一次爬坑, 以及在內核利用當中一種相當重要的技術, pool fengshui.

Anyway, 希望能對您有一點點小小的幫助 :)

一點小小的吐槽

這篇漏洞有另外兩篇詳細的分析. 在先知另外一個網站上. 所以在我一開始的計劃當中, 我只是調一下寫一下利用就好. 沒打算放在這個系列里面的. 但是在寫這個利用的時候, 發生了一點點事, 讓我一度懷疑我是一個孤兒.

我一開始copy了代碼和原文件嘗試運行失敗了. 于是在讀文章的過程中, 修復了一些代碼. 我在pool fengshui那里折騰了將近半天的時間, 因為原文的exp的數據大小在我這里是不適用的(我的環境也是windows 8.1 x64). 先知和原作者都成功的運行了exp, 于是就給了我一種為毛你們都可以, 就我不可以的孤兒感 :(

另外一個方面, 在我計劃的第五篇和第六篇文章里面, 會牽扯到這里面的知識. pool fengshui, 所以最后決定寫一下自己的爬坑之旅.

exp的運行

img

查找錯誤原因

于是我在源代碼的觸發漏洞的地方插入了兩個__debugbreak()語句.

img

在進行漏洞函數xxx分配pool的地方下了斷點, 然后得到如下的結果. 觀察其分配的pool. 得到如下的結果:

img

我們看到在他原來的文章當中理想的風水布局的結果如圖:

img

于是我們可以判斷出原作者在我的環境上面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分配出了錯).

相關鏈接

后記

這個漏洞我還沒有調試完成, 還有個比較大的坑沒有爬完. 后續爬完之后, 我會把這個漏洞的修改的exp放到我的github上面, 同時更新此博客.

其實我更希望您能在此文當中看到的不只是pool fengshui的技巧, 而是在內核當中調試器下見真章的那種感覺, 這一個思想幫助我(我是一個很笨很笨的人)解決了很多的困惑.

Anyway, 謝謝您閱讀這篇又丑又長的博客 :)

最后, wjllz是人間大笨蛋.


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