Buffer overflow, un asesino en serie
El problema del buffer overflow (bof) data de tiempos inmemorables... diría que desde que se inventaron las computadoras, aunque recién fueron entendidos años más tarde, en la década de los 70s. Esta vulnerabilidad es tan simple como suena, pero tan peligrosa como para corromper programas y lograr que las máquinas hagan lo que el atacante quiera.
Buffer overflow trata sobre incluir en un buffer más datos de los que éste está destinado a almacenar, ocasionando que los bytes sobrantes sobreescriban otras posiciones de memoria, donde hay otros datos. Si los datos "extra" son creados y posicionados correctamente en la sobre escritura, un atacante puede lograr que el programa ejecute el código que desea.
Este problema está presente siempre que el programa lea datos de alguna fuente y los asigne a un buffer interno, ya sea una entrada de usuario por línea de comandos, por ventana a lo windows, leyendo un archivo, leyendo un socket, etc, etc.
Si bien uno creería que al ser un problema conocido hace taaaaantos años, actualmente debería ser menos frecuente y menos peligroso, la realidad demuestra totalmente lo contrario. Los buffer overflows "caminan" entre nosotros y son tan peligrosos como hace 20 años. Basta buscar un poco en internet para encontrar exploits de este tipo a montones. El bof es de las vulnerabilidades más peligrosas, permitiendo a un atacante ejecutar código localmente, remotamente, escalar privilegios, atacar otras máquinas, cualquier cosa que se les ocurra.


Secciones de un programa en memoria
Para poder entender los buffer overflows, es necesario conocer cómo se dividen los programas en la memoria. Estos se dividen en varias secciones, y las 6 más importantes son las siguientes:

- .text: contiene las instrucciones del programa, es decir, la parte ejecutable. El tamaño de esta sección es fijada en tiempo de ejecución, cuando se carga el proceso.

- .data: se utiliza para almacenar variables globales inicializadas (por ejemplo: int enData = 0;). Esta sección también es fija en tiempo de ejecución.

- .bss (below stack section): contiene las variables globales no inicializadas, como por ejemplo: int enbss;. El tamaño es fijado en tiempo de ejecución.

- Heap: esta sección se usa para almacenar variables alocadas dinámicamente y crece desde las direcciones de memoria más bajas hacia las más altas. La alocación se controla mediante las funciones malloc() y free(). Un ejemplo de variable alocada en tiempo de ejecución sería una declarada así: int dinamica = malloc(sizeof(int));

- stack: esta es la conocida pila. La sección de la pila permite guardar datos entre llamadas a funciones y crece desde las direcciones altas de memoria hacia las direcciones bajas. Esta forma de crecimiento es la que permite buffer overflows.

- entorno/argumentos: se usa para almacenar una copia de las variables a nivel sistema que pueden ser requeridas por el proceso durante su ejecución. Como ejemplo, alguna de estas variables son el path, nombre del shell, hostname, etc. Esta sección es escribible, permitiendo exploits del tipo format string y buffer overflows.

En la figura pueden observar mejor que lugar ocupa cada sección dentro de la memoria:



Tipos de buffer overflow

Básicamente los buffer overflow pueden ser de dos tipos, stack based o heap based.
Para entender los ataques stack based overflow, necesito explicar un poco de lo que sucede cuando llamamos a una función en un programa. En una llamada a función, sucede lo siguiente:
- se colocan los parámetros pasados a la función dentro de la pila, en orden inverso.
- se salva el registro que indica la dirección de la próxima instrucción (eip) dentro de la pila. Esta será la dirección de retorno.
- se ejecuta el comando call y la dirección de la función llamada se coloca dentro de eip.
La función recién llamada debe encargarse de lo siguiente:
- salvar el valor del registro que indica la base de la pila de la función anterior (ebp).
- colocar el valor de esp (registro que apunta al tope de la pila) en ebp. Con esto se setea la base de la pila de la función llamada.
- decrementar esp para hacer espacio para las variables locales de la función llamada. Cabe destacar que la pila crece en dirección contraria a las direcciones de memoria, por eso se decrementa en lugar de aumentar.
- se ejecuta la función
Una vez que la ejecución de la función se completa, sucede lo siguiente:
- se coloca el valor de ebp en esp (limpiamos la pila actual).
- se saca el valor de la pila actual y se coloca en ebp (restauramos la pila de la función llamadora).
- se saca de la pila el valor de retorno y se coloca en eip. Con esto (si todo sale bien) volvemos a donde se quedó la función anterior antes de llamar a la actual.

Los exploits stack-based utilizan las variables almacenadas en la pila para explotar un programa, sobreescribiendo la dirección de retorno de una función (almacenada en la pila), ya sea para llamar a un código insertado por el atacante o bien para llamar alguna función de librería que realice una función particular (el llamado return to libc attack).

Teniendo en mente la explicación de lo que sucede al llamar una función, paso a explicar el ataque utilizando un siempre más entendible ejemplo. Supongamos que tenemos la siguiente función:

void explotable()
{
char explotame[4];
strcpy(explotame, "AAAAAAAAAAAA");
}

La pila en este caso tendrá el siguiente formato:

Dado que sólo se reservaron 4 bytes para "explotame" y se colocaron 12 As, osea, 12 bytes, en dicha variable, estos sobre-escribirán tanto el valor del ebp anterior como el de la dirección de retorno, la cual ahora será 0x41414141, osea AAAA. Como se imaginarán, cuando la función intente retornar, intentará ir a la dirección 0x41414141, ocasionando un segmentation fault. Con esto ya se pueden imaginar que un atacante metódico, en lugar de colocar puras As, insertará los bytes justos para que la dirección de retorno apunte a código insertado por él (ataque conocido como arbitrary code execution), el cual probablemente haya ingresado en la misma pila al sobre escribir otras variables. También puede colocar la dirección de una función de librería (conocido como return to libc attack), la cual es conocida. Una función interesante para llamar es system().


Para el caso de los ataques heap-based, hay que pensar de forma distinta. La memoria heap es la memoria donde se almacenan las variables que son alocadas en tiempo de ejecución, comúnmente utilizando la función malloc. En el heap no hay direcciones de retorno ni algún otro registro salvado, así que los atacantes tendrán que utilizar una estrategia distinta. El heap crece en la dirección que crecen las direcciones de memoria, esto es, contrario a la dirección que crece la pila.
Algunos ataques interesantes consisten en:
- inyectar valores arbitrarios en variables para sobre-escribir variables adyacentes y cambiar la funcionalidad de un procedimiento.
- sobre escribir direcciones de punteros a funciones. En el caso que una variable en el heap almacene un puntero a una función, podríamos cambiar esta dirección y hacer que apunte a una función de sistema u otra que nos interese.
- sobre escribir variables para lograr algún beneficio. Pueden imaginarse si una función bancaria pensaba aumentar la cuenta de un atacante en u$s10 y despues de sobre escribir dicha variable ahora la incrementa en u$s10.000???

Si bien estos ataques no ser tan redituables como los stack-based, si han captado buena atención y han vulnerado multitud de aplicaciones.


Return to libc attack

Este ataque surge para lograr explotar un buffer overflow en un sistema que cuenta con pilas no-ejecutables (osea, el SO no ejecuta código que se encuentre en la pila). Esta técnica utiliza el eip (dirección de retorno) almacenado en la pila para apuntar a funciones de la librería libc. Las funciones de libc son usadas por todos los programas, así que estarán siempre presentes para ser llamadas. Una función interesante es system() que permite ejecutar programas en el sistema. En este caso, un atacante podría sobreescribir datos en la pila para que la función system() ejecute algún programa de nuestra elección, como por ejemplo /bin/bash.
Por supuesto que este ataque no se limita a libc, sino que puede utilizar cualquier librería que sepamos se encuentra en el programa.


Exploits encadenados

Si bien el código de un programa puede estar libre, o relativamente libre, de vulnerabilidades buffer overflow, puede que las librerías que utiliza no lo estén, ocasionando que el resultado final sea un programa explotable. De esto tratan los exploits encadenados, de heredar exploits presentes en librerías y haciendo al programa resultante vulnerable.
Cabe destacar que esta definición se aplica a cualquier vulnerabilidad, no sólo a los buffer overflows.
Es interesante el reciente caso de net/tun, donde el programa presentaba un bug que no estaba escrito en net/tun, sino que aparecía luego de utilizar el compilador para optimizar, el cual removía una sentencia if-then que creía innecesaria, introduciendo un exploit del kernel. Pueden leer un poco más sobre este caso en infoworld.com.


Conclusión

Buffer overflow sigue asesinando aplicaciones, pasan los años y sigue omnipresente, a la cabeza de los problemas más graves que debe enfrentar un usuario/administrador al utilizar sus programas. En los últimos 6 meses he recibido 1480 mensajes en la lista de distribución bugtrack, de los cuales 220 trataron sobre buffer overflows, representando un 6,73% del total... un número gigante, teniendo en cuenta que se codea con los exploits más recurrentes XSS y SQL injection.
A diferencia de XSS y SQL injection, lograr explotar un buffer overflow es muchisimo más complejo, no solo por los mecanismos de defensa actuales, sino porque requiere de experiencia, conocimientos de bajo nivel, tiempo y mucha paciencia. Realmente admiro los hackers que logran explotar buffer overflows, no es un trabajo que podría hacer cualquiera, como en XSS y SQL injection.

La idea original de este artículo era hacer un review de los mecanismos de detección más importantes, pero me parecía bastante descolgado explicar eso sin dar una explicación sobre los buffer overflows... el artículo se terminó extendiendo tanto que decidí partirlo en dos. En los próximos días estaré publicando los mecanismos de defensa contra los buffer overflows... stay on-line!


Referencias

Gray Hat Hacking - The Ethical Hacker's Handbook
Buffer Overflow wiki
Prevention and Detection of Buffer Overflow Attacks

3 comentarios:

Zerial dijo...

Una tecnica para explotar con exito este tipo de vulnerabilodades es ASLR:
http://en.wikipedia.org/wiki/ASLR

V3kt0r dijo...

sisi, en la segunda parte del informe de buffer overflow tengo preparada la explicación de técnicas como ASRL. Espero poder terminarlo hoy o mañana =)

Anónimo dijo...

Gracias Victor! me clarificó las ideas. Saludos! Emi.

Publicar un comentario