Description
This program mishandles memory. Can you exploit it to get the flag?
Setup
Download chall and chall.c for local analysis.
Connect to tethys.picoctf.net <PORT_FROM_INSTANCE> to interact with the menu.
wget https://artifacts.picoctf.net/c_tethys/6/chall && \
chmod +x chall && \
wget https://artifacts.picoctf.net/c_tethys/6/chall.c && \
nc tethys.picoctf.net <PORT_FROM_INSTANCE>Solution
Walk me through it- Step 1Free the chunk firstOption 5 must run before anything else so the next allocation lands on the same chunk that x still points at. If you skip the free, option 2 returns a brand-new chunk somewhere else in the heap, your bytes never reach x, and option 4's win check silently fails.
Learn more
A use-after-free (UAF) vulnerability occurs when a program frees a heap chunk but retains a pointer to it (a dangling pointer) and later uses that pointer to read or write memory. The freed chunk may be reallocated for a different purpose, so data written through a new allocation overwrites memory that the old dangling pointer still points to.
Tcache lifecycle of x's chunk: step 1 (init): x = malloc(N) -> chunk @ A; x = A step 2 (free): free(x) -> tcache[size N]: A -> NULL (NB: x STILL points at A!) step 3 (re-alloc with our data): buf = malloc(N) -> tcache pops A back buf = A read(buf,...) -> writes our bytes into chunk @ A step 4 (use after free): print(*x) -> reads chunk @ A -> sees our bytes, prints "pico..." Key: nothing zeroes the chunk on free or alloc - tcache literally pops the old address straight off a singly-linked list and returns it as-is. The old user data is still there until you overwrite it (which is exactly what we do).The sequence matters critically: free first, then allocate. If you allocate before freeing, the new allocation goes to a different chunk (the old one is still in use). Only after freeing does the chunk enter the allocator's freelist, available for the next allocation of the same or smaller size.
In modern glibc (the standard C library), freed chunks < 0x410 bytes go into the per-thread tcache as a singly-linked LIFO list keyed by size class. The next
malloc()of that size pops the head of the list - which is the chunk we just freed. This deterministic recycling is what makes UAF exploitation reliable.Use-after-free is one of the most common vulnerability classes in modern software, particularly in web browsers (V8, SpiderMonkey, JavaScriptCore) and media parsers. The Chrome bug tracker regularly contains UAF vulnerabilities rated Critical. Browser sandboxes partially mitigate the impact, but UAF remains a primary pathway to browser exploitation.
- Step 2Allocate with controlled dataOption 2 asks for a length. The win check inside chall.c compares offset 30 of x's struct against "pico", so request 31 bytes and send 30 filler chars followed by pico to land the string in the right field.bash
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAApicoLearn more
After freeing the original chunk, requesting a new allocation of the same or slightly smaller size causes the allocator to return the same chunk that was just freed. Your new write then populates the freed chunk with fresh data, which the dangling pointer from the previous allocation will read.
The 30-byte offset comes from
chall.c: the struct thatxpoints to has padding/header fields ahead of the string the win check inspects. Without source you can recover the same number in GDB by setting a breakpoint right after option 2 finishes its read, then comparing the chunk's base address to the field the check actually reads:pwndbg> b *check_win+offset_of_strcmp pwndbg> r ... (run options 5, 2, 4) pwndbg> p x # base of struct $1 = 0x55555556a2a0 pwndbg> p (char *)$rsi # the field strcmp reads $2 = 0x55555556a2be # 0x1e = 30 bytes after baseThe length of 31 bytes is chosen so that the total write (30 filler bytes + "pico" + null terminator) fits within the freed chunk without overflowing into the allocator's metadata. Exceeding the chunk size would corrupt the heap metadata, likely causing a crash when the next allocation or free occurs.
Heap grooming generalizes this: an attacker controls the sequence and size of allocations and frees to achieve a desired heap layout. Advanced UAF exploits may require dozens of carefully sized allocations to position the target chunk exactly where the dangling pointer expects it, especially in modern hardened allocators.
- Step 3Verify and printOption 3 now echoes pico, and option 4 prints the flag because the dangling pointer points to your crafted data.If the check fails, ensure you freed first and used exactly 30 filler characters before pico.
Learn more
Option 3 ("print current x value") reads through the dangling pointer that still references the freed chunk. Since you reallocated that chunk with your controlled data, the read now returns
"pico". This demonstrates the UAF: the program believes it's reading fromx, but the memory at that address now contains attacker-controlled data.Option 4's win check (
x == "pico") uses the same dangling pointer, finds the string you wrote, and prints the flag. The original value ofx(whatever was in the structure before freeing) is irrelevant - the UAF lets you substitute any value you can allocate.In real-world UAF exploitation, the "controlled allocation" step is often more complex. Browser exploits might allocate JavaScript ArrayBuffers, Uint8Arrays, or other typed arrays at a predictable size to reclaim freed DOM element structures. The attacker must understand both the target object's layout and the layout of available attacker-controlled objects to find a suitable "replacement" that the vulnerable code will misinterpret usefully.
UAF mitigations layer at three different boundaries: allocator-level defences (PartitionAlloc segregates objects by type so a freed DOM node can't be reclaimed by a UInt8Array; hardened_malloc and Scudo quarantine freed chunks), OS-level defences (delayed free pools, kernel zero-on-free), and hardware-level defences (ARM MTE tags pointers and traps stale pointer dereferences in silicon, CHERI capabilities revoke entirely on free). The right one depends on what you control: app authors reach for smart pointers and hardened allocators, OS vendors enable MTE, and language designers pick borrow-checking (Rust) to prevent the bug at compile time.
Flag
picoCTF{now_thats_free_real_estate_a11...}
Once the freed chunk is reallocated with pico, the win check passes and prints the flag.
How to prevent this
How to prevent this
Use-after-free is a lifetime bug. The fix is making freed pointers unusable, not adding more validation.
free(p); p = NULL;on every release. Then the next dereference crashes immediately instead of reading attacker data. Better: smart pointers (std::unique_ptr,std::shared_ptr) so the lifetime is enforced by the type system.- Use a hardened allocator: PartitionAlloc (Chrome), Scudo (Android/Fuchsia), or GraphenOS's hardened_malloc. These segregate objects by type and quarantine freed chunks, making UAF reallocation attacks dramatically harder.
- On ARMv8.5+, enable MTE (Memory Tagging Extension). Pointers carry a 4-bit tag that must match the tagged memory; stale pointers fault on dereference. For new code, write in Rust where the borrow checker prevents UAF at compile time.