Linux Binary Protections
Understand ASLR, NX, PIE, Stack Canaries, RELRO - and the standard bypasses for each.
Linux Binary Protections
Every compiled program ships with a set of protections — security features that make exploitation harder. Before you write any exploit, your first job is to figure out which protections are active. This tells you what's possible and what isn't.
The tool for this is checksec.
Running checksec
checksec --file=./vulnOutput:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledEach line tells you about one protection. Below, I'll explain what each one means, why it exists, and how attackers get around it.
Always run checksec first. Your entire exploit strategy depends on which protections are active. Don't start writing code until you've read this output.
NX (Non-Executable Stack)
The problem it solves: In the early days of exploitation, attackers would inject shellcode (raw machine code) onto the stack and jump to it. NX marks the stack and heap as non-executable, so any code placed there will crash instead of running.
What checksec shows:
NX: NX enabled ← stack/heap are NOT executable
NX: NX disabled ← stack/heap ARE executable (shellcode works)When NX is off: You can write shellcode directly onto the stack and redirect execution to it. See Shellcode.
When NX is on (most modern binaries): You can't run code you wrote. Instead, you need to reuse code that already exists in the binary or its libraries. This is where ROP and ret2libc come in.
Chain together small snippets of existing code (gadgets) that end in ret. Each gadget does one small thing — load a register, call a function — and the chain combines them into full exploitation.
See ROP Chains.
ASLR (Address Space Layout Randomization)
The problem it solves: If libc is always loaded at the same address, attackers can hardcode addresses like system() in their exploits. ASLR randomizes the base addresses of the stack, heap, and shared libraries on every run, so those addresses change each time.
What checksec shows: ASLR is a kernel-level setting, not a per-binary one. checksec doesn't show it directly, but you can check:
cat /proc/sys/kernel/randomize_va_space
# 0 = disabled, 1 = partial, 2 = full (default)What this means in practice:
- ASLR off:
system()is at the same address every run. You can hardcode it. - ASLR on:
system()moves every run. You need to leak a libc address at runtime, then calculate the offset.
Bypass: Leak a known libc address (e.g., by calling puts(puts@GOT) to print the resolved address of puts), then compute the base:
libc.address = leaked_puts_address - libc.sym['puts']
# Now all libc symbols resolve correctlyOn 32-bit systems, ASLR only has ~16 bits of entropy, so brute-forcing is sometimes feasible.
PIE (Position Independent Executable)
The problem it solves: ASLR randomizes libraries, but the binary itself is still loaded at a fixed address (typically 0x400000). PIE randomizes the binary's base address too, so function addresses like win() change every run.
What checksec shows:
PIE: No PIE ← binary at fixed address (0x400000)
PIE: PIE enabled ← binary at random addressNo PIE: You can use elf.sym['win'] directly — it's a fixed address.
PIE enabled: elf.sym['win'] gives you an offset, not a real address. You need to leak a binary address first (e.g., via format string), then set the base:
elf.address = leaked_value - elf.sym['main']
# Now elf.sym['win'] resolves to the real addressWith PIE on, pwntools symbol addresses are offsets from zero, not real addresses. If you use them without setting elf.address from a leak, your exploit will target the wrong memory.
Stack Canary
The problem it solves: A classic stack overflow overwrites the return address to redirect execution. A canary is a random value placed between the local variables and the return address. Before the function returns, the program checks if the canary has been modified. If it has, the program aborts — stopping the exploit.
What checksec shows:
Stack: No canary found ← no protection, overflow freely
Stack: Canary found ← random value guards the return addressWhat the canary looks like in memory:
┌────────────────────┐
│ Local variables │ ← buffer overflow starts here
├────────────────────┤
│ Canary (8 bytes) │ ← random value, last byte is \x00
├────────────────────┤
│ Saved RBP │
├────────────────────┤
│ Return address │ ← what we want to overwrite
└────────────────────┘The null byte at the end is deliberate — it prevents printf/puts from leaking the canary as a string.
Bypass techniques:
| Method | When it works |
|---|---|
| Leak via format string | If you have a format string bug, use %N$p to read the canary value from the stack |
| Brute force byte-by-byte | Forked servers (fork()) inherit the parent's canary. Each byte is only 256 possibilities |
| Avoid the canary | If you can overflow into heap or other stack variables without touching the canary |
When you do leak the canary, include it in your payload at the exact offset:
payload = b'A' * offset_to_canary
payload += p64(canary) # put the real canary back
payload += p64(0) # saved RBP
payload += p64(win_addr) # return addressRELRO (Relocation Read-Only)
The problem it solves: The GOT (Global Offset Table) stores resolved addresses of libc functions. If an attacker can write to the GOT, they can redirect puts() to system() (or any other function). RELRO controls whether the GOT is writeable.
What checksec shows:
RELRO: No RELRO ← entire GOT is writable
RELRO: Partial RELRO ← .got.plt still writable
RELRO: Full RELRO ← GOT is read-onlyWhat this means:
| Level | Can you overwrite GOT? | Notes |
|---|---|---|
| No RELRO | Yes | Everything writable |
| Partial RELRO | Yes (.got.plt only) | Most common in CTFs |
| Full RELRO | No | GOT is read-only at runtime |
With Partial RELRO (most CTF binaries): You can overwrite entries in .got.plt. For example, overwriting puts@GOT with system means the next call to puts("/bin/sh") actually calls system("/bin/sh"). See GOT Overwrite.
With Full RELRO: The GOT is off-limits. You need to target other things:
__free_hook/__malloc_hook(glibc < 2.34)_IO_2_1_stdout_file structures (FSOP).fini_array(sometimes writable)- Stack-based overwrites
Putting It All Together
Most CTF binaries have multiple protections enabled. The order in which you deal with them matters:
Strategy by protection combination:
| Protections | What you need to do |
|---|---|
| None | Inject shellcode directly |
| NX only | ROP or ret2libc (fixed addresses) |
| NX + ASLR | Leak libc address → compute base → ROP |
| NX + ASLR + PIE | Leak binary address + libc address → full ROP |
| NX + ASLR + PIE + Canary | Leak canary + binary + libc → ROP |
| Everything + Full RELRO | Same as above, but target hooks/FSOP instead of GOT |
The general pattern: leak first, exploit second. Almost every modern exploit starts by leaking one or more addresses to bypass randomization, then uses those to build the real payload.
Quick Reference
checksec shows: Do this:
──────────────────────────────────────────────────────
No NX → shellcode injection
NX + No PIE + No ASLR → ret2win / ret2libc (fixed addresses)
NX + PIE + ASLR → leak binary/libc addr first
Canary found → leak canary before overflow
Full RELRO → target hooks/FSOP, not GOTLast updated on