今天小編分享的互聯網經驗:Win32k的内部結構以及可能出現的漏洞,歡迎閱讀。
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 對象,如視窗和菜單,還介紹了用于管理這些對象的用戶模式和内核模式數據結構。