2022 年 1 月下旬,一個新的微軟 Windows 特權升級漏洞 ( CVE-2022-21882 ) 被發現,經分析,這是 Win32k 用戶模式回調函數 xxxClientAllocWindowClassExtraBytes 中的一個漏洞。早在 2021 年,微軟報告了一個非常類似的漏洞(CVE-2021-1732),并進行了修複。不過分析發現,CVE-2021-1732 的補丁不足以阻止 CVE-2022-21882。
Win32k 背景信息
在 Windows NT 4.0 之前,Microsoft 在名爲 Client-Server Runtime SubSystem(CSRSS.exe)的用戶模式進程中實現了 Win32 API 的 GUI 功能。然而,用戶模式和内核模式之間的上下文切換計算成本高昂,并且需要大量内存消耗。
爲了消除這些問題并提高整個 Windows 操作系統的速度,微軟決定将 Windows 子系統 ( 窗口管理器、GDI 和圖形驅動程序 ) 轉移到内核中。這種轉變始于 1996 年的 Windows NT 4.0。
這一變化是通過一個名爲 Win32k.sys 的内核模式驅動程序實現的,現在被稱爲内核模式 Windows 子系統,Windows 子系統的用戶模式組件仍然駐留在 CSRSS 中。
盡管遷移到内核大大減少了所需的消耗,但微軟不得不采用一些老辦法,例如在客戶端地址空間的用戶模式部分緩存管理數據結構。事實上,爲了進一步避免上下文切換,一些管理結構在曆史上僅以用戶模式存儲。然而,爲了消除内核地址洩漏,微軟已經開始實現使用這些結構的用戶模式和内核模式副本的方法,以防止内核地址存儲在用戶模式結構中。
此外,由于 Win32k 需要一種方法來訪問這些用戶模式結構,并支持一些現有的用戶模式功能,如窗口挂鈎,因此實現了用戶模式回調來促進這些任務。
用戶模式回調允許 Win32k 回調到用戶模式,并執行諸如調用應用程序定義的挂鈎、提供事件通知以及将數據複制到用戶模式或從用戶模式複制數據等任務。這意味着微軟在實現用戶模式回調和保持數據完整性方面面臨着巨大安全挑戰。
研究發現,在進行用戶模式回調之前,許多對象沒有被正确鎖定,這使得用戶模式代碼可以在用戶模式回調期間銷毀這些對象,從而導緻釋放後使用(UAF)漏洞。盡管微軟已經解決了許多問題,但用戶模式回調在今天仍然被濫用。
Windows GUI API
在讨論 Win32k 内部結構之前,我們将簡要介紹一個使用 Win32 API 創建和銷毀窗口的簡單 C 程序。這将使我們開始了解圖形窗口是如何以編程方式創建和操作的。它還允許我們檢查定義每個窗口及其菜單的底層結構。
如下圖所示,示例程序首先定義一個窗口類。進程必須先注冊一個窗口類,然後才能創建 WNDCLASSEX 結構中定義的窗口類型。首先,窗口類對象被聲明爲 WNDCLASSEX wcx ={},然後填充窗口類結構。
定義窗口類
窗口類的元素如下所示:
cbSize:此結構的大小(以字節爲單位),将此成員設置爲 sizeof(WNDCLASSEX)。
style:窗口類樣式,它可以是類樣式的任意組合。
lpfnWndProc:指向處理類中發送到窗口的所有消息并定義窗口行爲的函數的指針。通常,默認窗口過程至少用于某些消息。但是,自定義窗口過程通常用于創建獨特的窗口體驗。
cbClsExtra:在窗口類結構之後要分配的額外字節數,系統将字節初始化爲零。
cbWndExtra:在窗口實例之後要分配的額外字節數。系統将字節初始化爲零。不要将其與 cbClsExtra 混淆,後者對該窗口類的所有窗口都是通用的。該值通常爲 0,但如果不是 0,則内存通常用于存儲跨窗口的非恒定數據;
hInstance:包含類的窗口過程的實例的句柄。标識注冊該類的應用程序或 .DLL。在此處将 hinstance 參數分配給 WinMain。
hIcon:類的句柄,LoadIcon ( NULL, IDI_APPLICATION ) 加載默認圖标。
hCursor:類光标的句柄,LoadCursor ( NULL, IDC_ARROW ) 加載默認光标。
hbrBackground:類背景筆刷的句柄,GetStockObject ( WHITE_BRUSH ) 返回一個白色筆刷的句柄。返回值必須強制轉換,因爲 GetStockObject 返回的是泛型對象。
lpszMenuName:指向一個以 null 結尾的字符串的指針,該字符串指定類菜單的資源名稱,該名稱顯示在資源文件中。如果不需要菜單欄,則此字段可以爲 NULL。
lpszClassName:用于标識此窗口類結構的類名。
hIconSm:小類圖标的句柄。
既然窗口類的屬性已經定義好了,我們需要使用 RegisterClassEx ( ) 在應用程序中注冊,如下圖所示。如果失敗,RegisterClassEX ( ) 返回 0。否則,它返回一個惟一标識所注冊類的類原子。注冊窗口類将定義該類及其關聯的結構成員到 Windows。
正在注冊的窗口類
創建窗口
一旦注冊了窗口,我們就可以通過調用 CreateWindowExA ( ) 來創建窗口類的實例,如下圖所示。
創建窗口的代碼
CreateWindowEX 的參數如下圖所示。
CreateWindowExA 函數原型
下面列出了每個參數的簡要說明:
dwExStyle:正在創建的窗口的擴展窗口樣式,在這種情況下,我們将其設置爲 WS_EX_LEFT 的默認窗口常數,這爲窗口提供了通用的左對齊屬性。
lpClassName:類名,取自調用 RegisterClassEX 時聲明的 wcx.lpszClassName。
lpWindowName:窗口名稱。
dwStyle:所創建窗口的樣式,在本例中,我們使用 WS_OVERLAPPEDWINDOW,它創建了一個頂層 ( 父 ) 窗口。
X:窗口的初始水平位置。對于重疊窗口或彈出窗口,x 參數是以屏幕坐标表示的窗口左上角的初始 x 坐标。對于子窗口,x 是窗口左上角相對于父窗口客戶區左上角的 x 坐标。如果 x 設置爲 CW_USEDEFAULT,系統将選擇窗口左上角的默認位置,并忽略 y 參數。
Y:與上述相同,但适用于 Y 坐标。
nWidth:窗口的寬度。
nHeight:窗口的高度。
hWndParent:正在創建的窗口的父窗口或所有者窗口的句柄。
hMenu:菜單的句柄,或指定子窗口标識符,具體取決于窗口樣式。對于重疊或彈出窗口,hMenu 标識要與該窗口一起使用的菜單;如果要使用類菜單,則它可以爲 NULL。
hInstance:要與窗口關聯的模塊實例的句柄。
lpParam:傳遞給窗口的窗口過程的額外信息。如果沒有要傳輸的額外信息,則傳遞 NULL。
一旦調用 CreateWindowEx ( ) 創建了窗口,就在内部創建了窗口,也就是說,已經分配了内存并填充了其結構,但沒有顯示。爲了顯示窗口,我們調用 ShowWindow ( ) 函數。
ShowWindow ( ) 獲取從 CreateWindowEXW ( ) 調用獲得的句柄和從 WinMain ( ) 獲得的狀态變量 nCmdShow。nCmdShow 變量确定窗口在屏幕上的顯示方式,例如,它是正常的、最大化的還是最小化的。
ShowWindow ( ) 僅控制應用程序窗口的顯示方式。這包括諸如标題欄、菜單欄、窗口菜單、最小化按鈕等元素。客戶端區域是應用程序顯示數據的區域,例如在文本編輯器中輸入文本的區域。客戶端區域是通過調用 UpdateWindow ( ) 函數繪制的。
如果将 WS_VISIBLE 窗口樣式指定爲 CreateWindowEXW ( ) 函數的 dwStyle 參數,則不需要調用 ShowWindow ( ) 函數,Windows 會爲用戶調用它。同樣,如果不指定 WS_VISIBLE 樣式,也不調用 ShowWindow ( ) 函數,窗口将對視圖保持隐藏狀态。
窗口消息和窗口過程
調用 UpdateWindow ( ) 之後,窗口就完全可見并可以使用了。在爲 Windows 編寫更簡單的控制台應用程序時,該應用程序會根據控制台中的用戶輸入進行顯式函數調用。
在窗口應用程序中,用戶通常可以通過輸入文本、點擊按鈕和菜單或僅僅通過移動鼠标來與應用程序交互。這些操作中的每一個都有自己的特殊功能。爲了實現這一點,微軟實現了一個事件驅動系統,該系統将用戶輸入(如鍵盤、鼠标或觸摸)的消息中繼到每個應用程序中的各個窗口。這些消息由每個窗口内的函數處理,稱爲窗口過程。
Windows 爲每個線程維護一個消息隊列,該隊列将中繼任何影響窗口狀态的用戶輸入事件。然後,Windows 将這些事件轉換爲消息,并将它們放入消息隊列。應用程序通過執行類似于下面中的代碼來處理這些消息。
窗口消息隊列循環
GetMessage ( ) 函數用于從消息隊列中檢索下一條消息。MSG 參數是一個結構,它包含所分配的窗口過程正确處理消息所需的消息信息。
MSG 結構的成員中包括其窗口過程接收消息的窗口的句柄(hwnd),以及包含标識符的消息,該标識符确定對窗口過程的請求内容。例如,如果消息包含一個 WM_PAINT 消息,它會告訴窗口過程窗口的工作區已更改,必須重新繪制。
TranslateMessage ( ) 函數可将虛拟密鑰消息轉換爲字符消息,但這對于當前的讨論并不重要。DispatchMessage ( ) 将消息發送到由 msg 結構中的窗口句柄标識的窗口,由該窗口類定義的窗口過程處理。
到目前爲止,通過執行以下操作,示例代碼已經完成了定義窗口類:
注冊窗口;
創建由窗口類定義的窗口實例;
在屏幕上顯示窗口;
進入消息循環;
窗口過程決定了顯示什麽以及如何響應用戶輸入。Windows 提供了一個默認的窗口過程來處理應用程序未處理的任何窗口消息,并且它爲任何窗口正常運行提供了最基本的功能。
窗口過程是定義窗口的所有功能的地方,且它們可能會非常複雜。不過,我們目前隻對 Microsoft 的默認窗口過程 DefWindowProc ( ) 感興趣。
窗口結構
如上所述,Windows 現在通過 Win32k.sys 在内核中管理 GUI 對象,如菜單、窗口等。當創建窗口對象時,會在稱爲 tagWND 的數據結構中跟蹤其屬性。
不過,微軟删除了許多 Win32k 調試符号,這使得獲得這些結構的透明度變得更加困難。基于一些逆向工程,下圖顯示了 Windows 10 版本 21H1 中的結構。
标記 WND 父結構
在調用 xxxCreateWindowEx 期間查看 HMAllocObject,其中發生了結構的分配,我們可以确認該結構的大小爲 0x150(336)字節。
在調用 HMAllocObject 之前的 WinDbg 輸出如下圖所示。你可以看到第四個參數,它表示分配大小,存儲在 r9 寄存器中,等于 0x150。
WinDbg 輸出顯示 HMAllocObject 的輸入參數
tagWND 結構在線程環境塊 ( TEB ) 的 Win32ClientInfo 條目中被引用,爲了防止内核模式地址洩露,它已被删除了。
内核 tagWND 結構中的第一個條目是窗口句柄。在内核中,每個窗口都有一個與之相關聯的代表性 tagWND 結構。
在分析 CVE-2022-21882 期間,此結構将很重要,但現在,我們将重點關注偏移量 0x28。我将其标記爲 *pWND,因爲微軟不再提供符号。此外,微軟不再爲這個結構提供名稱,在過去它被稱爲 state 或 WW。據微軟稱,這些名稱已被棄用,不再在内部使用。要知道它是 tagWND 數據的用戶模式版本,不包括内核地址,且它的結構與其父 tagWND 結構不同。這個子結構既存在于内核中,也存在于用戶模式中。這就是 Windows 管理數據的方式,以避免洩露内核地址,因爲任何用戶模式應用程序都将使用位于用戶模式桌面堆上的 tagWND 結構的副本,因此将無法看到任何内核模式地址。
接下來繼續将子結構稱爲 tagWND 結構,不過,它的結構與上面的父 tagWND 結構不同,但在其他研究中仍然通常稱爲 tagWND。
子标簽 WND 結構如下圖所示,通過逆向工程确定了元素及其偏移量。
在關于創建窗口的部分中讨論的 WNDCLASSEX 結構的許多元素可以在 tagWND 結構中看到。因此,很明顯,當創建窗口時,通過 WNDCLASSEX 結構分配的屬性被傳遞給内核并存儲在 tagWND 結構中。然後将屬性傳播到内核和用戶模式桌面堆中的用戶副本。
tagWND 結構的用戶模式安全副本
下面兩個圖分别顯示了父 tagWND 和用戶模式安全 tagWND 結構的内核副本。
父 tagWND 結構的内存轉儲
上圖是父 tagWND,你可以看到句柄 ( 偏移量 0x00 ) 與下面的複制 tagWND 的句柄相同。你還可以看到父結構具有内核地址,而用戶模式安全副本僅具有用戶模式地址。最後,注意父 tagWND+0x28 是指向子 tagWND 副本地址的指針。
子 tagWND 結構的内存轉儲
有幾種方法可以洩露窗口對象的内核模式地址,Win32k 中存儲由用戶模式代碼設置的屬性的所有對象(例如,窗口、菜單)通常被稱爲用戶對象。
所有用戶對象(tagWND 結構的用戶模式副本是衆多對象之一)都在通常稱爲 UserHandleTable 的每個會話句柄表中進行索引。盡管 tagWND 結構并不總是用戶模式安全的副本,并且曾經包含内核地址。
過去,可以通過 UserHandleTable 用 User32.dll 中名爲 gSharedInfo 的可導出結構來定位 tagWND 對象。從 Windows 10 版本 1703 起,這個方法将不再有用。由于微軟不斷努力消除内核地址洩露,他們已經從 UserHandleTable 中删除了桌面堆中對象的内核地址。
窗口管理器使用位于 User32.dll 中的非導出函數 HMValidateHandle 驗證句柄。在 Windows 10 版本 1803 之前,窗口管理器返回内核模式指針,指向要驗證其句柄的對象,該指針通常用于洩露此地址。盡管内核地址洩漏已經被修複,但當我們稍後查看這兩個漏洞時,這種方法将非常重要。
從 Windows 10 版本 1703 開始,任何由 SetWindowLong 寫入的字節都不再寫入内核。這個修複有效地消除了這種創建任意寫入的技術。
确定感興趣的對象在内核中的位置,以繞過内核地址空間布局随機化(KASLR)。因此,非常需要知道桌面堆的位置。從 Windows 10 1607 版本開始,微軟就開始添加緩解措施,試圖阻止漏洞編寫者在内核中定位桌面堆及其相關對象。這些緩解措施包括從 UserHandleTable 中删除内核地址,如上所述,以及在每個進程的線程環境塊 ( TEB ) 中的 Win32ClientInfo 結構中删除對桌面堆的内核指針引用。此外,HMValidateHandle 現在爲傳遞給它的任何對象句柄返回用戶模式(相對于内核模式)指針。
用戶模式回調
由于 windows 子系統主要位于 windows 内核中,而 windows 本身在用戶模式下運行,因此 Win32k 必須頻繁地從内核調用到用戶模式。用戶模式回調提供了一種機制來實現諸如應用程序定義的挂鈎、事件通知以及從用戶模式向内核複制 / 從内核複制數據等項目。
當進行用戶模式回調時,Win32k 調用 KeUserModeCallback,并使用它想要調用的用戶模式函數的關聯 ApiNumber。ApiNumber 是位于 USER32 .dll ( USER32!apfnDispatch ) 中的函數表的索引。在每個進程的 User32.dll 初始化期間,該表的地址被複制到進程環境塊 ( PEB ) ( PEB. kernelcallbacktable ) 。
在即将進行的漏洞分析中,我們将展示如何通過 KernelCallback 表鈎住用戶模式回調,并展示該表在 WinDbg 中的樣子。KeUserModeCallback 的函數原型及其相關參數如下圖所示。
KeUserModeCallback 函數原型
用戶模式回調輸入參數通過 InputBuffer 傳遞,而回調函數的輸出在 OutputBuffer 中返回。在調用系統調用時,ntdll!KiSystemService 或 ntdll!KiFastCallEntry 在内核線程堆棧上存儲一個 trap 幀 ( TRAP_FRAME ) ,以保存當前線程上下文,并在返回到用戶模式時啓用寄存器恢複。
爲了在用戶模式回調中轉換回用戶模式,KeUserModeCallback 首先使用線程對象持有的 trap 幀信息将 InputBuffer 複制到用戶模式堆棧。然後創建一個新的 trap 幀,EIP 設置爲 ntdll!KiUserCallbackDispatcher,替換線程對象的 TrapFrame 指針,最後調用 ntdll!KiServiceExit 将執行返回給用戶模式回調調度程序。
KiUserCallbackDispatcher 函數原型
一旦用戶模式回調完成,就會調用 NtCallbackReturn 以恢複内核中的執行。此函數将回調的結果複制回原始内核堆棧,并恢複保存在 kernel_stack_CONTROL 結構中的 trap 幀(PreviousTrapFrame)和内核堆棧。在跳轉到它先前停止的位置之前 ( 在 ntdll!KiCallUserMode 中 ) ,内核回調堆棧被删除。
函數原型
如果 Win32k 在調用用戶模式回調時沒有釋放資源,并且該用戶模式回調允許應用程序凍結 GUI 子系統,則 Win32k 将無法在 GUI 子系統被凍結時執行其他任務。因此,Win32k 總是在調用用戶模式回調時釋放資源。在從用戶模式回調返回時,Win32k 必須确保引用的對象仍然處于不受信任的狀态。在未執行适當檢查或對象鎖定的情況下對此類對象進行操作可能也确實會造成安全漏洞。事實上,研究人員确定了這些類型漏洞的多個實例。
總結
本文介紹了如何使用 Win32 API 創建 GUI 對象,如窗口和菜單,還介紹了用于管理這些對象的用戶模式和内核模式數據結構。