最新消息:图 床

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

COOL IAM 167浏览 0评论

作者: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 緩衝區重新分配

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

[6] John McDonald, Chris Valasek: Practical Windows XP/2003 Heap Exploitation. Black Hat Briefing 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 Briefings USA 2010.

http://illmatics.com/Understanding_the_LFH.pdf

原文鏈接

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


转载请注明:IAMCOOL » 通過 Windows 用戶模式回調實施的內核攻擊

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x