Taz
⌘Ctrl K

The Heap

Hope Exploitation

3 min read

Contents
  1. Overview
  2. Vulnerability analysis
    1. 1) Use-After-Free: delete() does not clear the pointer
    2. 2) Helpful behavior: view() prints attacker-controlled heap bytes
    3. 3) Why this works with safe-linking
  3. Exploitation strategy (matches solve.py)
    1. Step 1 — Leak the safe-linking XOR key
    2. Step 2 — Leak a heap pointer and compute a heap address
    3. Step 3 — Target a heap location that holds flag bytes
    4. Step 4 — Tcache poisoning via the single allowed edit
    5. Step 5 — Allocate the poisoned chunk and print the flag
  4. Notes / gotchas

Overview

The program is a small heap-based “calendar manager” with add/edit/delete/view actions. It runs with seccomp enabled and the binary is compiled with modern mitigations (PIE, full RELRO, NX). The intended solution avoids ROP entirely and instead abuses a Use-After-Free (UAF) to:

  1. leak the safe-linking XOR key (heap >> 12)
  2. leak a heap pointer
  3. poison tcache to allocate a chunk overlapping memory that already contains the flag bytes
  4. print that memory via the existing view() functionality

Vulnerability analysis

1) Use-After-Free: delete() does not clear the pointer

In delete() the chunk is freed but the entry in MyCalendar[] is never set back to NULL.

free(MyCalendar[idx]);
// missing: MyCalendar[idx] = NULL;

Because view() and edit() only check MyCalendar[idx] == 0, the stale (freed) pointer still passes validation, giving UAF read/write primitives on a freed heap chunk.

2) Helpful behavior: view() prints attacker-controlled heap bytes

view() prints event and description using strlen(), so if we can control the first bytes of a freed chunk we can exfiltrate whatever allocator metadata ends up there.

write(1, MyCalendar[idx]->event, strlen(MyCalendar[idx]->event));

3) Why this works with safe-linking

On modern glibc, tcache “next” pointers are protected by safe-linking:

$$\text{stored_fd} = \text{next} \oplus (\text{heap} >> 12)$$

If we free a chunk into an empty tcache bin, next == NULL, so the stored value becomes just (heap >> 12). That gives us the XOR key directly.

Exploitation strategy (matches solve.py)

Step 1 — Leak the safe-linking XOR key

The solver allocates one calendar entry, frees it, then views it (UAF read) and parses the bytes printed from the freed chunk.

Solver excerpt:

add(0)
delete(0)
view(0)
p.recvuntil(b"Name of the event : ")
xorkey = u64(p.recvline()[:-1].ljust(8, b"\x00"))

Interpretation:

  • after free(), glibc writes the (safe-linked) tcache fd pointer into the freed chunk
  • since the bin is empty, the encoded fd equals heap >> 12
  • the solver calls it xorkey

Step 2 — Leak a heap pointer and compute a heap address

Next the solver prepares two chunks of the same size, frees them, and views one of them to read a non-NULL tcache fd.

add(1)
add(2)
delete(1)
delete(2)
view(2)
p.recvuntil(b"Name of the event : ")
leak = u64(p.recvline()[:-1].ljust(8, b"\x00"))
heapaddr = leak ^ xorkey

Why leak ^ xorkey works:

  • the freed chunk’s fd points to the previously freed chunk
  • that pointer is stored as fd ^ (heap >> 12)
  • XORing with the key recovers the real heap pointer

Step 3 — Target a heap location that holds flag bytes

The binary calls read_flag() at startup. Although the local buffer is zeroed, glibc’s fopen()/fread() internals still create heap allocations (e.g., FILE structure and buffering). In this challenge, the author made the heap layout stable enough that the solver can derive a target address with a fixed offset from the heap pointer leaked above.

Solver excerpt:

flagaddr = heapaddr - 0x5b80 - 0x10
xordflagaddr = flagaddr ^ xorkey

Step 4 — Tcache poisoning via the single allowed edit

edit() is limited to one call (c global counter), so the exploit uses that single edit to overwrite the tcache fd of a freed chunk (UAF write) with an encoded pointer to flagaddr.

edit(2, p64(xordflagaddr))

After this, the tcache freelist for that size class will eventually return a chunk at flagaddr.

Step 5 — Allocate the poisoned chunk and print the flag

Two allocations are made to walk the freelist until the forged pointer is returned. Then view() is used to print the memory at that location.

add(3)
add(4, b"", b"")
view(4)

At this point MyCalendar[4] points into the chosen heap region and view(4) prints the bytes there, revealing the flag.

Notes / gotchas

  • There is also no bounds check on idx in add()/delete()/view()/edit(). The provided solver doesn’t rely on it, but it’s another bug.
  • The seccomp policy blocks many syscalls, so the exploit avoids shellcode/ROP and instead reuses already-read flag bytes from process memory.
Esc

    ↓ results · Enter open · Esc close

    Keys

    jk
    Next and previous row
    ↓↑
    The same, once a row has focus
    Enter
    Open the row
    1234
    Home, Projects, Journal, About
    /
    Search
    CtrlK
    Search, from anywhere (⌘ K on a Mac)
    ?
    This list
    Esc
    Close a layer
    gg
    Back to the top