作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/29.html

這篇文章翻譯自一篇多年之前的論文,原文系統地描述了 win32k 的用戶模式回調機制以及相關的原理和思想,可以作為學習 win32k 漏洞挖掘的典范。早前曾經研讀過,近期又翻出來整理了一下翻譯,在這里發出來做個記錄。原文鏈接在文后可見。

摘要

十五年之前,Windows NT 4.0 引入了 win32k.sys 來應對舊的客戶端-服務端圖形子系統模型的固有限制。至今為止,win32k.sys 仍舊是 Windows 架構的基礎組件之一,管理著窗口管理器(User)和圖形設備接口(GDI)。為了更精確地與用戶模式數據相連接,win32k.sys 使用了用戶模式回調:一種允許內核反向調用到用戶模式的機制。用戶模式回調啟用各種任務,例如調用應用程序定義的掛鉤、提供事件通知,以及向/從用戶模式拷貝數據等。在這篇文章中,我們將討論涉及在 win32k 中用戶模式回調的很多挑戰和問題。我們將特別展示 win32k 的全局鎖機制依賴性在提供一個線程安全環境時與用戶模式回調的思想融合時的缺陷。雖然與用戶模式回調有關的很多漏洞已被修補,但它們的復雜特性表明,仍有更多潛在的缺陷可能仍舊存在于 win32k 中。因此,為了緩解一些更加普遍的 BUG 類型,關于對用戶自身來說如何預防將來可能遭受的內核攻擊,我們將總結性地提出一些建議。

關鍵詞:win32k,用戶模式回調,漏洞

1. 簡介

在 Windows NT 中,Win32 環境子系統允許應用程序與 Windows 操作系統相連接,并與像窗口管理器(User)和圖形設備接口(GDI)這樣的組件進行交互。子系統提供一組統稱為 Win32 API 的函數,并遵循一個主從式模型,在該模型中客戶端應用程序與更高特權級的服務端組件進行交互。

傳統情況下,Win32 子系統的服務端在客戶端-服務端運行時子系統(CSRSS)中執行。為了提供最佳性能,每個客戶端的線程在 Win32 服務端都有一個對應的線程,在一種被稱作快速 LPC 的特殊的進程間通信裝置中等待。由于在快速 LPC 中配對線程之間的切換不需要內核中的調度事件,服務端線程能夠在搶占式線程調度程序中輪到其執行之前,執行客戶端線程的剩余時間片。另外,在大數據傳遞和向客戶端提供對服務端管理的數據結構的只讀訪問時使用共享內存,用來最小化在客戶端和 Win32 服務端之間進行切換的需要。

雖然在 Win32 子系統中進行了性能優化,微軟仍決定在 Windows NT 4.0 版本中將大部分服務端組件移至內核模式實現。這導致了 win32k.sys 的引入,一個負責管理窗口管理器(User)和圖形設備接口(GDI)的內核模式驅動程序。通過擁有更少的線程和上下文的切換(并使用更快的用戶/內核模式傳遞)以及減少的內存需求,到內核模式的遷移極大地減少了與陳舊的子系統設計有關的開銷。然而,由于與在同一特權級下的直接代碼/數據訪問相比,用戶/內核模式傳遞仍是相對緩慢的,因此在客戶端地址空間的用戶模式部分中,例如管理器結構緩存之類的一些陳舊機制仍舊被維持下來。此外,一些管理器結構被特地存儲在用戶模式下,以避免環的傳遞。由于 win32k 需要一種訪問這些信息并支持例如窗口掛鉤的基礎功能的途徑,它需要一種途徑來傳遞對用戶模式客戶端的控制。這通過用戶模式回調機制實現。

用戶模式回調允許 win32k 反向調用到用戶模式并執行像調用應用程序定義的掛鉤、提供事件通知,以及向/從用戶模式拷貝數據之類的任務。在這篇文章中,我們將討論涉及 win32k 中的用戶模式回調的很多挑戰和問題。我們將特別展示 win32k 在維護數據完整性(例如在依賴全局鎖機制方面)方面的設計與用戶模式回調的思想融合時的缺陷。最近,MS11-034 [7] 和 MS11-054 [8] 修復了一些漏洞,以實現修復多種與用戶模式回調相關的 BUG 的目的。然而,由于其中一些問題的復雜特性,以及用戶模式回調的普遍應用,更多潛在的缺陷很可能仍舊存在于 win32k 中。因此,為了緩解一些更加普遍的 BUG 種類,關于對微軟和終端用戶來說能夠做的進一步緩解在將來 win32k 子系統中遭受攻擊風險的事情,我們總結性地討論一些觀點。

這篇文章的剩余部分組織如下。在第 2 節,我們將審查必要的背景材料,來理解這篇文章的剩余部分,專注于用戶對象和用戶模式回調。在第 3 節,我們將討論在 win32k 中的函數命名修飾,并將展示針對 win32k 和用戶模式回調的某些特殊的漏洞種類。在第 4 節,我們將評估被用戶模式回調觸發的漏洞的利用,同時在第 5 節將嘗試為普遍漏洞種類提出緩解措施以應對這些攻擊。

最后,在第 6 節我們將就 win32k 的未來提供的一些想法和建議,并在第 7 節提出這篇文章的結論。

2. 背景

在這一節中,我們審查必要的背景信息來理解這篇文章的剩余部分。在移步更多像窗口管理器(專注于用戶對象)和用戶模式回調機制這樣的特定組件之前,我們從簡要地介紹 Win32k 和它的架構開始。

2.1 Win32k

微軟在 Windows NT 4.0 的改變中將 Win32k.sys 作為改變的一部分而引入,用以提升圖形繪制性能并減少 Windows 應用程序的內存需求 [10]。窗口管理器(User)和圖形設備接口(GDI)在極大程度上被移出客戶端/服務端運行時子系統(CSRSS)并被落實在它自身的一個內核模塊中。在 Windows NT 3.51 中,圖形繪制和用戶接口管理由 CSRSS 通過在應用程序(客戶端)和子系統服務端進程(CSRSS.EXE)之間使用一種快速形式的進程間通信機制來執行。雖然這種設計已被進行過性能優化,但是 Windows 的圖形集約化特性導致開發人員轉向一種通過更快的系統調用的方式的基于內核的設計。

Win32k 本質上由三個主要的組件組成:圖形設備接口(GDI),窗口管理器(User),以及針對 DirectX 的形實替換程序,以支持包括 Windows XP/2000 和 LongHorn(Vista)在內的操作系統的顯示驅動模型(有時也可認為是 GDI 的一部分)。窗口管理器負責管理 Windows 用戶接口,例如控制窗口顯示,管理屏幕輸出,收集來自鍵盤和鼠標的輸入,并向應用程序傳遞消息。圖形設備接口(GDI),從另一方面來說,主要與圖形繪制和落實 GDI 對象(筆刷,鋼筆,Surface,設備上下文,等等)、圖形繪制引擎(Gre)、打印支持、ICM 顏色匹配、一個浮點數學庫以及字體支持有關。

與 CSRSS 的傳統子系統設計被建立在每個用戶一個進程的基礎上相類似地,每個用戶會話擁有它自己的 win32k.sys 映射副本。會話的概念也允許 Windows 在用戶之間提供一個更加嚴格的隔離(又稱會話隔離,session isolation)。從 Windows Vista 開始,服務也被移至它自己的非交互式會話 [2] 中,用來避免一系列與共享會話相關的問題,例如粉碎窗口攻擊 [12] 和特權服務漏洞。此外,用戶接口特權隔離(UIPI) [1] 實施完整級別的概念并確保低特權級的進程不能干擾(例如發送消息給)擁有高完整性的進程。

為了與 NT 執行體進行適當的連接,win32k 注冊若干呼出接口(Callout,PsEstablishWin32Callouts)來支持面向 GUI 的對象,例如桌面和窗口站。重要的是,win32k 也為線程和進程注冊呼出接口來定義 GUI 子系統使用的每線程和每進程結構體。

int __stdcall PsEstablishWin32Callouts(int a1)
{
  int result; // eax@1

  PspW32ProcessCallout = *(int (__stdcall **)(_DWORD, _DWORD))a1;
  PspW32ThreadCallout = *(int (__stdcall **)(_DWORD, _DWORD))(a1 + 4);
  ExGlobalAtomTableCallout = *(_DWORD (__stdcall **)())(a1 + 8);
  KeGdiFlushUserBatch = *(_DWORD *)(a1 + 28);
  PopEventCallout = *(_DWORD *)(a1 + 12);
  PopStateCallout = *(_DWORD *)(a1 + 16);
  PopWin32InfoCallout = *(_DWORD *)(a1 + 20);
  PspW32JobCallout = *(_DWORD *)(a1 + 24);
  ExDesktopOpenProcedureCallout = *(_DWORD *)(a1 + 32);
  ExDesktopOkToCloseProcedureCallout = *(_DWORD *)(a1 + 36);
  ExDesktopCloseProcedureCallout = *(_DWORD *)(a1 + 40);
  ExDesktopDeleteProcedureCallout = *(_DWORD *)(a1 + 44);
  ExWindowStationOkToCloseProcedureCallout = *(_DWORD *)(a1 + 48);
  ExWindowStationCloseProcedureCallout = *(_DWORD *)(a1 + 52);
  ExWindowStationDeleteProcedureCallout = *(_DWORD *)(a1 + 56);
  ExWindowStationParseProcedureCallout = *(_DWORD *)(a1 + 60);
  result = *(_DWORD *)(a1 + 68);
  ExWindowStationOpenProcedureCallout = *(_DWORD *)(a1 + 64);
  ExLicensingWin32Callout = result;
  return result;
}

GUI 線程和進程

由于并不是所有的線程都使用 GUI 子系統,預先為所有的線程分配 GUI 結構體將造成空間浪費。因此,在 Windows 中,所有的線程都作為非 GUI 線程啟動(12 KB 棧)。如果某線程訪問任意 USER 或 GDI 系統調用(調用號 >= 0x1000),Windows 將該線程提升為 GUI 線程(nt!PsConvertToGuiThread)并調用進程和線程呼出接口。GUI 線程在極大程度上擁有一個更大的線程棧,用來來更好地處理 win32k 的遞歸特性,以及更好地支持會為陷阱幀和其他元數據請求額外棧空間(在 Vista 及更新的系統中,用戶模式回調使用專用的內核線程棧)的用戶模式回調。

int __stdcall PsConvertToGuiThread()
{
  _KTHREAD *Thread; // esi@1
  int result; // eax@2

  Thread = KeGetCurrentThread();
  if ( !Thread->PreviousMode )
  {
    return 0xC000000D;
  }
  if ( !PspW32ProcessCallout )
  {
    return 0xC0000022;
  }
  if ( Thread->ServiceTable != &KeServiceDescriptorTable )
  {
    return 0x4000001B;
  }

  result = PspW32ProcessCallout(Thread->ApcState.Process, 1);
  if ( result >= 0 )
  {
    Thread->ServiceTable = &KeServiceDescriptorTableShadow;
    result = PspW32ThreadCallout(Thread, 0);
    if ( result < 0 )
      Thread->ServiceTable = &KeServiceDescriptorTable;
  }
  return result;
}

當進程的線程首次被轉換成 GUI 線程并調用 W32pProcessCallout 時,win32k 將調用 win32k!xxxInitProcessInfo 來初始化每進程 W32PROCESS/PROCESSINFO 結構體(W32PROCESS 是 PROCESSINFO 的子集, 處理 GUI 子系統,而 PROCESSINFO 還包含特定于 USER 子系統的信息)。該結構體具體保存針對于每個進程的 GUI 相關的信息,例如相關聯的桌面、窗口站,以及用戶和 GDI 句柄計數。在調用 win32k!xxxUserProcessCallout 初始化 USER 相關的域及隨后調用 GdiProcessCallout 初始化 GDI 相關的域之前,該函數通過調用 win32k!xxxAllocateW32Process 分配結構體自身。

另外,win32k 也為所有被轉換為 GUI 線程的線程初始化一個每線程 W32THREAD/THREADINFO 結構體。該結構體存儲與 GUI 子系統相關的特定信息,例如線程消息隊列中的信息,注冊的窗口掛鉤,所有者桌面,菜單狀態,等等。在這里,W32pThreadCallout 調用 win32k!AllocateW32Thread 來分配該結構體,隨后調用 GdiThreadCallout 和 UserThreadCallout 來初始化 GUI 和 USER 子系統各自特有的信息。在該處理過程中最重要的函數是 win32k!xxxCreateThreadInfo,其負責初始化線程信息結構體。

2.2 窗口管理器

窗口管理器的重要功能之一是追蹤實體,例如窗口,菜單,光標,等等。其通過將此類實體表示為用戶對象來實現該功能,并通過用戶會話維護自身句柄表來追蹤這些實體的使用。這樣一來,當應用程序請求在某個用戶實體中執行行為時,將提供自己的句柄值,句柄管理器將這個句柄有效地映射在內核內存中對應的對象。

用戶對象

用戶對象被劃分成不同的類型,從而擁有它們自己類型的特定結構體。例如,所有的窗口對象由 win32k!tagWND 結構體定義,而菜單由 win32k!tagMENU 結構體定義。雖然對象類型在結構上不同,但它們都共享一個通用的被稱為 HEAD 結構體的頭部。

HEAD 結構體存儲句柄值(h)的一份副本,以及一個鎖計數(cLockObj),每當某對象被使用時其值增加。當該對象不再被一個特定的組件使用時,它的鎖計數減小。在鎖計數達到零的時候,窗口管理器知道該對象不再被系統使用然后將其釋放。

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

雖然 HEAD 結構體相當小,但很多時候對象使用像 THRDESKHEAD 和 PROCDESKHEAD 這樣的進程和線程特有的頭結構體。這些結構體提供一些特殊的域,例如指向線程信息結構體 tagTHREADINFO 的指針,和指向關聯的桌面對象(tagDESKTOP)的指針。通過提供這些信息,Windows 能夠限制對象在其他桌面中被訪問,并因此提供桌面間隔離。同樣地,由于此類對象通常被一個線程或進程所有,共存于同一桌面的線程和進程間的隔離也能夠被實現。例如,一個線程不能通過簡單地調用 DestroyWindow 銷毀其他線程創建的對象,而是需要發送一個經過完整性級別檢查等額外校驗的窗口消息。然而,對象間隔離并未規定成一種統一集中的方式,任何不做必要檢查的函數都能夠允許攻擊者用以繞過這個限制。不可否認,這是引入高特權級的服務和已登錄用戶會話之間的會話間隔離(session separation,Vista 及更新)的原因之一。由于運行在同一會話中的所有進程共享同一個用戶句柄表,低特權級的進程能夠潛在地發送消息給某個高特權級的進程,或者與后者所擁有的對象進行交互。

句柄表

所有的用戶句柄被索引在所屬會話的句柄表中。該句柄表在 win32k!Win32UserInitialize 函數中被初始化,每當 win32k 的新實例被加載時調用該函數。句柄表自身存儲在共享段的基地址(win32k!gpvSharedBase),同樣在 Win32UserInitialize 函數中初始化。隨后該共享段被映射進每個新的 GUI 進程,這樣一來將允許進程在不發起系統調用的情況下從用戶模式訪問句柄表信息。將共享段映射進用戶模式的決策被視為有益于改善性能,并且也被應用在基于非內核的 Win32 子系統中,用以緩解在客戶端應用程序和客戶端-服務端運行時子系統進程(CSRSS)之間頻繁的上下文切換。在 Windows 7 中,在共享信息結構體(win32k!tagSHAREDINFO)中存在一個指向句柄表的指針。在用戶模式(user32!gSharedInfo,僅 Windows 7)和內核模式(win32k!gSharedInfo)都存在一個指向該結構體的指針。

用戶句柄表中的每項都被表示為 HANDLEENTRY 結構體。具體來說,該結構體包含關于其對象特定于句柄的信息,例如,指向對象自身的指針(pHead),它的所有者(pOwner),以及對象類型(bType)。所有者域要么是一個指向某進程或線程結構體的指針,要么是一個空指針(在這種情況下其被認為是一個會話范圍的對象)。舉個例子會是監視器或鍵盤布局/文件對象,其被認為在會話中是全局的。

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 比特位,導致當足夠多的對象被分配和釋放時其值將會回繞的現象,所以這種機制不應被當作是一種安全特性。

ID  TYPE              OWNER        MEMORY
 0  Free
 1  Window            Thread       Desktop Heap / Session Pool
 2  Menu              Process      Desktop Heap
 3  Cursor            Process      Session Pool
 4  SetWindowPos      Thread       Session Pool
 5  Hook              Thread       Desktop Heap
 6  Clipboard Data                 Session Pool
 7  CallProcData      Process      Desktop Heap
 8  Accelerator       Process      Session Pool
 9  DDE Access        Thread       Session Pool
10  DDE Conversation  Thread       Session Pool
11  DDE Transaction   Thread       Session Pool
12  Monitor                        Shared Heap
13  Keyboard Layout                Session Pool
14  Keyboard File                  Session Pool
15  Event Hook        Thread       Session Pool
16  Timer                          Session Pool
17  Input Context     Thread       Desktop Heap
18  Hid Data          Thread       Session Pool
19  Device Info                    Session Pool
20  Touch (Win 7)     Thread       Session Pool
21  Gesture (Win 7)   Thread       Session Pool

為了驗證句柄的有效性,窗口管理器會調用任何 HMValidateHandle API。這些函數將句柄值和句柄類型作為參數,并在句柄表中查找對應的項。如果查找到的對象具有所請求的類型,對象的指針將作為返回值被函數返回。

內存中的用戶對象

在 Windows 中,用戶對象和其相關的數據結構能夠存在于桌面堆、共享堆或會話內存池中。通用規則是,與某個特定桌面相關的對象被存儲在桌面堆中,其余對象被存儲在共享堆中。然而,每個句柄類型的實際位置由一個被稱作句柄類型信息表(win32k!ghati)的數據表定義。這個表保存針對每個句柄類型的屬性,當分配或釋放用戶對象時,句柄管理器會用到該值。具體來說,句柄類型信息表中的每項由一個不透明的結構(未編制的)定義,該結構保存對象分配標記、類型標志,以及一個指向類型特定的銷毀例程的指針。每當某對象鎖計數到達零時,這個銷毀例程就會被調用,在這種情況下窗口管理器調用類型特定的銷毀例程來恰當地釋放該對象。

臨界區

不像 NT 執行體管理的對象那樣,窗口管理器不會特定地鎖定每一個用戶對象,而是在 win32k 中通過使用臨界區(資源)實行每個會話一個全局鎖的機制。具體來說,操作用戶對象或用戶管理結構的每個內核例程(通常是 NtUser 系統調用)必須首先進入用戶臨界區(即請求 win32k!gpresUser 資源)。例如,更新內核模式結構體的函數,在修改數據之前,必須首先調用 UserEnterUserCritSec 并為獨占訪問請求用戶資源。為減少窗口管理器中鎖競爭的數量,只執行讀取操作的系統調用進入共享的臨界區(EnterSharedCrit)。這允許 win32k 實現某些并行處理而無視全局鎖設計,因為多線程可能會同時執行 NtUser 調用。

2.3 用戶模式回調

Win32k 很多時候需要產生進入用戶模式的反向調用來執行任務,例如調用應用程序定義的掛鉤、提供事件通知、以及向/從用戶模式拷貝數據等。這種調用通常被以用戶模式回調 [11][3] 的方式提交處理。這種機制自身在 KeUserModeCallback 函數中執行,該函數被 NT 執行體導出,并執行很像反向系統調用的操作。

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

當 win32k 產生一個用戶模式回調時,它通過想要調用的用戶模式函數的 ApiNumber 調用 KeUserModeCallback 函數。這里的 ApiNumber 是表示函數指針表(USER32!apfnDispatch)項的索引,在指定的進程中初始化 USER32.dll 期間該表的地址被拷貝到進程環境變量塊(PEB.KernelCallbackTable)中。Win32k 通過填充 InputBuffer 緩沖區向相應的回調函數提供輸入參數,并在 OutputBuffer 緩沖區中接收來自用戶模式的輸出。

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 在內核線程棧中存儲一個陷阱幀(TRAP_FRAME)來保存當前線程上下文,并使在返回到用戶模式時能夠恢復寄存器的值。為了在用戶模式回調中實現到用戶模式的過渡,KeUserModeCallback 首先使用由線程對象保存的陷阱幀信息將輸入緩沖區拷貝至用戶模式棧中,接著通過設為 ntdll!KiUserCallbackDispatcher 的 EIP 創建新的陷阱幀,代替線程對象的 TrapFrame 指針,最后調用 nt!KiServiceExit 返回對用戶模式回調分發的執行。

由于用戶模式回調需要一個位置存儲例如陷阱幀等線程狀態信息,Windows XP 和 2003 會擴大內核棧以確保足夠的空間可用。然而,因為通過遞歸調用回調棧空間會被很快耗盡,Vista 和 Windows 7 轉而在每個用戶模式回調中創建新的內核線程棧。為了達到追蹤先前的棧等目的,Windows 在棧的基地址位置為 KSTACK_AREA 結構體保留空間,緊隨其后的是構造的陷阱幀。

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 來恢復并繼續在內核中的執行。該函數將回調的結果復制回原來的內核棧,并通過使用保存在 KERNEL_STACK_CONTROL 結構體中的信息恢復原來的陷阱幀(PreviousTrapFrame)和內核棧。在跳轉到其先前棄用的位置之前,內核回調棧將被刪除。

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

由于遞歸或嵌套回調會導致內核棧的無限增長(XP)或創建任意數目的棧,內核會為每個運行中的線程在線程對象結構體(KTHREAD->CallbackDepth)中追蹤回調的深度(內核棧空間被用戶模式回調完全使用)。在每個回調中,線程棧已使用的字節數(棧的基地址 - 棧指針)被加到 CallbackDepth 變量中。每當內核嘗試遷移至新棧時,nt!KiMigrateToNewKernelStack 確保 CallbackDepth 總計不會超過 0xC000,否則將返回 STATUS_STACK_OVERFLOW 棧溢出的錯誤碼。

3. 通過用戶模式回調實施的內核攻擊

在這一節中,我們將提出一些會允許對手從用戶模式回調中執行特權提升的攻擊載體。在更詳細地討論每個攻擊載體之前,我們首先從研究用戶模式回調如何處理用戶臨界區開始。

3.1 Win32k 命名約定

像在 2.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() 函數來改變窗口的父級,如果內核在調用回調之前存儲對父級窗口的引用,并在返回后在沒有執行屬性檢查或對象鎖定的情況下繼續操作該引用,這將引發一處安全漏洞。

由于對潛在地反向調用至用戶模式的函數的追蹤非常重要(為了使開發者做出預防措施),win32k.sys 使用它自己的函數命名約定。需要注意的是,函數以 xxx 或 zzz 作為前綴取決于其會以何種方式調用用戶模式回調。以 xxx 作為前綴的函數在大多數情況下離開臨界區并調用用戶模式回調。然而,在一些情況下函數可能會請求特定的參數以偏離回調實際被調用的路徑。這就是為什么有時你會看到無前綴的函數調用 xxx 函數的原因,因為它們提供給 xxx 函數的參數不會引發一個回調。

以 zzz 作為前綴的函數調用異步或延時的回調。這通常是擁有確定類型的窗口事件的情況,因為各種各樣的原因,不能或不應立刻進行處理。在這種情況下,win32k 調用 xxxFlushDeferredWindowEvents 來清理事件隊列。對 zzz 函數來說需要注意的重要一點是,其要求在調用 xxxWindowEvent 之前確保 win32k!gdwDeferWinEvent 為非空值。如果不是這種情況,那么回調會被立即處理。

Win32k 使用的命名約定的問題是缺乏一致性。在 win32k 中一些函數調用回調,但是并未被視作其理應被視作的類型。這樣的原因是不透明,但一個可能的解釋是:隨著時間的推移,函數已被修改,但沒有更新函數名稱的必要。因此,開發者可能會被誤導地認為某個函數可能不會實際地調用回調,因此而避免做類似的不必要的驗證(例如對象保持非鎖定狀態,以及指針不重新驗證)。在 MS11-034 [7] 針對漏洞的應對方案中,有些函數名稱已被更新成正確反映其對用戶模式回調使用的格式。

Windows 7 RTM          Windows 7 (MS11-034)
MNRecalcTabStrings     xxxMNRecalcTabStrings
FreeDDEHandle          xxxFreeDDEHandle
ClientFreeDDEHandle    xxxClientFreeDDEHandle
ClientGetDDEFlags      xxxClientGetDDEFlags
ClientGetDDEHookData   xxxClientGetDDEHookData
3.2 用戶對象鎖定

像在 2.2 節中所解釋的那樣,用戶對象實行引用計數來追蹤對象何時被使用及應該從內存中釋放。正因如此,在內核離開用戶臨界區之后預期有效的對象必須被鎖定。鎖定通常有兩種形式:線程鎖定和賦值鎖定。

線程鎖定(Thread Locking)

線程鎖定通常在某些函數中用來鎖定對象或緩沖區。線程鎖定的每個項被存儲在線程鎖定結構體中(win32k!TL),通過單向的線程鎖定鏈表連接,線程信息結構體中存在指向該鏈表的指針(THREADINFO.ptl)。在表項被 push 進或被 pop 出時,線程鎖定鏈表的操作遵循先進先出(FIFO)隊列原則。在 win32k 中,線程鎖定一般內聯地執行,并能夠被內聯的指針增量識別,通常在 xxx 函數調用之前(如下清單所示)。當給定的一個 win32k 中的函數不再需要對象或緩沖區時,其調用 ThreadUnlock() 來從線程鎖定列表中刪除鎖定表項。

mov    ecx, _gptiCurrent
add    ecx, tagTHREADINFO.ptl  ; thread lock list
mov    edx, [ecx]
mov    [ebp+tl.next], edx
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 處理線程鎖定列表來釋放任何遺留的表項。這可以在 xxxDestroyThreadInfo 函數中調用 DestroyThreadsObjects 函數時被觀察到。

賦值鎖定(Assignment Locking)

不像線程鎖定那樣,賦值鎖定用于對用戶對象實施更加長期的鎖定。例如,當在一個桌面中創建窗口時,win32k 在窗口對象結構體中適當的偏移位置對桌面對象執行賦值鎖定。與在列表中操作相反,賦值鎖定項只是存儲在內存中的(指向鎖定對象的)指針。如果在 win32k 需要對某個對象執行賦值鎖定的位置有已存在的指針,模塊在鎖定前會先解鎖已存在的項,并用請求的項替換它。

句柄管理器提供提過執行賦值鎖定和解鎖的函數。在對對象執行鎖定時,win32k 調用 HMAssignmentLock(Address,Object) 函數,并類似地調用 HMAssignmentUnlock(Address) 來釋放對象引用。值得注意的是,賦值鎖定不提供安全保障,但線程鎖定會提供。萬一線程在回調中被銷毀,線程或用戶對象清理例程自身負責逐個釋放那些引用。如果不這樣做,將會導致內存泄漏;如果該操作能被任意重復的話,也將導致引用計數溢出(在 64 位平臺中,由于對象的 PointerCount 域的 64 位長度,導致似乎不可行)。

窗口對象釋放后使用(CVE-2011-1237)

在安裝計算機輔助訓練(CBT)掛鉤時,應用程序能夠接收到各種關于窗口處理、鍵盤和鼠標輸入,以及消息隊列處理的通知。例如,在新窗口被創建之前,HCBT_CREATEWND 回調允許應用程序通過提供的 CBT_CREATEWND 結構體檢查并修改用于確認窗口大小和軸向的參數。通過提供指向已有窗口(當前新窗口將會被插在該窗口的后面)的句柄(hwndInsertAfter),該結構體也允許應用程序選擇窗口的層疊順序。設置該句柄時,xxxCreateWindowEx 獲取對應的對象指針,在后面將新窗口鏈入層疊順序鏈表時會用到該對象指針。然而,由于該函數未能適當地鎖定該指針,攻擊者能夠在隨后的回調中銷毀在 hwndInsertAfter 中提供的窗口,并在返回時迫使 win32k 操作已釋放的內存。

獲取關于 CBT_CREATEWND 更多信息請訪問:https://msdn.microsoft.com/zh-cn/ms644962

在下面的清單中,xxxCreateWindowEx 調用 PWInsertAfter 來獲取(使用 HMValidateHandleNoSecure)在 CBT_CREATEWND 掛鉤結構體中提供的 hwndInsertAfter 句柄的窗口對象指針。隨后函數將獲取到的對象指針存儲在一個局部變量中。

.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 的末尾(如下清單所示),函數使用窗口對象指針并嘗試將其鏈入(通過 LinkWindow 函數)窗口層疊順序鏈表。由于該窗口對象可能已經不存在了,這就變成了一處“釋放后使用”漏洞,允許攻擊者在內核上下文中執行任意代碼。我們將在第 4 節討論“釋放后使用”漏洞對用戶對象的影響。

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

鍵盤布局對象釋放后使用(CVE-2011-1241)

鍵盤布局對象用來為線程或進程設置活躍鍵盤布局。在加載鍵盤布局時,應用程序調用 LoadKeyboardLayout 并指定要加載的輸入局部標識符的名稱。Windows 也提供未文檔化的 LoadKeyboardLayoutEx 函數,其接受一個額外的鍵盤布局句柄參數,在加載新布局之前 win32k 首先根據該句柄嘗試卸載對應的布局。在提供該句柄時,win32k 沒有鎖定對應的鍵盤布局對象。這樣一來,攻擊者能夠在用戶模式回調中卸載提供的鍵盤布局并觸發“釋放后使用”條件。

在下面的清單中,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 ?
3.3 對象狀態驗證

為了追蹤對象是如何被使用的,win32k 將一些標志和指針與用戶對象關聯起來。對象假設在一個確定的狀態,應該一直確保其狀態是已驗證的。用戶模式回調能夠潛在地修改狀態并更新對象屬性,例如改變一個窗口對象的父窗口、使一個下拉菜單不再被激活,或在 DDE 會話中銷毀伙伴對象。缺乏對狀態的檢查會導致向空指針引用和釋放后使用之類的 BUG,這取決于 win32k 如何使用對象。

DDE 會話狀態漏洞

動態數據交換(DDE)協議是一種使用消息和共享內存在應用程序之間交換數據的遺留協議。DDE 會話在內部被窗口掛力氣表示為 DDE 會話對象,發送者和接收者使用同一種對象定義。為了追蹤哪個對象正忙于會話中以及會話對方的身份,會話對象結構體(未文檔化)存儲指向對方對象的指針(使用賦值鎖定)。這樣一來,如果擁有會話對象的窗口或線程銷毀了,其在伙伴對象中存儲的賦值鎖定的指針未被解鎖(清理)。

由于 DDE 會話在用戶模式中存儲數據,它們依靠用戶模式回調來向/從用戶模式拷貝數據。在發送 DDE 消息時,win32k 調用 xxxCopyDdeIn 從用戶模式拷入數據。相似地,在接收到 DDE 消息時,win32k 調用 xxxCopyCopyDdeOut 將數據拷回到用戶模式。在拷貝行為已發生之后,win32k 會通知伙伴會話對象對目標數據起作用,例如,其等待對方的應答。

在用于向/從用戶模式拷入/出數據的用戶模式回調處理之后,一些函數未能適當地重新驗證伙伴會話對象。攻擊者能夠在用戶模式回調中銷毀會話,并從而在發送者或接收者對象結構體中解鎖伙伴會話對象。在下面的清單中,我們看到在 xxxCopyDdeIn 函數中會調用回調,但在將伙伴會話對象指針傳遞給 AnticipatePost 之前,沒有對其進行重新驗證。這樣反過來導致一個空指針引用,并允許攻擊者通過映射零頁(見第 4.3 節)來控制該會話對象。

.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)其自身如此簡單,并且只包含與實際菜單項有關的信息,但是菜單處理作為一個整體依賴于多種十分復雜的函數和結構體。例如,在創建彈出菜單時,應用程序調用 TrackPopupMenuEx 在菜單內容顯示的位置創建菜單類的窗口。接著該菜單窗口通過一個系統定義的菜單窗口類過程(win32k!xxxMenuWindowProc)處理消息輸入,用以處理各種菜單特有的信息。此外,為了追蹤菜單如何被使用,win32k 也將一個菜單狀態結構體(tagMENUSTATE)與當前活躍菜單關聯起來。通過這種方式,函數能夠知道菜單是否在拖拽操作中調用、是否在菜單循環中、是否即將銷毀,等等。

獲取關于 TrackPopupMenuEx 更多信息請訪問:https://msdn.microsoft.com/zh-cn/ms648003

在處理各種類型的菜單消息時,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 ?
3.4 緩沖區重新分配

很多用戶對象擁有與它們相關聯的條目數組或其他形式的緩沖區。在添加或刪除元素時,條目數組通常被調整大小以節省內存。例如,如果元素個數大于或小于某個特定的閾值,緩沖區將會以更合適的大小重新分配。類似地,如果數組置空,緩沖區會被釋放。重要的是,任何能夠在回調期間被重新分配或釋放的緩沖區都必須在返回時重新檢查(如下圖所示)。任何沒有做重新檢查的函數都可能會潛在地操作已釋放地內存,從而允許攻擊者控制賦值鎖定的指針或損壞隨后分配的內存。

菜單條目數組釋放后使用

為了追蹤由彈出或下拉菜單保存的菜單條目,菜單對象(win32k!tagMENU)定義一個指向菜單條目數組的指針(rgItems)。每個菜單條目(win32k!tagITEM)定義一些屬性,例如顯示的字符串、內嵌圖像、指向子菜單的指針等等。菜單對象結構體在 cItems 變量中追蹤數組所包含條目的個數,并在 cAlloced 變量中追蹤有多少條目能夠適應所分配的緩沖區。在向/從菜單條目數組中添加/刪除元素時,例如通過調用 InsertMenuItem() 或 DeleteMenu() 函數,如果 win32k 注意到 cAlloced 即將變得小于 cItems 變量(見下圖所示),或者如果 cItems 和 cAlloced 變量差異超過 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 反映的是在函數調用的時間點上包含在數組中的元素個數,xxxSentMenuInfo 將可能會操作所分配數組之外的條目。

.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 中引入了新的 MNGetpItemFromIntex 函數。該函數接受菜單對象指針和請求的菜單條目索引作為參數,并根據在菜單對象中提供的信息返回條目指針。

SetWindowPos 數組釋放后使用

Windows 允許應用程序延時窗口位置更新,這樣使多個窗口可以被同時更新。為此,Windows 使用一個特殊的 SetWindowPos 對象(SWP),該對象保存指向窗口位置結構體數組的指針。當應用程序調用 BeginDeferWindowPos() 時初始化 SWP 對象和這個數組。該函數接受數組元素(窗口位置結構體)的個數以對其進行預先分配。隨后應用程序通過調用 DeferWindowPos() 將窗口位置的更新推遲到下一個可用的位置結構體被填充時。萬一要求延時更新的數量超過預分配項的數量限制,win32k 用更合適的大小(4 個追加的項)重新分配數組。一旦所有要求的窗口位置更新都已被延時,應用程序調用 EndDeferWindowPos() 來處理窗口更新列表。

在操作 SMWP 數組時,win32k 在用戶模式回調之后并非總是適當地驗證數組指針。在調用 EndDerWindowPos 來處理多窗口位置結構體時,win32k 調用 xxxCalcValidRects 來計算在 SMWP 數組中引用的每個窗口的位置和大小。該函數遍歷每一項并執行各種操作,例如通知每個窗口它的位置正在改變(WM_WINDOWPOSCHANGING)。由于該消息會調用用戶模式回調,攻擊者能夠對同一個 SWP 對象產生多次 DeferWindowPos 的調用來引發 SMWP 數組的重新分配(見下面的清單)。由于 xxxCalcValidRects 將窗口句柄寫回原緩沖區中,這反過來會導致一個釋放后使用漏洞。

.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 數組處理期間拒絕緩沖區的重新分配來應對。這可以在 win32k!DeferWindowPos 函數中觀測到,函數在那里檢查“正被處理的”標志位并只允許不會導致緩沖區重新分配的項被添加進數組。

4. 可利用性

在這一節中,我們評估由用戶模式回調引發的漏洞的可利用性。由于我們關注兩種漏洞原型——釋放后使用和空指針引用,我們將聚焦于攻擊者是如何能夠將這類 BUG 施加在利用 win32k 漏洞上的。為了在第 5 節中提出合理的緩解措施或變通方案,評估它們的可利用性是必不可少的。

4.1 內核堆

如同在第 2.2 節中提到的,用戶對象和它們的相關數據結構位于會話內存池、共享堆,或桌面堆中。存儲在桌面堆或共享堆中的對象和數據結構由內核堆分配器管理。內核堆分配器可以看作是一個精簡版的用戶模式堆分配器,它使用類似的由 NT 執行體導出的函數來管理堆塊,例如 RtlAllocateHeap 和 RtlFreeHeap 等。

雖然用戶堆和內核堆極其相似,但它們有一些關鍵的不同之處。不像用戶模式堆那樣,被 win32k 使用的內核堆不采用任何前置分配器。這可以通過查看 HEAP_LIST_LOOKUP 結構體的 ExtendedLookup 值來觀察到,該結構體在堆基址(HEAP)中引用。當設置為 NULL 時,堆分配器不使用任何旁視列表或低分片堆 [13]。此外,在轉儲堆基址結構體(見下面的清單)時,我們可以觀察到,由于 EncodingFlagMask 和 PointerKey 都被設置為 NULL,所以并未使用任何堆管理結構體的編碼或混淆。前者決定是否使用堆頭編碼,而后者用來編碼 CommitRoutine 指針,每當堆需要被延伸時會調用該例程指針。

Kd> dt nt!_HEAP fea00000
   ...
   +0x04c EncodeFlagMask   : 0
   +0x050 Encoding         : _HEAP_ENTRY
   +0x058 PointerKey       : 0
   ...
   +0x0b8 BlocksIndex      : 0xfea00138 Void
   ...
   +0x0c4 FreeLists        : _LIST_ENTRY [ 0xfea07f10 - 0xfea0e4d0 ]
   ...
   +0x0d0 CommitRoutine    : 0x93a4692d  win32k!UserCommitDesktopMemory
   +0x0d4 FrontEndHeap     : (null)
   +0x0d8 FrontHeapLockCount : 0
   +0x0da FrontEndHeapType : 0 ''

Kd> dt nt!_HEAP_LIST_LOOKUP fea00138
   +0x000 ExtendedLookup   : (null)
   ...

當處理像“釋放后使用”這樣的內核堆損壞問題時,確切知道內核堆管理器如何工作是必不可少的。有很多非常好的文章詳細說明了用戶模式堆機制的內部工作機制 [13][6][9],這些可以在學習內核堆時作為參考。根據當前討論的需要,理解內核堆是一塊根據分配內存的數量可伸縮的相鄰內存區域就足夠了。由于未使用前置管理器,所有被釋放的內存塊被索引在一個單向的空閑列表中。一般情況下,堆管理器總是嘗試分配最近釋放的內存塊(例如通過列表建議使用),來更好地利用 CPU 緩存器。

4.2 釋放后使用利用

為了利用 win32k 中的釋放后使用漏洞,攻擊者需要能夠重新分配已釋放的內存并在某種程度上控制它的內容。因為用戶對象和相關的數據結構和字符串存儲在一起,通過設置存儲為 Unicode 字符串的對象屬性,有可能可以強制進行任意大小的分配以及完全控制最近釋放內存中的內容。只要避免空字符(除了字符串終止符),任意字節組合可以被用在操作作為對象或數據結構訪問的內存。

為了桌面堆中的釋放后使用漏洞,攻擊者會通過調用 SetWindowTextW 設置窗口標題欄的文本,以強制進行任意大小的桌面堆分配。相似地,可以通過調用 SetClassLongPtr 并指定 GCLP_MENUNAME 以設置與某窗口類關聯的某個菜單資源的菜單名稱字符串來觸發任意大小的會話內存池分配。

eax=41414141 ebx=00000000 ecx=ffb137e0 edx=8e135f00 esi=fe74aa60 edi=fe964d60
eip=92d05f53 esp=807d28d4 ebp=807d28f0 iopl=0         nv up ei pl nz na pe cy
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010207
win32k!xxxSetPKLinThreads+0xa9:
92d05f53 89700c   mov   dword ptr [eax+0Ch],esi ds:0023:4141414d=????????

kd> dt win32k!tagKL @edi -b
   +0x000 head             : _HEAD
      +0x000 h                : 0x41414141
      +0x004 cLockObj         : 0x41414142
   +0x008 pklNext          : 0x41414141
   +0x00c pklPrev          : 0x41414141
   ...

在上面的清單中(展示在 3.2 節中描述的漏洞,作為鍵盤布局對象的字符串,CVE-2011-1241),鍵盤布局對象已被用戶控制的字符串所替換,該字符串是在桌面堆中分配的。在這種特殊情況下,鍵盤布局對象已被釋放,但 win32k 嘗試將其鏈入鍵盤布局列表中。這允許攻擊者通過控制被釋放的鍵盤布局對象的 pklNext 指針來選擇寫入 esi 時的地址。

由于對象通常包含指向其他對象的指針,win32k 使用賦值鎖定機制來確保對象依賴性得到滿足。照此,在 win32k 嘗試釋放對象引用時,影響主體中包含賦值鎖定指針的對象的釋放后使用漏洞會允許攻擊者遞減任意地址。以下描述的攻擊方法的變體可作為這種利用的一種可能的方式:從用戶模式回調中返回一個已銷毀的菜單句柄索引。在線程銷毀時,這導致釋放類型為 (0) 的銷毀例程被調用。由于該釋放類型未定義銷毀例程,win32k 將調用零頁,而零頁在 Windows 中是允許用戶映射的(見第 4.3 節)。

eax=deadbeeb ebx=fe954990 ecx=ff910000 edx=fea11888 esi=fea11888 edi=deadbeeb
eip=92cfc55e esp=965a1ca0 ebp=965a1ca0 iopl=0         nv up ei ng nz na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010286
win32k!HMUnlockObject+0x8:
92cfc55e ff4804          dec     dword ptr [eax+4]    ds:0023:deadbeef=????????

965a1ca0 92cfc9e0 deadbeeb 00000000 fe954978 win32k!HMUnlockObject+0x8
965a1cb0 92c60cb1 92c60b8b 004cfa54 002dfec4 win32k!HMAssignmentLock+0x45
965a1cc8 92c60bb3 965a1cfc 965a1cf8 965a1cf4 win32k!xxxCsDdeInitialize+0x67
965a1d18 8284942a 004cfa54 004cfa64 004cfa5c win32k!NtUserDdeInitialize+0x28
965a1d18 779864f4 004cfa54 004cfa64 004cfa5c nt!KiFastCallEntry+0x12a

由于攻擊者會推測在內核內存中的用戶句柄表的地址,他或她會遞減窗口對象句柄表項 (1) 的類型(bType)值。在銷毀窗口時,這會導致釋放類型為 (0) 的銷毀例程被調用并引發任意內核代碼執行。在上面的清單中(作為 DDE 對象的字符串,CVE-2011-1242),攻擊者控制賦值解鎖定的指針,導致任意內核遞減。

4.3 空指針利用

不像其他類似 Linux 的平臺那樣,Windows(為保持向后兼容性)允許無特權的用戶通過用戶進程的上下文映射零頁。由于內核和用戶模式組件共享同樣的虛擬地址空間,攻擊者會潛在地能夠通過映射零頁并控制解引用數據來利用內核空指針解引用漏洞。為了在 Windows 中分配零頁,應用程序只需簡單地調用 NtAllocateVirtualMemory 并請求一個比 NULL 大但比比頁尺寸小的基地址。應用程序也可以通過使用這樣的基地址和 MEM_DOS_LIM 功能標志位啟用頁對齊的區段(僅 x86 有效)調用 NtMapViewOfSection 來內存映射零頁。

pwnd = (PWND) 0;
pwnd->head.h = hWnd; // valid window handle
pwnd->head.pti = NtCurrentTeb()->Win32ThreadInfo;
pwnd->bServerSideWindowProc = TRUE;
pwnd->lpfnWndProc = (PVOID) xxxMyProc;

在 win32k 中的空指針漏洞很多時候是由于對用戶對象指針的檢查不充分導致,因此,攻擊者能夠通過創建假的零頁對象來利用這樣的漏洞,并在隨后引發任意內存寫或控制函數指針的值。例如,由于在 win32k 中最近的很多空指針漏洞都與窗口對象指針有關,攻擊者可以在零頁安置假的窗口對象并定義一個自定義的服務端窗口過程(見上面的清單,在零頁安置假的窗口對象)。如果有任何消息隨后被傳遞給這個 NULL 對象,這會允許攻擊者獲得任意內核代碼執行的能力。

5. 緩解措施

在這一節中,我們將評估在第 4 節中討論的這類漏洞的緩解措施。

5.1 釋放后使用漏洞

如同在前面的章節中提到的,釋放后使用漏洞依靠攻擊者重新分配并控制先前釋放內存的能力。不幸的是,由于 CPU 沒有講述內存是否屬于特定對象或結構體的合法途徑,由于只有操作系統生成的抽象,因此緩解釋放后使用漏洞是非常苦難的。如果我們看得更仔細一些,這些問題本質上歸結于那些攻擊者,他們能夠在處理回調期間釋放對象或緩沖區,并隨后在回調返回時 win32k 再次使用對象之前對內存進行重新分配。這樣一來,通過還原內核內存池或堆的分配或通過隔離特定的分配以使像字符串這樣的簡單可控原型不被從相同資源分配,使得緩解釋放后使用漏洞的可利用性成為可能。

由于操作系統總是知道回調何時處于激活狀態(例如通過 KTHREAD.CallbackDepth),延遲釋放的方法可以被用在處理用戶模式回調時。這將阻止攻擊者立即重新使用已釋放的內存。然而,這樣的機制無法抵消在這種情況中的利用:在釋放后使用條件被觸發前調用多個連續的回調。另外,由于用戶模式回調機制不在 win32k 中執行,在回調返回時不得不執行附加邏輯來執行必要的延遲釋放列表的處理。

與其通過關注分配的可預見性來嘗試應對釋放后使用利用,我們也可以著眼于利用通常是如何執行的。如同在第 4 節中討論的,Unicode 字符串和大部分數據可控的分配(例如含 cbWndExtra 定義的窗口對象)對攻擊者來說是十分有用的。因此隔離這樣的分配可以用來阻止攻擊者為簡單地重新分配已釋放對象的內存而使用可伸縮的原型(例如字符串)。

5.2 空指針漏洞

為了應對 Windows 中的空指針漏洞我們需要進制用戶模式應用程序映射或控制零頁內容的能力。雖然有很多種方法處理這種問題,例如系統調用掛鉤(系統調用掛鉤不被微軟建議使用,并由于 Kernel Patch Protection 強制進行的完整性檢查而不能輕易在 64 位中使用)或頁表項(PTE)修改,但是使用虛擬地址描述符(AVD)似乎是一種更加合適的解決方案 [5]。由于 AVD 描述進程內存空間并提供給 Windows 用來正確設置頁表項的信息,所以可以用來以一種統一和通用的方式阻止零頁映射。然而,由于 32 位版本 Windows 的 NTVDM 子系統依賴于這種能力來正確支持 16 位可執行程序,阻止零頁映射也造成向后兼容成本的增加。

6. 備注

像我們在這篇文章中展示的,用戶模式回調似乎導致很多問題并在 win32k 中引入了很多漏洞。這在一定程度上是因為 win32k,或具體地說是窗口管理器,被設計來使用一種全局鎖機制(用戶臨界區段)來允許模塊是線程安全的。雖然在個案分析的基礎上應對這些漏洞足以作為一種短期的解決方案,但是 win32k 在某些點上需要大的改造,來更好地支持多核架構并在窗口管理方面提供更好的性能。在當前的設計中,同一會話中沒有兩個線程能夠同時處理它們的消息隊列,即使他們在兩個單獨的桌面上單獨的應用程序中。理想情況下,win32k 應該遵循 NT 執行體的更加一直的設計,并在每個對象或每個結構的基礎上執行互斥。

在緩解 win32k 中的利用以及 Windows 中的通用內核利用方面的重要的一步,是去除掉在用戶和內核模式之間的共享內存區段。那些共享內存區段歷來被視為對 win32k 不需要使用系統調用方面的優化,因此避免與它們相關的開銷。自從這種設計被決定以來,系統調用不再使用更慢的基于中斷的方式,因此性能的提升很可能是極小的。雖然在某些情況下,共享區段仍然是首選,但共享的信息應該被保持在最低限度。當前,win32k 子系統為對手提供了大量的內核地址空間信息,并且也在最近的 CSRSS 漏洞利用中開辟了所示的額外攻擊向量 [4]。因為子系統中的內存是進程間共享的而無視它們的特權級,攻擊者有能力從一個無特權進程中操作高特權進程的地址空間。

7. 結論

在這篇文章中,我們討論了有關 win32k 中用戶模式回調的很多挑戰和問題。尤其是,我們展示了窗口管理器的全局鎖設計不能很好地與用戶模式回調的概念相結合。雖然涉及圍繞用戶模式回調的使用的不充分驗證的大量漏洞已被應對,那些問題的一些復雜特性表明更多不易察覺的缺陷很可能仍舊存在于 win32k 中。這樣一來,為了實現緩解一些更猖獗的這類 BUG,我們總結性地提出一些觀點,作為對微軟以及終端用戶來說,能夠做什么來降低將來在 win32k 中可能面臨的攻擊的風險。

引用

[1] Edgar Barbosa: Windows Vista UIPI.

http://www.coseinc.com/en/index.php?rt=download&act=publication&file=Vista_UIPI.ppt.pdf

[2] Alex Ionescu: Inside Session 0 Isolation and the UI Detection Service.

http://www.alex-ionescu.com/?p=59

[3] ivanlef0u: You Failed!

http://www.ivanlef0u.tuxfamily.org/?p=68

[4] Matthew 'j00ru' Jurczyk: CVE-2011-1281: A story of a Windows CSRSS Privilege Escalation vulnerability.

http://j00ru.vexillium.org/?p=893

[5] Tarjei Mandt: Locking Down the Windows Kernel: Mitigating Null Pointer Exploitation.

http://mista.nu/blog/2011/07/07/mitigating-null-pointer-exploitation-on-windows/

[6] John McDonald, Chris Valasek: Practical Windows XP/2003 Heap Exploitation. Black Hat Brie?ng USA 2009.

https://www.blackhat.com/presentations/bh-usa-09/MCDONALD/BHUSA09-McDonald-WindowsHeap-PAPER.pdf

[7] Microsoft Security Bulletin MS11-034. Vulnerabilities in Windows Kernel-Mode Drivers Could Allow Elevation of Privilege.

http://www.microsoft.com/technet/security/bulletin/ms11-034.mspx

[8] Microsoft Security Bulletin MS11-054. Vulnerabilities in Windows Kernel-Mode Drivers Could Allow Elevation of Privilege.

http://www.microsoft.com/technet/security/bulletin/ms11-054.mspx

[9] Brett Moore: Heaps About Heaps.

http://www.insomniasec.com/publications/Heaps_About_Heaps.ppt

[10] MS Windows NT Kernel-mode User and GDI White Paper.

http://technet.microsoft.com/en-us/library/cc750820.aspx

[11] mxatone: Analyzing Local Privilege Escalations in Win32k. Uninformed Journal vol. 10.

http://uninformed.org/?v=10&a=2

[12] Chris Paget: Click Next to Continue: Exploits & Information about Shatter Attacks.

https://www.blackhat.com/presentations/bh-usa-03/bh-us-03-paget.pdf

[13] Chris Valasek: Understanding the Low Fragmentation Heap. Black Hat Brie?ngs USA 2010.

http://illmatics.com/Understanding_the_LFH.pdf

原文鏈接

http://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/575/