HackBlue

Tools · CTF categories for beginners · 11 / 18

Binary protections explained with checksec: NX, PIE, stack canaries, and RELRO

Read checksec output, understand what each mitigation does and why it exists, and compile the same program with and without protections to see the difference.

10 min read · practice only with authorization

Mitigations in one place

Modern compilers and operating systems add defenses that make memory corruption bugs harder to exploit. NX (no-execute) marks the stack and heap non-executable. PIE (position-independent executable) lets the loader place the program at a random address, working with ASLR. Stack canaries place a secret value before the saved return address and abort if it changes. RELRO makes parts of the dynamic linking tables read-only after startup. FORTIFY_SOURCE adds bounds checks to some libc calls.

In CTF pwn challenges, the first step is always to see which protections are enabled, because that shapes what the challenge is really about.

Reading checksec

checksec from pwntools or the standalone script prints each protection. Distribution packages are normally built with all of them on.

Snippet
pip install pwntools   # provides the checksec command
checksec --file=/usr/bin/ls
readelf -h ./prog | grep Type          # DYN means PIE
readelf -l ./prog | grep -A1 GNU_STACK  # RW (no E) means NX
readelf -d ./prog | grep -E 'BIND_NOW|FLAGS'

Lab: compile both ways

Compile one small C program twice: once with default hardening and once with protections disabled. Compare checksec output side by side. Then run the unprotected one under gdb with ASLR disabled and observe that addresses are the same on each run, while the PIE build with ASLR on moves each time. This shows why mitigations exist; it does not require writing any exploit.

Snippet
gcc -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -pie -fPIE -Wl,-z,relro,-z,now -o hardened prog.c
gcc -O0 -fno-stack-protector -no-pie -z execstack -o soft prog.c
checksec --file=hardened; checksec --file=soft
for i in 1 2 3; do ./hardened & sleep 0.2; grep hardened /proc/$!/maps | head -1; kill $!; done

The defensive takeaway

For software you ship, keep every mitigation on, use memory-safe languages where possible, and treat a missing protection in a build as a bug. Mitigations raise the cost of exploitation; they do not remove the underlying bug, so fixing memory safety issues still matters.

Diagram

Common binary mitigations

What each one protects.

  • NX

    Data regions are not executable.

  • PIE + ASLR

    Code loads at a random address each run.

  • Stack canary

    Detects overwrites of the saved return address.

  • Full RELRO

    Linking tables become read-only.

  • FORTIFY_SOURCE

    Bounds checks on some libc functions.

Check yourself

1. Which mitigation makes the stack non-executable?

2. What does a stack canary detect?

3. Do mitigations remove the need to fix memory safety bugs?