Description
Can you control your overflow?
Setup
Connect to the remote instance at tethys.picoctf.net <PORT_FROM_INSTANCE>.
Explore the menu options; they mirror heap0 (print/write/inspect safe_var/print flag).
nc tethys.picoctf.net <PORT_FROM_INSTANCE>Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Measure the offset to safe_varObservationI noticed that heap-1 checks safe_var against a specific string rather than just detecting corruption, which meant I needed the exact byte distance between the malloc'd input buffer and safe_var before crafting any payload.Set a breakpoint after the malloc and check pwndbg's heap output: the input chunk and the safe_var chunk are adjacent, with safe_var sitting 32 bytes after the buffer's start.bashgdb ./challbashpwndbg> b *main+offset_of_fgetsbashpwndbg> rbashpwndbg> heapbashpwndbg> p &safe_var - input_bufWhat didn't work first
Tried: Skipping GDB entirely and guessing an offset like 24 or 28 based on the buffer size declared in the source
The malloc'd chunk has a 16-byte header prepended by glibc, so the usable payload region starts 16 bytes into the chunk. Eyeballing the source variable size gives the wrong delta; only the runtime heap view (pwndbg heap or vmmap) shows the true distance between chunks, which on this binary is 32 bytes.
Tried: Using vanilla GDB without pwndbg and running 'info heap' or 'x/20gx' on the allocator internals
Vanilla GDB has no 'heap' command - it errors immediately. The 'x/20gx' approach dumps raw memory but requires knowing the heap base address first, which you do not have without the allocator plugin. pwndbg (or peda/gef) provides the 'heap' command that formats chunk boundaries and sizes automatically.
Learn more
This step advances beyond heap-0 by requiring a controlled overwrite rather than just a null byte. The challenge now checks that
safe_varequals a specific string value ("pico"), not just that it's zero, so you need to write exactly the right bytes at exactly the right position.pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555556a2a0 Size: 0x21 (with flag bits: 0x21) <- input buffer (0x20 = 32 user bytes) Allocated chunk | PREV_INUSE Addr: 0x55555556a2c0 Size: 0x21 (with flag bits: 0x21) <- safe_var chunk pwndbg> p (char *)0x55555556a2c0 - (char *)0x55555556a2a0 $1 = 0x20 <- 32 bytes between themPrecision matters: one byte too few and
safe_varis untouched; one byte too many with the wrong content corrupts it to the wrong value. In real exploits targeting allocator metadata (free-list pointers, size fields), the offset from the overflow buffer to the target comes from this same chunk-layout inspection.Step 2
Append the magic stringObservationI noticed from the source and binary inspection that the strcmp target is the lowercase string 'pico', so padding 32 filler bytes to reach safe_var and then appending those exact 4 bytes would satisfy the check and trigger the flag.Append pico immediately after the 32-byte filler when using menu option 2. The null terminator follows pico, leaving safe_var == "pico".bashAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApicoExpected output
picoCTF{starting_to_get_the_hang_c58...}What didn't work first
Tried: Sending exactly 32 A's hoping that the null terminator fgets appends will land inside safe_var and zero it out like in heap-0
heap-1 checks safe_var against the string 'pico', not against zero. A null byte at the 33rd position writes 0x00 into safe_var, which fails the strcmp. You must write the four bytes p-i-c-o starting at position 33, making the total payload 36 characters.
Tried: Sending 32 A's followed by 'PICO' (uppercase) because the variable name is safe_var and capitalization might not matter
The binary uses a case-sensitive strcmp against the lowercase string 'pico'. Sending 'PICO' (0x50 0x49 0x43 0x4f) writes different byte values than 0x70 0x69 0x63 0x6f, so the comparison fails and the flag is not printed.
Learn more
The string
"pico"is 4 bytes:p=0x70,i=0x69,c=0x63,o=0x6f. These 4 bytes, written starting at the 33rd position, overwritesafe_varwith exactly the right value. The null terminator fromfgetsfollows at position 37, which either lands in the next heap chunk (harmlessly) or terminates the string naturally.This illustrates controlled heap overflow: you write a chosen value (not just zeros or garbage) to an adjacent heap object. In real exploits, controlled overwrites target function pointers (to redirect execution), heap freelist pointers (to enable arbitrary allocation), or security-critical flags (as here). The technique scales from simple guard variables to complex heap management structures.
The 36-character total payload (
A*32 + "pico") fits within a reasonable input buffer size. If the input function's size limit were only 32 bytes, this attack would be impossible, which is why proper input length validation must account for all adjacent data that could be affected, not just the buffer itself.Precision overwrite is what works here because the offset to
safe_varis fixed and known. Heap spraying (filling the heap with many copies of a payload to land somewhere by chance) only earns its keep when the offset is unknown or jitter exists between runs, neither of which applies to this challenge.Step 3
Print the flagObservationI noticed menu option 3 can verify safe_var's current value before committing, so confirming the overwrite succeeded first eliminates ambiguity about whether a missing flag comes from a bad payload or a bad trigger.Once safe_var contains pico, selecting option 4 prints the flag without further tricks.Use option 3 first if you want to confirm safe_var now shows pico, then call option 4.Learn more
Using option 3 to verify the write before triggering the reward is good exploit development practice. Confirming intermediate state before proceeding helps isolate failures: if the flag doesn't print, you know whether the problem is the overflow payload or the flag-printing logic itself.
This two-step approach (write, verify, trigger) mirrors professional exploit development, where each stage is tested independently. In complex exploit chains with multiple vulnerabilities chained together (info leak, bypass ASLR, overflow, code execution), verifying each step prevents wasted time debugging the wrong stage.
The progression from heap-0 (zero any value), to heap-1 (write a specific value), to heap-2 (write a function pointer), to heap-3 (use-after-free) represents the learning ladder of heap exploitation. Each challenge adds one new concept: precision control, address knowledge, memory lifecycle awareness. Professional heap exploitation combines all of these plus allocator internals, making it one of the most technically demanding areas of binary exploitation.
Interactive tools
- 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{starting_to_get_the_hang_c58...}
As soon as safe_var == pico, option 4 prints the full flag (truncated above; the trailing 8-character random tail is per-instance).
Key takeaway
How to prevent this
How to prevent this
Same root cause as heap-0; the attacker just chose a specific value to write instead of zero.
- Bounds-check every write into a heap buffer.
strncpy,snprintf, and explicit length parameters are the minimum bar. - Do not place security-relevant variables adjacent to user-controlled buffers. The compiler often does this for you with
-fstack-protector-strongand modern allocators (mimalloc, jemalloc) but explicit struct layout is more reliable. - Run with AddressSanitizer (
-fsanitize=address) and HWASAN in CI. Both catch out-of-bounds writes the instant they happen, not three frames later when corruption manifests.