notekeeper is the archetypal CTF heap binary: a menu that creates, edits, shows,
and deletes notes. Those four verbs are also the four heap primitives you need —
the only bug is that delete forgets to clear the pointer. Here's the whole chain.
- arch
- amd64-64-little
- libc
- 2.31
- RELROfull
- canaryon
- NXon
- PIEon
recon — four verbs over the heap#
free leaves notes[idx] dangling, and both edit and show happily operate on
a freed index. That single oversight is a use-after-free giving me a read
and a write on freed chunks — everything else follows.
step 1 — leak libc through the unsorted bin#
A chunk too big for a tcache bin (>0x408) goes to the unsorted bin on free,
and its fd/bk get set to a main_arena address inside libc. show on that
freed chunk reads those bytes back:
|
With full RELRO the GOT is read-only, so I can't redirect a PLT call — but glibc
2.31 still has __free_hook, and that's writable.
step 2 — tcache poisoning to __free_hook#
tcache poisoning: free a small chunk into a
tcache bin, then use the UAF edit to overwrite its fd (the next pointer) with
the address I want allocated. The next two mallocs of that size return first the
real chunk, then my chosen address:
Now __free_hook == system, and note 3 holds "/bin/sh".
step 3 — free your way to a shell#
free(p) calls __free_hook(p) if it's set — which is now system(p). So freeing
the chunk that contains "/bin/sh" runs system("/bin/sh"):
flag
what made it easy, and what 2.34 would change#
- The whole exploit is one bug. A missing
notes[idx] = NULLafterfreeturned four benign verbs into read/write-after-free. Liveness checks (and zeroing freed pointers) kill it dead. __free_hookis a 2.31 luxury. On glibc ≥ 2.34 the hook is gone, so the same tcache poison would aim at a saved return address or pivot into House of Apple 2 FSOP instead. The leak and the poison are identical; only the final target moves.- PIE + full RELRO + canary didn't matter. None of them touch a heap UAF; the libc leak handled ASLR, and I never went near a return address or the GOT.
notekeeper is worth keeping as a reference shape: when you see create/edit/show/ delete over an array of pointers, the first thing to check is whether delete clears the slot. Half the time it doesn't, and the other three verbs hand you the exploit.