Description
Fermat's last theorem meets format strings.
Setup
Connect to the challenge server with netcat.
Download the binary for local analysis.
nc <challenge_host> <PORT_FROM_INSTANCE>wget <challenge_url>/fermat-strings # binary for local analysisSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Confirm the format string vulnerability
ObservationThe 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.%pas 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.bashecho '%p.%p.%p.%p' | nc <challenge_host> <PORT_FROM_INSTANCE>bashecho '%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 likeprintf("%s", user_input), the vulnerable code callsprintf(user_input)directly. This lets the attacker control what printf interprets as format specifiers.%pcauses printf to read a pointer-sized value from the stack and print it as a hex address. By sending many%pspecifiers, 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$preads 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.Step 2Leak the libc base address
ObservationASLR 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$pspecifiers with increasing N to scan stack slots for a libc pointer - a value in the0x7f...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 ofsystemto get its runtime address.bash# Leak a libc pointer from the stackpythonpython3 -c "pythonfrom pwn import *bashlibc = ELF('./libc.so.6') # the libc the challenge ships, not your local onebashp = remote('<host>', <PORT_FROM_INSTANCE>)bash# Probe increasing N until a 0x7f... value appearsbashp.sendline('%213\$lx') # example offset - adjust after testingpythonleak = int(p.recvline().strip(), 16)bashlibc_base = leak - libc.sym['__libc_start_main'] - 243 # adjust offsetbashsystem_addr = libc_base + libc.sym['system']pythonprint(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 (includingsystem) 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_mainin the challenge's libc build to get the base address.Step 3Write to a target address using %n
ObservationWith 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%nspecifier 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$nto write the desired value byte by byte.bash# pwntools fmtstr_payload helperpythonpython3 -c "pythonfrom pwn import *bash# fmtstr_payload(offset, {target_addr: value_to_write})bashpayload = fmtstr_payload(6, {0x404080: 0xdeadbeef})pythonprint(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
%nis 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
printfitself (overwrite withsystemso the nextprintf(input)callssystem(input)), or__free_hookon 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.