Fundamentos de StackOverflows

Notas Previas

Hace ya bastante tiempo, fácilmente unos 9 años, a la par que los artículos de ingeniería inversa con Ollydbg (quizá un poco después) escribí unos artículos sobre las vulnerabilidades del tipo stackoverflow para un ezine de aquella época. He decidido reeditarlos añadiendo muchas mas explicaciones y muchas más imagenes, para que se entienda mejor, los artículos que escribí inicialmente no tenían apenas imágenes y en algunos puntos quizá eran un poco complicados de entender. Así que sin más preámbulos empezamos.

Antes de cada artículo voy a comentar la forma de comentar el programa vulnerable. Para este caso es necesario compilar el programa name.c mostrado más abajo de la siguiente forma:

gcc -m32 -fno-stack-protector -z execstack name.c -o name

También es necesario desactivar ASLR ejecutando el siguiente comando con permisos de administrador

echo 0 > /proc/sys/kernel/randomize_va_space

Fundamentos de StackOverflow

En este artículo nos vamos a centrar en las vulnerabilidades de tipo Stack Overflow.

Para seguir este artículo vamos a necesitar una serie de herramientas que se enumeran a continuación.

- GCC
- objdump
- gdb

En la mayoría de distribuciones Linux estas herramientas vienen por defecto instaladas, por último para compilar nuestros programas vamos a necesitar el siguiente comando de gcc

gcc -fno-stack-protector -z execstack fichero.c -o programa

Donde fichero.c es el fichero fuente que queremos compilar y programa el nombre del ejecutable.

También vamos a nacesitar desactivar ASLR ejecutando la siguiente instrucción como root

echo 0 > /proc/sys/kernel/randomize_va_space

Para volver a activar ASLR:

echo 2 > /proc/sys/kernel/randomize_va_space

Lo que estamos haciendo con esto es desactivar todas las protecciones que implementa el compilador y el sistema operativo.

Introducción

En estos artículos vamos a hablar sobre aspectos bastante técnicos de la arquitectura de un programa y del Sistema Operativo, así que es necesario explicar antes una serie de conceptos importantes.

Para empezar tenemos que tener en cuenta que cada proceso se ejecuta en su propio espacio de direcciones y a grandes rasgos la estructura de memoria de cada proceso es algo parecido a lo siguiente:

process_va

En la primera dirección del proceso se coloca el valor 0x00000000 a continuación va el nombre del programa, después las variables de entorno que han sido heredadas del proceso padre y los argumentos.

A continuación nos encontramos con 2 secciones bastante interesantes, por una parte la pila o stack donde se almacenan los argumentos de los métodos y las variables de los métodos, entre otras cosas. El stack va creciendo hacia direcciones inferiores de la memoria. Por otra parte tenemos el heap o monton, donde se almacenan las variables que se hayan definido reservando memoria con la familia de funciones malloc. El heap está estructurado en zonas contiguas de la memoria formadas por una cabecera y los datos en si y no puede haber dos regiones del heap vacias, si esto es así, estas 2 regiones se fusionan en una sola. El heap al contrario que el stack va creciendo hacia direcciones superiores de la memoria y también es susceptible de ataques, los conocidos heap overflow.

La estructura que nos interesa a nosotros es la pila, que es una estructura de tipo LIFO (Last-In First-Out). ¿Qué significa esto? Significa que el último dato en entrar será recogido el primero cuando se ejecute una instrucción de recogida de la pila.

Podemos hacer una analogía con un conjunto de libros apilados, si tenemos un libro encima de otro, cuando dejemos otro libro lo dejaremos encima del todo y cuando queramos coger un libro tendremos que ir retirando libros desde el final hasta llegar al deseado.

Las dos instrucciones básicas que trabajan con la pila en ensamblador son POP y PUSH. PUSH sirve para introducir un dato en la pila y POP para retirarlo.

Es necesario recordar que el procesador internamente trabaja con registros generales (EAX, EBX, ECX, EDX, EIP, ESI, EDI, EBP, ESP y un registro de flags), registro de segmentos (CS, DS, SS, ES, FS, GS) y unos cuantos registros más internos que no vienen al caso. La E delante de los registros significa extendido, ya que estos registros son heredadeos de las arquitecturas de 16 bits donde EAX era AX.

Los registros que más nos interesan son EBP (Extended Base Pointer), ESP (Extended Stack Pointer) y EIP (Extended Instruction Pointer). EBP y ESP son registros de pila, EBP apunta a la base de la pila y ESP apunta a la cima de la pila, EIP almacena la siguiente instrucción a ejecutar.

A continuación vamos a presentar un programa en C.

Podemos pensar que a simple vista es un programa sin nada en especial, de hecho si lo ejecutamos obtenemos una salida normal:0

Pero vamos a introducir más caracteres de los que permite (que son 32). Para ello me voy a valer de python para generar cadenas, concretamente voy a meter 35 caracteres a ver que ocurre, para ello voy a usar el siguiente comando:

./name `python -c "print 'A'*35"`

1

Vemos que en contra de lo que hemos indicado permite una cadena mayor de 32 bytes, vamos a introducir ahora una cadena bastante más larga a ver que ocurre.

2

Como podemos ver ahora si que nos muestra un error, concretamente el conocido fallo de segmentación. Vamos a proceder a ejecutar este programa con el depurador de linux gdb a ver que ocurre

3

Aquí podemos obtener mucha más información, vemos que gdb nos indica lo siguiente:

0x41414141 in ?? ()

¿Qué significa esto? Significa nada más y nada menos que el programa ha intentado ejecutar una instrucción en la dirección 0x41414141 pero que no ha podido ejecutarla porque dicha dirección no está mapeada y de ahí que de un fallo de segmentación. 0x41 es la representación del carácter A.

¿Que es lo que ha ocurrido?

Lo que ha ocurrido es que hemos sobreescrito la dirección de retorno del método func con el parámetro que le hemos proporcionado y ahora está intentando retornar a una dirección no mapeada. Para entender mejor todo lo que ha pasado es necesario explicar un poco más a fondo el funcionamiento de la pila y de la llamada a métodos.

Cuando se va a llamar a una función o método el programa introduce los parámetros (esto depende de la convención de llamada usada, cdecl, stdcall o fastcall) en la pila mediante la instrucción push, acto seguido se ejecuta la instrucción ensamblador call que básicamente lo que hace es realizar un salto a la dirección de memoria de la función a llamar y además introduce el registro EIP en la pila, a continuación, se introduce en la pila el registro EBP y por último se introducen las variables locales. Por tanto tenemos la siguiente representación gŕafica:

stack

Podemos ver que si proporcionamos más bytes de los que admite nuestro buffer sobreescribiremos valores superiores, esto es EBP, EIP…

Vamos a desensamblar el código del programa para ver realmente que es lo que se hace en la función func

4

Vemos como, nada más entrar, en la función se ejecuta la instrucción push %ebp seguida de un mov %esp, %ebp. Lo que está haciendo aquí es crear un nuevo marco de pila. Pone EBP en la pila (como se ha mostrado en la imagen superior) e iguala ESP a EBP, posteriormente resta 0x28 (50 en decimal) a ESP, es decir, reserva 50 bytes en la pila, esto corresponde a nuestro buffer local que hemos declarado. Nosotros hemos definido que tenga capacidad para 32 bytes pero realmente nos ha reservado 50 bytes, esto realmente depende del compilador usado y de la versión.

Las dos primeras instrucciones ensamblador, junto a las 2 ultimas (leave y ret) forman el prólogo y el epílogo de la función que es común a todas las funciones. De hecho, si ponemos un breakpoint en la función func con break func veremos que no pone el breakpoint en la dirección 0x0804842b sino en la dirección 0x08048431 que corresponde básicamente con la instrucción ensamblador siguiente a la reserva de memoria para las variables locales.

5

Esto es porque gdb interpreta que es el prólogo común de todas las funciones y no lo tiene en cuenta (si queremos que lo tenga en cuenta deberemos poner el breakpoint mediante break *func).

Vale, sabemos que hemos sobreescrito la dirección de retorno del método pero esto nos sirve de poco, necesitamos saber la longitud exacta que tenemos que pasarle al argumento para sobreescribir la dirección de retorno del método. De forma artesanal podemos optar por una técnica bastante sencilla (y que usan algunos de los frameworks de exploiting existentes en el mercado).

La técnica consiste en invocar al programa de la siguiente forma:

./name AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN

De esta forma cuando sobreescribamos el EIP podremos identificar en que parte de la cadena se ha sobreescrito, si ejecutamos esto en gdb obtenemos lo siguiente:

6

Vemos que EIP =0x4c4c4c4c, 0x4c es L, contando desde el principio hasta la primera L de la cadena pasada por argumentos obtenemos que la primera L está en la posición 45, por tanto para sobreescribir EIP necesitamos exactamente 48 caracteres en la cadena.
Vamos a probar a redirigir el flujo del programa, si sabemos donde está EIP, podemos pasarle una dirección válida y ver si ejecuta el código de esa dirección de memoria. Hay que recordar que el valor de EIP se recupera cuando se sale de la función, mediante RET, por tanto, la dirección de memoria que pongamos ahí se ejecutará cuando se finalice la función func. Vamos a hacer algo sencillo, queremos que cuando acabe la función func se vuelva a llamar a si misma. Para esto necesitamos conocer la dirección en la que se encuentra la función func, podemos hacerlo mediante gdb, pero ahora vamos a usar objdump, otra herramienta de volcado de binarios que nos será realmente útil

7

Podemos observar que la función func está en la dirección de memoria 0x0804842B

Para pasar una dirección hexadecimal de memoria como parte del argumento del programa necesitamos hacerlo de la siguiente forma:

./name `python -c "print 'A'*44+'\x2b\x84\x04\x08'"`

Lo que hacemos aquí es imprimir 44 veces A y después la dirección de memoria con la que queremos sobreescribir el registro EIP, hay que recordar que en una arquitectura little-endian como en la que estoy, el byte menos significativo se debe colocar en la primera posición. La dirección de memoria no la podemos pasar como un string normal porque no se va a interpretar correctamente, por eso python y otros muchos lenguajes de programación tienen mecanismos para escribir valores hexadecimales directamente, con la sintaxis \xXX donde XX es el valor hexadecimal. Si ejecutamos el programa con estos parametros veremos lo siguiente:

8

Vemos que se ha vuelto a ejecutar la función porque ha vuelto a imprimir por pantalla “Bienvenido” lógicamente después del estropicio que hemos hecho da un fallo de segmentación. No es el objetivo de este ejemplo intentar restaurar los valores de la pila para salir del programa de forma limpia, solamente demostrar como desbordar un buffer en el stack.

Bueno, hemos conseguido sobreescribir la dirección del registro EIP y poder saltar a una dirección de memoria arbitraría. Esto es interesante pero podría ser mucho más interesante, ¿Y si se puediese saltar a una dirección donde nosotros hayamos inyectado código para poder hacer lo que queramos? La buena noticia es que se puede hacer y lo veremos en el siguiente artículo.

Deja una respuesta

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