Athena Wiki

Linux Binary Protections

pwnbeginner

Understand ASLR, NX, PIE, Stack Canaries, RELRO - and the standard bypasses for each.

pwnaslrnxpiecanaryrelrochecksecprotections

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=./vuln

Output:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Each 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 correctly

On 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 address

No 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 address

With 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 address

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

MethodWhen it works
Leak via format stringIf you have a format string bug, use %N$p to read the canary value from the stack
Brute force byte-by-byteForked servers (fork()) inherit the parent's canary. Each byte is only 256 possibilities
Avoid the canaryIf 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 address

RELRO (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-only

What this means:

LevelCan you overwrite GOT?Notes
No RELROYesEverything writable
Partial RELROYes (.got.plt only)Most common in CTFs
Full RELRONoGOT 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:

pwnlinuxprotection

Strategy by protection combination:

ProtectionsWhat you need to do
NoneInject shellcode directly
NX onlyROP or ret2libc (fixed addresses)
NX + ASLRLeak libc address → compute base → ROP
NX + ASLR + PIELeak binary address + libc address → full ROP
NX + ASLR + PIE + CanaryLeak canary + binary + libc → ROP
Everything + Full RELROSame 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 GOT

Last updated on

On this page