Local Target picoGym Exclusive Solution

Published: March 5, 2024

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 1
    Measure the offset
    Observation
    I noticed the source code declared a fixed 16-byte input buffer immediately before a neighboring int variable, which suggested the buffer size and compiler alignment padding together determined exactly how many bytes to write before reaching the target variable.
    num is stored after the 16-byte input buffer with 8 bytes of alignment padding between them. Feeding exactly 24 bytes fills through the buffer and its alignment padding without touching num; the 25th byte overwrites the low byte of num.
    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. On the stack, the compiler adds 8 bytes of alignment padding between input and num, so the effective offset from the start of the buffer to the start of num is 24 bytes rather than 16. Writing exactly 24 bytes fills input and its trailing padding without touching num; writing 25 bytes overwrites the first byte of num.

    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 2
    Overflow by one byte
    Observation
    I noticed that num needed to equal 65 (0x41) and that the ASCII value of 'A' is exactly 65, which suggested sending a 25-byte payload where the final byte is 'A' to overwrite the low byte of num with the correct value.
    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 - <<'PY'
    print('12345678901234567890123AA')
    PY | nc saturn.picoctf.net 64108

    Expected output

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

    Tried: Sending exactly 24 bytes of padding (filling the buffer and alignment gap) and expecting num to change

    24 bytes fills input[16] and the 8-byte alignment padding but does not reach num, so num stays at 64 and the win condition is never triggered. You need 25 bytes total - 24 to fill through the padding, then one more byte that actually overwrites the low byte of num.

    Tried: Sending 'A' * 25 and expecting num to become 65 because 25 bytes of 'A' are sent

    The 25th byte lands on the least-significant byte of num and sets it to 0x41 (65) only if that 25th byte is the character 'A'. Sending a different character like 'B' (0x42 = 66) or '\x00' would write a different value, failing the check. The payload must ensure the overflowing byte is exactly 'A' (0x41) to satisfy num == 65.

    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 address - immediately adjacent to the end of the buffer. Writing one extra byte past the buffer writes directly into that low byte of num, changing it 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 3
    Capture the flag output
    Observation
    I noticed the binary's win condition was a simple equality check on num, which suggested that once the overflow sets num to 65 the program would immediately print the flag without any 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 payload that increases num from 0x40 to 0x41 will work; the example string above is just one convenient option.

Key takeaway

Stack buffer overflows corrupt adjacent memory by writing more bytes than a fixed-size buffer can hold, overwriting neighboring variables or, in more advanced cases, the saved return address to redirect execution. Any C or C++ code that copies user-supplied data into a fixed buffer without a length check is potentially vulnerable, and off-by-one errors in bounds comparisons create the same class of overflow with a single extra byte. Protections like stack canaries, ASLR, and non-executable stacks raise the exploitation bar but do not eliminate the vulnerability class, which is why bounds-safe functions and memory-safe languages remain the lasting fix.

Related reading

Want more picoGym Exclusive writeups?

Tools used in this challenge

What to try next