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

该漏洞是弹出菜单 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
该函数接收通过参数 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); // xxxMNCloseHierarchyThreadUnlock1();
}
函数 xxxMNDestroyHandler 的代码片段
接着函数判断目标弹出菜单对象的成员标志位 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(