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.
Setup
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.
wget https://artifacts.picoctf.net/c/519/local-targetwget https://artifacts.picoctf.net/c/519/local-target.ccat local-target.cSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Measure the offset
ObservationThe 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 byint num = 64, and main also declares aFILE *fptr. The disassembly shows the buffer at[rbp-0x20], the file pointer at[rbp-0x10], andnumat[rbp-0x8], so the effective offset from the start of the buffer to the start ofnumis 24 bytes rather than 16: the eight bytes in between belong tofptr, not to padding. Writing 24 characters fills the buffer and that gap, and the NUL bytegets()appends drops onto the low byte ofnum. 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.Step 2Overflow by one byte
ObservationThe 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.pythonpython3 -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 variablenumis initialized to 64 (0x40). Since x86 is little-endian, the least significant byte ofnumis stored at the lowest of its four addresses, which is the first byte reached once the buffer and the 8-bytefptrslot behind it are full. The twenty-fifth character written lands there, so an'A'in that position changesnumfrom0x40to0x41(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 betweenstrlen()(excludes null terminator) andsizeof()(includes null), can create exactly this type of overflow.The payload here uses
python3 -c 'print(...)'piped intonc(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, andprocess()for local testing.Step 3Capture the flag output
ObservationThe 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
numto 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.