clutter-overflow picoMini by redpwn Solution

Published: April 2, 2026

Description

Clutter, clutter everywhere and not a byte to use. Overflow the buffer to set code = 0xdeadbeef.

Remote

Download the binary from the challenge page.

Install pwntools: pip install pwntools

bash
nc mars.picoctf.net <PORT>
bash
pip install pwntools

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
New to binary exploitation? Buffer Overflow and Binary Exploitation for CTF covers stack overflows, ret2win, format strings, heap exploitation, and PIE bypass.
  1. Step 1
    Find the buffer size and variable offset
    Observation
    I noticed the binary uses gets() with a declared 256-byte buffer and compares a local variable code against 0xdeadbeef, which suggested I needed to find the exact byte offset between the buffer start and code before crafting 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.
    python
    python3 -c "from pwn import *; print(cyclic(300))" | ./clutter-overflow
    python
    python3 -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 to code is 264, not 256 - the compiler often adds alignment padding between the declared buffer and adjacent variables. Sending only 256 padding bytes leaves 8 bytes of padding before code, so the value written there is 0x4141414141414141 (eight A's) instead of 0xdeadbeef, and the check fails silently.

    Tried: Feed the cyclic pattern via gdb and read the rip register to find the offset

    rip is the return address, not the code variable - reading rip gives the offset to overwrite the saved return address (typically much larger than 264). The challenge only needs to overwrite the local variable code on the stack, which is at a completely different offset. cyclic_find should be applied to the value that ends up in code at the crash, not the value in rip.

    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 by cyclic(). When the program crashes with code = 0x61616174 (from reading the overwritten memory), passing that value to cyclic_find() returns the byte offset in the cyclic pattern where that 4-byte sequence appears - which equals the number of padding bytes needed.

  2. Step 2
    Build the overflow payload
    Observation
    I noticed the target value 0xdeadbeef is a 64-bit integer on an x86-64 binary and the offset to code was confirmed as 264 bytes, which suggested constructing a payload of 264 bytes of padding followed by p64(0xdeadbeef) to write the value in the correct little-endian format.
    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.
    python
    python3 -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 the literal ASCII characters '0xdeadbeef' puts 10 ASCII bytes (0x30 0x78 0x64 ...) into memory instead of the 4-byte integer 0xdeadbeef. The comparison in the binary checks the integer value of code, not a string, so it never equals 0xdeadbeef and the flag is never printed. p64() is required to pack the integer into its 8-byte little-endian binary representation.

    Tried: Use p32(0xdeadbeef) instead of p64(0xdeadbeef) since 0xdeadbeef fits in 32 bits

    On a 64-bit binary, the code variable is a 64-bit integer occupying 8 bytes on the stack. p32() only packs 4 bytes, leaving the upper 4 bytes of the 8-byte slot as whatever padding follows in the payload. The comparison checks all 8 bytes, so the upper 4 bytes (0x41414141 from the 'A' padding) make the full value 0x41414141deadbeef, which does not equal 0x00000000deadbeef.

    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 0x00000000deadbeef is 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.

    0xdeadbeef is 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' * 264 fills the gap between the start of the buffer and the start of the code variable with the byte 0x41 (ASCII 'A'). The next 8 bytes overwrite code with 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.

  3. Step 3
    Exploit remotely and read the flag
    Observation
    I noticed the challenge provides a remote netcat address at mars.picoctf.net, which suggested using pwntools remote() with sendline() to deliver the payload over TCP and keep the connection open long enough to receive the full flag output.
    Send the payload to the remote server. When code equals 0xdeadbeef, the binary prints the flag.
    python
    python3 -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 raw bytes through nc works for sending the payload, but nc closes stdin immediately after the pipe ends, so the flag printed by the server often gets cut off before it arrives. pwntools conn.interactive() keeps the connection open and flushes all remaining output, ensuring the full flag line is received and printed.

    Tried: Use conn.send(payload) instead of conn.sendline(payload)

    gets() reads until it encounters a newline character. Using send() sends the payload bytes without the trailing newline, so gets() on the server keeps blocking waiting for more input and never processes the overflowed buffer. sendline() appends b'\n' automatically, which signals gets() to stop reading and return, triggering the code comparison and flag print.

    Learn more

    pwntools remote() opens a TCP connection to the specified host and port, returning a tube object that supports sending and receiving data. sendline(payload) sends the payload bytes followed by a newline (which gets() uses as the terminator, stopping reading). The newline is appended automatically by sendline() - using send() 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 include scanf("%255s", buf) with an explicit width specifier. Modern compilers emit warnings when gets() 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.

Key takeaway

gets() has no length limit and is unconditionally unsafe; a buffer overflow with gets() can overwrite any adjacent stack variable, not just the return address, and the exact distance from the buffer start to the target must be determined from the disassembly or via a cyclic pattern rather than assumed from the declared buffer size alone. Modern compilers warn on any use of gets() and the function was removed from the C11 standard; the safe replacement is fgets(buf, sizeof(buf), stdin).

Related reading

Want more picoMini by redpwn writeups?

Tools used in this challenge

What to try next