Everybody knows the stack canary: a random value the compiler drops between your
locals and the saved return address, checked before ret. Fewer people have
followed where that value actually comes from — and the source answers the two
questions that matter for breaking it: why does it end in a null byte, and
when does it change.
- component
- -fstack-protector / __stack_chk_guard
- arch
- amd64-64-little
- property
- per-process, low byte 0x00
the prologue the compiler emits#
Build with -fstack-protector-strong and every protected function reads the
canary from the thread pointer (fs on x86-64) at offset 0x28, stashes it
above the locals, and re-checks it on the way out:
|
A linear buffer overflow that wants the return address at [rbp+8] has to write
through [rbp-8] first. Clobber the canary, and the epilogue aborts before
ret ever runs. That's the whole idea — but every property of that value was
decided much earlier, at process startup.
where fs:0x28 is filled#
At execve, the kernel drops 16 random bytes into the auxiliary vector under
the key AT_RANDOM. glibc startup picks them up and turns them into the guard:
|
That result is stored in the global __stack_chk_guard and copied into the
thread control block so it lands at fs:0x28 (THREAD_SET_STACK_GUARD). The
chain, end to end:
Two consequences fall straight out of that code, and both are doors:
- The low byte is forced to
0x00. Deliberate: it makes the canary terminate a C string, so anfgets/strcpy-style overflow that runs over it stops, and a printed-string leak of the canary won't bleed past it. The practical tell — a leaked canary always ends in00:a stack frame: buffer | canary | saved rbp | rethex 00000000 41 41 41 41 41 41 41 41 00 a3 7c 1e d9 4b f2 6e |AAAAAAAA..|..K.n| 00000010 30 e8 ff ff ff 7f 00 00 1a 92 04 00 00 00 00 00 |0...............|
- It is per-process, not per-call. Set once at startup, never refreshed.
fork()copies the parent's memory — so every child of a forking server shares the exact same canary. That single fact is what makes brute force viable.
four ways through#
1 — leak it, then write it back (the common one)
A format string, an out-of-bounds read, or an uninitialised print hands you
the 8 bytes. Then your overflow simply includes the real canary at [rbp-8],
so sub … ; jne sees no change and the check passes:
2 — brute force it on a fork server
Because fork() children inherit the parent's canary, you can recover it a
byte at a time: overwrite one canary byte and see whether the child crashes
(wrong) or runs on (right). The low byte is already known to be 0x00, so it's
at most attempts — versus the of guessing blind.
Same trick recovers the saved return
address / PIE base byte-by-byte afterward.
3 — overwrite the master copy
With an arbitrary write you can set the reference the epilogue compares
against — __stack_chk_guard and the TLS fs:0x28 slot — to a value you also
write on the stack. Make both sides match (even match to zero) and the check is
a formality.
4 — don't cross it at all
The canary only guards the path to ret. A local function pointer above the
canary, a saved-RBP-only corruption feeding a later pivot, or a write
primitive that never touches [rbp-8] reaches control flow without disturbing
it. The canary protects one edge, not the whole frame.
Reading the source turns the canary from a magic number into a value with a biography: born in the kernel's RNG, laundered through glibc with its low byte clipped, frozen for the life of the process, and inherited verbatim by every fork. Each of those is a sentence in the same paragraph — and each is a way in.