Description
Clutter, clutter everywhere and not a byte to use. Overflow the buffer to set code = 0xdeadbeef.
Setup
Download the binary from the challenge page.
Install pwntools: pip install pwntools
nc mars.picoctf.net <PORT>pip install pwntoolsSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Find the buffer size and variable offset
ObservationThe binary calls gets() into a 256-byte buffer and compares a local variable against 0xdeadbeef. Find the exact distance from the buffer to that variable before writing any payload.The program calls gets() into a 256-byte buffer on the stack. The code variable sits 264 bytes from the start of the buffer. Use pwntools cyclic to confirm the exact offset.pythonpython3 -c "from pwn import *; print(cyclic(300))" | ./clutter-overflowpythonpython3 -c "from pwn import *; print(cyclic_find(0x61616174))"Expected output
264
What didn't work first
Tried: Manually count 256 bytes of buffer size and assume the offset is exactly 256
The offset is 264, not 256, because the compiler adds alignment padding between the buffer and its neighbors. Send only 256 bytes and eight bytes of padding still stand in the way, so the variable receives your filler instead of the target value and the check fails silently.
Tried: Feed the cyclic pattern via gdb and read the rip register to find the offset
RIP holds the return address, not the variable, so measuring against it gives the offset to the saved return address, which is much further out. This challenge only needs the local variable, at a different offset entirely. Search for the value that lands in that variable at the crash.
Learn more
gets() is one of the most notoriously dangerous functions in the C standard library. It reads characters from stdin into a buffer until it encounters a newline or EOF, with no length limit whatsoever. There is no way to use
gets()safely - it was deprecated in C99 and removed from the C11 standard entirely. Any program that calls it is unconditionally vulnerable to a stack buffer overflow.The stack frame for a function contains: the saved frame pointer (rbp), the saved return address (rip), and local variables. Variables declared as local arrays (like
char buf[256]) are allocated on the stack in a contiguous block. A variable declared immediately after the buffer (at a higher address on x86 stack, which grows downward) is overwritten when the buffer is overflowed. Ghidra's decompiler shows the stack layout with variable offsets relative to rbp, making the distance calculation straightforward.The
cyclic_find()function works by looking up the 4-byte value in the de Bruijn sequence generated bycyclic(). When the program crashes withcode = 0x61616174(from reading the overwritten memory), passing that value tocyclic_find()returns the byte offset in the cyclic pattern where that 4-byte sequence appears - which equals the number of padding bytes needed.Step 2Build the overflow payload
ObservationOn this 64-bit binary the target occupies eight bytes, and the offset works out to 264. Pad to that, then append the value packed little-endian.Send 264 bytes of padding followed by 0xdeadbeef in little-endian 64-bit format. The gets() call has no length limit, so it writes all bytes including the overwrite of code.pythonpython3 -c " from pwn import * payload = b'A' * 264 + p64(0xdeadbeef) print(payload) "What didn't work first
Tried: Write the magic value as a raw ASCII string: b'A' * 264 + b'0xdeadbeef'
Writing those characters literally puts ten ASCII bytes into memory rather than the integer they spell. The comparison checks an integer, not a string, so it never matches. Pack the value into its little-endian binary form.
Tried: Use p32(0xdeadbeef) instead of p64(0xdeadbeef) since 0xdeadbeef fits in 32 bits
The variable is a 64-bit integer occupying eight bytes. A 32-bit pack fills only four, leaving the upper half as whatever padding follows. The comparison reads all eight, so your filler ends up in the high bytes and the value never matches.
Learn more
Little-endian byte order is the storage format used by x86 and x86-64 processors. In little-endian, multi-byte integers are stored with the least significant byte at the lowest memory address. So the 64-bit value
0x00000000deadbeefis stored in memory as the bytes\xef\xbe\xad\xde\x00\x00\x00\x00. pwntools'p64()packs an integer into this 8-byte little-endian format automatically.0xdeadbeefis a classic magic number in programming - historically used as a placeholder or sentinel value in C programs, debugger outputs, and memory initialization. In CTF binary exploitation, challenges frequently use it as the "magic value" that must be written to trigger a win condition, making it immediately recognizable as the target.The payload structure is deliberate:
b'A' * 264fills the gap between the start of the buffer and the start of thecodevariable with the byte0x41(ASCII 'A'). The next 8 bytes overwritecodewith the target value. Any bytes after that would continue overwriting the stack - the saved rbp, then the return address - but for this challenge, only the variable overwrite is needed.Step 3Exploit remotely and read the flag
ObservationThe challenge gives a remote address, so connect with pwntools and send the payload as a line, keeping the connection open long enough to read the whole response.Send the payload to the remote server. When code equals 0xdeadbeef, the binary prints the flag.pythonpython3 -c " from pwn import * conn = remote('mars.picoctf.net', <PORT>) payload = b'A' * 264 + p64(0xdeadbeef) conn.sendline(payload) conn.interactive() "What didn't work first
Tried: Pipe the payload directly via nc instead of using pwntools remote: python3 -c '...' | nc mars.picoctf.net PORT
Piping bytes through nc delivers the payload fine, and then nc closes stdin the moment the pipe ends, often cutting off the flag before it arrives. An interactive pwntools session keeps the connection open and flushes the rest of the output.
Tried: Use conn.send(payload) instead of conn.sendline(payload)
gets() reads until it sees a newline. Send the payload without one and the server blocks waiting for more, never processing the buffer. sendline appends it for you, which lets gets return and the comparison run.
Learn more
pwntools remote() opens a TCP connection to the specified host and port, returning a
tubeobject that supports sending and receiving data.sendline(payload)sends the payload bytes followed by a newline (whichgets()uses as the terminator, stopping reading). The newline is appended automatically bysendline()- usingsend()instead would skip it.This exploit works identically on the remote server as it does locally because the challenge binary is the same and stack layout is deterministic (no ASLR, no stack canary based on the challenge description). In more hardened binaries, ASLR would randomize the stack base address, requiring an information leak to defeat. Stack canaries would require either leaking the canary value or using a different exploitation primitive entirely.
The broader lesson of this challenge:
gets()makes buffer overflow trivial. Its replacement,fgets(buf, sizeof(buf), stdin), takes an explicit length limit and prevents the overflow. Other safe alternatives includescanf("%255s", buf)with an explicit width specifier. Modern compilers emit warnings whengets()is used - always treat these as errors.
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{c0ntr0ll3d_clutt3r_1n_my_buff3r}
gets() has no length limit - it reads until newline regardless of buffer size, making any program that calls it unconditionally vulnerable to stack overflows.