Skip to main content

Local Target picoGym Exclusive Solution

A binary exploitation challenge involving memory corruption to alter a neighboring variable's value.

Published: March 5, 2024Updated: August 25, 2026

Description

Overflow the stack buffer so that the neighboring local variable num becomes 65. Once num holds the magic value the binary prints the flag.

Buffer overflow practiceDownload local-target

Grab both the binary and its source to understand how the 16-byte buffer and num variable sit in memory.

Run it locally to test candidate payload lengths before attacking the remote instance.

bash
wget https://artifacts.picoctf.net/c/519/local-target
bash
wget https://artifacts.picoctf.net/c/519/local-target.c
bash
cat local-target.c

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Measure the offset
    Observation
    The source puts a fixed 16-byte buffer before an int, with another local in between. The distance the compiler actually leaves between them is exactly how far you write before reaching that variable.
    In the disassembly the buffer sits at [rbp-0x20] and num at [rbp-0x8], so num starts 24 bytes past the start of the buffer. Feeding exactly 24 characters fills the buffer and the 8-byte gap, and the terminating NUL that gets() appends lands on the low byte of num and zeroes it. The 25th character is the one that lands on that low byte instead.
    Learn more

    A stack buffer overflow occurs when more data is written to a fixed-size buffer than it can hold. The excess bytes spill into adjacent memory, overwriting whatever variables happen to be stored there. On the stack, local variables are allocated in a predictable order (though compilers may reorder them or add padding), so overflowing one variable can reliably reach and overwrite a neighboring one.

    In this binary, the source code declares char input[16] followed by int num = 64, and main also declares a FILE *fptr. The disassembly shows the buffer at [rbp-0x20], the file pointer at [rbp-0x10], and num at [rbp-0x8], so the effective offset from the start of the buffer to the start of num is 24 bytes rather than 16: the eight bytes in between belong to fptr, not to padding. Writing 24 characters fills the buffer and that gap, and the NUL byte gets() appends drops onto the low byte of num. The 25th character is what actually chooses the value that low byte takes.

    Reading the source code (when available) is always the first step. The source reveals the buffer size, the target variable, its initial value, and the win condition. With source code, you can calculate the exact offset mathematically rather than guessing. Without source code, you would use GDB to map the stack layout or use a cyclic pattern (e.g., from pwntools' cyclic()) to determine offsets experimentally.

  2. Step 2Overflow by one byte
    Observation
    The check wants that variable equal to 65, and 65 is the ASCII code for 'A'. Send 25 bytes ending in 'A' and the low byte lands correctly.
    Adding a single extra byte overwrites the low byte of num. Writing 'A' bumps it from 64 (0x40) to 65 (0x41), which satisfies the win condition.
    python
    python3 -c "print('12345678901234567890123AA')" | nc saturn.picoctf.net <PORT_FROM_INSTANCE>

    Expected output

    picoCTF{l0c4l5_1n_5c0p...8441a}
    What didn't work first

    Tried: Sending exactly 24 characters (filling the buffer and the 8-byte gap) and expecting num to become 65

    The program prints 'num is 0': gets() terminates the string with a NUL, and that NUL lands on the low byte of num, zeroing it. You need 25 characters so that the twenty-fifth, not the terminator, is the byte sitting on num.

    Tried: Sending 25 filler characters such as '1' * 25 and expecting num to become 65 because the length is right

    Length alone is not the point. The twenty-fifth character lands on the least significant byte of the variable and sets it to that character's ASCII value, so '1' writes 49 and 'B' writes 66. Only 'A' writes 65, so any payload of the right length has to end in 'A'.

    Learn more

    The byte 'A' has ASCII value 65 (0x41). The variable num is initialized to 64 (0x40). Since x86 is little-endian, the least significant byte of num is stored at the lowest of its four addresses, which is the first byte reached once the buffer and the 8-byte fptr slot behind it are full. The twenty-fifth character written lands there, so an 'A' in that position changes num from 0x40 to 0x41 (i.e., from 64 to 65).

    This is a classic example of a one-byte overflow or off-by-one overflow. In real-world vulnerabilities, off-by-one errors in bounds checking are surprisingly common and have led to serious exploits. The difference between <= and < in a length check, or between strlen() (excludes null terminator) and sizeof() (includes null), can create exactly this type of overflow.

    The payload here uses python3 -c 'print(...)' piped into nc (netcat) to send data to the remote service. Netcat is the standard tool for sending raw data to TCP services in CTF exploitation. For more complex exploits, pwntools (a Python library) provides a much richer API: p32()/p64() for packing integers into bytes, remote() for connections, and process() for local testing.

  3. Step 3Capture the flag output
    Observation
    The win condition is a plain equality check, so once the overflow lands the program prints the flag with no further interaction.
    Once num == 65, the binary congratulates you and prints the picoCTF flag directly.
    Learn more

    When the overflow successfully sets num to 65, the program's conditional check (if (num == 65)) passes and it prints the flag. This demonstrates the core principle of buffer overflow exploitation: corrupting program state to reach code paths that were not intended to be accessible with normal input.

    Local Target is a deliberately simple example of this class of vulnerability. Real-world stack overflows target the return address - the address on the stack that the function will jump to when it returns. By overwriting the return address with the address of a useful function (like a win() function that the developer left in the binary) or with shellcode, an attacker can gain arbitrary code execution. Protections like stack canaries, ASLR, and non-executable stacks (NX/DEP) exist specifically to make this harder.

    The picoGym buffer overflow series (local-target, buffer-overflow-0, buffer-overflow-1, buffer-overflow-2, buffer-overflow-3) progressively removes training wheels: first you overwrite a neighbor variable, then a return address in a binary without protections, then with increasing numbers of mitigations active. This progression mirrors the actual learning path for binary exploitation and pwn CTF categories.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{l0c4l5_1n_5c0p...8441a}

Any 25-character payload ending in 'A' sets num to 0x41; the example string above is just one convenient option.

Key takeaway

A stack overflow corrupts neighboring memory by writing past a fixed buffer, hitting adjacent variables or, further along, the saved return address. Any C or C++ that copies user data into a fixed buffer without a length check is a candidate, and an off-by-one in the bounds comparison produces the same class from a single extra byte. Stack canaries, ASLR, and a non-executable stack raise the cost without removing the bug class, which is why bounds-safe functions and memory-safe languages are the lasting answer.

Related reading

Tools used in this challenge

Where to go next