heap 1 picoCTF 2024 Solution

Published: April 3, 2024

Description

Can you control your overflow?

Remote only

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).

bash
nc tethys.picoctf.net <PORT_FROM_INSTANCE>
This builds on heap 0 by requiring a specific string instead of just zeroing. After mastering this, progress to heap 2 for function pointer overwrites and heap 3 for use-after-free exploitation. The Buffer Overflow and Binary Exploitation guide covers heap exploitation techniques in depth, the Heap Exploitation for CTF guide walks through tcache and overflow primitives, and the GDB for CTF guide shows how to inspect heap chunks at runtime.
  1. Step 1Measure the offset 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.
    bash
    gdb ./chall
    bash
    pwndbg> b *main+offset_of_fgets
    bash
    pwndbg> r
    bash
    pwndbg> heap
    bash
    pwndbg> p &safe_var - input_buf
    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_var equals 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 them

    Precision matters: one byte too few and safe_var is 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.

  2. Step 2Append the magic string
    Append pico immediately after the 32-byte filler when using menu option 2. The null terminator follows pico, leaving safe_var == "pico".
    bash
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico
    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, overwrite safe_var with exactly the right value. The null terminator from fgets follows 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_var is 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.

  3. Step 3Print the flag
    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.

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).

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-strong and 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.

Want more picoCTF 2024 writeups?

Tools used in this challenge

Related reading

Do these first

What to try next