作者:ze0r @360A-TEAM
公眾號:360安全監測與響應中心
相關閱讀:[上篇]從補丁diff到EXP--CVE-2018-8453漏洞分析與利用
CVE-2018-8453漏洞是一個Windows內核提權漏洞,由卡巴斯基官方于野外發現用于APT中攻擊中東地區國家。
相關鏈接:
微軟官方的補丁和漏洞簡介可以看鏈接:https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2018-8453
卡巴斯基的分析文章鏈接:https://securelist.com/cve-2018-8453-used-in-targeted-attacks/88151/
文末附本文相關EXP下載鏈接
正文內容
在CVE-2018-8453分析得上篇中--[[上篇]從補丁diff到EXP--CVE-2018-8453漏洞分析與利用],我們已經分析了漏洞成因,知道它是一個double-free類型的漏洞。本篇中,我們將利用這個漏洞獲取SYSTEM最高權限。
我們知道,雙重釋放類型的漏洞,可以給我們一個多釋放一次內存的機會。通過上次分析,我們發現釋放的目標內存—SBTrack僅僅0x50大小,無論是利用Bitmap還是Palette對象,空間都嚴重不夠:

這里為了方便已經在SBTrack申請的時候,直接斷點并將內存地址賦值給了偽寄存器$t0(下同):
ba e 1 win32kfull!xxxSBTrackInit+0x59 ".echo SBTrack INIT;r eax;r @$t0=@eax;g";
由于空間根本不夠我們去部署一個GDI對象,所以需要想辦法擴展這個空間。在內存管理中,對于這種動態劃分的內存塊,無法預知大小,就是用戶態的堆、內核態的池。所以給了我們一個可利用的機會,就是雖然申請時是0x50大小,但釋放時只是按照內存管理機制來釋放,沒有判斷內存大小釋放已經改變的方法。所以我們可以在第二次釋放前,可以讓這塊內存為任意大小,也就是在二次釋放前,我們重新申請到這塊內存,并且申請時是一個足以容納Palette的大小(由于bitmap在win 10 RS2中已經更改修復,所以我們選擇Palette對象)。從而實現擴展目標內存大小的目的,我們想要達到如下效果:

下面來看代碼實現。在系統分配池內存的時候,會按照0x1000大小來調撥內存。為了避免干擾,我們首先申請大量大塊內存,讓系統調撥到新的頁面:

每次都申請0xC10大小的內存,這么大的內存塊,在系統已調撥的頁面已經被占完時,只能調撥新的內存頁來滿足申請,而一個頁面剩下的空間0x1000-0xC10不足以滿足下一次申請,所以造成的結果就是每個頁面都只有一個0xC10大小的空間。之后申請內存占用余下的部分,這里說一下,系統池分配的機制是,第一次在最上面,第二次在最下面,之后再申請就是從第二個往上(地址小的方向)挨個排開:

這會導致一個頁面中,只在中間留下了0x50大小空間。就如上圖(以后均指紅色那個理想圖)中第二個情形。這里已經滿足我們的要求,但是SBTrack不一定會放在我們部署好的頁面中,因為系統中本來就存在0x50大小的間隙。所以之后我們再申請0x50大小內存,用以填補系統中本來的0x50大小間隙:

這里在申請了3000個0x50大小后,又釋放了2000/2個內存,而且是跳一個釋放一個,這造成一個頁面是滿的、相鄰下一個又是有空余的,這樣挨個依次排開。接下來發送消息讓系統分配SBTrack,根據我們空一頁、滿一頁的布局,SBTrack只能在這其中之一,想跑都跑不了:

之后就是在回調到用戶態中,更改FNID、SetCapture等操作釋放真正的SBTrack。這里注意一下,在fnDWORDCallBack函數中,用于退出xxxSBTrackLoop函數的SetCapture調用,在各個版本的EXP中位置稍有不同,只是因為各個版本中系統對fnDWORD回調不大一樣,但目的都是為了退出循環,不用在意:

在發送了WM_CANCELMODE消息后,系統已經釋放了真正的SBTrack,此時目標頁面的布局如上圖中第四種情形:

可看到中間的SBTrack已經被釋放,但上面的c10和下面的3a0還在,之后釋放掉下面的0x3a0,由于系統管理機制要避免碎片化,以盡量滿足以后大塊內存申請。所以系統會對相鄰的free的內存整合為一個大塊0x3f0:

現在已經滿足了我們理想圖中的倒數第二種情形。我們已經把一個0x50大小的內存轉化成了0x3f0大小的內存,這足以容納我們的Palette對象。回翻一下漏洞分析中,在之后回到內核態后,系統會繼續釋放d2aabc10這塊內存。所以我們在這里放的任何對象,都會被釋放。那么放什么合適呢?
這里說一點故事,本人一開始想在這里直接放一個Palette更改掉大小來完成利用,在各種嘗試后發現,這思路根本不對,這個坑的結果是:Palette如果被釋放了,那么會在它源GDI header的handle處(第一個DWORD)寫上內存管理結構。這造成的結果是,雖然內核的句柄表還有這個句柄項,內存也指向正確,但是你卻不能操作它,因為會在驗證句柄階段直接發現異常殺死線程。在苦苦分析后依然不得其法,所以只好尋找另一條路徑。
這里總結一下,我們現在擁有了一個釋放任意對象的能力。只需要找到一個對象:首先它是要在頁會話池中分配的,然后需要用戶態可以指定它的整體大小,再然后需要有API可以操作它,可以讀寫它的內存----至少要可寫它的內存。苦苦搜索后,沒有發現。然后想到,如果有一個對象,即使它本身不是在頁會話池中分配的,但如果它的某個成員是在頁會話池中分配、可以控制大小、有API可以對這個成員的內存進行讀寫那也可以完成目的。很幸運,依據這個想法,本人找到了一個在網上并沒有公布的一個系統調用: NtGdiSetLinkedUFIs:

該函數首先根據a3在內核池中用PALLOCMEM2臨時申請了內存,之后判斷后進入了59行的XDCOBJ::bSetLinkedUFIs函數:

可以看到第11行的V5來自于該對象0xe0處的一個成員。而如果為零的話,則直接到了36行(其中a3來自于上層調用又來自于API調用的參數)調用PALLOCMEM2申請了一塊用戶指定大小乘以8大小的內存,只要簡單看PALLOCMEM2函數的第二個參數是個tag就知道這個肯定也是在頁會話池中。之后就把申請到的內存放在了0xe0處,并且在跳到19行后在自身成員中保存了a3。其中重要的是17行的memcpy,它的a2來自于上層的用戶指定----這意味著我們可以直接控制往E0處所指的內存寫任意字節,完全沒有改變!而如果是第二次調用此API,則根據API的參數來判斷是要新申請更大內存還是直接更改已保存的內存----這意味著我們可以第二次直接平坦的寫目標內存!這完全滿足我們的需求!再回頭仔細看看NtGdiSetLinkedUFIs,第一個參數是一個HDC對象,第二個參數是要寫的內容,第三個參數就是要寫的字節數除以8。
所以利用思路如下圖:

大致思路有了,但上面提到過,GDI對象本身頭部的handle如果驗證錯誤,線程會被殺而導致利用失敗。而我們對目標內存沒有讀能力(其實還有另一個系統調用NtGdiGetLinkedUFIs可以用來讀內存,但測試時發現首先判斷了另一個成員變量,有興趣的讀者可以深入看下這個成員),所以這里需要做一點小變動,在不改動Palette前四字節的要求下,改動Palette的大小。這里提一下,palette的大小字段位于對象的0x14處:

這里eca8cc1c處的0x28就是本Palette對象的元素個數(大小),而前面的0x501是它的版本號,這里是個固定值。所以我們只需要讓目標BYTES區域對準eca8cc18即可,這樣寫內存更改掉eca88c1c處為0xffff即可(這也是一開始申請0xC10大小而不是0xc00的原因)!
那么利用過程就變成了這樣:

這樣申請palette對象后,原來的BYTES區域其實指向的是某個Palette對象+0x10的地方,正好對準了對象大小的成員。另外這里說一點,在上圖中,釋放了0xC10后,由于本頁面已經全部都釋放狀態,系統其實是會回收釋放整個頁面,整個頁面變成了未分配狀態。之后再次申請0xC00大小時,這個頁面又被重新調撥分配了。而這里有個小坑就是由于目標頁面被釋放,同時又大量申請Palette對象,這很大幾率造成本頁面被分配用作二級句柄表了,避免這個意外的辦法就是提前申請大量對象,讓系統早分配句柄表,避免干擾我們的布局:

回到主題,按照思路,我們首先申請0x3f0大小的BYTES區域:

之后返回到內核態中,目標內存被釋放:

可以看到原SBTrack內存區域被釋放(Usst)后,被再次分配(Gadd),最后又被釋放的過程(Free狀態的Gadd)。之后,我們就再次申請0xC00并且繼續申請要被越界的Palette:

這里可以提一下,就是為什么不是理想中的C00大小而是0xB30,這里主要是考慮到,在申請越界Palette的過程中,系統其他進程也可能需要使用內存,如果正好碰到這種情形,那我們布局就會亂。所以這里預留下剛好不夠一個Palette的空間,即使系統其他進程也同時申請了內存,也是會被放在前幾個0xd0空隙中。最后布局如上圖,這完全符合我們的需要,并且保證成功率(本人測試下,布局100%成功)!
之后調用NtGdiSetLinkedUFIs系統調用更改掉b4c21c00處Palette的大小:



現在我們已經有了一個越界的Palette。但由于這個Palette內存地址是交叉的,所以我們還是盡量少用這個Palette,盡早切換到一個新的Palette。利用這個Palette更改下一個Palette作為Manager,再下一個作為Worker:

之后就是常規操作,獲取SYSTEM進程EPROCESS->獲取本進程EPROCESS->復制TOKEN-->創建新進程->恢復本進程TOKEN,不再贅述:


至此,我們已經有了一個SYSTEM權限的進程。但是系統中還存在一個有問題的HDC的句柄,這個DC對象的某個成員釋放會造成BSOD。在逆向了這個對象的方法后,有兩個思路:一是直接找到對象,把0xe0處直接改成0即可,但這個涉及的問題是如何通過句柄找到對象地址?二是在系統的句柄表里直接找到這一項,清零。但同樣的問題,如何句柄表中找到這一項?網絡翻找文章,也沒找到WIN10下可用的具體方法。查看win32kfull.sys+win32kbase.sys發現,這個XDCOBJ對象從句柄得到對象地址會經過HmgLockEx函數(所有GDI對象都經過這個函數轉換),而該函數又各種轉換。沒有心思深入,即使繼續深入代碼也難以實現查找。于是換了一個思路:在退出進程前,先釋放掉交叉的Palette,這造成XDCOBJ+0xe0處成員指向了一個Free的內存。然后再次申請大量的AcceleratorTable,這就把一個查找GDI對象的問題換成了查找普通用戶句柄的問題!然后釋放掉DC句柄。此時,句柄表中有一個AcceleratorTable對象指向了Free的內存。那么這個AcceleratorTable句柄如何找到呢?
在NtUserDestroyAcceleratorTable中,主要通過HMValidateHandle來獲取AcceleratorTable地址,它的主要轉換過程如下:

其中gSharedInfo+8在win10上固定為0x10,則計算方法為:*gpKernelHandleTable + 8 * (handle & 0xffff)。那么只剩最后一個問題:gpKernelHandleTable的值如何得到?由于這是win32kbase.sys的全局變量,獲取win32kbase.sys+偏移的方式?一是有通用性的問題,二是獲取win32kbase.sys加載地址也麻煩,所以換一個方法。我們回到HMValidateHandle函數中,這個轉換過程就直接有gpKernelHandleTable。所以我們采用搜索的方式,那么它的上層函數NtUserDestroyAcceleratorTable地址如何得到呢?
由于這是直接的系統調用,所以肯定在win32k!W32pServiceTable中,查找資料發現這個調用表可以在KeServiceDescriptorTableShadow+固定偏移中找到,而KeServiceDescriptorTableShadow雖然沒有導出,但它固定在KeServiceDescriptorTable-0x40處。所以這就找到了一條可通行的路徑:

所以直接找到這個句柄項,清零即可:

之后即完美退出EXP:

本文EXP下載鏈接:
https://github.com/360-A-Team/cve-2018-8453-exp/
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/798/
暫無評論