Segmentación de memoria de un programa y el stack.

Hola:

logofinal

bugExploiting [Overflow].
Parte I. Segmentación de memoria de un programa y el stack.

En este artículo asentaremos las bases para poder adentrarse en el oscuro mundo del exploiting. Se trata de un artículo fundamental, de obligada lectura, para poder asimilar correctamente todos y cada uno de los conceptos posteriores. Comprenderás como se divide la memoria de un programa en ejecución, lo que se conoce como segmentación de memoria de un programa, cuales son sus segmentos, que objetivos tienen y que se almacena en ellos. Nos centraremos en el segmento del stack, su estructura, su comportamiento, nos acercaremos al concepto de marco de pila o frame stack. Todo ello necesario para comprender que sucede internamente cuando llamamos a una subrutina y, como posteriormente podemos aprovecharnos de todo esto para manipular el flujo del programa a nuestro antojo y obtener, por ejemplo, una shell con permisos de administración.

Este artículo pertenece al Taller bugExploiting. Puedes visitar la página del taller para obtener el índice y todos los artículos publicados.

La segmentación de memoria de un programa se divide en varias zonas que pasamos a detallar a continuación.

Segmentación de memoria de un programa

Segmentación de memoria de un programa

.text : Es de tamaño fijo y de solo lectura. En esta parte se almacenan todas y cada una de las instrucciones en código máquina que componen el programa que se está ejecutando. Como curiosidad añadir que en caso de existir varias instancias en ejecución del mismo programa o binario el sistema operativo actúa de forma inteligente manteniendo una sola copia en memoria del código y permitiendo que los procesos puedan compartirla para ahorrar recursos.

.data [data segment]: Aquí se almacenan las variables globales inicializadas del programa. De tamaño fijo y permite la escritura, se puede escribir en él.

.bss [bss segment]: Aquí se almacenan las variables globales sin inicializar. De tamaño fijo y permite la escritura, se puede escribir en él.

Heap [heap segment]: Segmento de memoria reservado para la memoria dinámica del programa. El tamaño de este segmento no está predefinido, va variando. Crece hacia a arriba, en el mismo sentido que las direcciones de memoria. Para reservar memoria utilizamos, por ejemplo, las conocidas funciones de asignación malloc(), calloc(), o realloc() del lenguaje C.

STACK [stack segment]: Para lo que nos ocupa, lo más interesante. La pila o stack. Aquí se guardan los argumentos pasados al programa, las cadenas del entorno donde este es ejecutado (el comando env permite su visualización), argumentos pasados a las funciones, las variables locales que todavía no contienen ningún contenido, y además es donde se almacena el registro IP cuando una función es llamada (valores de retorno).

 sizeyenv

Como ejercicio, para entender mejor los segmentos de la memoria de un programa, os invito a comentar y descomentar las variables globales “i” y “a” para ver que parte del segmento de la memoria crece y decrece. En el lenguaje C, las líneas se comentan con “//”.

Para la compilación y ejecución del programa:

gcc código_fuente.c –o nombre_ejecutable

./nombre_ejecutable

Tipos de desbordamiento:

Ahora que conoces la segmentación de un programa en memoria entenderás que clasificaremos un desbordamiento de buffer (buffer overflow), según el segmento de memoria al que pertenezca.

Estos son:

  • Pila o Stack [Stack overflow].
  • Heap [heap overflow].
  • Bss [bss overflow].

Cada uno de ellos se explota de forma diferente.

 

Estructura PILA o patrón LIFO:

Antes de meternos de lleno a explicar el stack de un programa vamos a explicar cómo se almacenan los datos en esta parte del segmento.

Una pila o stack es una lista ordenada o estructura de datos en la que el modo de acceso a sus elementos es de tipo LIFO (del inglés, Last In First Out, último en entrar, primero en salir) que permite almacenar y recuperar datos.

Para el manejo de los datos se cuenta con dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su operación inversa, retirar o desapilar (pop), que retira el último elemento apilado.

En cada momento sólo se tiene acceso a la parte superior de la pila, es decir, al último objeto apilado (denominado TOS, Top of Stack en inglés). La operación retirar permite la obtención de este elemento, que es retirado de la pila permitiendo el acceso al siguiente (apilado con anterioridad), que pasa a ser el nuevo TOS.

pila

Dicho de otra forma, para que no queden dudas:

Vamos a imaginarnos que has tenido una cena en casa, que has tenido muchos invitados y esto ha derivado en un montón de platos sucios que lógicamente te toca limpiar.

Vamos a pensar que tu cocida consta de un fregadero de dos pozos, vamos a pensar que cada uno de los pozos del fregadero es una pila.

Para comenzar con la tarea tienes que ir metiendo uno a uno cada uno de los platos sucios en el primer pozo, para ello, lo que haces es un push por cada uno de los platos, hasta llegar a la altura máxima del fregadero, en el caso de una pila, hasta la zona de memoria que permita. A partir de aquí, no podrás hacer más push.

Una vez que tenemos todos los platos apilados en el primer pozo, lo siguiente que tenemos que hacer es ir sacando cada uno de ellos, limpiarlos, y colocarlos en el nuevo pozo para que se sequen.

Dicho de otra forma. Pop al primer pozo del fregadero, lavado, y push al segundo pozo. Así hasta terminar. Cuando no queden más platos en el primer pozo, no podremos realizar más pop, lógico.

Realizaremos el mismo proceso para colocar los platos limpios una vez que estén secos.

Si puedes visualizar este ejemplo, te darás cuenta que el plato que está en la cima de la pila fue el último en ser colocado y será obligatoriamente el primero en salir de la pila.

Creo que ahora se entiende claramente en que consiste una estructura en pila o acceso mediante patrón LIFO.

Marco de pila o Stack frame:

Cada vez que un programa ejecuta una función en memoria se genera una estructura de datos en el STACK siguiendo el patrón LIFO.

En esta sección, llamada pila o stack, es donde se almacenan los datos que son necesarios para la correcta ejecución de las funciones de un programa.

Esta estructura que se construye cuando se ejecuta una función se llama stack frame o marco de pila. Una vez se ha ejecutado la función, el marco de pila creado puede ser sobrescrito por los marcos de pila que generen otras funciones.

Para los más noveles, decir que una función, también conocida como procedimiento, método o subrutina, dependiendo del lenguaje de programación utilizado, es una parte del código separado del bloque principal y que puede ser invocado en cualquier momento desde este, desde otra función o por si misma (funciones recursivas, por ejemplo).

#include <stdio.h>

void msg(){
printf("Hola mundo\n");
}

int main (int argc,char **argv){
msg();
msg();
return 0;
}

En el ejemplo, podemos ver que el programa está compuesto por el programa principal, conocido como main(), y una función llamada msg(). El programa llama dos veces a la función msg() que imprime por pantalla el texto “Hola mundo”.

Composición de un marco de pila o stack frame.

Sin entrar en los detalles específicos de cada compilador, un marco de pila está compuesto por:

- Stack Frame anterior:  Lo que serían los otros stack frames de otras funciones almacenadas en la pila.

- Argumentos: Aquí se almacenan los argumentos que se le pasan a las funciones.

Por ejemplo: void msg(int i);

El argumento que se almacenara en esta parte es un valor entero (int), un número.

- Dirección de retorno [EIP o RET]: Aquí se almacena la dirección de memoria de retorno. La dirección de memoria que apunta a la siguiente instrucción por la que debe continuar el flujo del programa una vez finalizada la función.

Más adelante entraremos un poco más a fondo en esto. Por ahora quédate que es como un marcapáginas de un libro que nos indica la última página vista para que posteriormente sepamos por donde debemos de continuar la lectura.

- Dirección de marco de pila anterior [EBP]: Dirección de memoria que apunta al marco de pila anterior. Aquí se guarda la dirección de memoria a la que apuntaba anteriormente el registro EBP. En los ejemplos posteriores se entenderá mejor esto.

- Espacio reservado para variables locales: Aquí se reserva el espacio para almacenar las variables locales de la función.

En el esquema expuesto: Se reservan 12 bytes. Un byte por carácter. “Hola Mundo.” se compone de 11 caracteres contando el espacio y el punto. Si no se tiene en cuenta que es necesario almacenar “\0” que indica donde termina la cadena, puede parecer que sobra un byte. Ojo con esto, muy importante cuando trabajamos con cadenas en lenguaje C.

Lo siguiente a la memoria reservada para las variables locales sería un segmento de memoria no reservado y libre, siempre y cuando no se haya consumido toda la memoria.

En el registro EBP almacenamos la dirección de memoria que apunta al segmento reservado para “Dirección de marco de pila anterior EBP”.

En el registro ESP almacenamos la dirección de memoria del último dato insertado en la memoria de la pila.

 

marcopila

Como supongo que esto no ha quedado del todo claro vamos a pasar a detallar con un ejemplo como funciona todo esto.

¿Qué ocurre cuando llamamos a una función?

Para ello vamos a utilizar el siguiente código:

#include <stdio.h>
void suma(int x, int y){
int resultado;
resultado = x + y;
return resultado;
}

int main (){
int i = 1;
int j = 2;
suma(x, y);
return 0;
}

Este programa lo que hace es inicializar dos variables “i” y “j” con los valores 1 y 2 respetivamente y sumarlos.

Para ellos utiliza una función llamada suma(), que recibe dos argumentos, que son los números a sumar. Por último, devuelve el resultado. Muy sencillo.

Pasemos a explicar cómo se comporta el stack al ejecutar este programa. Para hacer un poco de repaso, empezaremos por el principio.

A partir de la dirección de memoria más baja reservada para el programa se carga el [.text] que almacena todas las instrucciones en lenguaje máquina del programa. Segmento de código.

Se cargan las variables globales inicializadas en el segmento de datos [.data]. En este ejemplo, no hay variables globales inicializadas, por lo tanto, no se cargan.

Se cargan las variables globales no inicializadas en el segmento bss [.bss]. Tampoco existen variables globales no inicializadas. Por lo tanto, tampoco se guarda nada.

Como en el programa no se utiliza memoria dinámica, tampoco se reserva espacio en este segmento.

Pasemos ahora a detallar que ocurre en el segmento de la pila.

Función main():

[PASO I]

Antes de comenzar a apilar, el registro EBP apunta a la cima actual de la pila.

En la función main() tenemos dos variables locales. Estas variables se guardan en el espacio de memoria asignado para variables.

Llamada a función suma():

[PASO II]

La función suma recibe dos argumentos, en este caso dos enteros, “i” e “j” que se almacenaran en el espacio de memoria reservado para argumentos.

En la zona reservada para la dirección de retorno se almacena la dirección de memoria por donde debe de continuar el flujo del programa, en este caso será la dirección de memoria que apunta a return 0.

En la zona de dirección de marco de pila anterior almacenamos el valor del registro EBP. Que apunta al marco de pila o frame stack del main(). Este valor es también una dirección de memoria.

[PASO III]

El registro EBP pasa a apuntar al segmento del stack “dirección de marco de pila del main()”

Por último, se reservará memoria, en este caso 12 bytes, para las variables locales.

El registro ESP almacena la dirección del último dato que se ha insertado en la región de memoria.

subrutina

Es importante aclarar que una cosa es el registro EBP y otra diferente el segmento “Dirección de marco de pila anterior EBP”.

El registro EBP apunta hacia el segmento de “marco de pila anterior” de la subrutina.

El segmento “marco de pila anterior” almacena la dirección de memoria que apunta al marco de pila de otra función.

Creo que ahora ha quedado más claro.

 

Conclusiones finales:

Comprender la segmentación en memoria de un programa y cómo se comporta el stack cuando llamamos a funciones resulta imprescindible para entender el exploiting. En este artículo hemos asentado esas bases fundamentales. Ahora estamos preparados para asimilar mejor artículos posteriores.

En el próximo capítulo comprobaremos y probaremos todos estos conceptos. Empezaremos ya a ensuciarnos las manos.

Hasta el próximo capítulo.

Wadalsaludos ,  -<|: b|.

 

Autor: NeTTinG - Enrique Andrade
Perito Informático Forense Judicial

Publicado en Artículos, [bug]Exploiting

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: