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。
1 | g_fNtUserConsoleControl = (FNtUserConsoleControl)GetProcAddress(GetModuleHandle(L"win32u.dll"), "NtUserConsoleControl"); |
接下来注册了两个WND类,主要区别在于ClassName和ExtraBytes的大小。
1 | WNDCLASSEX WndClass = { 0 }; |
尝试堆布局
该exp会尝试5次堆布局,直到成功或者布局失败。
首先会创建50个Window,其中0号、1号会最终保留。其他48个会释放,但这48个在后续也会发挥作用。
其中,i == 1
对应的if body代码用于实现数据的读取。(数据的写入使用了SetWindowLongPtr)
1 | //start memory layout |
接下来获取0、1号Window的内核tagWnd在桌面堆中的地址。
1 | g_dwpWndKernel_heap_offset0 = *(ULONG_PTR*)((PBYTE)g_pWnd[0] + g_dwKernel_pWnd_offset); |
使用NtUserConsoleControl API将0号Window的ExtraBytes从用户堆转移到内核堆。
1 | ULONG_PTR ChangeOffset = 0; |
首先判断0号Window的内核tagWnd在1号的前面,为了保证可以通过0号内存越界修改到1号数据,然后成功结束堆布局或者进行下一次重新尝试。
1 | dwpWnd0_to_pWnd1_kernel_heap_offset = *(ULONGLONG*)((PBYTE)g_pWnd[0] + 0x128); |
修改内核数据
创建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之外的数据。
1 | HWND hWnd2 = CreateWindowEx(NULL, L"Class2", NULL, WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInstance, NULL); |
通过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指向传入的地址。
1 | ULONGLONG ululStyle = *(ULONGLONG*)((PBYTE)g_pWnd[1] + g_dwExStyle_offset); |
修改1号Window内核tagWnd的spMenu,使得后续可以实现数据的读取。
1 | //My spmenu memory struct For read kernel memory |
权限提升
通过读取内核数据吗,获取当前进程的eprocess。
1 | ULONG_PTR ululValue1 = 0, ululValue2 = 0; |
遍历所有进程,找到pid=4,将其token复制到当前进程,实现提权。
1 | ULONG_PTR pSystemEProcess = 0; |
清理现场
接下来工作主要是防止系统蓝屏,修复被破坏的系统数据。
1 | //Recovery bug |