Jump to ESP

Para compilar el programa name.c para este artículo es necesario desactivar DEP:

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

En el artículo anterior conseguimos hacer funcionar nuestro exploit dentro de gdb, pero tuvimos problemas a la hora hacer funcionar nuestro exploit fuera de gdb.

¿Cual puede ser la razón para esto?

Asumimos que la protección ASLR (que espero hablar más tarde de ella) la tenemos desactivada.

El comando que siempre he indicado para desactivarla:

echo 0 > /proc/sys/kernel/randomize_va_space

tiene efecto temporal y después de reiniciar nuestro ordenador ASLR volverá a estar activo, así que hay que estar atento a desactivarlo cada vez que practiquemos con estos artículos. Aunque ahora no venga al caso hay que decir que gdb desactiva la protección ASLR, si quisiesemos activarla, dentro de gdb debemos ejecutar el siguiente comando:

set disable-randomization off

Volviendo al hilo del artículo, la razón de la que pase esto tiene que ver con un gráfico que puse en el primer artículo:

process_va

Nuestro exploit fuera de gdb no funciona porque las direcciones que hemos calculado dentro de gdb no coinciden con las que se ejecutan fuera de gdb, hay 2 posibles culpables de esto. Los primeros 4 bytes del espacio de direcciones de un proceso son constantes, los segmentos, la pila y el heap también son constantes. Lo único que queda que puede variar son el nombre del programa, los argumentos y las variables de entorno.

Efectivamente si alguna de estas regiones no coincide exactamente con lo que tenemos en un entorno de ejecución gdb las direcciones no coincidiran y el exploit no funcionará.

En este caso concreto los argumentos no pueden ser los culpables porque tanto en gdb como fuera de él se pasa un argumento con la misma cantidad de bytes (48 bytes). Aunque pueda parecer que el nombre también es el mismo, se podría dar el caso de que no, el nombre del programa fuera de gdb se almacena tal cual lo ejecutemos, es decir, si ejecutamos ./name se almacenará ./name, sin embargo, en gdb el nombre se almacena como la ruta absoluta, para mi caso sería /home/angelluis/Documentos/exploiting/name. Aquí ya nos encontramos con que si hemos ejecutado nuestra aplicación fuera de gdb como ./name tendriamos que sumarle 36 a la dirección de memoria que obtuvimos en el artículo anterior (36 = la diferencia entre la longitud de la cadena /home/angelluis/Documentos/exploiting/name y ./name). O más sencillo aún, conservar la dirección de memoria obtenida en gdb y fuera de gdb ejecutar nuestro programa mediante su ruta absoluta.

Las variables de entorno también cambian cuando se ejecuta el programa en gdb. Si en gdb ejecutamos el comando

show env

podremos ver las variables de entorno que hay definidas y que, por tanto, forman parte del espacio de memoria del proceso. Por lo general gdb define dos variables nuevas, LINES y COLUMNS.

Para automatizar el proceso he creado un script que por fuerza bruta va comprobando direcciones. Ajustando la dirección inicial a la que encontramos en gdb podremos encontrar facilmente la nueva dirección para explotar la vulnerabilidad, ya que las direcciones de memoria deben estar proximas, porque como se ha dicho solo habrá cambiado el nombre y alguna variable de entorno.

Este es el script:

Y aquí una muestra del script en funcionamiento, encontrando la dirección:

0

La dirección que nos da el shell es la 0xffffd6c0 y el shellcode completo es

\x90\xeb\x14\x5e\x31\xc0\x88\x46\x07\xb0\x0b\x89\xf3\x31\xc9\x31\xd2\xcd\x80\xb0\x01\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x90\x90\x90\x90\x90\x90\x90\x90\x90\xbf\xd6\xff\xff

Vamos a probarlo ahora sin script y ….

1

Nada, que no hay forma… Esto se debe de nuevo a que el script introduce variables de entorno y por tanto la ejecución en el script y la ejecución sin el script no corren en el mismo entorno y las direcciones en el espacio de memoria del proceso están desplazadas.

En mi caso, la dirección exacta es 0xFFFFd670

2

Esta solución no es muy elegante, y los fallos de segmentación son registrados en logs que posteriormente un administrador de sistemas puede examinar y podría detectar el posible ataque.

Para no tener este problema con las direcciones de memoria existen ciertos script que actuan de envoltura entre el programa vulnerable, gdb y la shell donde se ejecute el programa, un script que he comprobado que funciona es el de la siguiente dirección:

https://github.com/hellman/fixenv/blob/master/r.sh

El uso es bastante sencillo, si queremos usar gdb deberemos ejecutar

r.sh gdb ./program [args]

Y si queremos ejecutarlo sin gdb ejecutamos

r.sh ./program [args]

Con este envoltorio nos aseguramos de que las direcciones de memoria no varien dependiendo del entorno.
Pero nosotros vamos a usar una técnica más refinada para solucionar nuestro problema.

Desde la entrada de nuestro programa podemos modificar la pila del programa, ¿Y si pudiesemos saltar a directamente a la cima de la pila con una instrucción del tipo “Saltar a la cima de la pila”? Pues se puede, esta instrucción es la jmp %esp (Jump to Extended Stack Pointer).

Lo que queremos hacer es despreocuparnos de en que posición se encuentra nuestro shellcode y saltar directamente a nuestro shellcode mediante la instrucción jmp %esp. En nuestro código no vamos a encontrar esta instrucción. Podemos intentar buscarla mediante:

objdump -D name | grep jmp | grep esp

que nos arrojará 0 resultados. Lo ideal sería buscar esta instrucción en el propio código de la aplicación vulnerable o en alguna librería propia del programa vulnerable.

Además jugamos con una ventaja en sistemas x86 y es que a diferencia de otras arquitectura no es necesario que las instrucciones estén alineadas, ¿Que significa esto?

3

La imagen de arriba lo explica perfectamente, según que byte leamos las instrucciones se re-interpretan, en la imagen de arriba leo una dirección determinada acto seguido leo una dirección donde no empieza ninguna instrucción (la dirección 0x080484d2) y vemos como ya no aparecen las mismas instrucciones que antes. Aún así no vamos a usar el programa vulnerable para buscar la instrucción jmp %esp. Vamos a usar las librerias compartidas de las que hace uso el programa vulnerable, aunque como he dicho antes esto no es recomendable, y más aún si está ASLR activado.

Para listar las librerias de las que hace el programa vulnerable podemos ejecutar desde gdb el comando

info sharedlibrary

En este caso usaremos el comando

ldd name

desde fuera de gdb. Con esto veremos que el programa vulnerable usa la librería libc que es la librería estandar de c de Linux, además nos dice la ruta en la que se encuentra, así que nos disponemos a ejecutar objdump sobre dicha librería en busca de un jmp esp

4

Esto nos ha servido para dos cosas. Por una parte hemos descubierto el offset del primer jmp %esp que es 0x15a55b y por otra parte hemos descubierto que el op code de jmp %esp es 0xffe4. No es mi intención explicar el funcionamiento del linker del compilador y el loader del sistema operativo pero las librerías compartidas se compilan con la opción de gcc -fPIC donde PIC significa Position Independent Code. Esto es porque las librerías compartidas (.so en Linux y .dll en windows) comparten el segmento de texto con todo proceso que la use (el segmento de datos por el contrario se copia y es único para cada proceso, no es compartido). El loader del sistema operativo decide en tiempo de carga donde posicionar las librerías dinámicas y completa información que el linker ha dejdo en blanco.

Por tanto, las librerías compartidas cuando se examinan aisladamente no nos ofrecen una dirección de memoria absoluta, sino un offset con respecto a su dirección de carga. ¿Como adivinar dicha dirección de carga?

Para ello podemos ejecutar el comando

info proc mapping

En gdb, estando cargado el programa que queremos examinar

5

Cuando hemos ejecutado

ldd name

o

info sharedlibrary

Nos ha dado unas direcciones de memoria de la librería libc, pero como se dijo en el artículo anterior, ldd no ofrece resultados precisos e info sharedlibrary nos devuelve la posición del segmento de código.

Con info proc mapping vemos que aparecen varias referencias a la librería libc, la posición de inicio de la librería es la posición de inicio de la primera referencia, esto es 0xF7DF0000.

Por tanto si a 0xF7DF0000 le sumamos el offset 0x15a55b nos da como resultado 0xF7F4A55B y ahí es donde deberiamos encontrar nuestro jmp %esp. Vamos a comprobarlo

6

Como no podía ser de otra forma nos encontramos con la instrucción jmp %esp y su op code.

Tenemos ya la dirección a un salto a la pila con su dirección absoluta, sabemos también la cantidad de bytes que necesitamos proporcionar a la entrada para sobreescribir el registro EIP y redirigir la ejecución del programa, tenemos un shellcode capaz de obtener una shell, ¡Lo tenemos todo!

Ahora hay que tener una cosa en cuenta, nuestro shellcode se va a ejecutar cuando retorne la función func del programa vulnerable (igual que en el artículo anterior) que es cuando se recupera el valor del registro EIP que hemos sobreescrito, pero hay una diferencia con el artículo anterior.

En el articulo anterior la entrada era de la siguiente forma

SHELLCODE + NUEVO_VALOR_EIP

Ahora mediante esta técnica la entrada tiene que ser de la siguiente forma

RELLENO + NUEVO_VALOR_EIP + SHELLCODE.

¿Por qué? Porque en los artículo anteriores es verdad que nos fijabamos en el registro ESP pero nos fijabamos antes de que la función retorne y recupere el valor del registro EIP manipulado, esta vez nuestro registro ESP va a ser consultado después de salir de la función func, y para entonces la pila habrá eliminado el marco creado para nuestra función func y apuntará a la dirección de pila justo antes de introducir el EIP.

El relleno en nuestro caso es de 44 bytes, porque después de esto sobreescribimos el registro EIP

Por tanto el argumento que le tenemos que pasar al programa vulnerable es:

`python -c "print 'A'*44 + '\x5b\xa5\xf4\xf7' + '\x90\xeb\x14\x5e\x31\xc0\x88\x46\x07\xb0\x0b\x89\xf3\x31\xc9\x31\xd2\xcd\x80\xb0\x01\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68'"`

Y vemos como así funciona sin problemas tanto dentro de gdb como fuera.

7

Esto a simple vista no parece tener mucha utilidad, hemos obtenido una shell pero sigo siendo angelluis, puedo hacer lo mismo que podía hacer antes. ¿Pero y si la vulnerabilidad la tiene un programa cuyo propietario es root y con el bit s activo como puede ser por ejemplo el programa passwd?

8

Pues que nada más y nada menos hemos conseguido escalar privilegios y ejecutar una shell como root.

A parte de obtener una shell, los payloads o shellcodes también se suelen usar para realizar denegaciones de servicio u obtener shells remotas.

Con esto ya queda explicado lo básico de los ataques stack overflow

GDB, shellcodes y nuestro primer exploit

Para compilar el programa name.c para este artículo es necesario desactivar DEP:

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

GDB

Antes de proseguir donde lo dejamos en el artículo anterior es necesario repasar unas cuantas nociones de gdb.

Como se ha dicho anteriormente, gdb es el depurador de GNU, para invocarlo basta con ejecutar

gdb programa

o

gdb -q programa

Con la opción -q no nos mostrará la información inicial y ahorraremos espacio en pantalla.

Mediante la orden disas o disass podremos desensamblar el código, si no proporcionamos ningún parámetro nos desensamblará el código desde el registro EIP, si el programa no está en ejecución no nos desensamblará nada, por contra, le podemos pasar una dirección de memoria o nombre de método y nos desensamblará desde esa dirección de memoria, como segundo parámetro le podemos pasar la longitud en bytes que queremos desensamblar o la dirección final hasta la que queremos desensamblar.

disas func, +10 (desensamblaremos 10 bytes)
disas func, 0xffeeddcc (desensamblaremos hasta la dirección expecificada)

Con la orden break podremos poner una breakpoint, bien usando una dirección de memoria o un nombre de método.

Con la orden r o run seguido de argumentos (si es que nuestro programa tiene argumentos) podremos empezar a correr nuestro programa, si queremos movernos de línea en línea de ensamblador debemos usar las ordenes stepi/si o nexti/ni.

A continuación se muestra una sesión básica de gdb

0

Si queremos ver el estado actual de los registros podremos ejecutar la orden

info registers

1

A parte de disas, que sirve para desensamblar código, tenemos otra herramienta muy ponente que sirve para examinar la memoria. El comando para dicho fin es “x” seguido de la cantidad a examinar y del formato y por último la dirección de memoria. El formato que mas vamos a usar es el hexadecimal “x” y el de instrucción “i”, para más información se puede escribir help x en gdb.

Un ejemplo de este comando en ejecución puede ser el siguiente:

2

En el primer comando hemos ejecutado el comando de examinar memoria “x” con el parámetro /16x que significa que desensamble 16 direcciones en formato hexadecimal y como último parámetro le hemos pasado el registro $eip, es decir, nos va a mostrar a partir de la siguiente instrucción a ejecutar.

En el segundo ejemplo vemos que en vez de /16x le hemos pasado /10i que significa que nos muestre 10 instrucciones.

Aquí podemos ver una cosa muy interesante que nos servirá más adelante, que la memoria es un conjunto de bytes y que todo depende de como se interprete, si se interpreta como instrucción nos da el resultado del segundo “x” y si directamente no se interpreta obtenemos el conjunto de bytes del primer “x”.

Hay veces que necesitaremos saber donde están localizadas las librerías compartidas que usa nuestro programa, para ello hay 3 formas, pero una de ellas es la más completa. La primera de ellas y que personalmente no recomiendo es la herramienta de linux ldd

3

No la recomiendo porque no suele ser exacta.

Otra opción que podemos usar es ejecutar la orden info sharedlibrary en gdb

4

Para usar este comando deberemos haber ejecutado nuestro programa (run args) sino nos mostrará un error parecido al que me ha mostrado a mi.

Este método es preciso pero la dirección que nos da es la dirección en la que se encuentra el código, es decir la dirección 0xf7e07480 es la dirección en la que se encuentra la directiva .text de la librería libc.

A veces no nos interesa solo saber donde se encuentra la sección text, sino que nos interesa el rango de direcciones total donde se carga una librería, para ello podemos usar el comando.

Info proc mapping

5

Podemos ver que la librería compartida libc tiene 3 entradas, el espacio de direcciones en la que se encuentra esta librería será desde el primer start addr hasta el ultimo end addr, es decir, desde 0xf7df0000 hasta 0xf7fa9000 en mi caso particular.

Por último, gdb dispone de un comando muy útil que nos permite buscar bytes en el espacio de memoria del proceso, su sintaxis es:

find [/size-char] [/max-count] start-address, +length, expr1 [, expr2 …]

donde /size-char puede ser /b para bytes /h para 2 bytes /w para 4 bytes y /g para 8 bytes, para más información se puede escribir help find en gdb.

6

En este ejemplo primero busco 2 bytes 0xf7fd en la direccion 0xf7def000 (que es donde se mapea libc) y busco en un rango de 3000 bytes. Se puede ver como me han aparecido 3 ocurrencias y compruebo que en la primera de ellas efectivamente se puede encontrar la secuencia de bytes. En el segundo ejemplo busco únicamente 1 byte desde una dirección de memoria hasta otra dirección de memoria, gdb me devuelve 2 ocurrencias y compruebo que en la primera ocurrencia que me devuelve gdb se puede encontrar el byte buscado.

Shellcodes

Después de esta pequeña introducción a gdb ya podemos continuar por donde se quedó en el artículo anterior, bueno, aún no podemos meternos a continuar el artículo anterior porque aún queda algo por explicar.

En el capítulo anterior conseguimos redirigir el flujo de ejecución modificando el registro EIP y para ello modificamos un valor de la pila. Lo que nos gustaría es redirigir EIP hacia el inicio de nuestro buffer y ejecutar lo que se encuentre allí. ¿Pero que es lo que se encuentra ahí? En el artículo anterior nuestro buffer era rellenado con 44 A's pero eso no nos sirve.

Lo que necesitamos introducir en nuestro buffer es código que el procesador pueda ejecutar, este código que se mete en el buffer y que la CPU ejecutará recibe el nombre de payload o shellcode.

En una gran cantidad de ataques el objetivo es obtener una shell para poder manejar el ordenador a nuestro antojo, resulta entonces obvio que a este código se le ponga el nombre de shellcode.

Vale ¿Pero que tipo de código tenemos que meter en nuestro buffer? ¿código C? No, la máquina no entiende el código C, deberemos meter directamente código máquina que es el único que la máquina sabe interpretar.

Llegados aquí podemos pensar en compilar un programa C y obtener su código ensamblador, esto es un error, primero porque el código que nos generará seguramente será bastante más largo que el que nosotros mismos podamos escribir, y el tamaño en los shellcodes importa, ya que tiene que caber en nuestro buffer de 44 bytes y por otra parte gcc va a generar código ensamblador para un programa estructurado, con su region de memoria, de código etc, nosotros necesitamos un programa que no necesite regiones, es decir, no debe estar bien estructurado.

Nuestro objetivo es escribir un código en ensamblador capaz de ejecutar una shell en tan solo 44 bytes. Además tenemos problemas añadidos, no vamos a tener regiones, entonces no tendremos region de datos ni variables que podamos referenciar de forma sencilla.

Antes de escribir ensamblador que nos ejecute una shell es necesario explicar que en linux muchas operaciones básicas las ejecuta realmente el sistema operativo, el programa de usuario que se ejecuta en el espacio de usuario hace una llamada al kernel y el kernel maneja esa llamada en espacio kernel y posteriormente devuelve el control al usuario. Esto es lo que se conoce como llamadas al sistema. Por ejemplo, cuando queremos abrir un fichero, hacemos la llamada al sistema open, el sistema operativo abre el fichero por nosotros y nos devuelve un descriptor de fichero.

Cada una de estas llamadas al sistema tiene un numero asociado, por ejemplo, open es el numero 4. Pues bien, hay una llamada al sistema que nos permite ejecutar procesos, esta es execve y tiene el número 0x0B (12). Lo que debemos hacer es invocar esta llamada al sistema y pasarle como proceso a ejecutar /bin/sh. ¿Pero como se ejecutan estas llamadas al sistema?

En c es bastante fácil:

int execve(const char *filename, char *const argv[], char *const envp[]);

Pero en ensamblador se complica un poco, primero hay poner en eax el numero de la llamada al sistema que queramos hacer, después en ebx ponemos el primer parámetro, en ecx el segundo y en edx el tercer parámetro y por último llamar a la interrupción 0x80. Quedaría algo así

mov $0xb, %eax
mov 1º parámetro, %ebx
mov 2º parámetro, %ecx
mov 3º parámetro, %edx
int $0x80

Pero además tenemos un problema añadido con nuestra shellcode, no puede contener bytes nulos (0x00) y tampoco es recomendable que lleve espacios (0x20), saltos de linea (0x0A) y el byte (0x09), después explicaré un poco más detalladamente esto.

Para establecer valores númericos en los registros no es complicado, basta hacer un mov al registro y ya lo tenemos, pero lo complicado viene cuando queremos almacenar en un registro una cadena de texto, como puede ser “/bin/sh”, el registro no almacena la cadena como tal sino que almacena la dirección de memoria donde se encuentra la cadena y, como he dicho anteriormente, no tenemos sección de datos, no es un programa estructurado. Para resolver esto hay un truco bastante ingenioso que expongo a continuación

jmp truco
inicio:
pop %esi

truco:
call inicio
db “/bin/sh”

El sistema es bastante sencillo, nada más empezar hacemos un salto a truco, en truco hacemos un call a inicio ¿Y que es lo que hacía un call? Un call era como un jmp pero además almacenaba la dirección de la siguiente instrucción a ejecutar en la pila, es decir, este call va a almacenar la dirección de la cadena “/bin/sh” en la pila, y cuando saltemos a inicio la vamos a recuperar en ESI mediante pop %esi. Por tanto, tendremos en ESI la dirección del primer parámetro que le tenemos que pasar a execve. El código en ensamblador encargado de ejecutar una shell y posteriormente salir del programa me ha llevado apenas 34 bytes y es el siguiente:

para compilar y enlazar este código necesitamos los siguientes comandos:

as --32 shellcode.s -o shellcode.o
ld -m elf_i386 shellcode.o

Aún no he comentado que en lenguaje ensamblador hay 2 sintaxis principales, la sintaxis AT&T que es la mostrada arriba y la sintaxis INTEL que sería la siguiente

Esta sintaxis os será más cercana si programais en ensamblador bajo windows. Para compilar en linux mediante esta sintaxis se ejecutan los siguientes comandos:

nasm -f elf shellcode_intel.asm
ld -m elf_i386 shellcode_intel.o

En cualquier caso ambas sintaxis generan el mismo binario. Ahora necesitamos obtener el código máquina del binario que hemos compilado, para ello podemos ejecutar el siguiente comando:

objdump -D shellcode

7

Y obtendremos algo parecido a la imagen. Lo que nos interesa son los números centrales, los que están entre las direcciones de memoria (80480XY) y las instrucciones ensamblador. Estos números los tendremos que poner de la siguiente forma:

\xeb\x14\x5e\x31\xc0\x88\x46\x07\xb0\x0b\x89\xf3\x31\xc9\x31\xd2\xcd\x80\xb0\x01\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68

Que si nos acordamos es la forma mediante la cual algunos lenguajes de programación nos permiten introducir directamente valores hexadecimales aunque su representación gráfica no sea visible (Como ya hicimos a la hora de poner las direcciones en el artículo anterior).

Llegados a este punto es bastante interesante hacerse un tester de shellcodes, esto es, un pequeño programita que nos permita ejecutar shellcodes, como puede ser el siguiente:

Si ejecutamos este programa veremos como hemos conseguido obtener una shell

8

Ya tenemos nuestro shellcode listo para ser ejecutado y además sabemos que podemos sobreescribir el registro EIP para redirigir la ejecución del programa, lo único que nos queda es saber en que posición de memoria se almacenará nuestra shellcode. Como ya se explico en el artículo anterior, al tener solo 1 buffer, nuestra shellcode se almacenará en la dirección de memoria que apunte ESP pero necesitamos averiguar dicha dirección, para ello vamos a gdb y ponemos el siguiente breakpoint:

break *func+44

y acto seguido ejecutamos nuestro programa mediante, ponemos 44 porque es la cantidad exacta que ocupa nuestro buffer.

run `python -c “print 'A' *44”`

Cuando nos pare el breakpoint ejecutamos

x /16x $esp

9

Vemos que la entrada que hemos metido se almacena en 0xFFFFD600 y nos encontramos con que tenemos un problema, hay un byte nulo 0x00 en la dirección de ESP, esto implica que vamos a tener que meter ese byte nulo en la posición 45 de nuestra cadena de entrada (recordad que en little endian el byte menos significativo se escribe primero).

Un byte nulo implica el final de una cadena y muchas funciones de cadenas de C acaban de leer cuando se introduce un byte nulo o un espacio, o un salto de linea (de ahí que los bytes 0x00, 0x0A, 0x09 y 0x20 no puedan aparecer)

¿Que podemos hacer?
Poner como primer byte el byte 0x90 que es la instrucción NOP y no hace exactamente nada y en vez de llamar a la dirección de memoria 0xFFFFFD600, llamar a la dirección 0xFFFFFD601. Por tanto, tenemos 34 bytes del shellcode + 1 byte NOP que en total hacen 35 bytes, pero nuestra entrada debe ser de 44 bytes para empezar a sobreescribir el registro EIP (y 48 bytes para sobreescribirlo totalmente) por tanto necesitamos 9 bytes más a parte de nuestro shellcode, estos bytes serán NOPs (0x90) y al final le concatenamos \x01\xd6\xff\xff (dirección 0xFFFFD601 en formato little-endian). Vamos a probar

10

¡Obtuvimos una shell!. Hemos conseguido obtener una shell a partir de un programa vulnerable, vamos a probar a explotar esta vulnerabilidad fuera de gdb.

El shell lo obtenemos con el siguiente shellcode

\x90\xeb\x14\x5e\x31\xc0\x88\x46\x07\xb0\x0b\x89\xf3\x31\xc9\x31\xd2\xcd\x80\xb0\x01\xcd\x80\xe8\xe7\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x90\x90\x90\x90\x90\x90\x90\x90\x90\x01\xd6\xff\xff'

11

¿Como es posible? Nos da un fallo de segmentación, pero si hemos sido capaces de explotar la vulnerabilidad….
La respuesta en el siguiente artículo 😉

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.