Para compilar el programa name.c para este artículo es necesario desactivar DEP:
gcc -m32 -fno-stack-protector -z execstack name.c -o name
Esta vez no hay que desactivar ASLR, si estuviese desactivado se puede activar de la siguiente forma:
echo 2 > /proc/sys/kernel/randomize_va_space
En el artículo anterior estudiamos la protección DEP y ahora toca estudiar ASLR.
ASLR significa Address Space Layout Randomization y es una técnica implementada a nivel de sistema operativo para aleatorizar las direcciones de memoria dentro del espacio de memoria de un proceso, de esta forma una librería como puede ser libc no será cargada siempre en la misma zona de memoria y por tanto técnicas como Return to libc o Jump To Esp no se podrán llevar a cabo tan facilmente.
En este articulo vamos a activar ASLR. Si no lo tenemos activo podemos ejecutar el siguiente comando como root para activarlo
echo 2 > /proc/sys/kernel/randomize_va_space
Pero vamos a volver a desactivar DEP para que nos resulte un poco más sencillo.
Por tanto, el programa vulnerable tendremos que compilarlo mediante la siguiente instrucción
gcc -z execstack -fno-stack-protector name.c -o name
¿Como podemos comprobar si ASLR está activo?
Podemos comprobarlo facilmente ejecutando el comando ldd sobre nuestro programa para ver si las direcciones de memoria varian.
Aquí podemos ver como las direcciones de las librerías cargadas varian, si no es así significa que no tenemos ASLR activo. Por lo general algunas librerías de terceros en Windows no son aleatorizadas y en linux el segmento text de nuestro programa tampoco se aleatoriza a no ser que se compile como código independiente de la posición (-fpic).
Este descuido nos deja la puerta a abierta ataques del tipo Return to PLT que veremos en el siguiente artículo, que nos será bastante efectivo cuando DEP y ASLR está activo.
Además podemos imprimir la direccion de una variable de entorno cualquiera y ver que también cambia de dirección de memoria.
Pero podemos observar algo realmente interesante. Podemos observar que el byte más significativo no varía, además el byte menos significativo tampoco varia, ni el nibble a continuación del byte menos significativo tampoco varia. Si tomamos una muestra (empezando por el último) de los bytes que varian y lo pasamos a binario obtenemos.
Podemos ver como el bit más significativo tampoco varía.
ASLR nos promete que para la pila hay 24 bits aleatorios, obviando los bytes más significativos y teniendo en cuenta lo observado más arriba de que el bit más significativo tampoco es aletorio obtendriamos que el número de bits aleatorios es realmente 23 (2^23 = 8MB que es el tamaño máximo de pila).
Además en el artículo anterior pasamos por alto un pequeño detalle
¿Lo véis? En la sección GNU_STACK pone que debe de haber un alineamiento de 0x10 (16). Por tanto el número total de posibilidades para una variable en el stack seria 2^23/16 = 524288. Siguen siendo muchas posibilidades para un ataque de fuerza bruta.
¿Pero y si creamos una variable de entorno con una gran cantidad de nops? ¿Por ejemplo 100.000 nops? Pues que tendriamos una variable bastante larga y si diese la casualidad de que cuando ejecutamos el programa esta variable se encuentra en una dirección de memoria que nosotros especificamos ira recorriendo todos los nops hasta llegar a nuestro shellcode. En efecto, vamos a proceder a realizar una atque de fuerza bruta.
Para ello primero exportamos la siguiente variable de entorno:
Aquí exportamos la variable xploit con 100.000 nops y acto seguido nuestro conocido shellcode.
Ahora procedemos a crear el siguiente script:
En este script básicamente llamamos al programa vulnerable con un parametro que es capaz de desbordar el valor de EIP y en EIP proporcionamos una dirección de memoria más o menos cercana o igual a las que hemos obtenido un poco más arriba cuando mostrabamos la accion de ASLR sobre la variable de entorno SHELL
Ejecutamos este script y obtenemos:
¡Tachan! Volvimos a poder obtener una shell. Además el ataque ha sido relativamente rápido, unos 2 segundos ha tardado en obtener una shell.
Si no conseguimos obtener una shell, podemos ampliar el número de nops en nuestra variable de entorno (pero con cuidado, ¡algunas shell limitan el tamaño de las variables de entorno!) o podemos aumentar el número de intentos del script.
Para compilar el programa name.c para este artículo es necesario desactivar DEP:
gcc -m32 -fno-stack-protector name.c -o name
También es necesario desactivar ASLR ejecutando el siguiente comando con permisos de administrador
echo 0 > /proc/sys/kernel/randomize_va_space
Hasta ahora lo hemos tenido relativamente fácil porque hemos desactivado algunas de las protecciones que implementa el compilador y el sistema operativo, pero en el mundo real las cosas no son tan sencillas, así que a partir de ahora vamos a empezar a estudiar estas protecciones y la manera de saltárselas.
A partir de ahora nos vamos a encontrar con restricciones en cuanto a la forma de trabajar, por ejemplo en los artículos anteriores hemos podido poner nuestro shellcode en la dirección de memoria que queriamos, ¡incluso hemos podido usar un shellcode personalizado!
En estos artículos nos vamos a centrar concretamente en dos protecciones que he ido nombrando por encima y que hasta ahora no le he dado mucha importancia, estas dos protecciones son DEP y ASLR.
En este primer artículo voy a empezar hablando de DEP, así que voy a hacer una pequeña introducción al mismo.
DEP viene de Data Execution Prevention y es una implementación hardware que debe ser soportada por el Sistema Operativo, aunque Linux puede emular dicha protección sin estar presente en el hardware. Como su nombre indica esta protección evita que zonas de datos se puedan ejecutar.
¿En que afecta esto a nuestros métodos anteriores? Nosotros estabamos inyectado nuestro shellcode en una zona de datos, la pila, ahora con esta medida de protección no podremos ejecutar un shellcode desde la pila. El parámetro que deshabilita esta protección en gcc es -z execstack, con ese parámetro podremos ejecutar código desde la pila, pero si quitamos ese parámetro ya no se nos permitirá ejecutar código desde la pila.
Hasta ahora nuestro programa compilado con -z execstack al ejecutar el siguiente comando
readelf -l ./name
Nos mostraba lo siguiente
Podemos ver como el segmento GNU_STACK tiene RWE en la penúltima columna que significa que tiene permisos de lectura, escritura y ejecución. Sin embargo si compilamos el programa sin la opción -z execstack veremos lo siguiente
Nuestro programa ya no tiene permisos de ejecución en la pila y por tanta ya no podremos ejecutar shellcodes alojados en la pila.
Si probamos a ejecutar nuestro programa pasándole el shellcode del artículo anterior, obtendremos:
Vemos como efectivamente nuestro shellcode no nos proporciona una shell, sino que nos da un segmentation fault
¿Que podemos hacer entonces? Aquí surge una técnica que se llama Return to Libc, recordemos que libc es la librería estándar de C en sistemas Linux y por tanto todos los programs bajo entornos linux suelen llamar a esta librería. Por ejemplo, printf está definida dentro de esta librería.
La idea es muy sencilla, es sobreescribir el registro EIP con una dirección de una función definida en libc en vez de proporcionar una dirección donde se encuentre nuestro shellcode. Haciendo esto el programa saltaría a dicha función que al estar en el segmento de texto de Libc tendrá permisos de ejecución.
¿Que función podemos llamar? Aquí surgen 2 tendencias, aquellas que llaman directamente a una función para obtener una shell, como puede ser la función system() o proporcionar en EIP la dirección donde se encuentra el función mprotect que es una llamada al sistema que permite cambiar los permisos de ejecución/escritura/lectura de un segemento de un programa para hacer que la pila sea de nuevo ejecutable y proporcionar un shellcode que ejecutar desde la pila.
Por ser la opción más sencilla y la que mejor ilustra la técnica vamos a llamar directamente a la función system para obtener una shell.
Para esta técnica hay que tener claro la forma en la que se pasan los argumentos a las funciones y que fue explicada en artículos anteriores. Cuando una funcion va a ser llamada, sus argumentos se almacenan en la pila. En este caso la función system solo tiene un parámetro que es el comando a ejecutar, pero además tendremos que meter en la pila la dirección de la función exit para realizar una salida controlada del programa cuando salgamos de la shell y para que no nos de un fallo de segmentación (hay que recordar que los fallos de segmentación generan logs que los administradores de sistemas pueden examinar).
Por tanto nuestra pila debe quedar de la siguiente forma:
Recordemos que después de salir de la función func, el programa recuperará la el valor de EIP que nosotros hemos sobreescrito y que redirige a la función SYSTEM, una vez en la función system, se espera que el primer parámetro se haya introducido en la pila y está en la primera posición, y una vez acabada la ejecución de system se procederá a la ejecución de exit.
Por tanto, necesitamos saber unas cuantas cosas, la primera es la dirección de la función system, la segunda es la dirección del parámetro que le vamos a pasar a system y la tercera es la dirección de la función exit.
La dirección de system y de exit es fácil de saber, en gdb y con el programa ejecutado y parado en un breakpoint escribimos los comandos
p system
y
p exit
En mi caso system está en la posición 0xF7E2B160 y exit en la dirección 0xF7E1Ec90.
Nos falta por saber la dirección del primer parámetro. Por lo general siempre hemos introducido los datos que necesitabamos en la pila, pero hay otro lugar donde podemos introducir también datos, esto es las variables de entorno. Además si compilamos y ejecutamos el siguiente programa podremos saber la dirección exacta de nuestra variable de entorno.
para compilarlo debemos tener cuidado de compilarlo para 32 bits
gcc -m32 getenv.c -o getenv
La función getenv() de linux nos devuelve la dirección de memoria de una variable de entorno, pero esa dirección de memoria se corresponde a la dirección en el proceso de ese programa, nosotros queremos saber la dirección de esa variable de entorno pero en el programa vulnerable y si nos acordamos lo que influia en el cambio de las direcciones era el nombre del programa y las variables de entorno en si, en este caso como se ejecuta sobre el mismo entorno las variables de entorno no cambian pero si que cambia el nombre, de ahí que el calculo se haga solamente con la diferencia de longitud entre el nombre del programa getenv y el nombre del programa vulnerable.
Una vez hecho esto ya tendremos los 3 datos que andabamos buscando.
La dirección de system: 0xF7E2B160
La dirección de exit: 0xF7E1Ec90
La dirección del parámetro de system: 0xFFFFDFAF
Ahora deberemos construir el parámetro que le tenemos que passar a nuestro programa vulnerable con estos datos. Como ya se sabe el programa sobreescribe el valor de EIP al 45 byte, en EIP deberemos proporcionar la dirección de system, después la dirección de exit, y por último la dirección del parámetro de system (para que cuando se ejecute system este esté en la cima de la pila y pueda ser recuperado por system). Por tanto el parámetro a pasarle será:
Para compilar el programa name.c para este artículo es necesario desactivar DEP:
gcc -m32 -fno-stack-protector -z execstack name.c -o name
También es necesario desactivar ASLR ejecutando el siguiente comando con permisos de administrador
echo 0 > /proc/sys/kernel/randomize_va_space
En el artículo anterior conseguimos hacer funcionar nuestro exploit dentro de gdb, pero tuvimos problemas a la hora hacer funcionar nuestro exploit fuera de gdb.
¿Cual puede ser la razón para esto?
Asumimos que la protección ASLR (que espero hablar más tarde de ella) la tenemos desactivada.
El comando que siempre he indicado para desactivarla:
echo 0 > /proc/sys/kernel/randomize_va_space
tiene efecto temporal y después de reiniciar nuestro ordenador ASLR volverá a estar activo, así que hay que estar atento a desactivarlo cada vez que practiquemos con estos artículos. Aunque ahora no venga al caso hay que decir que gdb desactiva la protección ASLR, si quisiesemos activarla, dentro de gdb debemos ejecutar el siguiente comando:
set disable-randomization off
Volviendo al hilo del artículo, la razón de la que pase esto tiene que ver con un gráfico que puse en el primer artículo:
Nuestro exploit fuera de gdb no funciona porque las direcciones que hemos calculado dentro de gdb no coinciden con las que se ejecutan fuera de gdb, hay 2 posibles culpables de esto. Los primeros 4 bytes del espacio de direcciones de un proceso son constantes, los segmentos, la pila y el heap también son constantes. Lo único que queda que puede variar son el nombre del programa, los argumentos y las variables de entorno.
Efectivamente si alguna de estas regiones no coincide exactamente con lo que tenemos en un entorno de ejecución gdb las direcciones no coincidiran y el exploit no funcionará.
En este caso concreto los argumentos no pueden ser los culpables porque tanto en gdb como fuera de él se pasa un argumento con la misma cantidad de bytes (48 bytes). Aunque pueda parecer que el nombre también es el mismo, se podría dar el caso de que no, el nombre del programa fuera de gdb se almacena tal cual lo ejecutemos, es decir, si ejecutamos ./name se almacenará ./name, sin embargo, en gdb el nombre se almacena como la ruta absoluta, para mi caso sería /home/angelluis/Documentos/exploiting/name. Aquí ya nos encontramos con que si hemos ejecutado nuestra aplicación fuera de gdb como ./name tendriamos que sumarle 36 a la dirección de memoria que obtuvimos en el artículo anterior (36 = la diferencia entre la longitud de la cadena /home/angelluis/Documentos/exploiting/name y ./name). O más sencillo aún, conservar la dirección de memoria obtenida en gdb y fuera de gdb ejecutar nuestro programa mediante su ruta absoluta.
Las variables de entorno también cambian cuando se ejecuta el programa en gdb. Si en gdb ejecutamos el comando
show env
podremos ver las variables de entorno que hay definidas y que, por tanto, forman parte del espacio de memoria del proceso. Por lo general gdb define dos variables nuevas, LINES y COLUMNS.
Para automatizar el proceso he creado un script que por fuerza bruta va comprobando direcciones. Ajustando la dirección inicial a la que encontramos en gdb podremos encontrar facilmente la nueva dirección para explotar la vulnerabilidad, ya que las direcciones de memoria deben estar proximas, porque como se ha dicho solo habrá cambiado el nombre y alguna variable de entorno.
Este es el script:
Y aquí una muestra del script en funcionamiento, encontrando la dirección:
La dirección que nos da el shell es la 0xffffd6c0 y el shellcode completo es
Nada, que no hay forma… Esto se debe de nuevo a que el script introduce variables de entorno y por tanto la ejecución en el script y la ejecución sin el script no corren en el mismo entorno y las direcciones en el espacio de memoria del proceso están desplazadas.
En mi caso, la dirección exacta es 0xFFFFd670
Esta solución no es muy elegante, y los fallos de segmentación son registrados en logs que posteriormente un administrador de sistemas puede examinar y podría detectar el posible ataque.
Para no tener este problema con las direcciones de memoria existen ciertos script que actuan de envoltura entre el programa vulnerable, gdb y la shell donde se ejecute el programa, un script que he comprobado que funciona es el de la siguiente dirección:
El uso es bastante sencillo, si queremos usar gdb deberemos ejecutar
r.sh gdb ./program [args]
Y si queremos ejecutarlo sin gdb ejecutamos
r.sh ./program [args]
Con este envoltorio nos aseguramos de que las direcciones de memoria no varien dependiendo del entorno.
Pero nosotros vamos a usar una técnica más refinada para solucionar nuestro problema.
Desde la entrada de nuestro programa podemos modificar la pila del programa, ¿Y si pudiesemos saltar a directamente a la cima de la pila con una instrucción del tipo “Saltar a la cima de la pila”? Pues se puede, esta instrucción es la jmp %esp (Jump to Extended Stack Pointer).
Lo que queremos hacer es despreocuparnos de en que posición se encuentra nuestro shellcode y saltar directamente a nuestro shellcode mediante la instrucción jmp %esp. En nuestro código no vamos a encontrar esta instrucción. Podemos intentar buscarla mediante:
objdump -D name | grep jmp | grep esp
que nos arrojará 0 resultados. Lo ideal sería buscar esta instrucción en el propio código de la aplicación vulnerable o en alguna librería propia del programa vulnerable.
Además jugamos con una ventaja en sistemas x86 y es que a diferencia de otras arquitectura no es necesario que las instrucciones estén alineadas, ¿Que significa esto?
La imagen de arriba lo explica perfectamente, según que byte leamos las instrucciones se re-interpretan, en la imagen de arriba leo una dirección determinada acto seguido leo una dirección donde no empieza ninguna instrucción (la dirección 0x080484d2) y vemos como ya no aparecen las mismas instrucciones que antes. Aún así no vamos a usar el programa vulnerable para buscar la instrucción jmp %esp. Vamos a usar las librerias compartidas de las que hace uso el programa vulnerable, aunque como he dicho antes esto no es recomendable, y más aún si está ASLR activado.
Para listar las librerias de las que hace el programa vulnerable podemos ejecutar desde gdb el comando
info sharedlibrary
En este caso usaremos el comando
ldd name
desde fuera de gdb. Con esto veremos que el programa vulnerable usa la librería libc que es la librería estandar de c de Linux, además nos dice la ruta en la que se encuentra, así que nos disponemos a ejecutar objdump sobre dicha librería en busca de un jmp esp
Esto nos ha servido para dos cosas. Por una parte hemos descubierto el offset del primer jmp %esp que es 0x15a55b y por otra parte hemos descubierto que el op code de jmp %esp es 0xffe4. No es mi intención explicar el funcionamiento del linker del compilador y el loader del sistema operativo pero las librerías compartidas se compilan con la opción de gcc -fPIC donde PIC significa Position Independent Code. Esto es porque las librerías compartidas (.so en Linux y .dll en windows) comparten el segmento de texto con todo proceso que la use (el segmento de datos por el contrario se copia y es único para cada proceso, no es compartido). El loader del sistema operativo decide en tiempo de carga donde posicionar las librerías dinámicas y completa información que el linker ha dejdo en blanco.
Por tanto, las librerías compartidas cuando se examinan aisladamente no nos ofrecen una dirección de memoria absoluta, sino un offset con respecto a su dirección de carga. ¿Como adivinar dicha dirección de carga?
Para ello podemos ejecutar el comando
info proc mapping
En gdb, estando cargado el programa que queremos examinar
Cuando hemos ejecutado
ldd name
o
info sharedlibrary
Nos ha dado unas direcciones de memoria de la librería libc, pero como se dijo en el artículo anterior, ldd no ofrece resultados precisos e info sharedlibrary nos devuelve la posición del segmento de código.
Con info proc mapping vemos que aparecen varias referencias a la librería libc, la posición de inicio de la librería es la posición de inicio de la primera referencia, esto es 0xF7DF0000.
Por tanto si a 0xF7DF0000 le sumamos el offset 0x15a55b nos da como resultado 0xF7F4A55B y ahí es donde deberiamos encontrar nuestro jmp %esp. Vamos a comprobarlo
Como no podía ser de otra forma nos encontramos con la instrucción jmp %esp y su op code.
Tenemos ya la dirección a un salto a la pila con su dirección absoluta, sabemos también la cantidad de bytes que necesitamos proporcionar a la entrada para sobreescribir el registro EIP y redirigir la ejecución del programa, tenemos un shellcode capaz de obtener una shell, ¡Lo tenemos todo!
Ahora hay que tener una cosa en cuenta, nuestro shellcode se va a ejecutar cuando retorne la función func del programa vulnerable (igual que en el artículo anterior) que es cuando se recupera el valor del registro EIP que hemos sobreescrito, pero hay una diferencia con el artículo anterior.
En el articulo anterior la entrada era de la siguiente forma
SHELLCODE + NUEVO_VALOR_EIP
Ahora mediante esta técnica la entrada tiene que ser de la siguiente forma
RELLENO + NUEVO_VALOR_EIP + SHELLCODE.
¿Por qué? Porque en los artículo anteriores es verdad que nos fijabamos en el registro ESP pero nos fijabamos antes de que la función retorne y recupere el valor del registro EIP manipulado, esta vez nuestro registro ESP va a ser consultado después de salir de la función func, y para entonces la pila habrá eliminado el marco creado para nuestra función func y apuntará a la dirección de pila justo antes de introducir el EIP.
El relleno en nuestro caso es de 44 bytes, porque después de esto sobreescribimos el registro EIP
Por tanto el argumento que le tenemos que pasar al programa vulnerable es:
Y vemos como así funciona sin problemas tanto dentro de gdb como fuera.
Esto a simple vista no parece tener mucha utilidad, hemos obtenido una shell pero sigo siendo angelluis, puedo hacer lo mismo que podía hacer antes. ¿Pero y si la vulnerabilidad la tiene un programa cuyo propietario es root y con el bit s activo como puede ser por ejemplo el programa passwd?
Pues que nada más y nada menos hemos conseguido escalar privilegios y ejecutar una shell como root.
A parte de obtener una shell, los payloads o shellcodes también se suelen usar para realizar denegaciones de servicio u obtener shells remotas.
Con esto ya queda explicado lo básico de los ataques stack overflow