概述
CVE-2021-1732是一個發生在windows內核win32kfull模塊的LPE漏洞,並且由於創建窗口時調用win32kfull!xxxCreateWindowEx過程中會進行用戶模式回調(KeUserModeCallback),從而給了用戶態進程利用的機會。
該漏洞由安恆信息在2020年12月在野外攻擊樣本中發現,在2021年2月份公開披露。相關樣本在2020年APT組織蔓靈花針對國內的一次攻擊中作為提權組件被發現。
分析
Windows中創建窗口時,會調用API CreateWindowEx,最終在內核會調用至win32kfull!xxxCreateWindowEx。在win10 1909上調試時調用堆棧回溯如下:
... ... win32kfull!xxxCreateWindowEx+0x1259
... ... win32kfull!NtUserCreateWindowEx+0x6a0
... ... nt!KiSystemServiceCopyEnd+0x25
... ... win32u!NtUserCreateWindowEx+0x14
... ... USER32!VerNtUserCreateWindowEx+0x211
... ... USER32!CreateWindowInternal+0x1b4
... ... USER32!CreateWindowExW+0x82
win32kfull模塊的xxxCreateWindowEx函數為最終負責窗口對象創建的過程。CVE-2021-1732主要是在win32kfull!xxxCreateWindowEx調用win32kfull! xxxClientAllocWindowClassExtraBytes進行窗口擴展內存時觸發。xxxClientAllocWindowClassExtraBytes函數中會調用KeUserModeCallback進行用戶模式回調,以在用戶模式執行回調。該函數中指定的回調ApiNumber為0x7B,即為user32! _xxxClientAllocWindowClassExtraBytes。相關回調函數表可在PEB->KernelCallBackTable中查看。
查看user32! _xxxClientAllocWindowClassExtraBytes,只是在用戶模式當前進程堆中分配了指定大小的空間,並將分配的堆地址通過NtCallbackReturn傳回內核。
由於用戶模式回調函數的執行是在用戶態進行,因此用戶可以直接從進程中對該函數進行Hook,改變執行流程。
分析時使用POC為https://github.com/KaLendsi/CVE-2021-1732-Exploit,經過和原始樣本的比對,可以發現該POC是對原始樣本的完全還原,僅是在部分變量名含義上不正確。
漏洞首先對user32! _xxxClientAllocWindowClassExtraBytes進行Hook,在之后進程每次調用CreateWindowExW創建窗口時將會走到Hook函數處。替換后的KernelCallBackTable如下所示:
接着創建多個普通窗口,后續都會經過Hook函數。對於普通窗口,Hook函數仍舊按照舊流程,為其調用user32! _xxxClientAllocWindowClassExtraBytes。判斷依據是傳入的參數值,即tagWnd. cbwndExtra,相關細節在創建利用窗口時再說。
不過雖然普通窗口的創建仍是走的正常流程,但是會記錄每個創建窗口的對象地址。窗口對象地址利用HMValidateHandle進行泄露。該函數未導出,不過可以通過調用了該函數的其他API進行搜尋,比如IsMenu。
調用方式為HMValidateHandle(HANDLE h, int type),傳入窗口句柄和type值,如果句柄類型和參數type一致,返回句柄對應的對象在用戶態內存的地址,值得注意的是,該調用成功返回值實際為poi(tagWnd+0x28)。窗口傳入type為1。
1:TYPE_WINDOW
如此連續創建多個窗口,查詢(VirtualQuery)每個窗口對象所在內存塊的基址,記錄其中最小的基址。接着除了窗口0和1,調用DestroyWindow銷毀其余窗口。保留下的窗口0和1將結合后續將創建的magicWnd進行漏洞利用,而記錄的最小基址將用於搜尋magicWnd。
對比窗口0和1分別相對於桌面堆的偏移,較小者和較大者分別記為WndMin、WndMax。偏移值位於窗口對象tagWnd對象偏移0x08處。
tagWnd對象結構部分偏移如下:
+0x00 Handle
+0x08 cLockObj
+0x10 unk
++0x00 ETHREAD
... ...
+++0x220 EPROCESS
... ...
++++0x2e8 UniqueProcessId
++++0x2f0 ActiveProcessLinks
++++0x360 Token
++++0x3e8 InheritedFromUniqueProcessId
... ...
... ...
+0x18
++0x80 桌面堆基址
... ...
+0x20 pSelf
+0x28
++0x00 Handle
++0x08 *(tagWnd+0x28)相對於桌面堆基址的偏移
++0x18 exStyle
++0x1c dwStyle
++0x98 spMenu
... ...
+++0x50 tagWnd
... ...
++0xc8 cbwndExtra,指定Extrabytes字節數
++0xe8 不明flag,flag|=0x800可指定pExtrabytes屬性為偏移
++0x128 pExtrabytes,指向分配的Extrabytes內存
... ...
+0xa8 spMenu
... ...
++0x50 tagWnd
... ...
窗口銷毀后調用NtUserConsoleControl,指定參數ConsoleControl為6,ConsoleCtrlInfoLength為0x10,將窗口WndMin對象pExtrabytes(0x128)字段屬性設置為偏移,設置成功后pExtrabytes字段值為相對於桌面堆的偏移值,而0xe8處的flag將|=0x800。重新申請后的Extrabytes內存大小由poi(poi(tagWnd+0x28)+0xc8)指定。
(由於中間反復調試過幾次,截圖之間的數據可能有些對不上)
然后創建一個magic窗口WndMagic,同之前一樣,會執行到xxxClientAllocWindowClassExtraBytes的Hook函數處。此時將進入另一分支,觸發Hook函數真正作用流程。判斷方式是傳入的參數值,之前創建的普通窗口和現在的magic窗口指定的cbWndExtra值是不同的,普通窗口固定為32字節,magic窗口為一個隨機值。
而wndClass.cbWndExtra值將被賦值到窗口對象poi(tagWnd+0x28)+0xc8處,並作為ExtraBytes內存分配時的大小指定值,然后進行用戶模式回調。用戶態回調函數執行結束后返回內存地址到內核,賦值到poi(tagWnd+0x28)+0x128處。而Hook函數的目的就是為了返回一個虛假偏移,指向其他地址,實現可任意地址寫的功能。
窗口創建過程中,執行到Hook函數中,通過比對傳入的參數值和隨機值,可確定此次創建是WndMagic。不過此時win32kfull! xxxCreateWindowEx尚未執行完畢,所以HWND句柄值還未返回,尚不可知。然而在進行額外內存進行創建時,窗口對象部分屬性已經完成初始化,比如句柄值、窗口屬性、擴展屬性等。
所以通過匹配cbWndExtra值,再比對窗口擴展屬性值exStyle(此次利用中所有窗口屬性值都設置為了WS_EX_NOACTIVATE [0x8000000]),一致的情況下可以大概率確認WndMagic位置,自然可通過偏移獲取到相應屬性值。
獲取WndMagic窗口句柄后,調用NtUserConsoleControl設置magic窗口pExtrabytes屬性為相對於桌面堆的偏移。接着再借助NtCallbackReturn將普通窗口WndMin對象poi(tagWnd+0x28)+0x08處的值傳回內核,從而結束回調。而poi(tagWnd+0x28)+0x08的值為poi(tagWnd+0x28)基於桌面堆基址的內存偏移,因此這里將導致WndMagic對象pExtrabytes值實際是指向WndMin窗口對象的偏移。
之后調用SetWindowLongW,指定參數為(WndMagic句柄、Index=0x128、WndMin對象在內存中的偏移),返回數據應為原偏移處的舊數據,所以此處返回值為Hook函數中返回的WndMin虛假偏移。
LONG SetWindowLongW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG dwNewLong
);
調用API SetWindowLongW最終執行到win32kfull! xxxSetWindowLong。Index大於等於0的情況下會執行到下圖所示的位置。而此次利用中wndClass.cbClsExtra指定為0 ,poi(tagWnd+0x28)+0xfc也持續為0,可以忽略。因為poi(tagWnd+0x28)+0xe8已被設置0x800屬性,所以poi(poi(tagWnd+0x28)+0x128)+DesktopHeapBaseAddr+Index=tagWnd_WndMin+0x128。也就是說雖是對WndMagic進行的操作,實際上實對WndMin對象pExtrabytes字段的寫入,值為自身WndMin在桌面堆中的偏移。
然后執行SetWindowLongW(hWndMagic, offset_0xc8, 0xFFFFFFF),設置WndMin對象poi(tagWnd+0x28)+0xc8處cbwndExtra值設為0xFFFFFFF,擴大可以寫入的范圍,在xxxSetWindowLong和xxxSetWindowLongPtr中都存在對該值和Index的大小比較判定。
現在WndMagic可控制WndMin,而WndMax對象偏移已知,因此也可控制,可以實現任意位置寫。接着就是對任意位置數據讀,這里采用的的是API GetMenuBarInfo,對Menu Bat信息的獲取,這種利用一次可以讀取8字節內容。
BOOL GetMenuBarInfo(
[in] HWND hwnd,
[in] LONG idObject,
[in] LONG idItem,
[in, out] PMENUBARINFO pmbi
);
利用中構造了一個fakeMenu,將復制給WndMax,SetWindowLongPtr指定Index為-12,且窗口dwStyle為WS_CHILDWINDOW(0x40000000L),那么窗口spMenu字段可以被設置為指定的值。spMenu字段有兩處位置,poi(tagWnd+0x28)+0x98和tagWnd+0xa8。而SetWindowLongPtr成功調用后返回的值為窗口的原spMenu,記錄該值。
但是此時窗口並不是子窗口類型,所以在這之前需要對該字段手動進行設置。調用SetWindowLongPtrA,參數為(hWndMin, offset_0x18+WndMax_offset-WndMin_offset, poi(poi(tagWnd+0x28)+0x18)^0x4000000000000000),可以將WndMax窗口類型添加上WS_CHILDWINDOW屬性,從而通過檢測。
為WndMax設置WS_CHILDWINDOW屬性,並添加spMenu后,再次調用SetWindowLongPtrA恢復其dwStyle,去除WS_CHILDWINDOW屬性,原因是后續在使用GetMenuBarInfo讀取指定地址數據時,窗口不能為子窗口類型。
WndMax的fake spMenu設置完成,且已獲取了舊spMenu,記為old_spMenu。而在spMenu結構的0x50偏移處是spMenu所屬窗口對象地址,即poi(spMenu+0x50)==tagWnd。
了解以上信息后,需要對指定地址進行讀,該漏洞利用對GetMenuBarInfo進行了封裝,傳入地址,封裝函數返回該地址下的內容。
對GetMenuBarInfo的利用核心主要是指定idObject為-3,idItem為1,pmbi接收數據。API最終會走到win32kfull! xxxGetMenuBarInfo函數,傳參數據同GetMenuBarInfo。對該函數分析,可以看到需要對一些特殊的位置進行偽造,從而進入目的代碼處。其中poi(tagWnd+0x28)+0x58和poi(tagWnd+0x28)+0x5C處的值常為0,忽略。
最終讀取時,可以看到pmbi->left讀取值為poi(poi(poi(poi(menu)+0x58))+0x40),pmbi->top為poi(poi(poi(poi(menu)+0x58))+0x44),其中poi(poi(poi(menu)+0x58))值可由用戶進行控制,令其為X,也就意味着我們通過控制X值,可以讀取X+0x40處的8字節內容,即pmbi.rcBar.left+(pmbi.rcBar.top<<32)。那么只需要控制X為欲要讀取的目的地址減去0x40,即可獲取相應數據。
回到漏洞利用時封裝的讀取函數中,函數中首先向X指向的內存中每4個字節填寫一個相對於X基址的偏移值,這樣GetMenuBarInfo讀取回的pmbi.rcBar.left即為目標讀取地址應減去的差值。這么做的目的可能是為了防止系統版本的不同導致的差值不同,比如此次調試時win10 1909就為0x40。
然后第二次調用GetMenuBarInfo,傳入(目的讀取地址- pmbi.rcBar.left),即可獲取目的地址8字節內容。
這么一步步通過讀取,可以獲取到EPROCESS,然后通過ActiveProcessLinks,遍歷找到當前進程和system進程EPROCESS位置。
再次兩次調用SetWindowLongPtrA,替換當前進程Token為system進程,獲取system權限。第一次將當前進程Token地址寫入WndMax對象pExtrabytes處,第二次將system進程Token寫入當前進程Token中。完成提權。
參考
https://www.freebuf.com/vuls/271177.html
https://github.com/KaLendsi/CVE-2021-1732-Exploit
https://xiaodaozhi.com/exploit/29.html
https://theevilbit.github.io/posts/a_simple_protection_against_hmvalidatehandle_technique/