CVE-2017-0263 是 Windows 操作系统 win32k
内核模块菜单管理组件中的一个 UAF(释放后重用)漏洞,据报道称该漏洞在之前与一个 EPS 漏洞被 APT28 组织组合攻击用来干涉法国大选。这篇文章将对用于这次攻击的样本的 CVE-2017-0263 漏洞部分进行一次简单的分析,以整理出该漏洞利用的运作原理和基本思路,并对 Windows 窗口管理器子系统的菜单管理组件进行简单的探究。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机。
在本分析中为了突出分析的重点,在对涉及的各个系统函数进行分析时,将与当前漏洞研究无关的调用语句进行忽略,只留意影响或可能影响漏洞触发逻辑的调用和赋值语句并对其进行分析和解释。
0x0 前言
这篇文章分析了发生在窗口管理器(User)子系统的菜单管理组件中的 CVE-2017-0263 UAF(释放后重用)漏洞。在函数 win32k!xxxMNEndMenuState
中释放全局菜单状态对象的成员域 pGlobalPopupMenu
指向的根弹出菜单对象时,没有将该成员域置零,导致该成员域仍旧指向已被释放的内存区域成为野指针,在后续的代码逻辑中存在该成员域指向的内存被读写访问或被重复释放的可能性。
在释放成员域 pGlobalPopupMenu
指向对象之后,函数 xxxMNEndMenuState
还将当前线程关联的线程信息对象成员域 pMenuState
重置,这导致大部分追踪和操作弹出菜单的接口将无法达成漏洞触发的条件。但在重置成员域 pMenuState
之前,函数中存在对全局菜单状态对象的成员域 uButtonDownHitArea
的解锁和释放,这个成员域存储当前鼠标按下位置所属的窗口对象(如果当前存在鼠标按下状态)指针。
如果用户进程先前通过利用技巧构造了特殊关联和属性的菜单窗口对象,那么从函数 xxxMNEndMenuState
释放成员域 pGlobalPopupMenu
到重置成员域 pMenuState
之前的这段时间,执行流将回到用户进程中,用户进程中构造的利用代码将有足够的能力改变当前弹出菜单的状态,致使执行流重新执行 xxxMNEndMenuState
函数,并对根弹出菜单对象的内存进行重复释放,导致系统 BSOD 的发生。
在内核第一次释放成员域 pGlobalPopupMenu
指向内存之后执行流回到用户进程时,在用户进程中通过巧妙的内存布局,使系统重新分配相同大小的内存区域以占用成员域 pGlobalPopupMenu
指向的先前释放的内存块,伪造新的弹出菜单对象并构造相关成员域。借助代码逻辑,实现对特定窗口对象的成员标志位 bServerSideWindowProc
的修改,使系统能够在内核中直接执行位于用户进程地址空间中的自定义窗口消息处理函数,得以通过内核上下文执行用户进程构造的利用代码,实现内核提权的目的。
0x1 原理
CVE-2017-0263 漏洞存在于 win32k
的窗口管理器(User)子系统中的菜单管理组件中。在内核函数 xxxMNEndMenuState
释放目标 tagMENUSTATE
结构体对象的成员域 pGlobalPopupMenu
指向对象的内存时,没有将该成员域置为空值。
在 win32k
模块中存在定义为 tagMENUSTATE
结构体类型的菜单状态 gMenuState
全局对象。在当前的操作系统环境下,该结构体的定义如下:
kd> dt win32k!tagMENUSTATE
+0x000 pGlobalPopupMenu : Ptr32 tagPOPUPMENU
+0x004 flags : Int4B
+0x008 ptMouseLast : tagPOINT
+0x010 mnFocus : Int4B
+0x014 cmdLast : Int4B
+0x018 ptiMenuStateOwner : Ptr32 tagTHREADINFO
+0x01c dwLockCount : Uint4B
+0x020 pmnsPrev : Ptr32 tagMENUSTATE
+0x024 ptButtonDown : tagPOINT
+0x02c uButtonDownHitArea : Uint4B
+0x030 uButtonDownIndex : Uint4B
+0x034 vkButtonDown : Int4B
+0x038 uDraggingHitArea : Uint4B
+0x03c uDraggingIndex : Uint4B
+0x040 uDraggingFlags : Uint4B
+0x044 hdcWndAni : Ptr32 HDC__
+0x048 dwAniStartTime : Uint4B
+0x04c ixAni : Int4B
+0x050 iyAni : Int4B
+0x054 cxAni : Int4B
+0x058 cyAni : Int4B
+0x05c hbmAni : Ptr32 HBITMAP__
+0x060 hdcAni : Ptr32 HDC__
结构体 tagMENUSTATE 的定义
菜单管理是 win32k
中最复杂的组件之一,菜单处理作为一个整体依赖于多种十分复杂的函数和结构体。例如,在创建弹出菜单时,应用程序调用 TrackPopupMenuEx
在菜单内容显示的位置创建菜单类的窗口。接着该菜单窗口通过一个系统定义的菜单窗口类过程 xxxMenuWindowProc
处理消息输入,用以处理各种菜单特有的信息。此外,为了追踪菜单如何被使用,win32k
也将一个菜单状态结构体 tagMENUSTATE
与当前活跃菜单关联起来。通过这种方式,函数能够知道菜单是否在拖拽操作中调用、是否在菜单循环中、是否即将销毁,等等。
菜单状态结构体用来存储与当前活跃菜单的状态相关的详细信息,包括上下文菜单弹出的坐标、关联的位图表面对象的指针、窗口设备上下文对象、之前的上下文菜单结构体的指针,以及其他的一些成员域。
在线程信息结构体 tagTHREADINFO
中也存在一个指向菜单状态结构体指针的 pMenuState
成员域:
kd> dt win32k!tagTHREADINFO -d pMenuState
+0x104 pMenuState : Ptr32 tagMENUSTATE
结构体 tagTHREADINFO 存在 pMenuState 成员域
当用户在操作系统中以点击鼠标右键或其他的方式弹出上下文菜单时,系统最终在内核中执行到 xxxTrackPopupMenuEx
函数。该函数调用 xxxMNAllocMenuState
函数来分配或初始化菜单状态结构体。
在函数 xxxMNAllocMenuState
中,系统将全局菜单状态对象 gMenuState
的所有成员域清空并对部分成员域进行初始化,然后将全局菜单状态对象的地址存储在当前线程信息对象的成员域 pMenuState
中。
menuState = (tagMENUSTATE *)&gMenuState;
[...]
memset(menuState, 0, 0x60u);
menuState->pGlobalPopupMenu = popupMenuRoot;
menuState->ptiMenuStateOwner = ptiCurrent;
menuState->pmnsPrev = ptiCurrent->pMenuState;
ptiCurrent->pMenuState = menuState;
if ( ptiNotify != ptiCurrent )
ptiNotify->pMenuState = menuState;
[...]
return menuState;
函数 xxxMNAllocMenuState 的代码片段
函数初始化了菜单状态结构体中的 pGlobalPopupMenu
/ ptiMenuStateOwner
和 pmnsPrev
成员。成员域 pGlobalPopupMenu
指针指向通过参数传入作为根菜单的弹出菜单结构体 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
结构体 tagPOPUPMENU 的定义
菜单状态结构体对象的成员域 ptiMenuStateOwner
指向当前线程的线程信息结构体对象。线程信息结构体对象中已存在的菜单状态结构体指针被存储在当前菜单状态结构体对象的 pmnsPrev
成员域中。
随后函数将菜单状态结构体的地址放置在通过参数传入的当前线程(和通知线程)的线程信息结构体 tagTHREADINFO
对象的成员域 pMenuState
中,并将菜单状态结构体的地址作为返回值返回给上级调用者函数。
当前线程信息对象和菜单状态对象的对应关系
当用户通过键鼠选择菜单项、或点击菜单范围之外的屏幕区域时,系统将向当前上下文菜单的窗口对象发送相关鼠标按下或菜单终止的事件消息。在菜单对象的类型为模态的情况下,这导致之前调用 xxxMNLoop
函数的线程退出菜单循环等待状态,使函数继续向后执行。
系统调用 xxxMNEndMenuState
函数来清理菜单状态结构体存储的信息与释放相关的弹出菜单对象和窗口对象。
ptiCurrent = gptiCurrent;
menuState = gptiCurrent->pMenuState;
if ( !menuState->dwLockCount )
{
MNEndMenuStateNotify(gptiCurrent->pMenuState);
if ( menuState->pGlobalPopupMenu )
{
if ( fFreePopup )
MNFreePopup(menuState->pGlobalPopupMenu);
else
*(_DWORD *)menuState->pGlobalPopupMenu &= 0xFFFEFFFF;
}
UnlockMFMWFPWindow(&menuState->uButtonDownHitArea);
UnlockMFMWFPWindow(&menuState->uDraggingHitArea);
ptiCurrent->pMenuState = menuState->pmnsPrev;
[...]
}
函数 xxxMNEndMenuState 的代码片段
在函数 xxxMNEndMenuState
中,系统从当前线程的线程信息对象中获取 pMenuState
成员域指向的菜单状态结构体对象。随后函数判断菜单信息结构体对象的成员域 pGlobalPopupMenu
是否为空,不为空则调用函数 MNFreePopup
释放该成员域指向的弹出菜单 tagPOPUPMENU
对象。在执行相应的预处理之后,函数 MNFreePopup
调用 ExFreePoolWithTag
释放传入的 tagPOPUPMENU
对象缓冲区。
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 的代码片段
这时问题就出现了:函数 xxxMNEndMenuState
在将菜单信息结构体对象的成员域 pGlobalPopupMenu
指向的弹出菜单对象释放之后,却没有将该成员域置为空值,这将导致该成员域指向的内存地址处于不可控的状态,并导致被复用的潜在问题。
0x2 追踪
在 user32.dll
模块中存在导出函数 TrackPopupMenuEx
用于在屏幕指定位置显示弹出菜单并追踪选择的菜单项。当用户进程调用该函数时,系统在内核中最终调用到 xxxTrackPopupMenuEx
函数处理弹出菜单操作。
菜单的对象
在本分析中将涉及到与菜单相关的对象:菜单对象,菜单层叠窗口对象和弹出菜单对象。
其中,菜单对象是菜单的实体,在内核中以结构体 tagMENU
实例的形式存在,用来描述菜单实体的菜单项、项数、大小等静态信息,但其本身并不负责菜单在屏幕中的显示,当用户进程调用 CreateMenu
等接口函数时系统在内核中创建菜单对象,当调用函数 DestroyMenu
或进程结束时菜单对象被销毁。
当需要在屏幕中的位置显示某菜单时,例如,用户在某窗口区域点击鼠标右键,在内核中系统将调用相关服务函数根据目标菜单对象创建对应的类型为 MENUCLASS
的菜单层叠窗口对象。菜单层叠窗口对象是窗口结构体 tagWND
对象的特殊类型,通常以结构体 tagMENUWND
的形式表示,负责描述菜单在屏幕中的显示位置、样式等动态信息,其扩展区域关联对应的弹出菜单对象。
弹出菜单对象 tagPOPUPMENU
作为菜单窗口对象的扩展对象,用来描述所代表的菜单的弹出状态,以及与菜单窗口对象、菜单对象、子菜单或父级菜单的菜单窗口对象等用户对象相互关联。
当某个菜单在屏幕中弹出时,菜单窗口对象和关联的弹出菜单对象被创建,当菜单被选择或取消时,该菜单将不再需要在屏幕中显示,此时系统将在适当时机销毁菜单窗口对象和弹出菜单对象。
弹出菜单
内核函数 xxxTrackPopupMenuEx
负责菜单的弹出和追踪。在该函数执行期间,系统调用 xxxCreateWindowEx
函数为即将被显示的菜单对象创建关联的类名称为 #32768
(MENUCLASS
) 的菜单层叠窗口对象。类型为 MENUCLASS
的窗口对象通常用 tagMENUWND
结构体表示,这类窗口对象在紧随基础的 tagWND
对象其后的位置存在 1 个指针长度的扩展区域,用来存储指向关联的 tagPOPUPMENU
对象指针。
pwndHierarchy = xxxCreateWindowEx(
0x181,
0x8000, // MENUCLASS
0x8000, // MENUCLASS
0,
0x80800000,
xLeft,
yTop,
100,
100,
(pMenu->fFlags & 0x40000000) != 0 ? pwndOwner : 0, // MNS_MODELESS
0,
pwndOwner->hModule,
0,
0x601u,
0);
函数 xxxTrackPopupMenuEx 创建 MENUCLASS 窗口对象
在函数 xxxCreateWindowEx
中分配窗口对象后,函数向该对象发送 WM_NCCREATE
等事件消息,并调用窗口对象指定的消息处理程序。类型为 MENUCLASS
的窗口对象指定的的消息处理程序是 xxxMenuWindowProc
内核函数。处理 WM_NCCREATE
消息时,函数创建并初始化与窗口对象关联的弹出菜单信息结构体 tagPOPUPMENU
对象,将菜单窗口 tagMENUWND
对象指针放入 tagPOPUPMENU->spwndPopupMenu
成员域中,并将弹出菜单 tagPOPUPMENU
对象指针放入关联窗口 tagMENUWND
对象末尾的指针长度的扩展区域中。
结构体 tagMENUWND 和 tagPOPUPMENU 对象的对应关系
在通过函数 xxxSendMessageTimeout
向窗口对象发送 WM_NCCREATE
等事件消息时,系统在调用对象指定的消息处理程序之前,还会调用 xxxCallHook
函数用来调用先前由用户进程设定的 WH_CALLWNDPROC
类型的挂钩处理程序。设置这种类型的挂钩会在每次线程将消息发送给窗口对象之前调用。
if ( (LOBYTE(gptiCurrent->fsHooks) | LOBYTE(gptiCurrent->pDeskInfo->fsHooks)) & 0x20 )
{
v22 = pwnd->head.h;
v20 = wParam;
v19 = lParam;
v21 = message;
v23 = 0;
xxxCallHook(0, 0, &v19, 4); // WH_CALLWNDPROC
}
函数 xxxSendMessageTimeout 调用 xxxCallHook 函数
接下来函数 xxxTrackPopupMenuEx
调用 xxxMNAllocMenuState
来初始化菜单状态结构体的各个成员域,并将前面创建的弹出菜单 tagPOPUPMENU
对象作为当前的根弹出菜单对象,其指针被放置在菜单状态结构体的成员域 pGlobalPopupMenu
中。
menuState = xxxMNAllocMenuState(ptiCurrent, ptiNotify, popupMenu);
函数 xxxTrackPopupMenuEx 初始化菜单状态结构体
接下来函数调用 xxxSetWindowPos
函数以设置目标菜单层叠窗口在屏幕中的位置并将其显示在屏幕中。在函数 xxxSetWindowPos
执行期间,相关窗口位置和状态已完成改变之后,系统在函数 xxxEndDeferWindowPosEx
中调用 xxxSendChangedMsgs
以发送窗口位置已改变的消息。
xxxSetWindowPos(
pwndHierarchy,
(((*((_WORD *)menuState + 2) >> 8) & 1) != 0) - 1,
xLParam,
yLParam,
0,
0,
~(0x10 * (*((_WORD *)menuState + 2) >> 8)) & 0x10 | 0x241);
函数 xxxTrackPopupMenuEx 显示根菜单窗口对象
在函数 xxxSendChangedMsgs
中,系统根据设置的 SWP_SHOWWINDOW
状态标志,为当前的目标菜单层叠窗口对象创建并添加关联的阴影窗口对象。两个窗口对象的关联关系在函数 xxxAddShadow
中被添加到 gpshadowFirst
阴影窗口关联表中。
从函数 xxxSetWindowPos
中返回后,函数 xxxTrackPopupMenuEx
调用 xxxWindowEvent
函数以发送代表“菜单弹出开始”的 EVENT_SYSTEM_MENUPOPUPSTART
事件通知。
xxxWindowEvent(6u, pwndHierarchy, 0xFFFFFFFC, 0, 0);
函数 xxxTrackPopupMenuEx 发送菜单弹出开始的事件通知
如果先前在用户进程中设置了包含这种类型事件通知范围的窗口事件通知处理函数,那么系统将在线程消息循环处理期间分发调用这些通知处理函数。
接下来菜单对象类型为模态的情况下线程将会进入菜单消息循环等待状态,而非模态的情况将会返回。
一图以蔽之:
函数 xxxTrackPopupMenuEx 的简略执行流
bServerSideWindowProc
窗口结构体 tagWND
对象的成员标志位 bServerSideWindowProc
是一个特殊标志位,该标志位决定所属窗口对象的消息处理函数属于服务端还是客户端。当函数 xxxSendMessageTimeout
即将调用目标窗口对象的消息处理函数以分发消息时,会判断该标志位是否置位。
if ( *((_BYTE *)&pwnd->1 + 2) & 4 ) // bServerSideWindowProc
{
IoGetStackLimits(&uTimeout, &fuFlags);
if ( &fuFlags - uTimeout < 0x1000 )
return 0;
lRet = pwnd->lpfnWndProc(pwnd, message, wParam, lParam);
if ( !lpdwResult )
return lRet;
*(_DWORD *)lpdwResult = lRet;
}
else
{
xxxSendMessageToClient(pwnd, message, wParam, lParam, 0, 0, &fuFlags);
[...]
}
函数 xxxSendMessageTimeout 执行窗口对象消息处理函数的逻辑
如果该标志位置位,则函数将直接使当前线程在内核上下文调用目标窗口对象的消息处理函数;否则,函数通过调用函数 xxxSendMessageToClient
将消息发送到客户端进行处理,目标窗口对象的消息处理函数将始终在用户上下文调用和执行。
诸如菜单层叠窗口对象之类的特殊窗口对象拥有专门的内核模式消息处理函数,因此这些窗口对象的成员标志位 bServerSideWindowProc
在对象创建时就被置位。而普通窗口对象由于只指向默认消息处理函数或用户进程自定义的消息处理函数,因此该标志位往往不被置位。
如果能够通过某种方式将未置位标志位 bServerSideWindowProc
的窗口对象的该标志位置位,那么该窗口对象指向的消息处理函数也将直接在内核上下文中执行。
阴影窗口
在 Windows XP 及更高系统的 win32k
内核模块中,系统为所有带有 CS_DROPSHADOW
标志的窗口对象创建并关联对应的类名称为 SysShadow
的阴影窗口对象,用来渲染原窗口的阴影效果。内核中存在全局表 win32k!gpshadowFirst
用以记录所有阴影窗口对象与原窗口对象的关联关系。函数 xxxAddShadow
用来为指定的窗口创建阴影窗口对象,并将对应关系写入 gpshadowFirst
全局表中。
全局表 gpshadowFirst
以链表的形式保存阴影窗口的对应关系。链表的每个节点存储 3 个指针长度的成员域,分别存储原窗口和阴影窗口的对象指针,以及下一个链表节点的指针。每个新添加的关系节点将始终位于链表的首个节点位置,其地址被保存在 gpshadowFirst
全局变量中。
全局变量 gpshadowFirst 指向阴影窗口关联链表
相应地,当阴影窗口不再需要时,系统调用 xxxRemoveShadow
来将指定窗口的阴影窗口关联关系移除并销毁该阴影窗口对象,函数根据通过参数传入的原窗口对象的指针在链表中查找第一个匹配的链表节点,从链表中取出节点并释放节点内存缓冲区、销毁阴影窗口对象。
子菜单
如果当前在屏幕中显示的菜单中存在子菜单项,那么当用户通过鼠标按键点击等方式选择子菜单项时,系统向子菜单项所属的菜单窗口对象发送 WM_LBUTTONDOWN
鼠标左键按下的消息。如果菜单为非模态(MODELESS
)类型,内核函数 xxxMenuWindowProc
接收该消息并传递给 xxxCallHandleMenuMessages
函数。
函数 xxxCallHandleMenuMessages
负责像模态窗口的消息循环那样处理非模态窗口对象的消息。在函数中,系统根据通过参数 lParam
传入的相对坐标和当前窗口在屏幕上的坐标来计算鼠标点击的实际坐标,并向下调用 xxxHandleMenuMessages
函数。
函数将计算的实际坐标点传入 xxxMNFindWindowFromPoint
函数查找坐标点坐落的在屏幕中显示的窗口,并将查找到的窗口对象指针写入菜单状态结构体的成员域 uButtonDownHitArea
中。当该值确实是窗口对象时,函数向该窗口对象发送 MN_BUTTONDOWN
鼠标按下的消息。
接着执行流又进入函数 xxxMenuWindowProc
并调用函数 xxxMNButtonDown
以处理 MN_BUTTONDOWN
消息。
case 0x1EDu:
if ( wParam < pmenu->cItems || wParam >= 0xFFFFFFFC )
xxxMNButtonDown(popupMenu, menuState, wParam, 1);
return 0;
函数 xxxMenuWindowProc 调用 xxxMNButtonDown 函数
函数 xxxMNButtonDown
调用 xxxMNSelectItem
函数以根据鼠标按下区域选择菜单项并存储在当前弹出菜单对象的成员域 posSelectedItem
中,随后调用函数 xxxMNOpenHierarchy
以打开新弹出的层叠菜单。
在函数 xxxMNOpenHierarchy
执行期间,系统调用函数 xxxCreateWindowEx
创建新的类名称为 MENUCLASS
的子菜单层叠窗口对象,并将新创建的子菜单窗口对象关联的弹出菜单结构体 tagPOPUPMENU
对象插入弹出菜单对象延迟释放链表中。
函数将新分配的子菜单窗口对象指针写入当前菜单窗口对象关联的弹出菜单信息结构体 tagPOPUPMENU
对象的成员域 spwndNextPopup
中,并将当前菜单窗口对象指针写入新分配的菜单窗口对象关联的 tagPOPUPMENU
对象的成员域 spwndPrevPopup
中,使新创建的弹出菜单对象成为当前菜单对象的子菜单。
新创建的子菜单窗口和原菜单窗口 tagMENUWND 对象的对应关系
函数将当前菜单窗口对象的弹出菜单信息结构体 tagPOPUPMENU
对象的标志成员域 fHierarchyDropped
标志置位,这个标志位表示当前菜单对象已弹出子菜单。
接下来函数调用 xxxSetWindowPos
以设置新的菜单层叠窗口在屏幕中的位置并将其显示在屏幕中,并调用函数 xxxWindowEvent
发送 EVENT_SYSTEM_MENUPOPUPSTART
事件通知。新菜单窗口对象对应的阴影窗口会在这次调用 xxxSetWindowPos
期间创建并与菜单窗口对象关联。
简要执行流如下:
点击子菜单项以弹出子菜单时的简要执行流
终止菜单
在用户进程中可以通过多种接口途径触达 xxxMNEndMenuState
函数调用,例如向目标菜单的窗口对象发送 MN_ENDMENU
消息,或调用 NtUserMNDragLeave
系统服务等。
当某调用者向目标菜单窗口对象发送 MN_ENDMENU
消息时,系统在菜单窗口消息处理函数 xxxMenuWindowProc
中调用函数 xxxEndMenuLoop
并传入当前线程关联的菜单状态结构体对象和其成员域 pGlobalPopupMenu
指向的根弹出菜单对象指针作为参数以确保完整的菜单对象被终止或取消。如果菜单对象是非模态类型的,那么函数接下来在当前上下文调用函数 xxxMNEndMenuState
清理菜单状态信息并释放相关对象。
menuState = pwnd->head.pti->pMenuState;
[...]
LABEL_227: // EndMenu
xxxEndMenuLoop(menuState, menuState->pGlobalPopupMenu);
if ( menuState->flags & 0x100 )
xxxMNEndMenuState(1);
return 0;
函数 xxxMenuWindowProc 处理 MN_ENDMENU 消息
函数 xxxEndMenuLoop
执行期间,系统调用 xxxMNDismiss
并最终调用到 xxxMNCancel
函数来执行菜单取消的操作。
int __stdcall xxxMNDismiss(tagMENUSTATE *menuState)
{
return xxxMNCancel(menuState, 0, 0, 0);
}
函数 xxxMNDismiss 调用 xxxMNCancel 函数
函数 xxxMNCancel
调用 xxxMNCloseHierarchy
函数来关闭当前菜单对象的菜单层叠状态。
popupMenu = pMenuState->pGlobalPopupMenu;
[...]
xxxMNCloseHierarchy(popupMenu, pMenuState);
函数 xxxMNCancel 调用 xxxMNCloseHierarchy 函数
函数 xxxMNCloseHierarchy
判断当前通过参数传入的弹出菜单 tagPOPUPMENU
对象成员域 fHierarchyDropped
标志位是否置位,如果未被置位则表示当前弹出菜单对象不存在任何弹出的子菜单,那么系统将使当前函数直接返回。
接下来函数 xxxMNCloseHierarchy
获取当前弹出菜单对象的成员域 spwndNextPopup
存储的指针,该指针指向当前弹出菜单对象所弹出的子菜单的窗口对象。函数通过 xxxSendMessage
函数调用向该菜单窗口对象发送 MN_CLOSEHIERARCHY
消息,最终在消息处理函数 xxxMenuWindowProc
中接收该消息并对目标窗口对象关联的弹出菜单对象调用 xxxMNCloseHierarchy
以处理关闭子菜单的菜单对象菜单层叠状态的任务。
popupMenu = *(tagPOPUPMENU **)((_BYTE *)pwnd + 0xb0);
menuState = pwnd->head.pti->pMenuState;
[...]
case 0x1E4u:
xxxMNCloseHierarchy(popupMenu, menuState);
return 0;
函数 xxxMenuWindowProc 处理 MN_CLOSEHIERARCHY 消息
函数 xxxSendMessage
返回之后,接着函数 xxxMNCloseHierarchy
调用 xxxDestroyWindow
函数以尝试销毁弹出的子菜单的窗口对象。需要注意的是,这里尝试销毁的是弹出的子菜单的窗口对象,而不是当前菜单的窗口对象。
在函数 xxxDestroyWindow
执行期间,系统调用函数 xxxSetWindowPos
以隐藏目标菜单窗口对象在屏幕中的显示。
dwFlags = 0x97;
if ( fAlreadyDestroyed )
dwFlags = 0x2097;
xxxSetWindowPos(pwnd, 0, 0, 0, 0, 0, dwFlags);
函数 xxxDestroyWindow 隐藏目标窗口对象的显示
在函数 xxxSetWindowPos
执行后期,与当初创建菜单窗口对象时相对应地,系统调用函数 xxxSendChangedMsgs
发送窗口位置已改变的消息。在该函数中,系统根据设置的 SWP_HIDEWINDOW
状态标志,通过调用函数 xxxRemoveShadow
在 gpshadowFirst
阴影窗口关联表中查找第一个与目标菜单窗口对象关联的阴影窗口关系节点,从链表中移除查找到的关系节点并销毁该阴影窗口对象。
接下来执行流从函数 xxxDestroyWindow
中进入函数 xxxFreeWindow
以执行对目标窗口对象的后续销毁操作。
函数根据目标窗口对象的成员域 fnid
的值调用对应的消息处理包装函数 xxxWrapMenuWindowProc
并传入 WM_FINALDESTROY
消息参数,最终在函数 xxxMenuWindowProc
中接收该消息并通过调用函数 xxxMNDestroyHandler
对目标弹出菜单对象执行清理相关数据的任务。在该函数中,目标弹出菜单对象的成员标志位 fDestroyed
和根弹出菜单对象的成员标志位 fFlushDelayedFree
被置位:
*(_DWORD *)popupMenu |= 0x8000u;
[...]
if ( *((_BYTE *)popupMenu + 2) & 1 )
{
popupMenuRoot = popupMenu->ppopupmenuRoot;
if ( popupMenuRoot )
*(_DWORD *)popupMenuRoot |= 0x20000u;
}
函数 xxxMNDestroyHandler 置位相关成员标志位
接着函数 xxxFreeWindow
对目标窗口对象再次调用函数 xxxRemoveShadow
以移除其阴影窗口对象的关联。如果先前已将目标窗口对象的所有阴影窗口关联移除,则函数 xxxRemoveShadow
将在关系表中无法查找到对应的关联节点而直接返回。
if ( pwnd->pcls->atomClassName == gatomShadow )
CleanupShadow(pwnd);
else
xxxRemoveShadow(pwnd);
函数 xxxFreeWindow 再次移除阴影窗口对象
函数在执行一些对象的释放操作和解除锁定操作之后向上级调用者函数返回。此时由于锁计数尚未归零,因此目标窗口对象仍旧存在于内核中并等待后续的操作。
函数 xxxDestroyWindow
返回后,执行流回到函数 xxxMNCloseHierarchy
中。接着函数对当前弹出菜单对象的成员域 spwndNextPopup
指向的子菜单窗口对象解锁并将成员域置空,然后将当前弹出菜单对象关联的菜单窗口对象带赋值锁地赋值给根弹出菜单对象的成员域 spwndActivePopup
中使当前窗口对象成为的活跃弹出菜单窗口对象,这导致原本锁定在成员域 spwndActivePopup
中的子菜单窗口对象解锁并使其锁计数继续减小。
HMAssignmentLock(
(_HEAD **)&popupMenu->ppopupmenuRoot->spwndActivePopup,
(_HEAD *)popupMenu->spwndPopupMenu);
函数 xxxMNCloseHierarchy 使当前窗口对象成为的活跃弹出菜单窗口对象
执行流从函数 xxxMNCloseHierarchy
返回到函数 xxxMNCancel
中,系统根据当前弹出菜单对象的成员标志位 fIsTrackPopup
选择调用 xxxDestroyWindow
以尝试销毁当前的菜单窗口对象。弹出菜单结构体的该成员标志位只在最开始通过函数 xxxTrackPopupMenuEx
创建根菜单窗口对象时对关联的弹出菜单对象置位。
接下来执行流返回到函数 xxxMenuWindowProc
中,函数对非模态类型的菜单对象调用 xxxMNEndMenuState
以清理菜单状态信息并释放相关对象。
菜单选择或取消时的简要执行流
弹出菜单对象延迟释放链表
在弹出菜单结构体 tagPOPUPMENU
中存在成员域 ppmDelayedFree
,该成员域用来将所有被标记为延迟释放状态的弹出菜单对象连接起来,以便在菜单的弹出状态终止时将所有弹出菜单对象统一销毁。
线程关联的菜单状态 tagMENUSTATE
对象的成员域 pGlobalPopupMenu
指向的是根弹出菜单对象,根弹出菜单对象的成员域 ppmDelayedFree
作为弹出菜单对象延迟释放链表的入口,指向链表的第一个节点。后续的每个被指向的弹出菜单对象的成员域 ppmDelayedFree
将指向下一个链表节点对象。
在函数 xxxMNOpenHierarchy
中,函数将新创建的子菜单窗口对象关联的弹出菜单结构体 tagPOPUPMENU
对象插入弹出菜单对象延迟释放链表。新的弹出菜单对象被放置在链表的起始节点位置,其地址被存储在根弹出菜单对象的成员域 ppmDelayedFree
中,而原本存储于根弹出菜单成员域 ppmDelayedFree
中的地址被存储在新的弹出菜单对象的成员域 ppmDelayedFree
中。
新的弹出菜单对象被插入弹出菜单对象延迟释放链表
xxxMNEndMenuState
在函数 xxxMNEndMenuState
执行时,系统调用函数 MNFreePopup
来释放由当前菜单状态 tagMENUSTATE
对象的成员域 pGlobalPopupMenu
指向的根弹出菜单对象。
函数 MNFreePopup
在一开始判断通过参数传入的目标弹出菜单对象是否为当前的根弹出菜单对象,如果是则调用函数 MNFlushDestroyedPopups
以遍历并释放其成员域 ppmDelayedFree
指向的弹出菜单对象延迟释放链表中的各个弹出菜单对象。
函数 MNFlushDestroyedPopups
遍历链表中的每个弹出菜单对象,并为每个标记了标志位 fDestroyed
的对象调用 MNFreePopup
函数。标志位 fDestroyed
当初在调用函数 xxxMNDestroyHandler
时被置位。
ppmDestroyed = popupMenu;
for ( i = &popupMenu->ppmDelayedFree; *i; i = &ppmDestroyed->ppmDelayedFree )
{
ppmFree = *i;
if ( *(_DWORD *)*i & 0x8000 )
{
ppmFree = *i;
*i = ppmFree->ppmDelayedFree;
MNFreePopup(ppmFree);
}
[...]
}
函数 MNFlushDestroyedPopups 遍历延迟释放链表
在函数 MNFlushDestroyedPopups
返回之后,函数 MNFreePopup
调用 HMAssignmentUnlock
函数解除 spwndPopupMenu
等各个窗口对象成员域的赋值锁。
在 Windows 内核中,所有的窗口对象起始位置存在成员结构体 HEAD
对象,该结构体存储句柄值(h
)的副本,以及锁计数(cLockObj
),每当对象被使用时其值增加;当对象不再被特定的组件使用时,它的锁计数减小。在锁计数达到零的时候,窗口管理器知道该对象不再被系统使用然后将其释放。
函数 HMAssignmentUnlock
被用来解除先前针对指定对象的实施的带赋值锁的引用,并减小目标对象的锁计数。当目标对象的锁计数减小到 0
时,系统将调用函数 HMUnlockObjectInternal
销毁该对象。
bToFree = head->cLockObj == 1;
--head->cLockObj;
if ( bToFree )
head = HMUnlockObjectInternal(head);
return head;
函数 HMUnlockObject 判断需要销毁的目标对象
函数 HMUnlockObjectInternal
通过目标对象的句柄在全局共享信息结构体 gSharedInfo
对象的成员域 aheList
指向的会话句柄表中找到该对象的句柄表项,然后通过在句柄表项中存储的句柄类型在函数 HMDestroyUnlockedObject
中调用索引在全局句柄类型信息数组 gahti
中的对象销毁函数。如果当前被销毁的目标对象类型是窗口对象,这将调用到内核函数 xxxDestroyWindow
中。
在函数 MNFreePopup
的末尾,由于已完成对各个成员域的解锁和释放,系统调用函数 ExFreePoolWithTag
释放目标弹出菜单 tagPOPUPMENU
对象。
通过分析代码可知,函数 xxxMNEndMenuState
在调用函数 MNFreePopup
释放菜单信息结构体的各个成员域之后,会将当前菜单状态对象的成员域 pmnsPrev
存储的前菜单状态对象指针赋值给当前线程信息结构体对象的成员域 pMenuState
指针,而通常情况下 pmnsPrev
的值为 0
。
kd> ub
win32k!xxxMNEndMenuState+0x50:
93a96022 8b4620 mov eax,dword ptr [esi+20h]
93a96025 898704010000 mov dword ptr [edi+104h],eax
kd> r eax
eax=00000000
函数 xxxMNEndMenuState 重置线程信息结构体 pMenuState 成员域
然而在菜单弹出期间,系统在各个追踪弹出菜单的函数或系统服务中都是通过线程信息对象的成员域 pMenuState
指针来获取菜单状态的,如果该成员域被赋值为其他值,就将导致触发漏洞的途径中某个节点直接失败而返回,造成漏洞利用失败。因此想要重新使线程执行流触达 xxxMNEndMenuState
函数中释放当前 tagPOPUPMENU
对象的位置以实现对目标漏洞的触发,则必须在系统重置线程信息对象的成员域 pMenuState
之前的时机进行。
在函数释放成员域 pGlobalPopupMenu
指向的根弹出菜单对象和重置线程信息对象的成员域 pMenuState
之间,只有两个函数调用:
UnlockMFMWFPWindow(&menuState->uButtonDownHitArea);
UnlockMFMWFPWindow(&menuState->uDraggingHitArea);
菜单状态结构体的成员域 uButtonDownHitArea
和 uDraggingHitArea
存储当前鼠标点击坐标位于的窗口对象指针和鼠标拖拽坐标位于的窗口对象指针。函数通过调用 UnlockMFMWFPWindow
函数解除对这两个成员域的赋值锁。
函数 UnlockMFMWFPWindow
在对目标参数进行简单校验之后调用 HMAssignmentUnlock
函数执行具体的解锁操作。
函数 xxxMNEndMenuState 的简要执行流
聚焦 uButtonDownHitArea
成员域,该成员域存储当前鼠标按下的坐标区域所属的窗口对象地址,当鼠标按键抬起时系统解锁并置零该成员域。因此,需要在系统处理鼠标按下消息期间,用户进程发起菜单终止的操作,以使执行流进入函数 xxxMNEndMenuState
并执行到解锁成员域 uButtonDownHitArea
的位置时,该成员域中存储合法的窗口对象的地址。
系统在销毁该窗口对象期间,会同时销毁与该窗口对象关联的阴影窗口对象。阴影窗口对象不带有专门的窗口消息处理函数,因此可以在用户进程中将窗口对象的消息处理函数成员域篡改为由用户进程自定义的消息处理函数,在自定义函数中,再次触发菜单终止的任务,致使漏洞成功触发。
链接
[0] 本分析的 POC 下载
https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2017-0263/x86.cpp
[1] Kernel Attacks through User-Mode Callbacks
http://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf
[2] 从 Dump 到 POC 系列一: Win32k 内核提权漏洞分析
http://blogs.360.cn/blog/dump-to-poc-to-win32k-kernel-privilege-escalation-vulnerability/
[3] TrackPopupMenuEx function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms648003(v=vs.85).aspx
[4] sam-b/windows_kernel_address_leaks
https://github.com/sam-b/windows_kernel_address_leaks
[5] Sednit adds two zero-day exploits using ‘Trump’s attack on Syria’ as a decoy
[6] EPS Processing Zero-Days Exploited by Multiple Threat Actors
https://www.fireeye.com/blog/threat-research/2017/05/eps-processing-zero-days.html