Buffer Overflows

¿Qué es un buffer overflow (desbordamiento de buffer)? Un buffer overflow es un desbordamiento de un buffer que ocurre cuando hay reservada una cantidad determinada para datos y se modifica el contenido de partes exteriores a los datos.

Un buffer overflow puede ocurrir cuando estamos modificando un vector. Por ejemplo:

int n; char data[10];
for (n = 0; n < 20; n++) data[n] = 0;

El ejemplo trata de poner a 0 el valor de cada elemento en "data", pero accede a direcciones de memória que no corresponden a data, modificando su valor y en algunos casos causando que el programa crashee o incluso que se ejecute código arbitrario.

Ni que crashee ni que permita ejecutar código arbitrario es bueno.

El ejemplo anterior aunque potencialmente vaya a crashear, no permite ejecutar código arbitrario de ninguna forma.

Si hay algo peligroso en un programa, es que ese programa permita hacer un exploit de overflow.

Exploits

Un exploit de overflow es un procedimiento para, manipulando ciertas entradas, conseguir hacer ciertas cosas que no estaban previstas, como puede ser ejecutar código arbitrario.

Esto es extremadamente peligroso porque ejecutar código arbitrario puede permitir a terceros, borrar datos importantes, ejecutar virus, instalar troyanos, enviar a través de internet información confidencial, hacer operaciones ilícitas a través de internet con el ordenador del usuario como proxy...

Los exploits pueden permitir también hacer cosas no perjudiciales para el usuario como por ejemplo el pirateo de consolas. Claro ejemplo el TIFF overflow de PSP que permitía ejecutar código arbitrario con un archivo TIFF especial usando NOPs en un Heap.

¿Cómo funcionan los exploits?

Para responder a esta pregunta hay que tener claro dónde pueden estar los buffers a exploitear. Los buffers pueden estar en la pila (Stack), en el Heap, o estáticamente junto al código.

Ejemplos:

void prueba() {
	int n;
	char data[10];
	gets(data);
	for (n = 0; n < 10; n++) printf("02X", data[n]);
}

Este ejemplo ilustra una función que pide al usuario una cadena (que se almacenará en un buffer de 10 bytes) y mostrará el contenido del buffer con una codificación hexadecimal.

El ejemplo solo permite almacenar 9 bytes en data sin producir un overflow, puesto que gets incluye al final el caracter \0.

Si estamos ejecuando el código en una máquina de 32 bits, podremos escribir hasta 13 caracteres sin que (aparentemente) pase nada extraño.

Si intentamos escribir mas de 13 caracteres, el programa MUY probablemenet crasheará.

Las variables locales en casi todos los compiladores de C, y en casi todos los casos, se almacenan en la Pila (Stack).

Si la pila de ejecución está en una posición X antes de llamar a la función, en ordenadores PC, la pila al empezar a ejecutar lo que es própiamente el código de la función, estará en X - 22. ¿Por qué? Cada vez que se inserta un entero en la pila la posición de la pila decrementa en 4 y cada vez que se extrae incrementa en 4 (al revés de lo que se podría pensar en un principio) por motivos de eficiencia.

Al llamar a una función (estándar) en C, normalmente BP-based:
- Se inserta en la pila la dirección de retorno. [X-4]
-----------
- Se guarda en la pila el registro EBP [X-8]
- Se guarda en EBP el valor de ESP
- Se reserva espacio en la pila decrementando la posición de ESP [X-8-DL]
- ...Se ejecuta el código...
- Se restaura el valor de EBP

DL en el ejemplo vale 10+4. Los 10 bytes de datos y los 4 bytes del entero.

En memoria quedaría algo así:

[ BUFFER(10) ] [ INT(4) ] [ EBP(4) ] [ DRET(4) ]

Así que si escribimos 22 bytes en el buffer escribiremos en el buffer, en int, en ebp y en dret.

La pila no permite ejecutar código, así que ¿cómo podemos ejecutar código arbirario?

La pila almacena la dirección de retorno de la función y casualmente está después del buffer (¡genial!).

Modificando la dirección de retorno por una dirección que esté dentro de los datos que acabamos de modificar, podremos ejecutar código libremente.
Así que tendremos que escribir nuestro código (en código máquina) en, como mucho, (en este caso) 18 bytes y guardar después 4 bytes con la dirección de retorno.
Por supuesto tenemos que conocer la dirección del buffer justo cuando se va a hacer el exploit para poder indicar la dirección de retorno (lo cual es un inconveniente) y no permite hacer exploits genéricos para un código fuente sino para ejecutables en sí.
Adicionalmente si el exploit se hará al cargar una stringz (cadena acaba en \0), el exploit no podrá contener \0 en ninguna parte, lo cual complica un poco las cosas. Si la dirección de retorno contiene \0, en ciertos casos el exploit será imposible de hacer con este método.

Hay otros métodos para hacer exploits y cada exploit es un mundo.
Como por ejemplo los exploits de Heap, que son mas sencillos de hacer, pero menos comunes de encontrar. En muchos casos de exploits de Heap, hay código que se ejecutará, que está después del buffer que estamos modificando y por lo tanto colocando una cantidad determinada de NOPs en el buffer y luego el código que queremos ejecutar podemos hacer un exploit sin necesidad de conocer la dirección del buffer en memoria.

Todo esto es una aproximación simple y hay un montón de documentación en internet al respecto.

Consejos para evitar overflows:

  • No uses funciones externas que trabajen sobre punteros y a las que no se les pasen la longitud máxima. Claro ejemplo: "gets".
  • No uses strcpy, sprintf con %s u otras funciones de trabajo con cadenas indiscriminadamente sin conocer de antemano la longitud que tendrán los elementos que entran en juego.
  • Siempre que recorras un puntero y escribas en otro; por ejemplo en descompresión o en desencriptación. A parte de limitar la lectura y la escritura con las condiciones correspondientes, comprueba siempre que ni se lee mas de la cuenta ni se escribe mas de la cuenta. Por ejemplo si al descomprimir un fragmento, se indica el tamaño comprimido y el descomprimido; podría ser que estuviese modificado para "hacer creer" que hay menos para descomprimir o cosas similares.

A parte de escribir el tutorial, he programado un ejemplo de un overflow para windows.

Dado el siguiente programa:

#include 
#include <string.h>
#include <ctype.h>

int main() {
unsigned char c;
unsigned char texto[0x400];
int n, l;
gets(texto);
for (n = 0, l = strlen(texto); n < l; n++) {
c = tolower(texto[n]);
if (!isalpha(c)) continue;
texto[n] = (c != 'z') ? texto[n] + 1 : texto[n] - 25;
}

printf("Has escrito: %s\n", texto);

return 0;
}

Aparentemente lo que hace es pedir una frase al usuario y cambiar cada letra por la letra siguiente.
Pero si se introduce un texto preparado, puede permitir ejecutar código arbitrario.
He preparado una entrada que se queda en un bucle infinito mostrando por pantalla "TU PROGRAMA HA SIDO HACKEADO" con ese programa.
Pero claro... es un ejemplo y solo hace eso. Pero con ciertas entradas se podrían hacer cosas muy peligrosas. Se podría pensar que en un buffer de 0x400 bytes no coge un programa "demasiado" peligroso. Pero si que hay espacio suficiente para hacer un programa que se descargue otro programa de internet y lo ejecute. Y eso SI es peligroso.

El ejemplo se puede descargar de la siguiente dirección:
Descargar ejemplo de Overflow

Última modificación: 26-01-2008 01:05:36