Resolución del ejercicio número 2 de Fusion (I) – «Controlando EIP»

Siguiendo con los ejercicios de exploiting con los que empezamos en un post anterior, hoy vamos a pegarnos con uno de la máquina virtual Fusion, concretamente nos enfrentaremos al ejercicio Fusion 02. La resolución de este ejercicio la he dividido en dos partes (dos entradas en el blog), en esta primera parte tenemos como objetivo controlar el registro EIP y dejaremos para la segunda parte explotar la vulnerabilidad. Esta entrada toma como guía para su resolución la entrada del blog de kroosec . Aunque intenta ofrecer algunos detalles más, que pueda hacer más fácil entender el ejercicio.

Para la resolución de este ejercicio vamos a ir respondiendo a preguntas que me he ido haciendo, y que al darles respuesta nos permiten ir dando un paso más en la comprensión del ejercicio y de la vulnerabilidad.

1. ¿Cómo funciona exactamente la función _read()?

Tras ejecutar el programa varias veces, con debugger y sin debugger uno ve que es importante entender como funciona exactamente la función nread y por lo tanto la función _read. Buscando un poco vemos que el prototipo de la función es:

ssize_t read(int fs, void *buf, size_t N);

Lo que hace exactamente esta función es leer n datos desde donde apunta el descriptor de fichero y lo almacena donde apunta la variable *buf. En el código del ejercicio vemos como el descriptor de fichero (fs) vale 0. Esto significa que está leyendo de stdin (entrada estándar). Ya tenemos claro lo que la función _read hace y ésta es invocada desde nread(). Solo mirando el prototipo está claro que uno ya podía intuir lo que hacía, pero de todos modos es necesario no presuponer nada y asegurarse de que funciona como parece.

2. ¿Cómo realizar una ejecución normal del programa y qué hace?

Es necesario saber cómo ejecutar el programa de manera normal, cómo debe funcionar y cuál debe ser su salida. El programa lo que hace es cifrar texto que se le introduce cuando nos conectamos al puerto 20002. Para cifrar una cadena tenemos que introducir primero el carácter ‘E’ en mayúsculas, seguido del tamaño del texto que vamos a introducir, el texto que queremos cifrar y una Q en mayúsculas para finalizar la ejecución.

Como ejemplo vamos a cifrar 100 A’s, mediante la siguiente instrucción:

fusion@fusion:~$ python -c 'print "E"+"\x64\x00\x00\x00"+100*"A"+"Q"' | nc localhost 
      20002
[-- Enterprise configuration file encryption service --]
[-- encryption complete. please mention 474bd3ad-c65b-47ab-b041-602047ab8792 to support 
      staff to retrieve your file --]
fusion@fusion:~$ 

Este es el resultado de una ejecución normal del programa y vemos como hemos llegado a la parte del programa donde se cifra viendo el mensaje que nos ha devuelto por pantalla.

3. ¿Dónde está la vulnerabilidad?

Entendido el código tras varias ejecuciones, tenemos que localizar donde se encuentra la vulnerabilidad y nos fijamos en la siguientes dos líneas de código:

nread(0, &sz, sizeof(sz));
nread(0, buffer, sz);

La vulnerabilidad radica en que en la segunda llamada a nread se utiliza la variable sz para definir la cantidad de datos a leer del usuario y esta variable ha sido fijada en la primera llamada a nread, que también está controlada por el usuario.

Una vez ya hemos introducido el carácter “E” en mayúscula el flujo del programa entrará en la zona de código destinada a cifrar. La primera llamada a nread() lee 4 bytes de la entrada estándar y lo almacena en el argumento sz (recordemos, introducidos por el usuario). Después en la segunda llamada se leen sz bytes y se almacenan en la variable buffer que tiene un tamaño de 32 * 4096 bytes = 131072 bytes. Como es obvio si los cuatro bytes primeros indican un tamaño superior a 131072 bytes y el usuario en la segunda llamada introduce la cantidad de bytes esperados (número superior a 131072), se producirá un buffer overflow en la pila.

4. ¿Cómo provocar el buffer overflow?

Para obtener el tamaño exacto que nos permita provocar el buffer overflow en la pila necesitaremos restar a la dirección de EBP la dirección donde se inicia el buffer.

(gdb) p &buffer
$6 = (unsigned char (*)[131072]) 0xbfd0ad2c
(gdb) p $ebp 
$7 = (void *) 0xbfd2ad38

>>> 0xbfd2ad38 - 0xbfd0ad2c
131084L

>>> hex(131084)
'0x0002000c'

Por tanto una ejecución normal que llenará todo el buffer sin desbordarlo será:

fusion@fusion:~$ python -c 'print "E"+"\x0c\x00\x02\x00"+131084*"A"+"Q"' | nc localhost
      20002
…..
0xb772b424 in __kernel_vsyscall ()
(gdb) set follow-fork-mode child
(gdb) c
Continuing.
[New process 25433]
[Inferior 2 (process 25433) exited with code 0121]
……

Vemos como la salida del programa es controlada. Ahora para producir un overflow le sumaremos 4 al valor anterior, dando como resultado: 0x00020010

fusion@fusion:~$ python -c ‘print “E”+”\x10\x00\x02\x00”+131084*”A”+4*”B”+Q | nc 
      localhost 20002

(gdb) c
Continuing.
[New process 25463]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 25463]
main (argc=Cannot access memory at address 0xbd35d263
) at level02/level02.c:74
74	level02/level02.c: No such file or directory.
	in level02/level02.c

Vemos como se ha producido en esta ocasión un segmentation fault, por lo que ya tenemos controlado el tamaño. Como bien nos advierten desde kroosec en su blog cuando retornamos de la función encrypt_file() (poniendo un breakpoint en la línea 49) vemos que en EBP están los inyectados (41’s – A’s y 42’s – B’s), como se muestra en la siguiente captura de la memoria:

En cambio cuando retornamos de la función cipher() ésta ha modificado estos valores que controlamos por lo que hemos perdido el control y para poder seguir teniéndolo después de que se ejecute la función cipher() los datos que inyectamos tendrán que estar alineados con la función de cifrado. Vamos, que lo que nos interesa es que si inyectamos A’s y B’s cuando volvamos de la función cipher() EIP se sobrescriba con las A’s y B’s que hemos inyectado.

5. ¿Cómo funciona la función cipher()?

Descripción simple de algunas variables importantes de la función:

  • blah: Variable donde está el contenido a cifrar.
  • len: Tamaño de los datos a cifrar.
  • keybuf[32]: clave de 1024 bits. Son 32 elementos de 4 bytes, lo que son 128 bytes. Podemos ver un ejemplo del contenido a continuación:

  • blocks: Para calcular el número de bloques divide por 4 el número el tamaño en bytes. Con cada XOR se hacen 4 bytes de blahi con un elemento de la clave que tiene un tamaño de 4 bytes.

El bucle va realizando la función XOR entre los elementos de la clave y el texto que queremos cifrar. A continuación tenemos un ejemplo de una iteración del bucle for:

La captura anterior muestra la ejecución del bucle for cuando j=0. Si se examina la dirección donde apunta blahi (0xbfba22dc) se ve como el primer elemento ya no tiene el valor 0x41414141 y ha sido modificado. Para modificarlo se ha hecho un XOR así:

# Primer elemento de keybuf y el elemento primero de blahi tras aplicarle un XOR
>>> hex(0xf87adc59 ^ 0x41414141)
'0xb93b9d18L'

Por tanto si queremos que después de la función nos quede el valor 0x41414141 para demostrar que tenemos el control de EIP tendremos que poner el siguiente valor como entrada de datos:

>>> hex(0xf87adc59 ^ 0xb93b9d18L)
'0x41414141L'

6. ¿Cómo obtenemos la clave de cifrado y sobrescribimos EIP?

Llegados a este punto queda patente que debemos conocer la clave para poder cifrar los datos que queremos inyectar para obtener el resultado que queremos, pero ésta es aleatoria lo que supone un problema. Si revisamos el código vemos que existe una variable de nombre keyed que sirve para controlar que sólo en una primera ejecución se genere la clave y en las siguientes se utilice la generada, sería como una especie de caché. Es de ésto de lo que nos vamos a aprovechar y vamos a lanzar para cifrar dos fragmentos de texto. Con el primer fragmento obtendremos la clave y con el segundo sobrescribiremos EIP. En github (https://github.com/jholgui/exploiting/blob/master/fusion02.py) he dejado un script en python que hace estas dos ejecuciones de manera muy simple.

Resultado de ejecutar el script en python será:

Como vemos ya hemos sobrescrito EIP con un valor controlado por nosotros (BBBB) y con esto tenemos finalizada la primera parte. En la siguiente entrada explotaremos la vulnerabilidad.

Comments

  1. Excelente guía. Saludos