Step 187. Protections: NX, ASLR, Canary, PIE — A Map of the Four-Layer Defense
Level 3 — Pwn Track | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: you’ve finished Step 186. You’ve succeeded at the RET overwrite attack on a defenseless binary, and you remember the three options we turned off then (-fno-stack-protector, -z execstack, -no-pie).
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- What you need: a WSL Ubuntu terminal, gcc, gdb, readelf, and Step 186’s vuln.c. The measured environment is Ubuntu 24.04, gcc 13.3.0, x86-64.
- Caution: today is a [concept] chapter, but you verify the concepts with your eyes. You’ll compile the same source with different protection options and see for yourself at which gate Step 186’s attack gets blocked. For the ASLR-off experiment, we use only a process attribute (
setarch -R), never a system setting.
In Step 186 we deliberately stripped three defensive membranes and attacked. Today we meet those membranes’ true identities, one by one. Modern binaries come wrapped by default in a four-layer defense — NX, Canary, ASLR, PIE. Today we organize which link of Step 186’s attack each one cuts, and what bypass concepts attackers built in response. An attack’s first step is always reading "what is turned on in this binary."
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain the working principle of each of the four protections — NX, Canary, ASLR, PIE — in one sentence
- Determine a binary’s enabled protections yourself with readelf and nm
- Compile the same source with different options and observe and record the differences
- Experiment with how Step 186’s attack fails in front of each protection
- Describe, at the concept level, the bypass ideas (ROP, leak) that answer each protection
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C, WSL Ubuntu bash, gcc 13.3.0, gdb 15.1, binutils (readelf, nm) |
| Today’s commands/options | readelf -h (ELF type — PIE check), readelf -lW | grep GNU_STACK (NX check), nm | grep stack_chk (canary check), setarch -R (ASLR off for this process only), compile options -fstack-protector-strong, -no-pie, -z execstack |
| Concepts needed | NX bit, stack canary, Address Space Layout Randomization (ASLR), Position-Independent Executable (PIE), information leak, ROP |
2-1. NX — No Execution from the Stack
NX (No eXecute) is a hardware/OS-level rule: "bytes in data areas cannot be executed." The classic attack of planting shellcode (machine code to run) on the stack and jumping to it becomes impossible with this single device — because the stack can be read and written (RW) but not executed.
The attacker’s answer is ROP (Return-Oriented Programming). If you can’t plant new code, you line up on the stack the addresses of code fragments (gadgets) already inside the program and chain them like a ret relay. If NX is "no new cooking allowed," ROP is "cooking only with what’s in the fridge."
2-2. Canary — A Watchdog Value That Detects Overwriting
The stack canary is a random watchdog value the compiler plants between the buffer and RET. If this value has changed when the function ends, it judges "something overflowed" and makes the program halt itself. The true identity of the *<strong> stack smashing detected </strong>* you met in Step 62.
Since an overwrite must step over the canary to reach RET, the attack becomes a "safe death" instead of a "success." The bypass concept is a leak — first read the canary value out through another vulnerability (e.g., a format string bug), then overwrite while including that value as-is.
2-3. ASLR — The OS That Shuffles Addresses
ASLR (Address Space Layout Randomization) is an OS device that randomly shuffles the addresses of the stack, heap, and libraries on every run. Step 186’s attack assumed "we know win’s address" — when stack addresses change every time, jumping to what you planted on the stack gets hard.
The bypass concept is again a leak. If the running program prints an address even once (an information leak vulnerability), you learn the current coordinates of the shuffled map. That’s why real-world exploits often run in two stages: "leak → calculate → overwrite."
2-4. PIE — Shuffling Even the Code
PIE (Position-Independent Executable) is a compilation style that lets the program’s own code load at a different address every time. If ASLR shuffles the stack and libraries, PIE shuffles your code too. win’s address 0x401196, read with nm in Step 186, is no longer valid in a PIE binary — because the base changes every run.
The bypass concept is relative offsets. Inside a PIE binary, the distance (offset) between functions is fixed at compile time. If the current address of any one function leaks, distance arithmetic yields win’s current address. "Even if the whole map moves, distances inside the map don’t change."
2-5. How to Tell — Attacks Begin with Reconnaissance
In the field, a tool called checksec shows all four at once (you’ll meet it with pwntools in Step 188). Today we learn its inner workings — how to verify each item by hand with readelf and nm. Only when you know the basis of what the tool shows can you make the call without the tool.
3. Follow Along
3-1. Four Binaries — Same Source, Different Defenses
Compile Step 186’s vuln.c with four option sets.
gcc -g -O0 -fno-stack-protector -z execstack -no-pie vuln.c -o vuln_alloff
gcc -g -O0 vuln.c -o vuln_default
gcc -g -O0 -fstack-protector-strong -no-pie vuln.c -o vuln_canary
gcc -g -O0 -fno-stack-protector vuln.c -o vuln_pie
How to read it: the four differ like this.
| Binary | NX | Canary | PIE |
|---|---|---|---|
| vuln_alloff | off (-z execstack) | off | off (-no-pie) |
| vuln_default | on | on | on (Ubuntu gcc default) |
| vuln_canary | on | on | off |
| vuln_pie | on | off | on |
-fstack-protector-strong explicitly turns the canary on, and the fourth turns off only the canary and leaves the rest (including PIE) at defaults.
3-2. Detection Practice — readelf and nm
PIE check — look at the ELF header’s Type.
readelf -h vuln_alloff | grep Type
readelf -h vuln_default | grep Type
Type: EXEC (Executable file)
Type: DYN (Position-Independent Executable file)
(Measured 2026-09-09.)
How to read it: EXEC is a traditional fixed-address executable (PIE off); DYN + the "Position-Independent" label is PIE. Shared libraries also use the same letters DYN, so you must read the parenthetical description too.
NX check — look at the GNU_STACK permissions in the program headers.
readelf -lW vuln_alloff | grep GNU_STACK
readelf -lW vuln_default | grep GNU_STACK
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
(Measured 2026-09-09.)
How to read it: RWE — the final E means "executable." A binary made with -z execstack allows even execution on the stack. RW alone means NX is on. A one-letter difference decides whether a shellcode attack is viable.
Canary check — look for traces of the watchdog in the symbol table.
nm vuln_alloff | grep stack_chk # (no result)
nm vuln_canary | grep stack_chk
U __stack_chk_fail
(Measured 2026-09-09.)
How to read it: __stack_chk_fail — the function called when the canary check fails. This symbol being linked in (U means it’s imported from outside) means the compiler planted canary-checking code.
3-3. The Canary in the Flesh — The Watchdog Value Planted on the Stack
Open the canary-enabled binary in gdb and look at that watchdog value directly.
gdb -q ./vuln_canary
(gdb) b vuln
(gdb) r
(gdb) disas vuln
0x00000000004011eb <+12>: mov %fs:0x28,%rax
0x00000000004011f4 <+21>: mov %rax,-0x8(%rbp)
...
0x0000000000401274 <+149>: mov -0x8(%rbp),%rax
...
0x0000000000401281 <+162>: je 0x401288 <vuln+169>
0x0000000000401283 <+164>: call 0x401090 <__stack_chk_fail@plt>
0x0000000000401288 <+169>: leave
0x0000000000401289 <+170>: ret
(Measured 2026-09-09.)
How to read the output: the guard’s entire patrol route, planted by the compiler, is visible.
+12:%fs:0x28— fetch the watchdog value from the random-value store the OS prepares per process.+21: plant that value at -0x8(%rbp) — between the buffer and saved rbp.+149~+164: at the function’s end, re-read that slot’s value, compare it with the original, and call__stack_chk_failif different.
A few steps in, look at the actual value:
(gdb) x/gx $rbp-8
0x7fffffffe658: 0x11d39c12fef0ef00
(Measured 2026-09-09. The value is a random number that changes per run.)
How to read it: notice it ends in 00. Not a coincidence — string input functions like gets often stop writing at a 0x00 byte (null character), so making the first byte 0 makes it hard to "overwrite while keeping the canary intact." The delicacy of defense design is packed into a single byte.
3-4. Replaying the Attack — In Front of the Canary
Feed Step 186’s attack (padding 24 + win’s address) as-is into the canary-enabled binary.
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*24 + b"\x96\x11\x40\x00\x00\x00\x00\x00" + b"\n")' | ./vuln_canary
buf address: 0x7ffc885aef10
input: *** stack smashing detected ***: terminated
received value: AAAAAAAAAAAAAAAAAAAAAAAA@
Aborted (core dumped)
(Measured 2026-09-09. Exit code 134.)
How to read the output: win did not execute. The overflowing input stepped on the canary, and the check at the function’s end discovered it and halted the program. Step 186’s attack was blocked at the first gate. Aborted instead of FLAG — the look of a program "dying safely."
3-5. Replaying the Attack — In Front of PIE
This time, attack the PIE-only binary with the fixed address 0x401196.
nm vuln_pie | grep " win"
00000000000011a9 T win
(Measured 2026-09-09.)
How to read it: the address is not in the 0x40xxxx range but 0x11a9 — this is not a finished address but an offset within the file. A PIE binary loads wholesale at a random base address when run, and win’s real address becomes "base + 0x11a9." The base changes every run.
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*24 + b"\x96\x11\x40\x00\x00\x00\x00\x00" + b"\n")' | ./vuln_pie
buf address: 0x7ffcd55db6d0
input: received value: AAAAAAAAAAAAAAAAAAAAAAAA@
Segmentation fault (core dumped)
(Measured 2026-09-09.)
How to read the output: no FLAG. With no canary, the overwrite itself succeeded, but the 0x401196 written into RET was an address that meant nothing in this run. Overwriting works, but you don’t know the address of where to go — this is the situation PIE creates.
3-6. Observing ASLR — A Map That Changes Every Time
Run the same program three times and look only at buf’s address.
echo A | ./vuln_default | head -1 (×3)
buf address: 0x7ffc93280190
buf address: 0x7ffcf317f320
buf address: 0x7ffec50675f0
(Measured 2026-09-09.)
Same program, same input, yet the stack address differs every time. This is ASLR. For the experiment, turn ASLR off for this process only:
echo A | setarch -R ./vuln_default | head -1 (×3)
buf address: 0x7fffffffe670
buf address: 0x7fffffffe670
buf address: 0x7fffffffe670
(Measured 2026-09-09.)
How to read it: setarch -R doesn’t change a system setting; it’s a process attribute that turns off randomization for "this run only." Since addresses changing between experiments make observation hard, we use it for measurement. Same address all three times — the map is fixed. A real attacker is never given this fixity. Which is exactly why a leak is needed.
3-7. Summary — Where the Four Layers Intersect
Let’s settle today’s experiments in a table (measured 2026-09-09, combined).
| Protection | Check command | What "on" looks like | The link it cuts in Step 186’s attack | Bypass concept |
|---|---|---|---|---|
| NX | readelf -lW → GNU_STACK |
RW (no E) | "Execution" of code planted on the stack | ROP (recycling existing code fragments) |
| Canary | nm → __stack_chk_fail |
Symbol present | "Detection" of the overwrite — halts before ret | Leak the canary value, then include it as-is |
| ASLR | Run repeatedly, compare addresses | Different every time | "Not knowing" stack/library addresses | Address leak |
| PIE | readelf -h → Type |
DYN | "Not knowing" your own code’s addresses | Offset + base leak |
What the four devices aim at is ultimately one thing — making rip hijacking useless, whether RET gets overwritten, or the overwritten value is unknown, or the overwrite is detected the instant it happens.
4. Missions & Exercises
Mission — A Protection Detection Report
- Compile Step 186’s vuln.c five times: all off / canary only / PIE only / all on (default) / with just
-z execstackadded - For each of the five binaries, determine three things and make a table: Type (readelf -h), GNU_STACK permissions (readelf -lW), presence of __stack_chk_fail (nm)
- Feed Step 186’s attack payload into the canary-only binary and the PIE-only binary respectively, and record the results (message, exit code)
- Write the difference between the two failures in one sentence each — "the canary blocked it because ___, and PIE blocked it because ___"
- Run vuln_default three times each with and without setarch -R, record buf’s addresses, and show ASLR’s effect in numbers
- Answer at the end: "For an attacker to break through all four, what information/techniques do they need?" — three lines or more at the concept level
Exercises
Exercise 1. In a binary with NX on, explain in terms of memory permissions why the classic shellcode attack (plant machine code on the stack and jump) fails.
Exercise 2. Why is the canary’s first byte 0x00? Against what kind of input functions is it a defense?
Exercise 3. The value 0x11a9 that nm shows for a PIE binary is not a finished address. Then what is it, and how is win’s real address computed at runtime?
Exercise 4. Explain why a "leak-free exploit" is hard against the ASLR+PIE combination, connecting it to Step 186’s premise ("we know win’s address").
5. Model Answers & Completion Criteria
Mission Model Answer
An example detection report (2026-09-09, Ubuntu 24.04, gcc 13.3.0):
[detection table]
Type GNU_STACK __stack_chk_fail
alloff EXEC RWE absent
canary only EXEC RW present
PIE only DYN RW absent
default(all) DYN RW present
execstack DYN RWE present
[attack experiments]
canary only: *** stack smashing detected ***: terminated / Aborted (exit code 134)
PIE only: Segmentation fault (exit code 139) — no FLAG output
[one-sentence conclusions]
The canary blocked it "because it detected the overwriting act and halted execution before ret,"
and PIE blocked it "because the written address (0x401196) was an address that didn't exist in this run."
[ASLR numbers]
normal runs: 0x7ffc93280190 / 0x7ffcf317f320 / 0x7ffec50675f0 (all different)
setarch -R: 0x7fffffffe670 ×3 (all same)
[sample final answer]
You need a separate vulnerability that can leak the canary value and valid
addresses (the base), and since NX prevents planting new code to execute,
you need the technique of chaining existing code fragments with ROP. In other
words, the modern exploit becomes a combination of "information leak + address
calculation + ROP chain."
How to verify: ① did the five rows of the detection table come from actual command output? ② are the two attack failures distinguished by message and exit code (134 = canary, 139 = segfault)? ③ are all six addresses from the ASLR experiment recorded?
Exercise Answers
Answer 1. NX restricts the stack memory’s permissions to read/write (RW) and removes execute (X). Shellcode can be "written" to the stack, but the moment ret jumps there and tries to "execute" it, the CPU refuses and a segfault occurs. The hardware guards the boundary between data and code.
Answer 2. String-based input functions like gets, strcpy, and scanf %s often recognize 0x00 (the null character) as the end of a string and stop writing or can’t include it. If the canary’s first byte is 0, an attacker trying to overwrite "while keeping the canary intact" can’t even write the first byte, making bypass hard. It’s a first-line defense design against string-function-class attacks.
Answer 3. It’s an offset within the file (a relative position). A PIE binary loads wholesale at a random base address chosen by the OS when run, and win’s real address at runtime becomes "base + 0x11a9." The offset is fixed but the base changes every run, so without knowing the base you can’t know the absolute address.
Answer 4. Half of Step 186’s attack was "the technique of overwriting," and the other half was "knowing the value to write (0x401196)." ASLR and PIE neutralize exactly that "knowing the value" — addresses change every run, so an address obtained by static analysis (nm) is void at runtime. That’s why you need an information leak vulnerability where the running program spills an address, and why most modern pwn problems are built in two stages: "leak + overwrite."
Completion Criteria Checklist
- [ ] I can explain NX, Canary, ASLR, and PIE in one sentence each
- [ ] I can detect PIE from readelf -h’s Type (EXEC/DYN)
- [ ] I can detect NX from GNU_STACK’s RW/RWE
- [ ] I can detect the canary from nm’s __stack_chk_fail symbol
- [ ] I confirmed the assembly route of the canary being planted and checked, via disas
- [ ] I observed Step 186’s attack failing differently in front of the canary and PIE
- [ ] I cross-checked ASLR’s effect in numbers with setarch -R
- [ ] Mission: I completed the five-binary detection report
6. Common Pitfalls & Fixes
Wall 1. It’s a default compile but the canary shows as absent
Symptom: you compiled with no options but nm shows no __stack_chk_fail.
Cause: gcc’s default canary (-fstack-protector-strong) plants only in functions it judges "dangerous." Functions without arrays get none.
Fix: run detection experiments on an obviously dangerous function like vuln.c, which has a buf array and gets. If it’s still absent, -fstack-protector-all plants it in every function.
Wall 2. It’s DYN but not PIE?
Symptom: you wrote down "PIE" because the readelf Type was DYN, and now you’re confused.
Cause: shared libraries (.so) also have Type DYN. Type alone isn’t enough.
Fix: read the parenthetical description — DYN (Position-Independent Executable file) is a PIE executable; DYN (Shared object file) is a library (per the measured output of 2026-09-09).
Wall 3. Addresses differ even with setarch -R
Symptom: you ran with setarch -R but the address differs from the book’s (0x7fffffffe670).
Cause: even with ASLR off, the stack’s starting point shifts slightly with the number of environment variables, path length, and whether you’re inside gdb. Inside gdb, gdb itself alters the environment.
Fix: the point of this experiment is not absolute values but agreement across repeated runs. Three identical results in your environment is enough.
Wall 4. I attacked the canary binary but got a segfault, not stack smashing
Symptom: you attacked the canary binary but got Segmentation fault instead of Aborted.
Cause: the input skipped the canary and overwrote something else (a pointer variable, etc.), or the payload never reached the canary and died by another route. Compilers also sometimes reorder variables.
Fix: stop in gdb and check the canary slot with x/gx $rbp-8 against how far your payload reached. Whatever the death, reading the scene reveals the cause.
Wall 5. Protection names and compile options get mixed up
Symptom: you can’t keep straight which of -no-pie, -z execstack, -fno-stack-protector turns off what.
Cause: the three are switches at different layers (linker options vs. code generation options), so the naming rules differ.
Fix: just connect them like this. -fno-stack-protector → canary off, -z execstack → NX off, -no-pie → PIE off. The principle: "off-switches are lab-only; defaults for deployment."
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| NX | The stack is readable and writable but not executable — shellcode blocked, the backdrop of ROP’s birth |
| Canary | A random watchdog value between buffer and RET — overwrite it and the program self-destructs before ret |
| ASLR | An OS device shuffling stack/heap/library addresses every run |
| PIE | Even your own code loads at a random base — only offsets stay fixed |
| leak | Spilling addresses/values at runtime to neutralize randomization — the starting point of bypass |
| ROP | An attack technique chaining existing code fragments (gadgets) without new code |
Today’s Commands
| Command | What it does |
|---|---|
readelf -h binary |
Check ELF type — PIE detection (EXEC/DYN) |
readelf -lW binary |
Check GNU_STACK permissions — NX detection (RW/RWE) |
nm binary | grep stack_chk |
Check the canary symbol |
setarch -R ./program |
ASLR off for this run only (for measurement) |
gcc -fstack-protector-strong |
Explicitly enable the canary |
An Instinct More Important Than Commands
Today’s settlement must be sober. The four membranes don’t make attacks "impossible." They make them "expensive." The canary demands a leak, ASLR and PIE demand an address leak, and NX demands the elaborate handiwork of ROP. Defense’s victory condition is not perfection but driving up the attacker’s cost — a mindset that runs through all of security design.
And remember the attacker’s first step. Before analyzing code, you read the binary’s defensive membranes. Because "what is turned on" is itself "what strategy is needed." pwntools’ checksec, coming in the next chapter, is the tool that does that reconnaissance in one line — and today you’ve verified the basis of that one line, all of it, by hand.
Once every box is checked, Step 187 is complete. Click the checkbox in the sidebar to save your progress.