You're working a heap-based CTF pwn challenge, the classic "note manager": allocate a note, write into it, free it when you're done, allocate a new one when you need one. The binary runs clean for every normal sequence you throw at it. Then you try one specific order of operations: free a note, then call "edit" on that same note's index again, without allocating anything new first. No bounds check trips, no obvious overflow. It just segfaults, and the crash address is somewhere deep inside libc's allocator internals, not in any function you can see in the disassembly of the challenge binary itself.
That's the tell. When a crash happens inside the allocator instead of your own code, the bug usually isn't "wrote past the end of a buffer." It's that you're touching memory the program already gave back.
Here's what free() actually does, and it's less than people assume. It doesn't zero the memory. It doesn't unmap it. It doesn't erase the pointer you're holding. All it does is tell the allocator's bookkeeping "this chunk is available again," typically by linking it into a free list (glibc uses tcache and fastbins for this) so a future allocation of the same size can hand that exact memory back out. Your program's pointer to that note is now dangling: it still holds the same address, the program has no way of knowing the memory changed hands, and nothing in the code stops you from calling edit() through it anyway. That's a use-after-free, CWE-416, and it's one of the most consistently exploited bug classes in software that touches heap memory in C or C++.
The exploit-relevant part isn't the dangling read, it's what happens if you allocate again before touching the dangling pointer. Heap allocators reuse same-size freed chunks first, because that's cheap. So: free the note, then allocate a different object of the same size. The allocator, following its own bookkeeping, hands you back the exact bytes the freed note used to occupy. Whatever you write into that new object is now sitting at the address your old, still-live dangling pointer points to. You didn't overflow anything. You groomed the heap into giving you write access to memory a stale reference still trusts.
Where this turns into code execution: a lot of note-manager challenges (and plenty of real C++ objects, for the same structural reason) store a function pointer inside the object, something like a per-type "print" callback. If that function pointer lives at a predictable offset inside the freed chunk, and you can land attacker-chosen bytes at that offset through the reallocation trick above, then the next time the program calls that callback through the dangling pointer, it jumps to wherever you put it instead of the function that used to live there. A read-after-free became a write-after-free became control of execution, and none of it required a single out-of-bounds byte.
The fix, in your own code, is one habit: pair every free() with immediately setting the pointer to NULL, so a later use-after-free becomes a clean null-deref crash instead of an exploitable dangling reference. At the tooling level this is exactly what AddressSanitizer's heap poisoning is built to catch, it deliberately marks freed memory as forbidden so this bug crashes loudly the moment you touch it in testing, not silently months later in someone else's exploit chain.
Finding the first segfault, the one deep in libc with a weird backtrace, is the easy part once you know what it means. Turning "this is a UAF" into a working exploit, the grooming, the offset-finding, the mitigation-bypass decisions when ASLR and PIE are both on, is the part the Exploit Development Book is built to walk you through end to end across memory corruption, heap bugs and the sanitizers meant to catch exactly this: https://resources.codelivly.com/product/exploit-development-fundamentals/
Top comments (0)