Buffer Overflows y protección del kernel: teoría.

Esta es mi primera (que no última) aportación al blog, y aunque es de un cierto nivel técnico, espero no aburriros. El artículo que presento a continuación trata de los ataques de desbordamientos de memoria (buffer overflow), y de una protección del núcleo Linux 2.6 contra éstos llamada “Virtual Address Space Randomization”, también llamado ASLR. Este último año la funcionalidad de esta protección se ha visto comprometida, como veremos en detalle mañana en la segunda parte de esta entrada.

En esta primera parte, creo necesario para comprender el funcionamiento de esta protección explicar las bases de los desbordamientos de memoria, que a partir de ahora llamaremos Buffer Overflow. De manera muy resumida, los ataques de buffer overflow aprovechan código donde no se comprueba correctamente la longitud de las cadenas de entrada, de manera que es posible introducir un dato de longitud mayor que el espacio reservado para ese dato. Es decir, si reservo un espacio de 10 bytes para la variable de la función, ésta recibe un dato de 25 bytes, y no compruebo correctamente el tamaño del dato recibido, escribiré en memoria los 10 bytes reservados más 15 bytes no legítimos. ¿Y esto para que sirve? Pues para entenderlo vamos a explicar el funcionamiento a nivel de memoria de un proceso.

Cuando un proceso se ejecuta, el espacio de memoria reservado para el proceso se divide en tres partes: la primera es el texto que no es más que el código a ejecutar, la segunda son los datos (como puedan ser variables inicializadas o no, y constantes), y por último la pila (stack). En nuestro caso, nos vamos a centrar en la pila o stack. Ésta se emplea para las llamadas a funciones, ya que permite guardar (apilar, que por eso es una pila) los valores que se requieran para la ejecución de la función, y al mismo tiempo restaurar el estado de los registros una vez termina la ejecución de la función. Es importante recordar que la pila va de la parte alta de la memoria a la parte baja.

Cuando una función se ejecuta, lo primero que hace es apilar los valores que la función recibe como argumentos. Lo segundo que se guarda es el estado actual de los registros para poderlos restaurar una vez finalice la ejecución de la función: EIP y EBP. El primero de ellos, EIP, indica la siguiente línea de código que el procesador debe ejecutar, y el segundo, EBP, indica la dirección del final de la pila actual. Dicho de otra forma, nos estamos asegurando de que al final de la ejecución de la función sabremos dónde hemos de volver. Una vez guardados los registros EIP y EBP de la pila actual, se modifica el registro EBP asignándole el valor del registro ESP (registro donde se guarda la dirección de la cima de la pila). ¿A que no hemos entendido nada? No os preocupéis, con el dibujo se ve (un poco) más claro:

pila

Ahora viene lo interesante. Cuando la función termina, ésta llama a la instrucción Ret, que se encarga de desapilar absolutamente todo hasta llegar al EBP de la pila. Una vez llegue al EBP de la pila quedan dos registros por desapilar: el primero contiene la dirección del EBP antiguo de la pila que había antes de la ejecución de la función, por lo que el registro EBP tomará dicho valor restaurando el estado anterior. Por último, y presten atención porque esto es lo importante, leerá el valor del EIP —que contiene la dirección de la siguiente linea de código a ejecutar—, que es justamente lo que un ataque de buffer overflow intentará falsificar, modificando la dirección de retorno de la siguiente línea a ejecutar por la dirección donde se encuentre el código del atacante que desea ejecutar. Como nota adicional, ya que seguramente nadie ha caído, en caso de que la función devuelva un valor, éste se guarda en el registro EAX. No obstante, voy a dejarles un tiempo para meditar todo esto, y mañana volvemos. No se preocupen si no han entendido mucho, seguro que mañana todo les resultará más sencillo.

Interesante, ¿no les parece?

Comments

  1. Esta bueno

  2. que buen post, de mucha utilidad, gracias !