作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/0aDmaEMXae1_tJXFZVdi6Q
CVE-2022-21882漏洞是Windows系統的一個本地提權漏洞,微軟在2022年1月份安全更新中修補此漏洞。本文章對漏洞成因及利用程序進行了詳細的分析。
1.漏洞介紹
CVE-2022-21882是對CVE-2021-1732漏洞的繞過,屬于win32k驅動程序中的一個類型混淆漏洞。
攻擊者可以在user_mode調用相關的GUI API進行內核調用,如xxxMenuWindowProc、xxxSBWndProc、xxxSwitchWndProc、xxxTooltipWndProc等,這些內核函數會觸發回調xxxClientAllocWindowClassExtraBytes。攻擊者可以通過hook KernelCallbackTable 中 xxxClientAllocWindowClassExtraBytes 攔截該回調,并使用 NtUserConsoleControl 方法設置 tagWNDK 對象的 ConsoleWindow 標志,從而修改窗口類型。
最終回調后,系統不檢查窗口類型是否發生變化,由于類型混淆而引用了錯誤的數據。flag修改前后的區別在于,在設置flag之前,系統認為tagWNDK.pExtraBytes保存了一個user_mode指針;flag設置后,系統認為tagWNDK.pExtraBytes是內核桌面堆的偏移量,攻擊者可以控制這個偏移量,從而導致越界R&W。
本篇文章分析了漏洞成因及漏洞利用手法分析,側重動態調試及利用手法分析。
2.漏洞影響版本
Windows 10 Version 21H2 for x64-based Systems
Windows 10 Version 21H2 for ARM64-based Systems
Windows 10 Version 21H2 for 32-bit Systems
Windows 11 for ARM64-based Systems
Windows 11 for x64-based Systems
Windows Server, version 20H2 (Server Core Installation)
Windows 10 Version 20H2 for ARM64-based Systems
Windows 10 Version 20H2 for 32-bit Systems
Windows 10 Version 21H1 for ARM64-based Systems
Windows 10 Version 21H1 for x64-based Systems
Windows 10 Version 1909 for x64-based Systems
Windows 10 Version 1909 for 32-bit Systems
Windows Server 2019 (Server Core installation)
Windows Server 2019
Windows 10 Version 1809 for ARM64-based Systems
Windows 10 Version 1809 for x64-based Systems
Windows 10 Version 1809 for 32-bit Systems
Windows 10 Version 20H2 for x64-based Systems
Windows 10 Version 1909 for ARM64-based Systems
Windows Server 2022 (Server Core installation)
Windows Server 2022
Windows 10 Version 21H1 for 32-bit Systems
3.分析環境
Windows 10 21H2 19044.1415 x64
Vmware 16.2.1
VirtualKD-Redux 2020.4.0.0
Windbg 10.0.22000.194
4.背景知識
本節內容描述了創建窗口時需要用到的結構體及函數:
- 用戶態的窗口數據結構體:WNDCLASSEXW,需要關注cbWndExtra。
- 窗口數據保存在內核態時使用:tagWND和tagWNDK結構體,需要關注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變量中。
使用函數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,那么返回索引越界錯誤。
5.漏洞成因
此漏洞是對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.利用漏洞的流程
本節介紹了漏洞觸發的流程,并介紹了觸發漏洞及利用漏洞需要的各個知識點。
漏洞觸發利用的流程:
要利用這個漏洞,需要以下背景知識:
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可讀寫的范圍。
現在我們開始內存布局:
1.創建窗口0,窗口0切換到模式1,pExtraBytes為擴展內存相對內核桌面堆基址的偏移量
窗口2觸發回調后,回調函數中對窗口2調用NtUserConsoleControl,所以窗口2也處于模式1,pExtraBytes為擴展內存相對內核桌面堆基址的偏移量。
2.回調函數中返回窗口0的OffsetToDesktopHeap,此時內存如下:
圖中紅色線條,此時窗口2的pExtraBytes為窗口0的OffsetToDesktopHeap,指向了窗口0的結構體地址,此時對窗口2調用SetWindowLong即可修改窗口0的內核數據結構
3.通過窗口2修改窗口0的cbWndExtra
SetWindowsLong(窗口2句柄, 0xC8(此處還有一個偏移量),0xFFFFFFFF),即可修改窗口0的cbWndExtra為極大值,且此時窗口0處于模式1,如果傳入一個較大的index且不大于0xFFFFFFFF,那么就可以越界修改到內存處于高地址處的其他窗口的數據。
4.再次創建一個窗口1,窗口1處于模式2,不用修改模式
窗口1剛開始pExtraBytes指向用戶態地址,使用模式2直接尋址。由于窗口0的pExtraBytes是相對于內核桌面堆基址的偏移量,窗口1的OffsetToDeskTopHeap是當前tagWNDK結構體與內核桌面堆基址的偏移量,所以這兩個值可以計算一個差值,對窗口0調用SetWindowLong時傳入這個差值即可寫入到窗口1的結構體,再加上pExtraBytes相對于tagWNDK結構體的偏移即可設置窗口1的pExtraBytes為任意值。
5.由于此時窗口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無法越界寫到低地址。
7.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切換了模式,如下:
上圖第三個窗口應為窗口2,在調用NtUserMessageCall之前,窗口0處于模式1,窗口1和2處于模式2。接下來調用HookUserModeCallBack 來Hook回調函數,代碼如下:
動態調試時查看KernelCallbackTable表:
kd> !peb
PEB at 0000001eb0c75000
kd> dt ntdll!_PEB KernelCallbackTable 0000001eb0c75000
+0x058 KernelCallbackTable : 0x00007ffe`bc6f2070 Void
查看KernelCallbackTable表項
我們需要查看123項的內容,如下:
調試運行HookUserModeCallBack函數后,再次查看:
在自定義的回調函數MyxxxClientAllocWindowClassExtraBytes中
接著下斷點:
并且在MyxxxClientAllocWindowClassExtraBytes函數中下斷點:

在調試器中輸入g運行,現在運行到如下位置:
在運行NtUserConsoleControl前后分別查看窗口2的模式:
繼續按g運行,中斷在SetWindowLong函數前
此時窗口2處于模式1,并且pExtraBytes為窗口0的OffsetToDesktopHeap,再調用SetWindowLong函數:
這是第一次越界寫,第一個參數為窗口2的句柄,第二個參數為index,為cbWndExtra相對tagWNDK結構體首地址的偏移量+cbWndServerExtra,由于窗口2調用了NtUserMessageCall,所以cbWndServerExtra為0x10,調用SetWindowLong時會使用index-cbWndServerExtra,所以此處要加上cbWndServerExtra來抵消,可參考前文SetWindowLong函數的分析。
單步運行后
可以看到窗口0的cbWndExtra變成了0xFFFFFFFF,接下來對窗口0調用SetWindowLong時傳入index可以傳入之前計算得到的extra_to_wnd1_offset和extra_to_wnd2_offset來分別修改窗口1和窗口2的窗口內核數據。
此時窗口1處于直接尋址模式,對窗口0調用SetWindowLongPtr修改窗口1的pExtraBytes為任意值,使用SetWindowLongPtr是因為此函數第三個參數可以傳入64位數據,將窗口1的pExtraBytes設置為任意值,接下來對窗口1調用SetWindowLong即可實現任意地址寫數據。
8.兩種提權方式
8.1 設置token
第一種為設置當前進程的token為system進程的token,將當前進程提升到system權限,這種需要讀取進程的EPROCESS結構,再定位到token變量的地址,修改token,公開的EXP中使用GetMenuBarInfo函數來實現內核任意地址讀原語。
我們先分析這種方式,先看下Menu內核結構體:
ptagWND
0x10 THREADINFO
0x1A0 PROCESSINFO
0x00 EPROCESS
0x18 unknown
0x80 kernel desktop heap base
0x28 ptagWNDk
0xA8 spMenu
0x28 obj28
0x2C cItems(for check) 設置為1 0x40 cxMenu(for check) 設置為1 0x44 cyMenu(for check) 設置為1
0x50 ptagWND
0x58 rgItems
0x00 unknown(for exploit) //要讀的地址-0x40 0x98 ppMenu
0x00 pSelf //指向spMenu
在EXP中先構造一個假的Menu

其中401行設置ppMenu偏移0x00處的值為spMenu,404、408、409設置spMenu結構體內部數據是為了繞過GetMenuBarInfo的驗證,GetMenuBarInfo函數會調用內核中的NtUserGetMenuBarInfo,最終調用到xxxGetMenuBarInfo,GetMenuBarInfo對應有四個參數,對應xxxGetMenuBarInfo的四個參數,其中參數2為idObject,參數3為idItem。xxxGetMenuBarInfo對參數有校驗:
164行判斷idObject!=3如果滿足,就不能觸發到下面讀內存的代碼路徑,所以idObject必須為-3。
316行代碼判斷dwStyle不能包含WS_CHILD屬性。
322行代碼從spMenu中偏移0x98取值,賦值給ppMenu。
325行代碼判斷idItem不能小于0。
328行代碼判斷idItem不能大于spMenu偏移0x28取值再偏移0x2c取值。
335行代碼判斷spMenu偏移0x40取值不為0并且偏移0x44取值不為0。
338行到344行,如果idItem不為0,可以讓idItem為1,那么_readAddrSub40的值為spMenu偏移0x58取值。
接下來程序進入353行
v5是傳入的第四個參數,用作保存讀取到的數據。
在353、354行,可以讀取傳入地址的數據+窗口RECT的left坐標。
在357、358行,可以讀取傳入地址的數據+4+窗口RECT的top坐標。
所以只要我們可以繞過構造一個假的Menu,繞過上述限制,在Menu偏移0x58再偏移0x00的地址處存放想讀取的地址-0x40,當GetMenuBarInfo返回時left和top中保存的就是目標地址處的8字節數據。
要想替換窗口的Menu為假的Menu,還是需要用到SetWindowLong函數,在內核態win32kfull!xxxSetWindowLong函數中會調用xxxSetWindowData函數:
xxxSetWindowData函數如下:
134、136行,判斷如果index為0xFFFFFFF4,為-12,對應為GWLP_ID。
138行判斷如果dwStyle是否包含WS_CHILD屬性。
140行取出原來的menu指針,賦值給retValue,最終會作為用戶態SetWindowLong函數的返回值。
142行修改spMenu為SetWindowLong傳入第三個參數newValue值。
所以我們需要如下步驟才能完成任意地址讀:
- 先對窗口0使用內核越界寫修改窗口1的dwStyle值為包含WS_CHILD,這樣調用SetWindowLong時即可繞過上面138行的判斷。
- 對窗口1使用SetWindowLong函數傳入index為GWLP_ID,修改窗口1的Menu為構造的假的Menu,并且SetWindowLong會返回原先的Menu的地址。
- 使用原先的Menu通過內核數據結構即可定位到當前進程的EPROCESS,進而定位到token的地址。
- 再次對窗口0使用內核越界寫修改窗口1的dwStyle值為不包含WS_CHILD,這樣調用GetMenuBarInfo時可以繞過xxxGetMenuBarInfo中316行代碼的判斷。
- 需要讀取數據時,將目標地址-0x40賦值給假的Menu偏移0x58對應的內存空間中,再調用GetMenuBarInfo函數。
單步運行413行代碼,窗口1的dwStyle就包含了WS_CHILD屬性。

可以看到修改完成后,窗口1的dwStyle包含了WS_CHILD屬性。
繼續執行415行代碼:
在416行下斷點后運行:
此時SetWindowLong函數剛執行完畢,返回值rax為0xfffffa49c0821e60,保存的是舊的spMenu指針,而根據之前的數據結構,可以使用spMenu定位到當前進程的EPROCESS。
執行419行代碼,移除窗口1的WS_CHILD屬性,為接下來調用GetMenuBarInfo做準備

窗口1的dwStyle移除了WS_CHILD屬性。然后構造讀原語如下:
根據之前的數據結構
ptagWND
0x10 THREADINFO
0x1A0 PROCESSINFO
0x00 EPROCESS
0x18 unknown
0x80 kernel desktop heap base
0x28 ptagWNDk
0xA8 spMenu
0x50 ptagWND
所以獲取到spMenu后可以使用如下代碼來獲取當前進程的EPROCESS
在調試器中查看如下:
上圖中可以看到通過spMenu取偏移和使用命令.process兩種方式獲取到的EPROCESS值是一致的。
查看當前進程的token
kd> !token
…
Privs:
19 0x000000013 SeShutdownPrivilege Attributes -
23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default
25 0x000000019 SeUndockPrivilege Attributes -
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes -
34 0x000000022 SeTimeZonePrivilege Attributes -
…
我們直接運行到454行,此時當前進程的token被替換為系統token
EPROCESS中token結構體為_EX_FAST_REF
kd> dt _EX_FAST_REF
ntdll!_EX_FAST_REF
+0x000 Object : Ptr64 Void
+0x000 RefCnt : Pos 0, 4 Bits
+0x000 Value : Uint8B
調試運行到454行,重新運行一次,所以EPROCESS值與之前不一樣。
可以看到此時調用到if(iCount<5000),_EX_FAST_REF結構體中的object值已經修改了。
查看system進程的EPROCESS
kd> dt nt!_EX_FAST_REF ffffe504`89885080+0x4b8
+0x000 Object : 0xffffbe09`9a242744 Void
+0x000 RefCnt : 0y0100
+0x000 Value : 0xffffbe09`9a242744
system進程EX_FAST_REF的Object也為0xffffbe09`9a242744,當前進程修改成功,使用!token命令驗證下:
修改token的代碼如下:
1.EPROCESS結構體中有一個進程鏈表,保存了當前系統的所有進程,我們主要關注ActiveProcessLinks和UniqueProcessId屬性
kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x438 ProcessLock : _EX_PUSH_LOCK
+0x440 UniqueProcessId : Ptr64 Void //進程ID
+0x448 ActiveProcessLinks : _LIST_ENTRY //進程鏈表
通過遍歷進程鏈表ActiveProcessLinks,找到進程PID UniqueProcessId為4的system進程,偏移0x4b8得到_EX_FAST_REF結構體地址,取出Object的值。
2.之前eprocess變量中保存了當前進程的EPROCESS地址,定位到_EX_FAST_REF結構體地址
3.通過窗口0越界寫窗口1的pExtraBytes,傳入第二步找到的地址,下面448行代碼。
4.449行通過窗口1調用SetWindowLong設置Object修改值為第一步找到的Object。
5.450行代碼恢復窗口1的pExtraBytes。
恢復內核數據:
407行到414行都是為了恢復內核窗口內容,防止藍屏。
408行設置窗口2的pExtraBytes為正常的用戶態指針。
409行設置窗口2的dwExtraFlag不包含0x800屬性,即從模式1修改為模式2。
411到414行恢復窗口1的Menu指針。
418行恢復KernelCallbackTable表項。

自定義的釋放內存的回調函數MyxxxClientFreeWindowClassExtraBytes,判斷如果是特定窗口,就不釋放內存,直接返回。
最終在回調函數表中恢復此項,釋放窗口2的pExtraBytes,之前恢復內核數據代碼處設置了窗口2的pExtraBytes為RtlAllocateHeap返回的指針。
8.2 修改Privileges
第二種漏洞利用要修改token的變量Privileges,這種實現相對來說簡單,不需要構造寫原語,為當前進程添加SE_DEBUG權限并啟用,遍歷進程,過濾與當前進程位于同一session下的winlogon登錄進程,此進程是system權限,打開此進程并注入代碼執行。
背景知識:
要打開系統安全進程和服務進程,并且有寫入數據權限,需要當前進程擁有SeDebugPrivilege權限,這個是調試進程會用到的權限,當一個進程啟動后,正常情況下,是無法提升權限的,正向開發時使用的AdjustTokenPrivileges函數只能是啟用某個權限或者禁用某個權限。
之前我們已經實現了任意地址寫數據,窗口1本身為用戶態直接尋址模式,通過設置窗口1的pExtraBytes值為任意值,調用SetWindowLongPtr時即可對任意地址寫數據,上一種利用手法是調用SetWindowsLong來構造寫原語,調用GetMenuBarInfo來構造讀原語,然后通過EPROCESS的ActiveProcessLinks鏈遍歷進程,當進程號為4時,認為是system進程,獲取system的Token變量覆蓋到當前進程的Token,當前進程就提權到了system級別。
漏洞利用思路為:使用OpenProcessToken打開當前進程調整權限的句柄,使用NtQuerySystemInformation函數泄露句柄在內核中的地址,泄露出的地址為進程Token在內核中的地址,然后偏移0x40:
0: kd> dt _TOKEN
nt!_TOKEN
…
+0x040 Privileges : _SEP_TOKEN_PRIVILEGES
…
在EPROCESS結構體中的token變量類型為nt!_EX_FAST_REF
kd> dt nt!_EX_FAST_REF
+0x000 Object : Ptr64 Void
+0x000 RefCnt : Pos 0, 4 Bits
+0x000 Value : Uint8B
其實這個結構體中Object才屬于TOKEN結構體,但Object的值不是簡單的對應TOKEN結構體,而是需要經過計算,上面的結構體中RefCnt也是位于偏移0x00,只占4位,這四位表示了Object對象的引用計數,這里我們使用上面第一種利用方法利用成功后的數據
kd> dt nt!_EX_FAST_REF ffffe504`89885080+0x4b8
+0x000 Object : 0xffffbe09`9a242744 Void
+0x000 RefCnt : 0y0100
+0x000 Value : 0xffffbe09`9a242744
Object為0xffffbe09`9a242744,RefCnt 為0y0100,需要經過如下換算才可以:
0xffffbe09`9a242744&0xFFFFFFFFFFFFFFF0=0xffffbe09`9a242740
Windbg中查看:

Token偏移0x40為Privileges,Privileges中Present和Enable分別表明進程當前是否可以啟用對應權限和是否啟用了對應權限,EnabledByDefault是默認啟用了對應權限,EnabledByDefault這個變量不需要修改,都是8字節數據,如果將Present和Enable都修改為0xFFFFFFFFFFFFFFFF,

在windbg中可以看到位與權限對應關系如下:
其中2位到32位是有效數據,我們只需要啟用第20位SeDebugPrivilege權限就可以打開winlogon進程,之后注入shellcode,運行shellcode啟動一個system級別的cmd進程。
內存布局與之前的第一種利用方法一樣,接著hook回調函數,對窗口2調用NtUserMessageCall,接下來就不一樣了:
調用LeakEporcessKtoken泄露token的地址,
LeakEporcessKtoken函數調用OpenProcessToken打開自身進程的token,第二個參數訪問掩碼設置為TOKEN_ADJUST_PRIVILEGES,為調整令牌權限,然后調用GetKernelPointer泄露token的內核地址:

其中結構體SYSTEM_HANDLE_TABLE_ENTRY_INFO和SYSTEM_HANDLE_INFORMATION在移植到64位版本時,筆者有對結構體內容進行一些修正,結構體中都多了一個變量ULONG xxxCDCDCD用來占位,保持8字節對齊。泄露token地址后,token+0x40即可定位到Privileges變量地址,
313行通過窗口0越界寫修改窗口0的pExtraBytes為token+0x40,定位到Privileges。
314到319行,設置新的權限值,其實只需要設置第20位,但是此處設置了第2到第36位都為1。
320行設置Present屬性。
321行設置Enabled屬性。
322行恢復窗口1的pExtraBytes值。
324行定位winlogon進程的pid,此處需要注意如果有多個用戶登錄那么存在多個winlogon進程,需要找到跟當前進程處于同一會話中的winlogon進程,否則最終啟動的cmd當前用戶無法看到。
325行寫shellcode到winlogon進程中并執行。
328到331行是為了修復窗口內核數據。
總結兩種漏洞利用方法的優劣:
第一種方法:對比第二種稍微有點復雜,要構造讀寫原語,優勢在于不管是低權限進程還是中等權限進程都可以進行提權。
第二種方法:只需要構造一個寫原語,然后開啟各種權限,通過注入的方法來獲取高權限,相對難度低點,但是要調用NtQuerySyetemInformation函數至少需要中等權限,對權限要求較高。
9.補丁分析
此漏洞對應的補丁為KB5009543,打補丁后調用NtUserMessageCall時觸發到內核函數的調用堆棧如下:
win32kfull!xxxClientAllocWindowClassExtraBytes
win32kfull!xxxValidateClassAndSize+0x171
win32kfull!xxxSwitchWndProc+0x5a
win32kfull!xxxWrapSwitchWndProc+0x3c
win32kfull!NtUserfnINLPCREATESTRUCT+0x1c4
win32kfull!NtUserMessageCall+0x11d
win32k!NtUserMessageCall+0x3d
在函數xxxClientAllocWindowClassExtraBytes中調用回調函數后,內核函數對窗口的dwExtraFlag屬性校驗:
43行判斷dwExtraFlag是否包含0x800屬性,如果包含,說明用戶態函數被hook,當前函數返回值不使用用戶態申請的空間,而是返回0,返回到xxxValidateClassAndSize函數后,
判斷返回值為0,直接返回,不會再去修改pExtraBytes為用戶偽造的值。
10.參考鏈接
https://www.anquanke.com/post/id/241804#h3-12
https://bbs.pediy.com/thread-266362.htm
https://www.4hou.com/posts/3KPr
https://blog.l4ys.tw/2022/02/CVE-2021-21882/
https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2022/CVE-2022-21882.html
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1888/
暫無評論