Hope Exploitation
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:
- leak the safe-linking XOR key (
heap >> 12) - leak a heap pointer
- poison tcache to allocate a chunk overlapping memory that already contains the flag bytes
- 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) tcachefdpointer into the freed chunk - since the bin is empty, the encoded
fdequalsheap >> 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
fdpoints 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
idxinadd()/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.