The stack canary protects return
addresses. Its less-famous sibling, pointer guard, protects a handful of
stored function pointers that an attacker with a write would otherwise love:
atexit handlers, TLS destructors, the rip/rsp saved in a jmp_buf. When
exit() walks those handlers, it demangles each pointer first — so overwriting
one with &system produces garbage, not a call. Unless you understand the mangle.
- arch
- amd64-64-little
- guard
- __pointer_chk_guard at fs:0x30 (sibling of the canary at fs:0x28)
- era
- the post-2.34 world, where the malloc hooks are gone
the mangle#
A guarded pointer isn't stored raw. It's XOR'd with a per-process secret and rotated:
and read back with the inverse, ptr = ror(mangled, 17) ⊕ guard. On x86-64 it's
two instructions inlined at every store/use:
__pointer_chk_guard is seeded at startup from the kernel's AT_RANDOM — the same
source as the canary — and parked in the
TLS at fs:0x30, one slot past the canary's fs:0x28. Per-process, constant for
the run, inherited across fork.
what it guards, and how exit reaches it#
Two structures matter, because both are walked on a normal exit():
__exit_funcs— theatexit/__cxa_atexitlist.__run_exit_handlersdemangles eachexit_function's pointer and calls it.tls_dtor_list— thread-local destructors.__call_tls_dtorsdoes, per entry:
So if you can write a fake dtor_list (or corrupt an existing entry), the
demangled func is called with obj in rdi. Point func at system and obj
at "/bin/sh" and exit() spawns a shell — if you can produce a correctly
mangled func. That's the whole problem, and there are two solutions.
move 1 — leak the guard#
The guard isn't a moving target; it's one value. Leak __pointer_chk_guard from
the TLS directly, or recover it from any already-mangled pointer whose plaintext
you know (a default libc handler, say): since ptr = ror(mangled,17) ⊕ guard,
With the guard in hand you can mangle any target, and the demangle on the exit path turns it straight back into your call.
move 2 — zero the guard#
If you have an arbitrary write but no leak, don't fight the secret — delete it.
Overwrite the TLS slot at fs:0x30 with 0. Now ptr ⊕ 0 = ptr, so mangling
collapses to a plain rotate you can compute with no secret at all:
This is the exact analogue of overwriting the canary's master copy — once the secret is zero, the protection is a fixed, public transform.
exit() ran my handler
A forged tls_dtor_list entry, a mangled system, obj = "/bin/sh", and the
program's own clean-up did the call:
segfault{exit_handlers_are_function_pointers_too}
where this sits in the modern playbook#
With __free_hook/__malloc_hook gone since 2.34, the targets that survive are
the ones glibc still calls on the way out — exit handlers, TLS destructors — and
those are guarded by PTR_MANGLE precisely to make this attack harder. It raises the
bar to "leak or zero one TLS qword," the same bar the canary sets. Know where the
guard lives and how the rotate works, and a mangled pointer stops being a wall: it's
a fixed function of a single secret you can read or erase.