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).
The challenge also ships chall and chall.c; grab them if you want to inspect the chunk layout locally in GDB.
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 1Measure the offset to safe_var
ObservationThis one checks safe_var against a specific string rather than merely noticing corruption, so the payload needs the exact byte distance from the input buffer to safe_var.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 (char *)safe_var - (char *)input_dataWhat 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
glibc prepends a 16-byte header, so the usable region starts 16 bytes into the chunk. Reading variable sizes off the source gives the wrong delta; only the runtime heap view shows the real distance, which is 32 bytes here.
Tried: Using vanilla GDB without pwndbg and running 'info heap' or 'x/20gx' on the allocator internals
Plain GDB has no heap command and errors out. Dumping raw memory with x/20gx works only if you already know the heap base, which you do not without an allocator plugin. pwndbg, peda, or gef adds a heap command that formats chunk boundaries and sizes for you.
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: 0x20 (with flag bits: 0x21) <- input buffer chunk (0x20 total) Allocated chunk | PREV_INUSE Addr: 0x55555556a2c0 Size: 0x20 (with flag bits: 0x21) <- safe_var chunk pwndbg> p (char *)0x55555556a2c0 - (char *)0x55555556a2a0 $1 = 32 <- 0x20 between the chunks, so 0x20 between the user pointers tooPrecision 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 2Append the magic string
ObservationThe strcmp target is the lowercase string 'pico'. Pad 32 filler bytes to reach safe_var, append those four, and the check passes.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 3Print the flag
ObservationMenu option 3 shows safe_var's current value. Checking it before triggering separates a bad payload from 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. Hardened allocators (mimalloc, jemalloc, hardened_malloc) make the adjacency less predictable, since glibc's ptmalloc puts nothing between two live chunks, but keeping the guard out of the neighbouring allocation entirely 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.