[翻譯] Kernel Attacks through User-Mode Callbacks

前言

本篇屬於前置知識,在後續的CVE復現中需要使用到。原文爲英文,一邊翻譯一邊記筆記了!儘量用中文描述的淺顯易懂一些。

1. 背景知識

1.1 Win32k.sys介紹

​ Win32k本質上由三個主要組件組成:圖形設備接口(GDI)、窗口管理器(用戶)和支持XP/2000和Longhorn (Vista)顯示驅動程序模型的DirectX api(有時也被認爲是GDI的一部分)。窗口管理器負責管理Windows用戶界面,例如控制窗口顯示、管理屏幕輸出、從鼠標和鍵盤收集輸入以及嚮應用程序傳遞消息;GDI主要關注圖形渲染,並實現GDI對象(畫筆、鋼筆、表面、設備上下文等)、圖形渲染引擎(Gre)、打印支持、ICM顏色匹配、浮點數學庫和字體支持。

​ 爲了正確地與NT執行程序接口,win32k註冊了幾個callout (PsEstablishWin32Callout)來支持面向GUI的對象,例如桌面和窗口站。win32k還爲線程和進程註冊了callout,以定義GUI子系統使用的每個線程和進程的結構。

GUI線程和進程。並不是所有線程都使用GUI子系統,因此預先爲所有線程分配GUI結構將是一種空間浪費。因此,Windows上的所有線程都以非gui線程(12 KB堆棧)的形式啓動。如果一個線程訪問任何用戶或GDI系統調用(編號>= 0x1000),Windows將該線程提升到一個GUI線程(nt!PsConvertToGuiThread),並調用進程和線程調用。值得注意的是,GUI線程具有更大的線程堆棧,以便更好地處理win32k的遞歸特性,並支持用戶模式回調,這可能需要額外的堆棧空間來存放陷阱幀和其他元數據。

​ 當進程的第一個線程被轉換成GUI線程並調用W32pProcessCallout 時,win32k調用win32k!xxxInitProcessInfo初始化每個進程的W32PROCESS/PROCESSINFO2結構。具體來說,這個結構保存每個進程的GUI相關信息,比如相關的桌面、窗口站、用戶和GDI句柄計數。函數先在win32k!xxxUserProcessCallout中初始化用戶相關字段,然後在GdiProcessCallout中初始化GDI相關字段,然後在win32k!xxxUserProcessCallout中分配結構本身。

​ 此外,win32k還爲所有被轉換成GUI線程的線程初始化一個W32THREAD/THREADINFO結構。該結構保存與GUI子系統相關的特定於線程的信息,如線程消息隊列、註冊的windows鉤子、所有者桌面、菜單狀態等信息。這裏,W32pThreadCallout調用win32k!AllocateW32Thread來分配結構,然後是GdiThreadCallout和UserThreadCallout來初始化GDI和用戶子系統特有的信息。在這個過程中最重要的函數是win32k!xxxCreateThreadInfo,它負責初始化線程信息結構。

1.2 窗口管理器(Windows Manager)

​ 窗口管理器的一個重要功能是跟蹤用戶實體,如窗口、菜單、指針等。它將這些實體表示爲用戶對象,並維護它自己的句柄表,以跟蹤它們在用戶會話中的使用。因此,當應用程序請求對用戶實體執行操作時,它提供句柄值,句柄管理器可以有效地將其映射到內核內存中的相應對象。

​ 用戶對象將用戶對象分爲不同的類型,因此對象都具有具有自己特定於類型的結構。例如,所有窗口對象都是由win32k!tagWND結構定義,而菜單是由win32k!tagMENU結構定義。儘管對象類型在結構上是不同的,但是它們都共享一個commonheader,即HEAD結構。

typedef struct _HEAD {
HANDLE h;
ULONG32 cLockObj;
} HEAD, *PHEAD;

​ HEAD結構保存了句柄值(h)的副本以及鎖計數(cLockObj),每當使用對象時,該計數都會遞增。當某個特定組件不再使用該對象時,該對象的鎖計數將減少。當鎖計數達到0時,窗口管理器會釋放它。

​ 儘管HEAD結構相當小,但對象很多時候都會使用較大的線程或進程特定的頭結構,如THRDESKHEAD和PROCDESKHEAD。這些結構提供了額外的字段,例如指向線程信息結構tagTHREADINFO的指針和指向關聯桌面對象(tagDESKTOP)的指針。在提供此信息時,Windows可以限制對其他桌面上的對象的訪問,從而在桌面之間提供隔離。類似地,由於對象總是由線程或進程擁有,所以可以實現共存於同一桌面的線程或進程之間的隔離。例如,一個給定的線程不能通過簡單地調用DestroyWindow來銷燬其他線程的窗口對象。相反,它需要發送一個窗口消息,該消息需要進行額外的驗證,如完整性級別檢查。然而,由於對象隔離不是以統一和集中的方式提供的,任何未執行所需檢查的函數都可能允許攻擊者繞過此限制。這無疑是特權服務和登錄用戶會話之間的會話分離(Vista和更高版本)的原因之一。由於同一會話中的所有進程和線程共享同一個用戶句柄表,低權限進程可能會向高權限進程擁有的對象傳遞消息或與之交互。

句柄表。所有的用戶對象都被索引到一個會話句柄表中。句柄表在win32k!Win32UserInitialize中初始化,該函數會在加載win32k的新實例時調用。句柄表本身存儲在共享部分(win32k!gpvSharedBase)的底部,也是由Win32UserInitialize設置的。此部分隨後被映射到每個新的GUI進程,從而允許進程從用戶模式訪問處理表信息,而不必求助於系統調用。將共享部分映射到用戶模式的決定被視爲一種性能優勢,並且也用於非基於內核的Win32子系統設計中,以防止在客戶機應用程序和客戶機-服務器運行時子系統進程(CSRSS)之間進行過多的上下文切換。在Windows 7中,指向句柄表的指針存儲在共享信息結構中(win32k!tagSHAREDINFO)。這個結構的指針可以從用戶模式(user32!gSharedInfo3)和內核模式(win32k!gSharedInfo)中獲得。

​ 用戶句柄表中的每個條目都由一個HANDLEENTRY結構表示。這個結構包含該句柄的對象信息,比如指向對象本身的指針(phead)、它的所有者(pOwner)和對象類型(bType)。owner字段是一個指向線程或進程信息結構的指針,如果它爲空,則它是一個會話範圍的對象。比如顯示器或鍵盤佈局/文件對象,它們被認爲是會話的全局對象。

typedef struct _HANDLEENTRY {
struct _HEAD* phead;
VOID* pOwner;
UINT8 bType;
UINT8 bFlags;
UINT16 wUniq;
} HANDLEENTRY, *PHANDLEENTRY

​ 用戶對象的實際類型由bType值定義,在Windows 7下的值範圍爲0到21。bFlags定義了附加的對象標誌,通常用於指示對象是否已被銷燬。如果一個對象被請求銷燬,但仍然保存在內存中,這可能是這樣的情況,因爲它的鎖計數是非零的。最後,wUniq值用作計算句柄值的惟一性計數器。句柄值被計算爲 handle =table entry id | (wUniq << 0x10)。當釋放對象時,計數器改變,以避免後續對象立即重用前一個句柄。需要注意的是,這個機制不能被看作是一個安全特性,因爲wUniq計數器只有16位,因此當分配和釋放了足夠多的對象時,它就會出錯。

在這裏插入圖片描述

​ 爲了驗證句柄,窗口管理器可以調用任何 HMValidateHandle API。這些函數將句柄值和句柄類型作爲參數,並在句柄表中查找相應的條目。如果請求對象,則函數返回對象指針。

內存中的用戶對象。在Windows中,用戶對象及其相關的數據結構可以駐留在桌面堆、共享堆或會話池中。一般的規則是,與特定桌面關聯的對象存儲在桌面堆中,其餘的對象存儲在共享堆或會話池中。每個對象類型的實際位置是由句柄類型信息表(win32k!ghati)定義的。此表保存每個對象類型的屬性,供句柄管理器在分配或釋放用戶對象時使用。具體來說,句柄類型信息表中的每個條目都由一個不透明的結構定義(未列出),該結構包含對象分配標記、類型標誌和一個指向特定類型銷燬例程的指針。當對象的鎖計數達到零時,窗口管理器將調用特定於類型的銷燬例程來正確釋放對象。

臨界區。與NT執行器管理的對象不同,窗口管理器不獨佔鎖定每個用戶對象。相反,它在win32k中使用臨界區(資源)爲每個會話實現一個全局鎖。每個對用戶對象或用戶管理結構(通常是NtUser系統調用)進行操作的內核例程必須首先進入用戶臨界區獲取資源(即獲取win32k!gpresUser資源)。例如,更新內核模式結構的函數必須首先調用UserEnterUserCritSec,並在修改數據之前獲取用於獨佔訪問的用戶資源。爲了減少窗口管理器中鎖爭用的數量,只執行讀操作的系統調用可以進入共享臨界區(EnterSharedCrit)。這允許win32k在全局鎖設計的情況下實現某種並行性,因爲多個線程可能同時執行NtUser調用。

1.3 用戶模式回調( User-Mode Callbacks)

​ Win32k需要多次使用用戶模式回調來執行任務,比如調用應用程序定義鉤子,提供事件通知,將數據從用戶模式複製到別處。該機制本身是在KeUserModeCallback中實現的,由NT執行體導出,其操作非常類似於逆向系統調用。

NTSTATUS KeUserModeCallback (
IN ULONG ApiNumber,
IN PVOID InputBuffer,
IN ULONG InputLength,
OUT PVOID *OutputBuffer,
IN PULONG OutputLength );

​ 當win32k執行一個用戶模式回調時,它會使用它想要調用的用戶模式函數的ApiNumber來調用KeUserModeCallback。在初始化USER32.dll的過程中,ApiNumber是函數指針表(USER32 ! apfnDispatch)的索引,在通過該索引得到函數地址後將地址複製到進程環境塊中的回調錶中(PEB.KernelCallbackTable)。Win32k提供了相應的回調函數的輸入參數通過填充InputBuffer OutputBuffer接收用戶模式的輸出。如下爲:用戶模式回調函數在USER32.dll中的分佈

0:004> dps poi($peb+58)
00000000‘77b49500 00000000‘77ac6f74 USER32!_fnCOPYDATA
00000000‘77b49508 00000000‘77b0f760 USER32!_fnCOPYGLOBALDATA
00000000‘77b49510 00000000‘77ad67fc USER32!_fnDWORD
00000000‘77b49518 00000000‘77accb7c USER32!_fnNCDESTROY
00000000‘77b49520 00000000‘77adf470 USER32!_fnDWORDOPTINLPMSG
00000000‘77b49528 00000000‘77b0f878 USER32!_fnINOUTDRAG
00000000‘77b49530 00000000‘77ae85a0 USER32!_fnGETTEXTLENGTHS
00000000‘77b49538 00000000‘77b0fb9c USER32!_fnINCNTOUTSTRING
...

​ 在調用系統調用時,nt!KiSystemService或nt !KiFastCallEntry在內核線程堆棧上存儲一個陷阱框架來保存當前線程上下文,並且能夠在返回到用戶模式時恢復寄存器。爲了在用戶模式回調中轉換回用戶模式,KeUserModeCallback首先使用線程對象持有的陷阱幀信息將輸入緩衝區複製到用戶模式堆棧,然後它創建一個新的陷阱框架並將EIP設置爲ntdll!KiUserCallbackDispatcher,然後替換線程對象的陷阱框架指針,最後調用nt!KiServiceExit將執行返回給用戶模式回調調度器。

​ 由於用戶模式回調需要存儲線程狀態信息(如陷阱幀)的位置,Windows XP和2003將增加內核堆棧,以確保有足夠的空間可用。因爲通過遞歸調用回調,堆棧空間很快就會被耗盡,所以Vista和Windows 7在每個用戶模式回調上創建一個新的內核線程堆棧。爲了跟蹤以前的堆棧等等,Windows在堆棧的底部爲KSTACK區域結構保留了空間,後面是僞造的陷阱幀。

kd> dt nt!_KSTACK_AREA
+0x000 FnArea : _FNSAVE_FORMAT
+0x000 NpxFrame : _FXSAVE_FORMAT
+0x1e0 StackControl : _KERNEL_STACK_CONTROL
+0x1fc Cr0NpxState : Uint4B
+0x200 Padding : [4] Uint4B
kd> dt nt!_KERNEL_STACK_CONTROL -b
+0x000 PreviousTrapFrame : Ptr32
+0x000 PreviousExceptionList : Ptr32
+0x004 StackControlFlags : Uint4B
+0x004 PreviousLargeStack : Pos 0, 1 Bit
+0x004 PreviousSegmentsPresent : Pos 1, 1 Bit
+0x004 ExpandCalloutStack : Pos 2, 1 Bit
+0x008 Previous : _KERNEL_STACK_SEGMENT
+0x000 StackBase : Uint4B
+0x004 StackLimit : Uint4B
+0x008 KernelStack : Uint4B
+0x00c InitialStack : Uint4B
+0x010 ActualLimit : Uint4B

​ 一旦用戶模式回調完成,它就會調用NtCallbackReturn來繼續在內核中執行。該函數將回調的結果複製回原始內核堆棧,並使用內核堆棧控制結構中包含的信息恢復原始陷阱幀(PreviousTrapFrame)和內核堆棧。在跳轉到之前停止的位置(nt!KiCallUserMode)之前,內核回調堆棧被刪除。

NTSTATUS NtCallbackReturn (
IN PVOID Result OPTIONAL,
IN ULONG ResultLength,
IN NTSTATUS Status );

​ 遞歸或嵌套的回調可能導致內核堆棧增長無限(XP)或創建任意數量的堆棧,內核跟蹤回調的深度保存在每個正在運行的線程在線程對象結構(KTHREAD - > CallbackDepth)中。在每次回調時,已經在線程堆棧上使用的字節(堆棧基堆棧指針)被添加到CallbackDepth變量中。每當內核試圖遷移到新的堆棧時,nt!KiMigrateToNewKernelStack確保總回調深度永遠不會超過0xC000字節,否則將返回狀態堆棧溢出錯誤代碼。

2. 通過用戶模式回調完成內核攻擊

​ 在本節中,我們將介紹幾個攻擊手法,它們可能允許對手通過用戶模式回調執行特權升級攻擊。我們首先查看用戶模式回調如何處理用戶臨界區,然後再更詳細地討論每個攻擊手法。

2.1 Win32k 命名約定

​ 如第1.2節所述,窗口管理器在內部管理結構上操作時使用關鍵部分和全局鎖。由於用戶模式回調可能會使應用程序凍結GUI子系統,所以win32k總是在回調到用戶模式之前離開臨界區。這樣在執行用戶模式代碼時,win32k可以執行其他任務。從回調函數返回後,win32k在函數在內核中繼續執行之前重新進入臨界區。我們可以在任何調用KeUserModeCallback的函數中觀察這種行爲。如下所示

call _UserSessionSwitchLeaveCrit@0
lea eax, [ebp+var_4]
push eax
lea eax, [ebp+var_8]
push eax
push 0
push 0
push 43h
call ds:__imp__KeUserModeCallback@20 ;離開臨界區
call _UserEnterUserCritSec@0 ;回到臨界區

​ 從用戶模式回調返回時,win32k必須確保引用的對象和數據結構仍然處於異常狀態。由於在輸入回調之前保留了關鍵部分,所以用戶模式代碼可以自由地更改對象的屬性、或重新分配數組等等。例如,回調可以調用SetParent()來更改窗口的父級。比如果內核在調用回調之前存儲對父類的引用,並且在返回後繼續對其進行操作,而沒有執行適當的檢查或對象鎖定,那麼這可能會導致安全漏洞。

​ 由於跟蹤可能會調用回用戶模式的函數非常重要(以便開發人員採取必要的預防措施),所以win32ksys使用自己的函數命名約定。特別是,函數的前綴xxx或zzz取決於函數調用用戶模式回調的方式。在大多數情況下,前綴xxx的函數將離開臨界區並調用用戶模式回調。但是,在某些情況下,函數可能需要一組特定的參數,以便分支到實際調用回調的路徑。有時會看到非前綴函數調用xxx函數,但它們提供給xxx函數的參數永遠不會導致回調。

​ 前綴爲zzz的函數調用異步或延遲迴調。這通常是某些類型的窗口事件的情況,由於各種原因不能或不應該立即處理。在本例中,win32k調用xxxFlushDeferredWindowEvents來刷新事件隊列。關於zzz函數需要注意的一件重要事情是,在調用xxxWindowEvent之前,win32k!gdwDeferWinEvent不能爲空。否則需要立即處理回調。

​ win32k使用的命名約定的問題是缺乏一致性。win32k中有幾個函數調用回調,但是沒有標記回調。原因尚不清楚,但一種可能的解釋是,隨着時間的推移函數已經被修改,但沒有對函數名進行必要的更新。因此,開發人員可能會認爲一個函數可能永遠不會真正調用回調,從而避免進行看似不必要的驗證(例如,對象保持未鎖定,指針沒有重新驗證)。在解決MS11-034[7]的漏洞時,更新了幾個函數名,以正確反映它們對用戶模式回調的使用(下表)。

在這裏插入圖片描述

2.2 用戶對象鎖定

​ 正如在1.2節中解釋的,用戶對象實現引用計數來跟蹤對象何時被使用,何時應該從內存中釋放。因此,在內核離開用戶臨界區之後,預期有效的對象必須被鎖定。通常,鎖有兩種形式——線程鎖和賦值鎖。

線程鎖定。線程鎖定通常用於鎖定函數中的對象或緩衝區。每個線程鎖定條目都存儲在一個線程鎖結構(win32k!TL)中,該結構位於一個單鏈表式的線程鎖列表中,由線程信息結構(THREADINFO.ptl)指向。線程鎖列表的操作非常類似於FIFO隊列,其中條目被推入並彈出列表。在win32k中線程鎖定通常是內聯執行,並且可以被內聯指針增量,通常在xxx函數中。當一個給定的函數在win32k不再需要的對象或緩衝區,它調用ThreadUnlock()來刪除鎖定線程鎖列表條目。

​ 如果對象已被鎖定但未被正確解鎖(例如,由於處理用戶模式回調時的進程終止),win32k將處理線程鎖列表,以釋放線程終止時的所有剩餘條目。可以在xxxDestroyThreadInfo函數調用DestroyThreadsObjects時觀察到。

mov ecx, _gptiCurrent
add ecx, tagTHREADINFO.ptl ; 線程信息結構
mov edx, [ecx]				;取出當前線程信息結構中的線程鎖結構
mov [ebp+tl.next], edx	;保存當前的線程信息結構中的線程鎖到線程鎖列表的Next指針
lea edx, [ebp+tl]		;取出當前線程的鎖
mov [ecx], edx ; push new entry on list 將當前線程的鎖加入到線程鎖列表
mov [ebp+tl.pobj], eax ; window object
inc [eax+tagWND.head.cLockObj]
push [ebp+arg_8]
push [ebp+arg_4]
push eax
call _xxxDragDetect@12 ; xxxDragDetect(x,x,x)
mov esi, eax
call _ThreadUnlock1@0 ; ThreadUnlock1()

賦值鎖定。與線程鎖不同,賦值鎖用於更長期的用戶對象鎖定。例如,當在桌面中創建一個窗口時,win32k賦值鎖會將桌面對象鎖定在窗口對象結構中適當的偏移位置。賦值鎖定項不是對列表進行操作,而是簡單地將指針(指向鎖定的對象)存儲在內存中。在win32k需要分配鎖定對象的地方已經存在一個指針,模塊在鎖定之前解鎖現有的條目,並將其替換爲所請求的條目。

​ 句柄管理器提供了賦值鎖定和解鎖的功能。在鎖定一個對象時,win32k調用HMAssignmentLock(Address, object)和類似的HMAssignmentUnlock(Address)來釋放對象引用。值得注意的是,賦值鎖定並不像線程鎖定那樣提供安全網。如果一個線程在回調中終止,線程或用戶對象清理例程本身負責單獨釋放這些引用。如果不這樣可能會導致內存泄漏,或者如果操作可以任意重複,引用計數可能會溢出。

窗口對象 Use-After-Free (CVE-2011-1237) 在安裝基於計算機的訓練(CBT)鉤子時,應用程序可能會收到關於窗口處理、鍵盤和鼠標輸入以及消息隊列處理的各種通知。例如,在創建新窗口之前,HCBT CREATEWND回調允許應用程序使用提供的CBT CREATEWND5結構檢查和修改用於確定窗口大小和方向的參數。這個結構還允許應用程序選擇窗口的z-order鏈,方法是提供窗口的句柄,在此之後將插入新窗口(hwndInsertAfter)。在設置這個句柄時,xxxCreateWindowEx獲得相應的對象指針,然後在將新窗口鏈接到z-order鏈中時使用它。但是,由於函數未能正確鎖定該指針,攻擊者可能會在隨後的回調中破壞hwndInsertAfter中提供的窗口,並強制win32k在返回時對釋放的內存進行操作。

​ 在下面的代碼中,xxxCreateWindowEx調用PWInsertAfter來獲取在CBT CREATEWND鉤子結構中提供的hwndInsertAfter句柄的窗口對象指針(使用HMValidateHandleNoSecure)。然後該函數將對象指針存儲在一個本地變量中。

.text:BF892EA1 push [ebp+cbt.hwndInsertAfter]
.text:BF892EA4 call _PWInsertAfter@4 ; PWInsertAfter(x)
.text:BF892EA9 mov [ebp+pwndInsertAfter], eax ; window object

​ 由於win32k沒有鎖定pwndInsertAfter,攻擊者可以在隨後的回調中釋放CBT鉤子中提供的窗口(例如,通過調用DestroyWindow)。在下面代碼函數的末尾,xxxCreateWindowEx使用窗口對象指針並試圖將其鏈接到窗口z-order鏈(通過LinkWindow)。由於窗口對象可能不再存在,這就變成了use-after - free漏洞,可能允許攻擊者在內核上下文中執行任意代碼。

.text:BF893924 push esi ; parent window
.text:BF893925 push [ebp+pwndInsertAfter]
.text:BF893928 push ebx ; new window
.text:BF893929 call _LinkWindow@12 ; LinkWindow(x,x,x)

鍵盤佈局對象Use-After-Free(CVE-2011-1241) 鍵盤佈局對象用於設置線程或進程的活動鍵盤佈局。在加載鍵盤佈局時,應用程序調用LoadKeyboardLayout並指定要加載的輸入本地標識符的名稱。Windows還提供了未文檔化的LoadKeyboardLayoutEx函數,該函數接受一個額外的鍵盤佈局句柄參數,win32k在加載新佈局之前首先嚐試卸載該參數。在提供此句柄時,win32k未能鎖定相應的鍵盤佈局對象。因此,攻擊者可以在用戶模式回調中卸載所提供的鍵盤佈局,並觸發一個use-after-free條件。

​ 在下面代碼中,LoadKeyboardLayoutEx獲取鍵盤佈局的句柄來首次卸載,並調用HKLToPKL來獲取鍵盤佈局對象指針。HKLtoPKL遍歷活動鍵盤佈局列表(THREADINFO.spklActive),直到找到與提供的句柄匹配的那個。LoadKeyboardLayoutEx然後將對象指針存儲在堆棧上的一個本地變量中。

text:BF8150C7 push [ebp+hkl]
.text:BF8150CA push edi
.text:BF8150CB call _HKLtoPKL@8 ; get keyboard layout object
.text:BF8150D0 mov ebx, eax
.text:BF8150D2 mov [ebp+pkl], ebx ; store pointer

由於LoadKeyboardLayoutEx沒有充分鎖定鍵盤佈局對象指針,攻擊者可以在用戶模式回調中卸載鍵盤佈局,從而釋放該對象。這個函數後來被稱爲xxxClientGetCharsetInfo,它可以從用戶模式中檢索字符集信息。在下面代碼中,LoadKeyboardLayoutEx繼續使用之前存儲的鍵盤佈局對象指針,因此可以在釋放的內存上操作。

text:BF8153FC mov ebx, [ebp+pkl] ; KL object pointer
.text:BF81541D mov eax, [edi+tagTHREADINFO.ptl]
.text:BF815423 mov [ebp+tl.next], eax
.text:BF815426 lea eax, [ebp+tl]
.text:BF815429 push ebx
.text:BF81542A mov [edi+tagTHREADINFO.ptl], eax
.text:BF815430 inc [ebx+tagKL.head.cLockObj] ; freed memory ?

2.3 對象狀態的驗證

​ 爲了跟蹤對象是如何使用的,win32k將幾個標誌以及指針與用戶對象相關聯。假定對象處於某種狀態,應該始終驗證其狀態。用戶模式回調可能會更改對象的狀態和更新對象的屬性,例如更改窗口的父級,導致下拉菜單不再處於活動狀態,或者終止DDE對話中的合作伙伴。缺少狀態檢查可能導致缺陷如空指針解引用和use-after-frees,取決於win32k使用對象。

​ 動態數據交換(DDE)協議是一種遺留協議,它使用消息和共享內存在應用程序之間交換數據。DDE對話在內部由窗口管理器表示爲DDE對話對象,該對象是爲發送方和接收方定義的。爲了跟蹤對象與哪些對象以及與哪些對象進行對話,對話對象結構(未公開)持有指向對方對話對象的指針(使用賦值鎖定)。如果擁有對話對象的窗口或線程終止,則其在合作伙伴對象中的賦值鎖定指針被解鎖(清除)。

DDE會話狀態漏洞。當DDE對話以用戶模式存儲數據時,它們依賴於用戶模式回調來在用戶模式之間來回複製數據。發送DDE消息後,win32k調用xxxCopyDdeIn從用戶模式複製數據。類似地,在接收DDE消息時,win32k調用xxxCopyDDEOut將數據複製回用戶模式。如果操作已經發生,win32k可能會通知合作伙伴對話對象對數據進行操作,例如,它期望得到響應。

​ 處理用戶模式後回調複製數據或從用戶模式,幾個函數未能正確地重新驗證對方的談話對象。攻擊者可以在用戶模式回調中終止會話,從而從發送方或接收方的對象結構中釋放夥伴會話對象。在下面代碼中,我們可以看到在xxxCopyDdeIn中可以調用回調,但是該函數在將對話對象指針傳遞給AnticapatePost之前沒有重新驗證它。這進而導致一個空指針解引用,並允許攻擊者在映射空頁時控制會話對象。

.text:BF8FB8A7 push eax
.text:BF8FB8A8 push dword ptr [edi]
.text:BF8FB8AA call _xxxCopyDdeIn@16
.text:BF8FB8AF mov ebx, eax
.text:BF8FB8B1 cmp ebx, 2
.text:BF8FB8B4 jnz short loc_BF8FB8FC
.text:BF8FB8C5 push 0 ; int
.text:BF8FB8C7 push [ebp+arg_4] ; int
.text:BF8FB8CA push offset _xxxExecuteAck@12
.text:BF8FB8CF push dword ptr [esi+10h] ; conversation object
.text:BF8FB8D2 call _AnticipatePost@24

菜單狀態處理漏洞。菜單管理是win32k中最複雜的組件之一,其中包含了可以追溯到現代Windows操作系統早期的未知代碼。雖然菜單對象(tagMENU)本身非常簡單,只包含與實際菜單項相關的信息,但是整個菜單處理依賴於多個相當複雜的函數和結構。例如,在創建彈出菜單時,應用程序調用TrackPopupMenuEx6來創建一個菜單類窗口,其中顯示菜單內容。然後,菜單窗口通過系統定義的菜單窗口類過程(win32k!xxxMenuWindowProc)處理消息輸入,以處理各種特定於菜單的消息。此外,爲了跟蹤菜單是如何使用的,win32k還將菜單狀態結構(tagMENUSTATE)與當前活動的菜單相關聯。這樣,函數就可以知道一個菜單是否包含在一個拖放操作中,在一個菜單循環中,或者將要終止,等等。

​ 在處理各種類型的菜單消息時,win32k在用戶模式回調後沒有正確驗證菜單。具體來說,在處理最後一個菜單消息時調用回調函數(例如,通過向菜單窗口類過程發送MN ENDMENU消息),win32k在很多情況下無法正確檢查菜單狀態是否仍然處於活動狀態,或者相關結構(如彈出菜單結構(win32k!tagPOPUPMENU))引用的對象指針是否爲非空。在下面代碼中,win32k嘗試通過調用xxxHandleMenuMessages來處理某些類型的菜單消息。由於此函數可能會調用回調,因此後續使用菜單狀態指針(ESI)將導致win32k對釋放的內存進行操作。這種特殊情況可以通過使用tagMENUSTATE結構(未公開)的dwLockCount變量鎖定菜單狀態來避免。

push [esi+tagMENUSTATE.pGlobalPopupMenu]
or [esi+tagMENUSTATE._bf4], 200h ; fInCallHandleMenuMessages
push esi
lea eax, [ebp+var_1C]
push eax
mov [ebp+var_C], edi
mov [ebp+var_8], edi
call _xxxHandleMenuMessages@12 ; xxxHandleMenuMessages(x,x,x)
and [esi+tagMENUSTATE._bf4], 0FFFFFDFFh ; <-- may have been freed
mov ebx, eax
mov eax, [esi+tagMENUSTATE._bf4]
cmp ebx, edi
jz short loc_BF968B0B ; message processed?

2.4 緩衝區重新分配

​ 許多用戶對象都有與之相關聯的項數組或其他形式的緩衝區。添加或刪除元素的項數組通常會調整緩衝區大小以保存內存。例如,如果元素的數量高於或低於某個閾值,就會以更合適的大小重新分配緩衝區。類似地,如果數組被清空,緩衝區就被釋放。重要的是,在回調期間可以重新分配或釋放的緩衝區必須在返回時重新檢查(如下圖)。任何沒有做到這一點的函數都可能在釋放的內存上操作,因此允許攻擊者控制賦值鎖定指針或破壞後續分配的內存。

在這裏插入圖片描述

菜單項數組Use-After-Free。爲了跟蹤由彈出菜單或下拉菜單保存的菜單項,菜單對象(win32k!tagMENU)定義一個指向菜單項數組的指針(rgItems)。每個菜單項(win32k!tagITEM)定義屬性,如顯示的文本字符串、嵌入的圖像、指向子菜單的指針等。菜單對象結構跟蹤cItems數組變量中包含的項的數量,以及在已調用的變量中分配的緩衝區中可以容納的項的數量。在從菜單項數組中添加或刪除元素時,例如通過調用InsertMenuItem()或DeleteMenu(),如果win32k注意到cAlloced將小於cItems(如下圖所示),或者cItems和cAllocated之間的差異超過8個項,它會嘗試調整數組的大小。
在這裏插入圖片描述

​ 在用戶模式回調之後,win32k中的幾個函數沒有充分驗證菜單項數組緩衝區。沒有辦法“鎖定”菜單項,例如用戶對象,任何可以調用回調的函數都需要重新驗證菜單項數組。這也適用於將菜單項作爲參數的函數。如果菜單項數組緩衝區在用戶模式回調中重新分配,則後續代碼可以在已釋放的內存或由攻擊者控制的數據上操作。

​ SetMenuInfo函數允許應用程序設置指定菜單的各種屬性。在設置所提供的菜單信息結構(MENUINFO)中的MIM APPLYTOSUBMENUS標誌掩碼值時,win32k還將更新應用於菜單的所有子菜單。這種行爲可以在xxxSetMenuInfo中觀察到,因爲該函數遍歷每個菜單項條目並遞歸處理每個子菜單以傳播更新的設置。在處理菜單項數組並進行遞歸調用之前,xxxSetMenuInfo在本地變量/寄存器中存儲菜單項(cItems)的數量以及菜單項數組指針(rgItems)(如以下代碼所示)。

.text:BF89C779 mov eax, [esi+tagMENU.cItems]
.text:BF89C77C mov ebx, [esi+tagMENU.rgItems]
.text:BF89C77F mov [ebp+cItems], eax
.text:BF89C782 cmp eax, edx
.text:BF89C784 jz short loc_BF89C7CC

​ 一旦xxxSetMenuInfo到達最裏面的菜單,遞歸就會停止,條目就會被處理。此時,函數可能在調用xxxMNUpdateShownMenu時調用一個用戶模式回調,因此可能允許調整菜單項數組的大小。但是,當xxxMNUpdateShownMenu返回時,當從遞歸調用返回時,xxxSetMenuInfo無法充分驗證菜單項數組緩衝區以及數組所包含的項的數量。如果攻擊者在xxxMNUpdateShownMenu調用的回調中調用InsertMenuItem()或DeleteMenu()來調整菜單項數組的大小,下面代碼中的ebx可能指向釋放的內存。此外,由於cItems反映了在調用函數時數組所包含的元素的數量,xxxSetMenuInfo可以對分配的數組之外的項進行操作。

.text:BF89C786 add ebx, tagITEM.spSubMenu
.text:BF89C789 mov eax, [ebx] ; spSubMenu
.text:BF89C78B dec [ebp+cItems]
.text:BF89C78E cmp eax, edx
.text:BF89C790 jz short loc_BF89C7C4
...
.text:BF89C7B2 push edi
.text:BF89C7B3 push dword ptr [ebx]
.text:BF89C7B5 call _xxxSetMenuInfo@8 ; xxxSetMenuInfo(x,x)
.text:BF89C7BA call _ThreadUnlock1@0 ; ThreadUnlock1()
.text:BF89C7BF xor ecx, ecx
.text:BF89C7C1 inc ecx
.text:BF89C7C2 xor edx, edx
...
.text:BF89C7C4 add ebx, 6Ch ; next menu item
.text:BF89C7C7 cmp [ebp+cItems], edx ; more items ?
.text:BF89C7CA jnz short loc_BF89C789

​ 爲了解決涉及菜單項處理的漏洞,微軟在win32k中引入了新的MNGetpItemFromIndex函數。該函數將菜單對象指針和請求的菜單項索引作爲參數,並根據菜單對象中提供的信息返回項。

SetWindowPos 數組 Use-After-Free。Windows允許應用程序延遲窗口位置更新,以便多個窗口可以同時更新。爲此,Windows使用一個特殊的SetWindowsPos對象,該對象持有一個指向窗口位置結構數組的指針。當應用程序調用BeginDeferWindowPos()時,將初始化SetWindowPos對象和這個數組。BeginDeferWindowPos()接受數組元素(窗口位置結構)的數量進行預分配。然後通過調用DeferWindowPos()來延遲窗口位置更新,其中填充下一個可用的位置結構。如果請求的延遲更新數量超過預先分配的條目數量,則win32k會以更合適的大小(4個附加條目)重新分配數組。一旦所有請求的窗口位置更新都被延遲,應用程序就會調用EndDeferWindowPos()來處理要更新的窗口列表。

​ 在對SMWP數組進行操作時,win32k並不總是在用戶模式回調之後正確地驗證數組指針。在調用EndDeferWindowPos來處理多個窗口位置結構時,win32k調用xxxCalcValidRects來計算SMWP數組中引用的每個窗口的位置和大小。該函數遍歷每個條目並執行各種操作,例如通知每個窗口其位置正在更改(WM WINDOWPOSCHANGING)。由於此消息可能會調用用戶模式回調,因此攻擊者可能會對同一個SWP對象進行多個DeferWindowPos調用,從而導致SMWP要重新分配的數組(如以下代碼)。xxxCalcValidRects將窗口句柄寫回原始緩衝區,造成use after free。

.text:BF8A37B8 mov ebx, [esi+14h] ; SMWP array
.text:BF8A37BB mov [ebp+var_20], 1
.text:BF8A37C2 mov [ebp+cItems], eax ; SMWP array count
.text:BF8A37C5 js loc_BF8A3DE3 ; exit if no entries
...
.text:BF8A3839 push ebx
.text:BF8A383A push eax
.text:BF8A383B push WM_WINDOWPOSCHANGING
.text:BF8A383D push esi
.text:BF8A383E call _xxxSendMessage@16 ; user-mode callback
.text:BF8A3843 mov eax, [ebx+4]
.text:BF8A3846 mov [ebx], edi ; window handle
...
.text:BF8A3DD7 add ebx, 60h ; get next entry
.text:BF8A3DDA dec [ebp+cItems] ; decrement cItems
.text:BF8A3DDD jns loc_BF8A37CB

​ 與菜單項不同,涉及SMWP數組處理的漏洞是通過不允許在處理SMWP數組時重新分配緩衝區來解決。在win32kDeferWindowPos中可以看到,該函數現在檢查一個“進行處理”的標誌,只有在不導致緩衝區重新分配的情況下,才允許添加條目。

明日計劃

繼續學習內核漏洞知識

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章