algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

the stack canary, from the source

2026-03-154 min readpwn glibc internals

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.

targetglibc
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:

prologue + epilogueasm
1
2
3
4
5
6
7
8
; prologue
mov    rax, qword ptr fs:[0x28]   ; load the per-thread guard
mov    qword ptr [rbp-8], rax     ; place it just below the return address
; ... function body, buffers live below [rbp-8] ...
; epilogue
mov    rax, qword ptr [rbp-8]
sub    rax, qword ptr fs:[0x28]   ; unchanged?
jne    __stack_chk_fail           ; no -> *** stack smashing detected ***, abort

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:

glibc: sysdeps/.../dl-osinfo.h (paraphrased)c
1
2
3
4
5
6
7
8
9
static inline uintptr_t
_dl_setup_stack_chk_guard (void *dl_random)   /* dl_random -> AT_RANDOM bytes */
{
  uintptr_t ret;
  memcpy (&ret, dl_random, sizeof ret);       /* 8 of the 16 random bytes */
  /* little-endian: force the least-significant byte to 0x00 */
  ret &= ~(uintptr_t) 0xff;
  return ret;
}

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:

one value, kernel to prologuetxt
1
2
3
4
kernel execve  ->  auxv AT_RANDOM (16 bytes)
glibc start    ->  _dl_setup_stack_chk_guard  ->  __stack_chk_guard
TLS setup      ->  tcbhead_t.stack_guard       ->  fs:0x28
every function ->  mov rax, fs:0x28 ; ... ; sub/jne

Two consequences fall straight out of that code, and both are doors:

  1. The low byte is forced to 0x00. Deliberate: it makes the canary terminate a C string, so an fgets/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 in 00:
    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...............|
  2. 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:

python
canary = u64(leak)                 # ends in \x00, by construction
payload  = b"A"*offset + p64(canary) + p64(saved_rbp) + rop

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 256×7=1792 attempts — versus the 256 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.