基於 GDI 對象的 Windows 內核漏洞利⽤

基於 GDI 對象的 Windows 內核漏洞利⽤

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 轉載請註明來自看雪論壇


分享到:


相關文章: