fermat-strings picoMini by redpwn Solution

Published: April 2, 2026

Description

Fermat's last theorem meets format strings.

Connect to the challenge server with netcat.

Download the binary for local analysis.

bash
nc <challenge_host> <PORT_FROM_INSTANCE>
bash
wget <challenge_url>/fermat-strings  # binary for local analysis

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Confirm the format string vulnerability
    Observation
    I noticed the challenge description links Fermat's theorem to 'format strings,' and the binary takes user input that is likely echoed back, which suggested the server passes input directly to printf without a format argument and that sending %p specifiers would reveal the vulnerability by printing stack addresses instead of the literal text.
    Connect and send %p.%p.%p.%p as input. If the server echoes back memory addresses instead of the literal string, you have a format string vulnerability - the input is passed directly to printf without a format string argument.
    bash
    echo '%p.%p.%p.%p' | nc <challenge_host> <PORT_FROM_INSTANCE>
    bash
    echo '%1$p.%2$p.%3$p' | nc <challenge_host> <PORT_FROM_INSTANCE>
    What didn't work first

    Tried: Send '%s.%s.%s' instead of '%p.%p.%p' to probe the vulnerability.

    %s dereferences the stack value as a pointer to a string, so instead of hex addresses you get garbage characters or a segfault from reading an invalid pointer. The server will likely crash or hang rather than echo recognizable output, making it impossible to confirm the vulnerability. Use %p to safely print pointer values as hex without any dereference.

    Tried: Pipe input with printf instead of echo: 'printf '%p.%p' | nc ...'

    Shell printf interprets the format specifiers itself before they reach netcat, expanding %p to an empty string or literal 'p' depending on the shell. The server never receives the raw percent signs. Use echo or quote the string as a Python one-liner to preserve the literal percent characters.

    Learn more

    A format string vulnerability arises when user-controlled input is passed as the format string to printf, sprintf, or similar functions. Instead of safe code like printf("%s", user_input), the vulnerable code calls printf(user_input) directly. This lets the attacker control what printf interprets as format specifiers.

    %p causes printf to read a pointer-sized value from the stack and print it as a hex address. By sending many %p specifiers, you can dump the entire printf argument list - which corresponds to consecutive stack words. This leaks stack data including saved return addresses and libc pointers left by the loader.

    Direct parameter access (%N$p) lets you read the Nth argument directly without iterating: %7$p reads the 7th stack word. This is useful for targeting a specific known offset, such as a libc pointer left on the stack by the program loader.

  2. Step 2
    Leak the libc base address
    Observation
    I noticed that ASLR randomizes libc's load address each run and that the format string read primitive gives direct access to stack values, which suggested scanning stack slots with %N$p to find a 0x7f... libc pointer left by the loader and computing the libc base by subtracting the known symbol offset.
    Use %N$p specifiers with increasing N to scan stack slots for a libc pointer - a value in the 0x7f... range pointing into libc. A common target is __libc_start_main+offset, which the loader leaves on the stack. Subtract the known offset for that symbol in your libc version to recover the libc base, then add the offset of system to get its runtime address.
    bash
    # Leak a libc pointer from the stack
    python
    python3 -c "
    python
    from pwn import *
    bash
    p = remote('<host>', <PORT_FROM_INSTANCE>)
    bash
    # Probe increasing N until a 0x7f... value appears
    bash
    p.sendline('%213$lx')  # example offset - adjust after testing
    python
    leak = int(p.recvline().strip(), 16)
    bash
    libc_base = leak - libc.sym['__libc_start_main'] - 243  # adjust offset
    bash
    system_addr = libc_base + libc.sym['system']
    python
    print(hex(libc_base), hex(system_addr))
    bash
    "
    What didn't work first

    Tried: Use %lx instead of %p to leak stack values, assuming %p might not work on 64-bit.

    %lx and %p both read a word-sized value on 64-bit Linux, so the output is functionally identical. The real mistake is hardcoding the libc offset (the '- 243' adjustment) from a local libc version without checking the server's libc. Subtract the wrong offset and the computed libc base will be thousands of bytes off, making every derived address invalid. Extract the remote libc with the binary's RUNPATH or match the build ID to the correct libc version first.

    Tried: Subtract libc.sym['__libc_start_main'] alone without the trailing +243 adjustment.

    The loader leaves a return address pointing into __libc_start_main at some instruction after the call to main, not at the symbol's base address. Omitting the instruction offset produces a base that is off by a fixed number of bytes. The resulting system() address will be wrong, and any shell attempt will segfault. Find the exact offset by checking the return address in GDB or by computing leak - libc_base from a known run.

    Learn more

    This challenge does not use a stack overflow, so there is no canary to bypass. The entire attack is carried out through the format string read and write primitives alone. No overflow payload is ever sent.

    The loader places pointers into libc on the stack before calling main, most notably the return address back into __libc_start_main. Because ASLR randomizes libc's load base each run, you must leak one of these pointers before you can compute any other libc address. Once you know the base, every symbol offset (including system) is fixed relative to it.

    To find the right stack position, look for a value that: (1) is in the 0x7f... range, and (2) falls within the libc mapping shown in /proc/self/maps. Subtract the known offset for __libc_start_main in the challenge's libc build to get the base address.

  3. Step 3
    Write to a target address using %n
    Observation
    I noticed that once we have system()'s runtime address, we need to redirect a function call to it, and the format string %n specifier writes the printed character count back into memory, which suggested using pwntools' fmtstr_payload to overwrite a GOT entry (such as atoi or strcspn) with system() so the next call passes our input directly to a shell.
    Use the %n specifier to write the number of characters printed so far into a target address on the stack. Craft a payload that positions the target address on the stack, then uses %Nc%offset$n to write the desired value byte by byte.
    bash
    # pwntools fmtstr_payload helper
    python
    python3 -c "
    python
    from pwn import *
    bash
    # fmtstr_payload(offset, {target_addr: value_to_write})
    bash
    payload = fmtstr_payload(6, {0x404080: 0xdeadbeef})
    python
    print(payload)
    bash
    "
    What didn't work first

    Tried: Use fmtstr_payload with offset 1 instead of finding the correct stack offset first.

    The offset argument tells pwntools which stack position holds the start of the attacker-controlled buffer. Using the wrong offset means the addresses embedded in the payload land on the wrong stack words, so the write targets point at garbage instead of the GOT entry. Confirm the offset by sending 'AAAA.%1$p.%2$p...' and finding the position where '0x41414141' appears in the output.

    Tried: Overwrite __free_hook with system() instead of a GOT entry like atoi or strcspn.

    __free_hook works in older glibc versions but was removed in glibc 2.34. If the challenge server runs glibc 2.34 or later, __free_hook no longer exists and overwriting that address writes into unmapped memory, causing a crash. Target a GOT entry for a function that receives user input (atoi, strcspn) instead, as those remain writable and function correctly across all glibc versions.

    Learn more

    %n is the most dangerous printf specifier - it writes the count of characters printed so far to the address stored in the corresponding argument. By controlling the character count (via padding like %100c) and the address on the stack, an attacker can write arbitrary values to arbitrary memory locations.

    pwntools' fmtstr_payload() automates this entirely. Given the offset (which stack position holds the first attacker-controlled word) and a dictionary of {address: value}, it generates the optimal format string. It uses %hhn (write 1 byte) to minimize the character count needed, writing one byte at a time.

    Common write targets: the GOT entry for printf itself (overwrite with system so the next printf(input) calls system(input)), or __free_hook, or the saved return address after leaking its position.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{f3rm4t_pwn1ng_s1nc3_th3_17th_c3ntury}

Format string vulnerability - no stack overflow or canary bypass needed. Leak a libc pointer from the stack with %N$lx to compute the libc base and system() address, then use %n writes (or pwntools fmtstr_payload) to overwrite a GOT entry (atoi or strcspn) with system(), and pass /bin/sh as input to get a shell.

Key takeaway

Format strings exploit printf trusting its first argument as the format spec when user input is passed directly instead of as a '%s' argument. This grants two powerful primitives: stack memory disclosure via %p/%x/%s (reading arbitrary stack words), and arbitrary memory writes via %n (writing the character count to any pointed-to address). These same primitives appear in logging libraries, network daemons, and embedded firmware wherever user-controlled strings reach variadic format functions.

Related reading

Want more picoMini by redpwn writeups?

Useful tools for Binary Exploitation

What to try next