Skip to main content
Como funciona un Buffer Overflow - parte I

Como funciona un Buffer Overflow – parte I

A pesar de los complejos mecanismos de seguridad que existen hoy en día se trata de una vulnerabilidad que persiste en el tiempo

Bienvenidos de nuevo a una nueva entrada en Byte Mind, en este caso vamos a tratar un tema bastante importante en el mundo de la seguridad informática y que a pesar de la antiguedad de este tipo de ataque sigue siendo una de las vulnerabilidades más persistentes, el Buffer Overflow.

A pesar de los complejos mecanismos de seguridad que existen hoy en día se trata de una vulnerabilidad que persiste en el tiempo, por ello, vamos a dedicar una serie de post a tratar de entender qué es, como funciona y como podemos evitar el mismo, pero vamos por partes.

 

¿Qué es un Buffer Overflow?

Buffer Overflow es una vulnerabilidad causada por la inserción de datos no controlados con un tamaño superior al esperado por la aplicación, lo que da lugar a la sobreescritura de espacios adyacentes en la memoria de la máquina, provocando un comportamiento no esperado en la misma.

Un ejemplo común y sencillo para explicar qué sucedería sería llenar una botella de 1 litro de agua. Cabe entender que la botella sólo podríamos llenarla hasta llegar a 1 litro, pero ¿qué pasaría si echamos más agua de la que cabe en la botella?, como es lógico una vez superada la capacidad de la botella, el agua se desbordaría, haciendo que perdamos parte del agua que estamos utilizando para llenar la misma. En informática es lo que llamamos un desbordamiento del buffer, pero con este ejemplo te entrarán varias dudas como ¿qué es un buffer?, ¿qué pasa si escribimos más datos de los que nos permiten los espacios adyacentes de memoria? o, ¿cuales son las consecuencias de esta vulnerabilidad?.

Para entender en detalle todos estos conceptos vamos a recordar algunos conceptos de la memoria del sistema.

 

Estructura de la memoria

Cuando se ejecuta un programa, el sistema operativo reserva una zona de la memoria para que la aplicación realice las instrucciones para las que ha sido creada y de la forma para la que ha sido configurada, este espacio se dividirá en zonas en función de los diferentes tipos de datos a procesar.

Lo primero que realizará será cargar el código ejecutable del programa, es decir, las instrucciones. Posteriormente se reservarán al menos dos espacios para los datos requeridos para la ejecución de las mismas, estos espacios se definen como stack y heap.

El stack almacena los argumentos de las funcionas, las variables locales y las direcciones de retorno de las llamadas a dichas funciones.

El heap se encargará de gestionar la memoria dinámica, es decir, la memoria solicitada durante el tiempo de ejecución, como por ejemplo el uso de malloc en C o new en C++.

Además, en aquellos lenguajes que permiten la creación de variables globales o de variables estáticas, como son C y C++, también se reserva una tercera zona, llamada zona de datos estáticos. Esta zona está dividida en dos apartados, los cuales se diferencian en función de si los datos han sido inicializados o no antes del inicio de la ejecución de la función principal, o main en inglés. Estas zonas ocupan un tamaño fijo de memoria durante toda la ejecución del programa, debido a que es posible determinar el espacio requerido por la aplicación en tiempo de compilación y que no variará en tiempo de ejecución.

Veamoslo de forma sencilla con la siguiente imagen:

Como funciona un Buffer Overflow
Figura 1. Estructura de la memoria

En la captura anterior podemos ver las diferentes secciones explicadas anteriormente, stack y heap, además de otras zonas que pasamos a explicar a continuación.

  • .bss  -> En esta zona se almacenan las variables globales sin inicializar y con un tamaño fijo.
  • .data -> En esta zona se almacenan las variables globales inicializadas del programa, también con un tamaño fijo.
  • .text -> En esta zona se almacenan todas las instrucciones en código máquina que forman el programa que se está ejecutando. En caso de que se realicen varias ejecuciones del mismo, el sistema operativo actuará de forma inteligente, manteniendo una sóla copia en memoria del código y permitiendo que los procesos puedan compartir los recursos.

Como indicábamos anteriormente, el heap, es el área de memoria utilizado para asignar bloques de bits. Cuando en una función o un método del programa encontramos una instrucción que realiza esta petición, el sistema operativo reserva la memoria solicitada en una zona libre del heap, la cual será marcada como asignada y devolverá la dirección de memoria de la primera posición de los bytes reservados con un puntero o referencia en función del lenguaje de programación utilizado. En el momento que ya no se necesite dicha memoria dinámica, es recomendable indicar al sistema operativo que libere la misma para que pueda ser asignada en una nueva petición.

En lenguajes más modernos como por ejemplo java, existe un mecanismo denominado recolector de basura (garbage collector, en inglés) que consiste en un proceso ejecutado en segundo plano que irá buscando bloques de memoria que no se estén referenciando en un momento dado, liberando el mismo y permitiendo que se encuentre disponible para otra petición.

Con la explicación dada hasta el momento se puede entender que el buffer es un espacio en memoria que sirve como almacenamiento temporal de datos de entrada de un programa.

Continuando con la explicción, el contenido relacionado para cada función se encuentra diferenciado gracias a un contexto que recibe el nombre de stack frame. Dicho de otra forma, el stack frame de una funcion determinada es todo lo que se encuentra almacenado en la pila debido al proceso de su ejecución. Veamos la siguiente imagen:

Como funciona un Buffer Overflow - parte I

 

En la imagen anterior podemos denotar diferentes cosas. Por una parte, y tal y como se comentaba anteriormente el crecimiento de la pila irá de abajo a arriba, recorriendo cada una de las funciones de nuestro programa. Cada uno de estos pasos recibiría el nombre de stack frame. Además podemos observar dos registros más que pasaremos a explicar a continuación, ESP y EBP.

El registro ESP (del inglés Extended Stack Pointer) apuntará siempre a la cima de la pila, es decir, la última posición de memoria ocupada por esta.

El registro EBP (del inglés Extended Base Pointer) es un registro que apunta a una posición de la pila que contiene la dirección de memoria que apuntaba el stack frame anterior, es decir, el stack frame de la función que ha llamado a la función actual. En otras palabras, apuntará a una posición que dará una dirección relativa de los parámetros y variables locales y, entrando más en detalle, en la parte superior se encuentran las variables locales de la función actual y en la parte inferior se encontrará el valor de retorno de esta y los valores de los parámetros que ha recibido.

Es necesario también destacar que en la pila se almacenarán valores temporales de operaciones, que en otras palabras, sería posible utilizar la pila para almacenar valores intermedios de operaciones más complejas. De la misma forma que las variables globales, estos valores temporales pueden ser referenciados por un registro negativo del registro EBP.

 

Para que todo esto quede más claro vamos a realizarlo con un ejemplo, respaldándonos en el siguiente código:

En el caso del código de ejemplo, observamos que la función «suma» recibe dos valores enteros por parámetro, define una variable local y devuelve un valor. Si analizásemos el código ensamblador generado por el ejemplo, en la función calc, el primer paso sería ejecutar la función suma, y para ello colocaria los parámetros recibidos, 1 y 2, en la pila de forma inversa a la lista de parámetros establecidos y seguido de ello ejecutar la instrucción CALL.

Esta instrucción realizará dos tareas. Primero almacenará en la pila el valor actual del registro EIP (en inglés Extended Instruction Pointer) que es el registro encargado de apuntar a la siguiente instrucción a ejecutar y, en segundo lugar, lo modificará para que apunte a la primera instrucción del código de la función. Esto es debido a que una vez finalizada la ejecución de la función suma() el sistema debe de saber donde se ha quedado para poder continuar correctamente con su camino.

Una vez completada almacenará el valor del registro EBP a la siguiente posición disponible de la pila y modificará su valor para que apunte a la dirección de esta. En este momento se almacenarán las variables locales de la función, que siguiendo el ejemplo dado, sólo se almacenará el valor de la variable local sum.

Completada esta función y realizada la suma de la variable sum, volverá al mismo punto desde el cual se hizo la llamada a la misma. Este mecanismo dependerá entre otros factores del tamaño ocupado por el valor dado. Si este ocupa 4 bytes o menos, el valor se almacenará en el registro EAX (del inglés Extended Accumulator Register), en cambio, si el valor es superior a 4 bytes, entonces se modificará el código de la llamada a la función añadiendo un parametro adicional que será la dirección de memoria en la que es almacenado el valor de retorno de la función.

Una vez completadas estas tareas, el procedimiento siguente será recuperar todos estos valores almacenados y colocarlos de nuevo en sus correspondientes registros.

Cabe destacar también que generalmente estos datos se almacenarán siguiendo el formato litle-endian, que quiere decir que el byte menos significativo se escribirá en primer lugar. De todas formas, podemos comprobar la configuración de nuestro sistema linux cualquiera de los siguientes comandos:

 

Otro aspecto inportante a comentar es que los registros explicados corresponden a arquitecturas de 32 bits, indicado mediante la sigla E (Extended) al inicio de su nombre. En arquitecturas más actuales, de 64 bits, cada elemento del registro vendrá precedido por la letra R, por ejemplo RAX, referenciando de este modo a este tipo de arquitectura.

Y esto es todo por el momento, en los próximos días publicaré la segunda parte de esta entrada en la cual llevaremos a cabo un ataque de buffer overflow.

Espero les haya gustado y ya saben, cualquier duda, sugerencia, etc… es bienvenida en los comentarios.

Ya está publicada la segunda parte, disponible desde este enlace.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *