0X00 前言
自从上次分析了CVE-2015-2546后并阅读了win32k用户回调的知识后 在小刀师傅的博客上看到了有关这个漏洞的相关知识 是一个菜单管理相关的漏洞 本来打算复习一下知识的 但着实是忍不住分析
0X01 漏洞描述
CVE-2017-0263是一个win32k的菜单管理组件中的一个UAF漏洞 函数win32k!xxxMNEndMenuState中释放全局菜单状态对象的成员域pGlobalPopupMenu
指向的根弹出菜单对象时 没有将该成员域置零 导致该成员仍然指向已被释放的内存区域 即有可能再次使用
0X02 分析环境
目标系统:Windows 7 32位
调试器:WinDbg
反汇编器:IDA Pro
0X03 漏洞分析
关于窗口の一些事
弹出菜单
xxxTrackPopupMenuEx负责菜单的弹出和追踪 调用xxxCreateWindowEx函数为即将被显示的菜单对象创建关联的类名称为#32768(MENUCLASS)的菜单窗口对象 类型为MENUCLASS 类型为MENUCLASS的窗口对象指定的的消息处理程序是 xxxMenuWindowProc 内核函数
1 | int __stdcall xxxTrackPopupMenuEx(int a1, int a2, int xLeft, int yTop, PVOID P, int a6) |
不过这其中有很多细节 我们再展开的详细一些 大致如此
1 | NtUserTrackPopupMenuEx |
销毁菜单
使用xxxMNEndMenuState函数终止菜单 可通过多种途径到达
1 | -->xxxMenuWindowProc//接收MN_ENDMENU(0x1F3) 消息值开始销毁 |
在执行完这一切后 由于锁计数尚未归零 因此目标窗口对象仍旧存在于内核中并等待后续的操作
1 | -->xxxDestroyWindow执行返回后回到下面 |
xxxMNEndMenuState
在函数xxxMNEndMenuState执行时 系统调用函数MNFreePopup来释放由当前菜单状态tagMENUSTATE对象的成员域pGlobalPopupMenu指向的根弹出菜单对象
函数MNFreePopup在一开始判断通过参数传入的目标弹出菜单对象是否为当前的根弹出菜单 如果是则调用函数MNFlushDestroyedPopups以遍历并释放成员域ppmDelayedFree 指向的弹出菜单对象延迟释放链表中的各个弹出菜单对象
1 | void __stdcall MNFreePopup(PVOID P) |
函数MNFlushDestroyedPopups遍历链表中的每个弹出菜单对象 并标记标志位fDestroyed的对象调用MNFreePopup函数 标志位fDestroyed当初在调用函数xxxMNDestroyHandler时被置位
1 | int __stdcall MNFlushDestroyedPopups(int a1, int a2) |
函数返回后MNFreePopup函数调用HMAssignmentUnlock函数解除spwndPopupMenu等各个窗口的赋值锁 因为在内核中没和窗口对象起始位置存在成员结构体HEAD对象 每当结构体被使用时 锁计数增加 当对象不再被特定组件使用时 锁计数减小 数值为0时 即该对象不再被使用会被释放 调用HMUnlockObjectInternal函数销毁该对象
在函数MNFreePopup的末尾 调用ExFreePoolWithTag函数释放目标弹出菜单tagPOPMENU对象
函数xxxMNEndMenuState在调用函数MNFreePopup释放弹出菜单信息结构体的各个成员域之后 会将当前菜单状态对象的成员域pmnsPrev存储的前菜单状态对象指针赋值给当前线程信息结构体对象的成员域pMenuState指针 通常情况下pmnsPrev的值为0
在弹出菜单期间 系统在各个追踪弹出菜单的函数或系统服务中都是通过线程信息对象的成员域pMenuState指针来获取菜单状态的 如果赋值为其他值 就会导致触发漏洞失败 所以抵达xxxMNEndMenuState必须在系统充值pMenuState之前进行
而在释放函数释放成员域 pGlobalPopupMenu指向的根弹出菜单对象和重置线程信息对象的成员域 pMenuState之间 只有两个函数调用
1 | UnlockMFMWFPWindow(&menuState->uButtonDownHitArea); //uButtonDownHitArea存储鼠标点击坐标为u与窗口对象指针 |
关于触发の一些事
触发前
首先已经得到漏洞点出在win32k!xxxMNEndMenuState函数中 看一些关于这个函数的事情 这个函数会调用MNFreePopup函数 上面也解释过
1 | void __stdcall MNFreePopup(PVOID P) |
在函数的最后将P指针释放后 并没有对指针置零 所以导致野指针的出现 出现了漏洞 crash的思路就是控制程序让根菜单对象二次释放
现在的问题是如何二次释放?
在弹出菜单的时候 有一个阴影窗口的概念 当我们创建每个窗口的时候 会产生与之对应的阴影窗口 每个阴影窗口也都有tagWND 但阴影窗口没有专门的窗口消息处理函数 所以我们可以在用户进程中将窗口对象的消息处理函数成员域改为由用户进程自定义处理消息处理函数 在自定义窗口中 再次触发菜单终止的任务 就可以导致二次释放
阴影窗口使用xxxAddShadow来创建对象(或许我该在上面窗口那部分讲 但总归是觉得放在这里好些)使用断点查看阴影窗口的创建过程 使用xxxTrackPopupMenuEx函数来创建 其中会调用xxxCreateWindowEx 而hook就是在调用xxxCreateWindowEx过程中
1 | ba e1 win32k!xxxAddShadow |
在函数调用中 使用xxxCreateWindowEx创建窗口对象成功后 函数向该窗口对象发送WM_MCCREATE消息 这一步通过xxxSendMessage函数实现 而xxxSendMessage函数又会调用xxxSendMessageTimeout函数 而在调用xxxSendMessageTimeout之前 会调用xxxCallHook函数来调用先前由用户进程设定的WH_CALLWNDPROC类型的挂钩处理程序 则我们可以将其执行到我们的挂钩函数中进程Hook
触发
emmm 使用小刀师傅的poc查看函数的执行流 代码如下:
1 | #include <stdio.h> |
至此 触发代码分析完毕~
运行之后WinDbg断下 使用!analyze
分析漏洞原因 然后kv查看函数栈回溯 我们可以观察到同一块地址被释放了两次
触发成功 我们来整理一下
1 | -->创建两个弹出菜单 第二个为第一个的子菜单 |
tag:这里需要解释一下 在我们自定义的阴影窗口中明明没有调用xxxMNEndMenuState函数却为什么说调用 先来看看这段代码
1 | LRESULT WINAPI |
NtUserMNDragLeave函数系统在进行一系列的判断和调用之后 最终在函数xxxUnlockMenuState中调用xxxMNEndMenuState函数:
1 | int __stdcall NtUserMNDragLeave() |
0X04 漏洞利用
现在唯一知道的可以利用点就是在消除阴影窗口对象的时候 会调用我们的自定义阴影窗口处理函数 所以我们只能从这个阴影窗口对象入手
代码の执行过程
shellcode的分配
首先是shellcode地址的分配 使用了结构体存储利用相关的数据
1 | typedef struct _SHELLCODE { |
在用户空间分配完整内存页RWX内存块存储shellcode的相关内容 并将shellcode拷贝到成员域pfWindProc起始的内存地址
1 | pvShellCode = (PSHELLCODE)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); |
主体函数xxTrackExploitEx来控制流程
创建窗口
接下来执行xxTrackExploitEx函数 调用CreateWindowEx函数来创建大量的窗口对象 然后伪造tagPOPUPEMENU由后面的实现
1 | for (INT i = 0; i < 0x100; i++) |
获取tagCLS
代码接下来获取tagCLS地址并存储在结构体SHELLCODE对象中
1 | for (INT i = 0; i < iWindowCount; i++) |
调用VirtualProtect函数
然后调用 VirtualProtect函数在调用进程的虚拟地址空间中更改对提交页面区域的保护
1 | DWORD fOldProtect = 0; |
伪造弹出菜单成员域
接下来在xxTrackExploitEx函数中调用了xxRegisterWindowClassW函数来注册窗口类和再次xxCreateWindowExW函数来创建窗口对象
前面伪造的tagPOPUPMENU对象重新占用了先前释放的根菜单对象的内存区域 并且其各个成员域在利用代码中分配时可以实施完全控制 但前面并未对成员域进行有效设置 则在执行xxxMNEndMenuState中解锁各个指针成员域指向的对象时会触发缺页异常等错误 所以就有了下面0x200的扩展区域
这里第二次创建新的载体窗口对象hWindowMain具有0x200字节大小的扩展区域 在利用代码中将用来伪造各种相关的内核用户对象 以使系统重新执行xxxMNEndMenuState期间 执行流能够稳定的执行
1 | xxRegisterWindowClassW(L"WNDCLASSMAIN", 0x000); |
通过HMValidateHandle内核对象地址泄露技术获取载体对象的tagWND内核地址
1 | PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hWindowHunt); |
tagWND的头部是一个THRDESKHEAD成员结构体对象 完整的如下 pSelf指向所属用户对象的内核首地址 加上tagWND结构体的大小定位到当前窗口对象扩展的内核地址
1 | kd> dt win32k!_THRDESKHEAD |
然后调用SetWindowLongW函数来更改窗口的属性 将载体窗口对象的扩展区域预留4字节 将剩余0x1FC字节的内存区域全部填充为扩展区域+0x04字节偏移的地址 填充的数值将作为各种伪造对象的句柄 引用计数或对象指针成员域 伪造的原因是xxxMNEndMenuState在执行的初始阶段调用函数MNEndMenuStateNotify用来通知窗口对象所属线程和当前菜单状态所属线程不同的情况下 清理了通知线程的线程信息对象的成员域pMenuState数值 伪造的tagPOPUPMENU对象已覆盖原有数据 所以要继续伪造对象
1 | PBYTE pbExtra = head->deskhead.pSelf + 0xb0 + 4; |
这两行代码单独拿出来说 成员标志位bServerSideWindowProc位于tagWND对象标志成员域的第18比特位 之前的两个标志位是bDialogWindow和bHasCreatestructName标志位 而bDialogWindow是bServerSideWindowProc所在字节的起始位置比特位 bServerSideWindowProc是用来决定所属窗口对象的消息处理函数属于服务端还是客户端 前面也说到过 该位置置位会使当前线程在内核上下文调用目标窗口对象消息处理函数
1 | kd> dt win32k!tagWND |
所以我们在利用代码填充载体窗口对象扩展区域内存期间 增加通过内核地址泄露技术获取窗口对象成员域bDialogWindow的地址调用 然后对先去初始化的SHELLCODE对象成员域pfnWindProc起始地址设置为载体窗口对象hWindowHunt的消息处理函数
1 | pvAddrFlags = *(PBYTE*)((PBYTE)xxHMValidateHandle(hWindowHunt) + 0x10) + 0x16; |
xxWindowHookProc函数の执行
接下来就是调用 xxWindowHookProc函数 其中调用函数xxShadowWindowProc然后调用函数SetClassLong函数对刚才大量的窗口对象设置MENUNAME字段的方式实现 而MENUNAME字段属于WCHAR字符串格式 因此在初始化缓冲区时需要将所偶数值设置为不包含连续2字节为0的情况 通过调用函数SetClassLongW为目标窗口对象设置MENUNAME字段时 系统最终在内核中为窗口对象所属的窗口类tagCLS对象的成员域分配并设置UNICODE字符串缓冲区
由于成员域lpszMenuName指向的缓冲区和弹出菜单tagPOPUPMENU对象的缓冲区同样是进程配额的内存块 大小形同 那么MENUNAME可以成为伪造的tagPOPMENU对象
1 | SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc, |
在自定义阴影窗口xxShadowWindowProc的最后 调用NtUserMNDragLeave之后 增加对载体窗口对象发送自定义提权消息0x9F9F的调用语句 并返回保存在bDoneExploit中
1 | xxSyscall(num_NtUserMNDragLeave, 0, 0); |
xxWindowEventProc函数の执行
接下来又回到xxShadowWindowProc函数 然后函数调用xxWindowEventProc函数
1 | SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART, |
在xxWindowEventProc函数中 首先指定了很多成员域 然后调用xxHMValidateHandle 这个函数在根据判断调用xxGetHMValidateHandle函数查找并计算HMValidateHandle函数地址
1 | static |
接下来在利用代码中获取窗口对象等类型用户对象的地址的时机调用该函数并传入对象句柄 调用成功则返回目标对象在用户进程桌面堆中的映射地址
1 | #define TYPE_WINDOW 1 |
接下来就是调用TrackPopupMenuEx函数来显示菜单了
然后回到main函数中 执行创建cmd窗口函数
还有一些小问题
关于shellcode
1 | static |
至此 代码分析结束~
利用成功~
0X05 补丁分析
我们已经知道是在xxxMNEndMenuState函数中 所以直接用Bindiff看 当我看到这个颜色的时候其实是内心有点崩溃的 这么多改变。。。。
好吧 其实是在xxxMNFreePopup函数中的 他的改变也蛮大的~
可以看到 有一部分没了
好吧 回到伪代码中 一目了然
至此 补丁分析结束 蛮清晰明了的~
0X06 经验总结
本次分析 是在CVE-2014-4113和CVE-2015-2564之后的 这其中 多多少少有很多的联系 在小刀师傅的博客中 总是能get到很多东西的
0X07 链接
小刀师傅的文章:从 CVE-2017-0263 漏洞分析到 Windows 菜单管理组件 - 小刀志 (xiaodaozhi.com)
0x2l师傅的文章:CVE-2017-0263 Win32k漏洞分析笔记 - 0x2l’s blog
用户回调:https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf
win32k思路:win32k.sys 漏洞挖掘思路解读 (seebug.org)
SDK窗口: Windows SDK窗口创建和消息机制理解_code_greenhand的博客-CSDN博客
0X08 end~
本来是没打算调试这个漏洞的 但在复习CVE-2015-2546的时候 看到小刀师傅的博客中提到了这个 恰巧还是UAF 由此 产生想法 所以有此~
自己总归是菜的一
许许多多的东西不会
学过的东西忘记惹~
很多东西似懂非懂 ~
确实如此 很难走 很难来~
每次和师傅交流的时候 他总是能耐心的给我讲一些东西 让我慢慢来
亦或者另一位师傅告诉我follow my herat~
如此~
end~