基於 GDI 物件的 Windows 核心漏洞利用

Editor發表於2018-05-09


0x00 引子

本⽂我們將討論造成 Windows 核心池(Kernel Pool)破壞的整數溢位問題,並基於 Bitmap 和 Palette 這兩個 GDI物件來探究核心漏洞的利⽤過程。當然,⽂中提出的觀點僅代表作者如何理解以及解決這些問題。

注:僅針對其中講解的前置知識點部分做了翻譯。

0x01 WinDbg 中與核心池相關的命令

!poolused :此命令可⽤於檢視具有特定標識或型別的核心池使⽤情況。

基於 GDI 物件的 Windows 核心漏洞利用

!poolfind :此命令⽤於查詢具有特定標識的核心池分配物件。

基於 GDI 物件的 Windows 核心漏洞利用

!pool :此命令⽤於檢視特殊地址所處的核心池資訊。

基於 GDI 物件的 Windows 核心漏洞利用

0x02 核心池


核心池的型別

核心池可類⽐於⽤戶態下的堆記憶體,不同之處在於它是在核心態中使⽤的。它有許多型別[1],其中最常⽤的⼏種型別如下:

桌⾯堆(Desktop Heap ):主要⽤於窗⼝、類、選單等桌⾯物件,分配函式為RtlAllocateHeap() 和DesktopAlloc(),釋放函式為RtlFreeHeap()。

⾮分⻚池(Non-Paged Session Pool ):在該類池上分配的物件,其對應的虛擬地址和實體地址是存在對映關係的,其中⼀些為系統物件,如訊號量、事件物件等。

分⻚池(Paged Session Pool ):此型別是本⽂主要關注的,對於該類池上分配的物件,其對應的虛擬地址和實體地址並不存在⼀⼀對映關係,只需保證物件在當前執⾏會話中是有效的,⽽在其餘的核心操作時並不要求這些物件必須在記憶體中,如 GDI 物件和⼀些⽤戶物件等。

對分⻚池和⾮分⻚池來說,分配函式均為 ExAllocatePoolWithTag(),其中以第⼀個引數作為型別區分,若為 0x21,則分配到分⻚池,若為 0x29,則分配到⾮分⻚池。⼆者的釋放函式為 ExFreePoolWithTag() 和 ExFreePool()。核心池的分配

通過檢視 Win32AllocPool() 函式我們可以知道核心是如何分配分⻚池物件的(型別引數為 0x21)。

基於 GDI 物件的 Windows 核心漏洞利用

關於核心池需要了解的另⼀點是它的記憶體空間以 0x1000 位元組⼤⼩劃分成⻚,對每個池⻚⾯來說,初次分配的 chunk 塊將位於⻚⾯的起始處,⽽接下去的 chunk 塊在⼤部分情況下將從⻚底開始分配。

基於 GDI 物件的 Windows 核心漏洞利用

此外,在 64 位系統中,核心 Pool Header 結構的⼤⼩為 0x10 位元組,相應的 32 位系統中的⼤⼩則為 0x8 位元組。

基於 GDI 物件的 Windows 核心漏洞利用

Pool Feng shui(池噴射)

Pool Feng shui 背後依據的原理是通過適當操作可將池記憶體置於⼀種可預測的狀態。即通過⼀系列的分配和釋放操作來構造與漏洞物件⼤⼩相同的記憶體空洞(holes),以便將存在漏洞的物件分配到我們可控物件的相鄰處,從⽽完成利⽤物件的記憶體佈局。

如果該漏洞物件在執⾏過程中沒有被釋放,那麼需要構造的空洞可位於池⻚⾯的任何地⽅,但如果該物件最終被釋放掉了,那麼就需要確保漏洞物件被置於池⻚⾯的最後,即下⼀ chunk 頭將不再有效,這樣物件被釋放後不會由於 BAD POOL HEADER ⽽觸發藍屏。

強制物件分配到池⻚⾯的尾部

我們假設漏洞物件的⼤⼩為 0x40 位元組(包含 Pool Header),則池⻚⾯初始 chunk 塊的⼤⼩需要為 0x1000 – 0x40 =0xFC0 位元組(包含 Pool Header)。

基於 GDI 物件的 Windows 核心漏洞利用

之後再分配池⻚⾯中餘下的 0x40 位元組。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

如果溢位利⽤中需要藉助其它物件,則在漏洞物件的特定偏移處進⾏相應的分配操作。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

0x03 池記憶體的破壞

引起池記憶體破壞的原因有很多,如釋放後重⽤(UAF)、池的線性溢位以及池的越界寫(OOBW)等。

⽆符號整型溢位

⽆符號整型溢位是由於在計算過程中沒有進⾏相應的檢查使得計算結果超出了整型數所能表示的最⼤範圍 MAX_UINT(0xFFFFFFFF, 32 位),從⽽導致最終結果遠⼩於預期,⽽按其後溢位值的不同使⽤情形⼜會造成不同的錯誤影響。

為了更好的理解⽆符號整型數的溢位,我們來看個例⼦:假設⽬標系統為 x86 架構,因此 UINT 佔 4 個位元組(32 位),考慮如下的加法運算:

0xFFFFFF80 + 0x81 = 00000001 ??

對 x86 系統來說上述計算結果為 0x1,然⽽真實的計算結果應該是 0x100000001,但這超出了 x86 系統上 4 位元組 UNIT數所能表示的範圍,因此截斷後得到結果 0x1。

基於 GDI 物件的 Windows 核心漏洞利用

⽽在 64 位系統中,雖然此概念仍然存在,但由於要求的數值過⼤,因此很難找到純粹的 64 位整型數溢位的情況。不過,許多存在漏洞的函式在實際使⽤前會先將數值儲存到 32 位暫存器中,所以⼜出現了前述解釋的整數截斷情形。

我們考慮如下的程式執⾏過程:

1. ⼊參變數為整型數,針對此整型數會進⾏⼀些運算操作;

2. 運算結果會導致整型數溢位;

3. 之後按此溢位值(較預期結果偏⼩)的⼤⼩進⾏新緩衝區分配操作;

4. 再按最初的⼊參整型數(未經過運算操作)進⾏相關操作:

a. 將原先內容拷⻉到新申請的緩衝區(這會導致線性溢位);

b. 向本應落在新分配緩衝區內的偏移進⾏寫⼊操作(這會導致越界寫, OOB Write)。

接著就來具體看⼀下。

線性溢位在物件資料拷⻉過程中,如果沒有對邊界進⾏檢查,那麼就有可能發⽣線性溢位。其原因有多種,例如傳給記憶體分配函式的⼤⼩是⼀個溢位值,這會導致新分配的空間偏⼩,⽽拷⻉函式卻按原先的⼤⼩將資料拷⻉到新分配的記憶體空間,⼜或者物件本身是以固定⼤⼩分配的,⽽拷⻉時使⽤的⼤⼩卻是未經校驗的⽤戶輸⼊值。

基於 GDI 物件的 Windows 核心漏洞利用

越界寫(OOB Write)

對於越界寫的情況,⾸先需有⼀物件,其⼤⼩⼤於某⼀特定值。⽽當該⼤⼩變數傳給分配函式後發⽣了整型溢位,使結果較期望值偏⼩。隨後,程式嘗試在新分配物件中按預期索引值進⾏讀寫操作,但由於分配的⼤⼩值發⽣了溢位,導致該物件⼤⼩⼩於預期,從⽽造成了越界讀寫。

基於 GDI 物件的 Windows 核心漏洞利用

通常對 Exp 開發⽽⾔,某些經第⼀階段記憶體破壞後的物件可被利⽤在獲取第⼆階段記憶體破壞的 primitives 中。這些物件⼀般擁有實現這些利⽤操作的特定成員,例如某些物件成員可以控制物件或物件中資料塊的⼤⼩,因⽽能夠實現相對的記憶體。


0x04 利⽤ GDI 物件獲取 ring0 層 ARW Primitives


讀寫操作,在某些情形中這⾜以實現 bug 的利⽤。更進⼀步,如果物件同時還擁有另⼀成員,即指向物件資料塊的指標,那麼就能將記憶體破壞的 primitives 轉換成記憶體 ARW primitives,這會讓利⽤程式的開發變得更加容易。要實現此利⽤技術通常需要藉助兩個物件,其中⼀個物件(manager)將⽤於修改第⼆個(通常是相鄰)物件(worker)的資料指標,使其獲得 ARW primitives(Game Over)。

在 Windows 核心中, GDI 物件恰好能夠滿⾜這些要求,如 Bitmap 物件利⽤技術,該技術⾸先是由 k33n 團隊提出的[3],後續被 Nicolas Economou 和 Diego Juarez 做了詳細補充[4]。⽽我則⾜夠幸運的發現了另⼀個能被利⽤的 GDI 物件,即Palette 物件, Vulcan 團隊同樣也提及了此利⽤技術[10]。 Palette 物件利⽤技術和 Bitmap 物件利⽤技術⼀樣強⼤,也能夠⽤於獲取核心的任意記憶體讀寫能⼒。

相對記憶體讀寫

相對記憶體 RW primitives 允許我們對特定地址區域進⾏讀寫操作。通過破壞 GDI 物件的記憶體可增加其⼤⼩,這通常是觸發bug 後⽤於獲取任意記憶體 RW primitives 所需邁出的第⼀步。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

任意記憶體讀寫

對⽤於實現任意記憶體讀寫的物件,我們通常要求其擁有⼀個能夠指向物件資料的指標成員。如果該指標被修改了,那麼當調⽤相應物件資料讀寫函式時就會轉⽽讀寫修改後指標所指向的地址,從⽽獲取強⼤的任意記憶體 RW primitives。

為了便於理解,我們來考慮這樣的 manager/worker 物件組合。對於 A 物件(manager),我們擴增了其⼤⼩,因⽽能夠實現相對的越界讀寫,即實現對 B 物件(worker)資料指標的讀寫,⽽後將該指標替換為我們需要進⾏讀寫的地址,這就使得 B 物件的資料讀寫操作能夠被我們所控制。

基於 GDI 物件的 Windows 核心漏洞利用

0x05 SURFOBJ - Bitmap 物件

Bitmap 物件在核心中對應的 Pool 標識為 Gh?5 或 Gla5 ,其結構體 _SURFOBJ 的定義在 msdn[5]、 ReactOS 項⽬(32 位) [6] 以及 Diego Juarez 的博⽂(64 位) [7] 中有說明。

SURFOBJ 結構體

SURFOBJ 結構中最值得我們關注的成員當屬 sizlBitmap,它是⼀個 SIZEL 結構體,系統由該變數來確定 bitmap 點陣圖的⻓寬。⽽ pvScan0 和 pvBits 成員變數均表示指向 bitmap 點陣圖的指標,按 bitmap 型別的不同,系統會選⽤⼆者之⼀。此外,在記憶體中 bitmap 點陣圖通常位於 SURFOBJ 結構之後。

基於 GDI 物件的 Windows 核心漏洞利用

分配

CreateBitmap 函式⽤於分配 Bitmap 物件,其定義如下:

基於 GDI 物件的 Windows 核心漏洞利用

分配 2000 個 bitmap 物件:

for (int y = 0; y < 2000; y++) {

HBITMAP bmp[y] = CreateBitmap(0x3A3, 1, 1, 32, NULL);

}

釋放

DeleteObject 函式則⽤於 Bitmap 物件的釋放:

基於 GDI 物件的 Windows 核心漏洞利用

DeleteObject(hBITMAP);

讀記憶體

GetBitmapBits 函式可⽤於讀取由 pvScan0 或 pvBits(取決於 bitmap 型別) 指標指向的 cBytes 位元組的 bitmap 點陣圖內容,其中 cBytes 的取值需⼩於sizlBitmap.Width * sizlBitmap.Height * BitsPerPixel 乘積。

基於 GDI 物件的 Windows 核心漏洞利用

寫記憶體

相對的, SetBitmapBits 函式則⽤於向 pvScan0 或 pvBits(取決於 bitmap 型別)指標指向的 bitmap 點陣圖寫⼊cBytes 位元組的內容,同樣 cBytes 的取值也需⼩於sizlBitmap.Width * sizlBitmap.Height * BitsPerPixel 的乘積。

基於 GDI 物件的 Windows 核心漏洞利用

相對記憶體讀寫 - sizlBitmap

sizlBitmap 成員變數為 SIZEL 型別的結構體,其中包含了 bitmap 點陣圖的⻓寬, SIZEL 結構體和 SIZE 結構體是等價的,定義如下:

基於 GDI 物件的 Windows 核心漏洞利用

後續的所有 Bitmap 物件操作,例如 bitmap 點陣圖讀寫,都依賴此變數來計算 bitmap 點陣圖的⼤⼩以執⾏對應操作,其中Size = Width * Height * BitsPerPixel。通過破壞物件的 sizlBitmap 變數可實現相對記憶體讀寫。

任意記憶體讀寫 - pvScan0/pvBits

pvScan0 指標⽤於指向 bitmap 點陣圖的第⼀⾏,但如果 bitmap 的格式為 BMF_JPEG 或 BMF_PNG ,那麼此成員變數會被置為 NULL,轉⽽由 pvBits 指標來指向 bitmap 點陣圖資料。這兩個指標在讀寫 bitmap 點陣圖資料時會⽤到,通過對其進⾏控制可以實現任意記憶體讀寫。

利⽤思路

Diego Juarez 和 Nicolas Economou 在之前的演講中對藉助Manager/Worker ⽅式的 Bitmap 物件利⽤技術做了詳盡分析,其思路是通過控制 Manager Bitmap 物件的 sizelBitmap 或 pvScan0 成員,從⽽達到控制 Worker Bitmap 物件 pvScan0成員的⽬的,最終實現核心任意記憶體讀寫(ARW primitives)。

我們這⾥給出的思路是通過控制 Manager Bitmap 物件的 sizlBitmap 成員,以擴增 bitmap 的⼤⼩來獲取相對記憶體讀寫的能⼒,接著再控制相鄰 Worker Bitmap 物件的 pvScan0 指標達到任意記憶體讀寫。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

0x06 XEPALOBJ - Palette 物件

下⾯我們將介紹基於 Palette 物件的新利⽤技術。此物件在核心中的Pool 標識為 Gh?8 或 Gla8 ,除錯中相應的符號名為 _PALETTE 、 XEPALOBJ 或 PALOBJ 。 msdn 上並沒有關於該物件的公開核心結構資訊,但我們可以在 ReactOS項⽬中[8]找到其 x86 版的定義,⽽在 Deigo Juarez 的 WinDbg 外掛項⽬ GDIObjDump 中[9]則可同時找到 x86 版和 x64版的定義。

PALETTE 結構體

對於 XEPALOBJ 結構體,我們⽐較感興趣的成員變數是 cEntries,它表示 PALETTEENTRY 陣列中的元素個數,此外還有 pFirstColor 成員變數,它是指向PALETTEENTRY 陣列 apalColors 的指標,可以看到, apalColors 表示的陣列位於此 0x06 XEPALOBJ - Palette 物件結構體的尾部。

基於 GDI 物件的 Windows 核心漏洞利用

分配

CreatePalette 函式⽤於分配 Palette 物件,唯⼀的⼊參 lplgpl 為 LOGPALETTE 結構體指標型別,對 x86 系統其分配⼤⼩需不⼩於 0x98 位元組,相應的對 x64 系統其分配⼤⼩需不⼩於 0xD8 位元組。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

不論 x86 系統還是 x64 系統, PALETTEENTRY 結構都是佔 4 個位元組:

基於 GDI 物件的 Windows 核心漏洞利用

分配 2000 個 Palette 物件:

LOGPALETTE *lPalette;

lPalette = (LOGPALETTE*)malloc(sizeof(LOGPALETTE) + (0x1E3 - 1) * sizeof(PALETTEENTRY));

lPalette->palNumEntries = 0x1E3;

lPalette->palVersion = 0x0300;

for (int k = 0; k < 2000; k++) {

hps[k] = CreatePalette(lPalette);

}

釋放

而 Palette 物件的釋放則由 DeleteObject 函式來完成:

DeleteObject(hPALETTE);

讀記憶體

GetPaletteEntries 函式被⽤來讀取 Palette 物件中的內容,即對應 hpal 控制程式碼表示的 XEPALOBJ 結構中 pFirstColor指標指向的apalColors 陣列⾃偏移 iStartIndex 開始的 nEntries 個元素,並將其儲存到緩衝區 lppe 上。函式定義如下:

基於 GDI 物件的 Windows 核心漏洞利用

寫記憶體

相對的, SetPaletteEntries 和 AnimatePalette 這兩個函式可⽤來向 Palette 物件寫⼊內容,即將緩衝區 lppe上的 nEntries 個元素寫⼊ hpal 控制程式碼表示的 XEPALOBJ 結構中 pFirstColor 指標指向的 apalColors 陣列⾃偏移 iStart 或iStartIndex 開始的位置。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

相對記憶體讀寫 - cEntries

XEPALOBJ 結構體的 cEntries 成員⽤於表示 Palette 物件中apalColors 陣列元素的個數,若將其覆蓋為⼀個⼤的數值,那麼藉助破壞後的 Palette 物件可以實現記憶體的越界讀寫。

任意記憶體讀寫 - pFirstColor

pFirstColor 指標指向的是 Palette 物件中 apalColors 陣列的起始位置,通過控制該指標,可以實現核心態下記憶體的任意讀寫。

利⽤思路

針對 Palette 物件的利⽤思路和之前討論的 Bitmap 物件利⽤思路是類似的,通過控制 Manager Palette 物件的 cEntries 或 pFirstColor 成員來達到控制相鄰 Worker Palette 物件 pFirstColor 成員的⽬的,從⽽獲取核心下的 ARW primitives。

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

基於 GDI 物件的 Windows 核心漏洞利用

0x07 基於 Palette 物件利⽤技術的⼏點限制

⾸先,在 x86 系統中要求 cEntires 成員溢位後得到的結果必須⼤於 0x26,相應的在 x64 系統中必須⼤於 0x36,這是因為在 XEPALOBJ 物件分配時, x86 系統要求其⼤⼩不⼩於 0x98 位元組,⽽ x64 系統要求其⼤⼩不能⼩於 0xd8 位元組。例如cEntires 成員經溢位後由 0x1 變為 0x6,但這顯然不滿⾜條件,該⼤⼩ 0x6 * 0x4 = 0x18 位元組⼩於所要求分配 Palette 物件時的最⼩值。

其次,如果利⽤程式通過 SetPaletteEntries 函式向記憶體寫⼊資料,那麼需保證 XEPALOBJ 結構中的 hdcHead、ptransOld 以及ptransCurrent 成員不會被覆蓋掉。

基於 GDI 物件的 Windows 核心漏洞利用

ring3 層的 SetPaletteEntries 調⽤會經由 NTSetPaletteEntries ⾄ GreSetPaletteEntries 函式,此時會對 hdcHead 成員進⾏檢查,如果該值不為零,則程式執⾏流程會報錯或直接藍屏當機,即下圖⻩⾊區域所示。

基於 GDI 物件的 Windows 核心漏洞利用

不過在此之前 GreSetPaletteEntries 還會先調⽤ XEPALOBJ::ulSetEntries 函式對 pTransCurrent 和 pTransOld 成員進⾏檢查,如果它們⾮零,程式會進⼊下圖所示的橘⾊區域,這有可能會導致藍屏當機。

基於 GDI 物件的 Windows 核心漏洞利用

最後我們看下使⽤ AnimatePalettes 函式向 Palette 物件進⾏寫操作的情況,唯⼀限制是要求 pFirstColor 指標所指內容的最⾼位元組為奇數,對應的 XEPALOBJ::ulAnimatePalette 函式程式碼段如下。雖然不會導致藍屏當機,但這使得我們⽆法完成寫⼊操作。

基於 GDI 物件的 Windows 核心漏洞利用

0x08 Token 的替換

核心藉助 _EPROCESS 結構來表示系統上運⾏的每⼀個程式,該結構包含很多重要成員,例如 ImageName、SecurityToken、ActiveProcessLinks 以及 UniqueProcessId,這些成員的偏移值因系統版本⽽異。

Windows 8.1 x64:

基於 GDI 物件的 Windows 核心漏洞利用

Windows 7 SP1 x86:

基於 GDI 物件的 Windows 核心漏洞利用

另外,核心中 SYSTEM 程式所對應的 EPROCESS 結構地址可通過如下⽅式計算得到:

KernelEPROCESSAddress = kernelNTBase + (PSInitialSystemProcess() - UserNTImageBase)

SecurityToken

SecurityToken 表示當前程式所持有的安全級別標識,當程式請求獲取特定許可權時,系統會藉此判斷其是否擁有所請求資源的許可權。

ActiveProcessLinks

ActiveProcessLinks 是⼀個 LIST_ENTRY 物件,可藉此遍歷各程式對應的 EPROCESS 結構。

typedef struct _LIST_ENTRY {

struct _LIST_ENTRY *Flink;

struct _LIST_ENTRY *Blink;

} LIST_ENTRY, *PLIST_ENTRY;

UniqueProcessId

UniqueProcessId 表示程式 PID。

步驟

1. 獲取核心中 SYSTEM 程式對應的 EPROCESS 結構地址;

2. 藉助 Read primitive 得到相應的 SecurityToken 和 ActiveProcessLinks;

3. 遍歷 ActiveProcessLinks 得到當前程式的 EPROCESS 結構地址,即 ActiveProcessLinks->Flink.UniqueProcessId 和 GetCurrentProcessId() 的值相同;

4. 藉助 Write primitive 將當前程式的 SecurityToken 替換為 SYSTEM 程式的SecurityToken。

*參考部分詳⻅原⽂

原文連結:[翻譯]基於 GDI 物件的 Windows 核心漏洞利用

本文由看雪論壇 BDomne 編譯,來源media@Saif El-Sherei  轉載請註明來自看雪論壇

相關文章