背景知識
本節內容描述了創建窗口時需要用到的結構體及函數:
- 用戶態的窗口數據結構體:WNDCLASSEXW。
- 窗口數據保存在內核態時使用:tagWND和tagWNDK結構體。
- 用戶態調用SetWindowLong可以設置窗口擴展內存數據,逆向分析SetWindowLong如何設置窗口擴展內存數據。
窗口類擁有如下屬性結構,此處僅列出比較重要的結構:
typedef struct tagWNDCLASSEXW {
UINT cbSize; //結構體的大小
…
UINT style; //窗口的風格
WNDPROC lpfnWndProc; //處理窗口消息的回調函數地址
int cbClsExtra; //屬于此類窗口所有實例共同占用的內存大小
int cbWndExtra; //窗口實例擴展內存大小
LPCWSTR lpszClassName; //類名
…
} WNDCLASSEXW
在用戶態創建窗口時,需要調用RegisterClass注冊窗口類,每個窗口類有自己的名字,調用CreateWindow創建窗口時傳入類的名字,即可創建對應的窗口實例。
當cbWndExtra不為0時,系統會申請一段對應大小的空間,如果回調到用戶態申請空間時,可能會觸發漏洞。
內核中使用兩個結構體來保存窗口數據tagWND和tagWNDK:
ptagWND //內核中調用ValidateHwnd傳入用戶態窗口句柄可返回此數據指針
0x18 unknown
0x80 kernel desktop heap base //內核桌面堆基址
0x28 ptagWNDk // 需要重點關注這個結構體,結構體在下方:
0xA8 spMenu
tagWNDK結構體,需要重點關注此結構體:
struct tagWNDK
{
ULONG64 hWnd; //+0x00
ULONG64 OffsetToDesktopHeap;//+0x08 tagWNDK相對桌面堆基址偏移
ULONG64 state; //+0x10
Dword dwExStyle; //+0x18
DWORD dwStyle; //+0x1C
BYTE gap[0x38];
DWORD rectBar_Left; //0x58
DWORD rectBar_Top; //0x5C
BYTE gap1[0x68];
ULONG64 cbWndExtra; //+0xC8 窗口擴展內存的大小
BYTE gap2[0x18];
DWORD dwExtraFlag; //+0xE8 決定SetWindowLong尋址模式
BYTE gap3[0x10]; //+0xEC
DWORD cbWndServerExtra; //+0xFC
BYTE gap5[0x28];
ULONG64 pExtraBytes; //+0x128 模式1:內核偏移量 模式2:用戶態指針
};
當WNDCLASSEXW 中的cbWndExtra值不為0時,創建窗口時內核會回調到用戶態函數USER32!
_xxxClientAllocWindowClassExtraBytes申請一塊cbWndExtra大小的內存區域,并且將返回地址保存在tagWNDK結構體的pExtraBytes變量中。
【一>所有資源關注我,私信回復“資料”獲取<一】
1、很多已經買不到的絕版電子書
2、安全大廠內部的培訓資料
3、全套工具包
4、100份src源碼技術文檔
5、網絡安全基礎入門、linux、web安全、攻防方面的視頻
6、應急響應筆記 7、 網絡安全學習路線
8、ctf奪旗賽解析
9、WEB安全入門筆記
使用函數SetWindowLong和GetWindowLong,可對窗口擴展內存進行讀寫,進入內核后調用棧如下:
win32kfull!xxxSetWindowLong
win32kfull!NtUserSetWindowLong+0xc7
win32k!NtUserSetWindowLong+0x16
nt!KiSystemServiceCopyEnd+0x25
win32u!NtUserSetWindowLong+0x14
USER32!_SetWindowLong+0x6e
CVE_2022_21882!wmain+0x25d
SetWindowLong函數形式如下:
第二個參數為index,含義為設置擴展內存偏移index處的內容。
在win32kfull!xxxSetWindowLong函數中,會對第二個參數index進行判斷,防止越界:
137行代碼判斷index+4如果大于cbWndServerExtra+ cbWndExtra,表明越界,一般情況下cbWndServerExtra為0,如果越界,會跳轉到117行LABEL_34,設置v18為1413,跳轉到LABEL_55,調用UserSetLastError設置錯誤值,我們可以在cmd下查看此錯誤值的含義:
如果沒有越界的話,接下來會根據不同的模式來使用pExtraBytes,如下:
在xxxSetWindowLong函數中:
正常情況下cbWndServerExtra為0,157行如果index+4< cbWndServerExtra,那么修改的是窗口的保留屬性,例如GWL_WNDPROC對應-4,含義為設置窗口的回調函數地址。我們需要設置的是窗口擴展內存,所以進入165行的代碼區域。
在167行會判斷dwExtraFlag屬性是否包含0x800,如果包含,那么168行代碼destAddress=pExtraBytes+index+內核桌面堆基址,此處pExtraBytes作為相對內核桌面堆基址的相對偏移量,(QWORD)(pTagWnd->field_18+128)為內核桌面堆基地址 ,對應的匯編代碼為
在171行處,dwExtraFlag屬性不包含0x800,此時destAddress=index+pExtraBytes,此處pExtraBytes作為用戶態申請的一塊內存區域地址。
dwExtraFlag的含義:
dwExtraFlag&0x800 != 0時,代表當前窗口是控制臺窗口。調用AllocConsole申請控制臺窗口時,調用程序會與conhost程序通信,conhost去創建控制臺窗口,調用棧如下:
conhost獲取到窗口句柄后,調用NtUserConsoleControl修改窗口為控制臺類型,調用棧如下:
dwExtraFlag&0x800 ==0時,代表當前窗口是GUI窗口,調用CreateWindow時窗口就是GUI窗口。
總結:
xxxSetWindowLong設置擴展內存數據時,有如下兩種模式:
模式1:tagWND的dwExtraFlag屬性包含0x800,使用間接尋址模式,基址為內核桌面堆基地址,pExtraBytes作為偏移量去讀寫內存。
模式2:tagWND的dwExtraFlag屬性不包含0x800,使用直接尋址模式,pExtraBytes直接讀寫內存。 xxxSetWindowLong會檢查index,如果index+4超過cbWndExtra,那么返回索引越界錯誤。
漏洞成因
此漏洞是對CVE-2021-1732漏洞的繞過,此處簡要介紹下CVE-2021-1732漏洞:
用戶調用CreateWindow時,在對應的內核態函數中檢查到窗口的cbWndExtra不為0,通過xxxCreateWindowEx->
xxxClientAllocWindowClassExtraBytes->調用回調表第123項用戶態函數申請用戶態空間,
1027行會調用USER32!
_xxxClientAllocWindowClassExtraBytes,EXP在回調函數中調用NtUserConsoleControl修改窗口的dwExtraFlag和pExtraBytes,修改窗口類型為控制臺。
windows修復代碼在1039行,檢查pExtraBytes是否被修改,此處查看匯編代碼更為清晰
rdi+0x140-0x118 = rdi+0x28,得到tagWNDK,偏移0x128得到pExtraBytes,判斷是否不等于0,如果不等于0,1045行代碼會跳轉,最終釋放窗口,漏洞利用失敗。
也就是說:CVE-2021-1732的修復方法是在調用
xxxClientAllocWindowClassExtraBytes函數后,在父函數CreateWindowEx中判斷漏洞是否被利用了,這個修補方法之前是沒有問題的。
但是在后續代碼更新后,有了新的路徑來觸發
xxxClientAllocWindowClassExtraBytes函數:
在xxxSwitchWndProc函數中調用
xxxClientAllocWindowClassExtraBytes后也有檢查pExtraBytes是否為0,如果不為0,那么就復制pExtraBytes內存數據到新申請的內存地址中,沒有檢查dwExtraFlag是否被修改。
總結:
由于CVE-2021-1732漏洞修補時是在父函數中修復的,雖然當時沒有問題,但是當多了
xxxClientAllocWindowClassExtraBytes函數的觸發路徑后,同樣的漏洞又存在了,而且 CVE-2021-1732漏洞觸發路徑是在xxxCreateWindowEx中,此時窗口句柄還未返回給用戶態,漏洞利用時需要更多的技巧,此漏洞利用時已經返回了窗口句柄,利用起來更加簡單。
利用漏洞的流程
本節介紹了漏洞觸發的流程,并介紹了觸發漏洞及利用漏洞需要的各個知識點。
漏洞觸發利用的流程:
要利用這個漏洞,需要以下背景知識:
6.1 觸發用戶態回調
本節描述如何觸發用戶態回調,使內核回調到USER32!
_xxxClientAllocWindowClassExtraBytes。
在IDA中查看
xxxClientAllocWindowClassExtraBytes的引用,有多處地方調用到了此函數,
查看xxxSwitchWndProc代碼如下:
98行代碼有cbWndServerExtra變量賦值,而在調用SetWindowLong時會使用index-cbWndServerExtra,所以我們真正想設置內存區域偏移index位置的變量時,參數2應該傳入index+cbWndServerExtra。
103行代碼調用
xxxClientAllocWindowClassExtraBytes返回值賦值給了v20變量。
111行代碼檢查原來的pExtraBytes是否為0,如果不為0,那么就復制內存的數據,還會釋放原來的pExtraBytes。
117、123行代碼都會將v20變量賦值給pExtraBytes。
而xxxSwitchWndProc函數是可以通過win32u! NtUserMessageCall函數來觸發的,在用戶態調用NtUserMessageCall函數會觸發內核態函數
xxxClientAllocWindowClassExtraBytes,函數調用棧如下:
win32kfull!xxxClientAllocWindowClassExtraBytes
win32kfull!xxxSwitchWndProc+0x167
win32kfull!xxxWrapSwitchWndProc+0x3c
win32kfull!NtUserfnINLPCREATESTRUCT+0x1c4
win32kfull!NtUserMessageCall+0x11d 內核態
…
win32u! NtUserMessageCall 用戶態
在內核態的win32kfull!
xxxClientAllocWindowClassExtraBytes函數中,會調用用戶態的xxxClientAllocWindowClassExtraBytes函數。
win32kfull!
xxxClientAllocWindowClassExtraBytes函數如下:
KernelCallbackTable第123項對應
_xxxClientAllocWindowClassExtraBytes函數,使用IDA查看函數內容:
此函數中調用RtlAllocateHeap函數來申請*(a1)大小的內存,內存地址保存在addr變量中,然后調用NtCallbackReturn函數返回到內核態,返回的數據為addr變量的地址,對應在上面win32kfull!
xxxClientAllocWindowClassExtraBytes函數中的v7變量,v7為addr變量的地址,*v7即為上圖中的addr。
總結:
觸發回調函數的路徑為:
Win32u!NtUserMessageCall(用戶態)->win32kfull!NtUserMessageCall(內核態)-> win32kfull!xxxSwitchWndProc(內核態)-> win32kfull!
xxxClientAllocWindowClassExtraBytes(內核態)-> nt!KeUserModeCallback(內核態)-> USER32!_xxxClientAllocWindowClassExtraBytes(用戶態,HOOK此函數)
本節講了如何從用戶態進入到內核,又回調到USER32!
_xxxClientAllocWindowClassExtraBytes函數的方法。
6.2 HOOK回調函數
上一小節講了觸發到USER32!
_xxxClientAllocWindowClassExtraBytes函數的流程,我們還需要hook此回調函數,在回調函數中觸發漏洞。下面代碼可以將回調函數表項第123、124分別修改為MyxxxClientAllocWindowClassExtraBytes、MyxxxClientFreeWindowClassExtraBytes。
6.3 修改窗口模式為模式1
上一小節講了如何進入到用戶態自定義的函數,本節講述在自定義的函數中通過用戶態未公開函數NtUserConsoleControl修改窗口模式為模式1,本節對NtUserConsoleControl函數進行逆向分析。
函數win32u! NtUserConsoleControl可以設置模式為內核桌面堆相對尋址模式,此函數有三個參數,第一個參數為功能號,第二個參數為一個結構體的地址,結構體內存中第一個QWORD為窗口句柄,第三個參數為結構體的大小。
NtUserConsoleControl函數會調用到內核態win32kfull模塊的NtUserConsoleControl函數,調用棧如下:
win32kfull!NtUserConsoleControl 內核態
win32k!NtUserConsoleControl+0x16 內核態
nt!KiSystemServiceCopyEnd+0x25
win32u!NtUserConsoleControl+0x14 用戶態
CVE_2022_21882!wmain+0x3f4 用戶態
win32kfull模塊NtUserConsoleControl判斷參數,然后調用xxxConsoleControl如下:
17行判斷參數index不大于6
22行判斷參數length小于0x18
26行判斷參數2指針不為空且length不為0
以上條件滿足時會調用xxxConsoleControl函數,傳入參數為index、變量的地址,傳入數據的長度, xxxConsoleControl函數會對index及len進行判斷:
110行代碼可知,index必須為6,113行代碼可知len必須為0x10,115行到119行代碼可知,傳入參數地址指向的第一個QWORD數據必須為一個合法的窗口句柄,否則此函數會返回。
134、136行判斷是否包含0x800屬性,如果包含,v23賦值為內核桌面堆基地址+偏移量pExtraBytes,得到的v23為內核地址。
140行代碼,如果不包含0x800屬性,那么調用DesktopAlloc申請一段cbWndExtra大小的內存保存在v23中。
149到156行代碼判斷原來的pExtraBytes指針不為空,就拷貝數據到剛申請的內存中,并調用
xxxClientFreeWindowClassExtraBytes->USER32!_xxxClientFreeWindowClassExtraBy釋放內存。
159、160行代碼使用內核地址v23減去內核桌面堆基址得到偏移量v21,將v21賦值給pExtraBytes變量。
使用如下代碼可以修改窗口模式為模式1:
ULONG64 buff[2]={hwnd};
NtUserConsoleControl(6, &buff, sizeof(buff));即可將hwnd對應的窗口模式設置為模式1。
總結:
在自定義回調函數中調用win32u!NtUserConsoleControl可以設置窗口模式為模式1,傳入參數需要符合下列要求:參數1 index必須為6 參數2指向一段緩沖區,緩沖區第一個QWORD必須為一個合法的窗口句柄 參數3 len必須為0x10
6.4 回調返回偽造偏移量
在
_xxxClientAllocWindowClassExtraBytes 函數中調用NtCallBackReturn回調函數可以返回到內核態:
偽造一個合適的偏移量Offset,然后應該取Offset地址傳給NtCallbackReturn函數,可以將offset賦值給pExtraBytes變量。
由于之前已經切換窗口為模式1,pExtraBytes含義為相對于內核桌面堆基址的偏移,再查看tagWNDK結構體,關注以下字段:
+0x08 ULONG64 OffsetToDesktopHeap; //窗口tagWNDK相對桌面堆基址偏移
+0xE8 DWORD dwExtraFlag; //包含0x800即為模式1
+0x128 ULONG64 pExtraBytes; //模式1:內核桌面堆偏移量 模式2:用戶態指針
OffsetToDesktopHeap為窗口本身地址tagWNDK相對于內核桌面堆基址的偏移,可以使用如下方法來偽造合適的偏移量:
- 創建多個窗口,如窗口0和窗口2(為了與EXP匹配),窗口2觸發回調函數,返回窗口0的OffsetToDesktopHeap ,賦值給窗口2的pExtraBytes變量。
- 對窗口2調用SetWindowLong時,寫入的目標地址為:內核桌面堆基址+pExtraBytes+index,此時pExtraBytes為窗口0的地址偏移,對窗口2調用SetWindowLong可以寫窗口0的tagWNDK結構數據,這是第一次越界寫。
總結:
調用NtCallbackReturn可以返回到內核中,偽造偏移量為窗口0的OffsetToDesktopHeap,賦值給窗口2的pExtraBytes,當對窗口2調用SetWindowLong時即可修改到窗口0的tagWNDK結構體。
接下來我們需要獲取窗口0的OffsetToDesktopHeap。
6.5 泄露內核窗口數據結構
上一小節中我們在用戶態中要返回窗口0的OffsetToDesktopHeap到內核態,OffsetToDesktopHeap是內核態的數據,要想獲取這個數據還需要一些工作。
調用CreateWindow只能返回一個窗口句柄,用戶態無法直接看到內核數據,但是系統把tagWNDK的數據在用戶態映射了一份只讀數據,只需要調用函數HMValidateHandle即可,動態庫中沒有導出此函數,需要通過IsMenu函數來定位:
定位USER32!HMValidateHandle的代碼如下:
定位到USER32!HMValidateHandle函數地址后,傳入hwnd即可獲取tagWNDK數據地址。
tagWNDK* p = HMValidateHandle(hwnd),通過tagWNDK指針即可獲取到OffsetToDesktopHeap數據。
6.6 如何布局內存
通過上面的知識,我們可以通過窗口2修改窗口0的tagWNDK結構體數據,本節描述如何布局內存,構造寫原語。
應該通過NtUserConsoleControl修改窗口0切換到模式1,這樣對窗口0調用SetWindowLong即可修改內核數據,但是調用SetWindowLong時index有范圍限制,所以通過窗口2將窗口0的tagWNDK. cbWndExtra修改為0xFFFFFFFF,擴大窗口0可讀寫的范圍。
現在我們開始內存布局:
創建窗口0,窗口0切換到模式1,pExtraBytes為擴展內存相對內核桌面堆基址的偏移量
窗口2觸發回調后,回調函數中對窗口2調用NtUserConsoleControl,所以窗口2也處于模式1,pExtraBytes為擴展內存相對內核桌面堆基址的偏移量。
回調函數中返回窗口0的OffsetToDesktopHeap,此時內存如下:
圖中紅色線條,此時窗口2的pExtraBytes為窗口0的OffsetToDesktopHeap,指向了窗口0的結構體地址,此時對窗口2調用SetWindowLong即可修改窗口0的內核數據結構
通過窗口2修改窗口0的cbWndExtra
SetWindowsLong(窗口2句柄, 0xC8(此處還有一個偏移量),0xFFFFFFFF),即可修改窗口0的cbWndExtra為極大值,且此時窗口0處于模式1,如果傳入一個較大的index且不大于0xFFFFFFFF,那么就可以越界修改到內存處于高地址處的其他窗口的數據。
再次創建一個窗口1,窗口1處于模式2,不用修改模式
窗口1剛開始pExtraBytes指向用戶態地址,使用模式2直接尋址。
由于窗口0的pExtraBytes是相對于內核桌面堆基址的偏移量,窗口1的OffsetToDeskTopHeap是當前tagWNDK結構體與內核桌面堆基址的偏移量,所以這兩個值可以計算一個差值,對窗口0調用SetWindowLong時傳入這個差值即可寫入到窗口1的結構體,再加上pExtraBytes相對于tagWNDK結構體的偏移即可設置窗口1的pExtraBytes為任意值。
由于此時窗口1處于模式1直接尋址,且我們可以設置窗口1擴展內存地址pExtraBytes為任意地址,所以對窗口1調用SetWindowLong即可向任意內核地址寫入數據。
總結:
內存布局的關鍵在于窗口0的pExtraBytes必須小于窗口1和窗口2的OffsetToDesktopHeap,這樣的話在繞過了窗口0的cbWndExtra過小的限制后,對窗口0調用SetWindowLong傳入的第二個參數,傳入一個較大值,即可向后越界寫入到窗口1和窗口2的tagWNDK結構體。
我們來設想一下不滿足內存布局的情況,假如窗口1的OffsetToDesktopHeap小于窗口0的pExtraBytes,即窗口1的tagWNDK位于低地址,窗口0的擴展內存位于高地址,那從窗口0越界往低地址寫內容時,SetWindowLong的index必須傳入一個64位的負數,但是SetWindowLong的第二個參數index是一個32位的值,調用函數時64位截斷為32位數據,在內核中擴展到64位后高位為0還是個正數,所以窗口0無法越界寫到低地址。
EXP分析調試
首先動態定位多個函數地址,接下來需要調用
#define MAGIC_CB_WND_EXTRA 0x1337
調用函數RegisterClassEx創建兩個窗口類:
類名為NormalClass的窗口,窗口的cbWndExtra大小為0x20。
類名為MagicClass的窗口,窗口的cbWndExtra大小為0x1337,使用MagicClass類創建的窗口會利用漏洞構造一個內核相對偏移量。
內存布局的代碼如下:
第241行到244行,創建了菜單,之后創建窗口使用此菜單。
第245行到250行,使用NormalClass類名創建了50個窗口存放在g_hWnd數組中,然后銷毀后面的48個窗口,這樣是為了后面創建窗口時可以占用被銷毀窗口的區域,縮短窗口之間的間距,此時g_hWnd[0]和g_hWnd[1]存放句柄,將這兩個窗口稱為窗口0和窗口1,其中247行調用HMValidateHandle函數傳入句柄得到對應窗口在用戶態映射的tagWNDK數據內存地址保存在g_pWndK數組中。
第245行到255行,調用NtUserConsoleControl函數設置窗口0由用戶態直接尋址切換為內核態相對偏移尋址,并且窗口0的pExtraBytes是相對于內核桌面堆基址的偏移。
第257行到258行,使用MagicClass類名創建窗口2保存在g_hWnd[2]中,稱為窗口2,然后調用HMValidateHandle獲得窗口2的tagWNDK數據映射地址保存在g_pWndK[2]中。
第260和278行代碼判斷內存布局是否成功,此時窗口0處于內核模式,所以窗口0的pExtraBytes為申請的內核內存空間(不是窗口內核對象地址)相對于內核桌面堆基地址的偏移,窗口1和窗口2為用戶態模式,OffsetToDesktopHeap為窗口內核對象地址相對于內核桌面堆基地址的偏移,內存布局必須滿足:
窗口0的pExtraBytes小于窗口1的OffsetToDesktopHeap,計算差值extra_to_wnd1_offset,為正數。
窗口0的pExtraBytes小于窗口2的OffsetToDesktopHeap,計算差值extra_to_wnd2_offset,為正數。
如果布局失敗,那就銷毀窗口繼續布局,如果最后一次布局失敗,就退出。
布局完成后,程序運行到此處:
程序在虛擬機中運行到DebugBreak()函數時,如果有內核調試器,調試器會自動中斷:
此時指令位于DebugBreak函數中,輸入k,棧回溯只顯示了地址,沒有顯示符號表,輸入
gu;.reload /user
.reload /user會自動加載用戶態符號,pdb文件位于本地對應目錄,再次輸入k,顯示棧回溯,可以看到顯示正常。
我們先查看三個窗口的內核數據結構
使用命令 dt tagWNDK poi(CVE_2022_21882!g_pWndK+0)可以以結構體方式查看窗口0的tagWNDK結構,在內存布局時已經對窗口0切換了模式,如下:
在調用NtUserMessageCall之前,窗口0處于模式1,窗口1和2處于模式2。