Skip to main content

fermat-strings picoMini by redpwn Solution

Exploit a classic input-handling vulnerability to read from or write to arbitrary memory.

Published: April 2, 2026Updated: August 25, 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 1Confirm the format string vulnerability
    Observation
    The name puns on format strings, and the binary echoes user input back. That suggests input reaching printf with no format argument, so send %p specifiers and see whether stack addresses come back instead of your 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 treats each stack value as a pointer to a string, so you get garbage characters or a segfault on an invalid pointer, and the server crashes or hangs rather than echoing anything you can read. %p prints the pointer as hex without following it.

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

    Shell printf consumes the specifiers itself before netcat ever sees them, expanding them to nothing or to a stray letter depending on the shell. The percent signs never reach the server. Use echo, or generate the string from Python.

    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 2Leak the libc base address
    Observation
    ASLR moves libc every run, and the read primitive reaches stack values directly. Scan indexed slots for a libc pointer the loader left behind and subtract the known symbol offset to get the base.
    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
    libc = ELF('./libc.so.6')  # the libc the challenge ships, not your local one
    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.

    On 64-bit Linux both specifiers read a word, so the output is the same either way. The real error is carrying a libc offset from your local install without checking the server's. Subtract the wrong one and the computed base is thousands of bytes off, invalidating every derived address. Identify the remote libc first, through the binary's RUNPATH or its build ID.

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

    The loader leaves a return address pointing partway into __libc_start_main, after the call to main, not at the symbol's start. Omit that instruction offset and the base is wrong by a fixed amount, so the computed system address segfaults. Measure the offset in GDB, or from a run where you know the base.

    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 3Write to a target address using %n
    Observation
    With system's runtime address in hand, something has to call it. The %n specifier writes the printed character count back into memory, so use fmtstr_payload to overwrite a GOT entry for a function that receives your input, and the next call hands that input 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 your buffer. Get it wrong and the addresses in the payload land on the wrong words, so the writes target garbage instead of the GOT entry. Confirm it by sending a marker followed by indexed specifiers and finding where the marker appears.

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

    __free_hook only helps on glibc older than 2.34. From 2.34 onward the allocator no longer consults the malloc and free hooks at all (the functionality moved into libc_malloc_debug.so, which has to be preloaded), and the symbol survives only as a compat object, so overwriting it silently does nothing instead of giving you a call. Target the GOT entry of a function that receives user input instead, which stays writable across 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 on glibc older than 2.34, 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

A format string bug is printf trusting its first argument as the format when user input arrives there instead of as data. That grants two primitives: reading arbitrary stack words through %p and its relatives, and writing the character count to any address through %n. Both show up in logging libraries, network daemons, and embedded firmware wherever user strings reach a variadic format function.

Related reading

Useful tools for Binary Exploitation

Where to go next