Linux binary hardening is not a contest to collect compiler flags. Each mechanism cuts one part of an exploit chain: a Canary detects some stack-frame corruption, NX blocks instruction execution from the stack, PIE lets ASLR move the main executable, and RELRO narrows writable relocation state.
This article focuses on modern GNU/Linux user-space programs. The goal is not to memorize names. It is to build one sample and verify which protections reached the final ELF file.
Start with the boundary of each protection
| Mechanism | Primary job | What it does not replace |
|---|---|---|
| Stack Canary | Detect some stack-frame overwrites before return | Does not stop the write or protect every function pointer |
_FORTIFY_SOURCE |
Add compile-time or runtime checks to some libc buffer operations | Depends on optimization, object-size knowledge, and supported functions |
| NX / No-exec stack | Prevent instruction execution from stack pages | Does not stop ROP, ret2libc, or data corruption |
| Stack Clash Protection | Probe large stack allocations page by page instead of skipping the Guard Page | Does not detect ordinary buffer overruns |
| ASLR + PIE | Randomize the main executable, shared libraries, stack, and other regions | Address disclosures can weaken randomization |
| Full RELRO | Reduce writable GOT and relocation state after initialization | Does not protect the heap, ordinary globals, or application callbacks |
| CET / Shadow Stack | Protect return-flow state on supported platforms | Requires compatible CPU, kernel, loader, and binaries |
These layers belong together, but none replaces bounds checks, memory-safe APIs, fuzzing, or code review.
Prepare a reproducible sample
Save this as overflow.c:
#include <stdio.h>
#include <string.h>
static void copy_input(const char *input) {
char buffer[32];
strcpy(buffer, input);
puts(buffer);
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <text>\n", argv[0]);
return 2;
}
copy_input(argv[1]);
return 0;
}
The strcpy call is intentionally unsafe. Build and run this sample only in an isolated lab; never ship it in a service.
Build a comparison binary with common protections disabled:
gcc -O0 -g \
-fno-stack-protector \
-U_FORTIFY_SOURCE \
-fno-pie -no-pie \
-Wl,-z,norelro,-z,execstack \
overflow.c -o overflow-weak
Build an explicitly hardened binary:
gcc -O2 -g \
-fstack-protector-strong \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
-fstack-clash-protection \
-fPIE -pie \
-Wl,-z,relro,-z,now,-z,noexecstack \
overflow.c -o overflow-hardened
Recent GCC versions also provide -fhardened on GNU/Linux to enable a production-oriented set of hardening options:
gcc --help=hardened
gcc -O2 -g -fhardened overflow.c -o overflow-auto
The exact -fhardened option set can change with GCC releases. Record the compiler version and full link command; “hardened enabled” is not enough for a release audit.
1. Stack Canary: check the frame before return
-fstack-protector-strong covers functions with risk signals such as local arrays or references to local frame addresses. It does not mean “every function with a local variable.” Use -fstack-protector-all only when every function must be instrumented.
Inspect the result:
readelf -Ws ./overflow-hardened | grep __stack_chk_fail
objdump -d ./overflow-hardened | sed -n '/<copy_input>/,+30p'
TLS registers and offsets differ by architecture. Do not treat %gs:0x14 or %fs:0x28 as a portable rule. Stable evidence is the function prologue saving a Canary, the epilogue comparing it, and failure calling __stack_chk_fail.
Test the Canary in isolation:
gcc -O0 -g \
-fstack-protector-strong \
-U_FORTIFY_SOURCE \
-fno-pie -no-pie \
overflow.c -o overflow-canary
set +e
./overflow-canary "$(printf 'A%.0s' {1..128})"
test $? -ne 0
A non-zero exit proves detection, not remediation. An attacker may still continue after leaking the Canary or corrupting other control data before the function returns.
2. _FORTIFY_SOURCE: use object sizes known by the compiler
With optimization enabled, glibc Fortify wrappers can transform some strcpy, memcpy, sprintf, and similar calls into checked variants. Inspect the dynamic symbols:
readelf -Ws ./overflow-hardened | grep -E '__.*_chk|__fortify_fail'
Whether a _chk call appears depends on optimization and whether the object size can be derived. No matching symbol does not automatically prove a configuration failure; inspect compiler diagnostics and disassembly as well.
_FORTIFY_SOURCE is not general memory safety. It may not help when the compiler cannot determine the destination size, code bypasses libc, or corruption occurs inside a custom data structure.
3. NX and Stack Clash Protection solve different problems
The linker describes stack permissions through PT_GNU_STACK:
readelf -lW ./overflow-weak | grep GNU_STACK
readelf -lW ./overflow-hardened | grep GNU_STACK
The hardened binary should show a readable and writable, but non-executable, stack—typically RW, not RWE. NX blocks direct execution of stack-resident shellcode; it does not stop attackers from reusing existing executable code.
-fstack-clash-protection addresses a separate issue. It probes large stack allocations page by page so an allocation cannot jump over the kernel Guard Page. It does not check an ordinary overrun such as buffer[32], and it does not replace a Canary.
4. ASLR and PIE: system policy and binary layout both matter
Common Linux randomize_va_space values are:
0: disable address-space randomization.1: randomize themmapbase, stack, VDSO, and other regions.2: add heap randomization to level 1; this is commonly the default.
Check the system setting:
cat /proc/sys/kernel/randomize_va_space
Then check whether the main executable is PIE:
file ./overflow-hardened
readelf -hW ./overflow-hardened | grep 'Type:'
readelf -lW ./overflow-hardened | grep INTERP
PIE commonly appears as ET_DYN, but shared libraries are also ET_DYN. Do not conclude “PIE” from Type: DYN alone; combine file, PT_INTERP, and the executable entry point.
5. RELRO: require both GNU_RELRO and BIND_NOW
Full RELRO needs two conditions: a GNU_RELRO Segment, plus dynamic binding during initialization.
readelf -lW ./overflow-hardened | grep GNU_RELRO
readelf -dW ./overflow-hardened | grep -E 'BIND_NOW|FLAGS.*NOW'
GNU_RELROalone usually proves only Partial RELRO.GNU_RELROplusBIND_NOWorNOWis the common evidence for Full RELRO.dlopenanddlsymstill work; Full RELRO does not ban runtime library loading.
Immediate binding can increase startup work and expose missing symbols earlier. Treat that as a release test, not as a default reason to disable Full RELRO.
6. CET and Shadow Stack: build properties are not runtime proof
x86 GNU/Linux can use -fcf-protection to emit control-flow protection instructions and ELF properties:
gcc -O2 -fcf-protection=full overflow.c -o overflow-cet
readelf -nW ./overflow-cet | grep -E 'IBT|SHSTK'
An IBT or SHSTK property only shows that the build declares capability. Execution still depends on the CPU, kernel, dynamic loader, and compatibility of loaded shared objects.
Linux runtime checks include:
grep -w user_shstk /proc/cpuinfo
grep '^x86_Thread_features' /proc/$$/status
grep '^x86_Thread_features_locked' /proc/$$/status
Do not infer CET status from a CPU release year or cloud instance name. Inspect the target node and target process.
7. A five-minute artifact check
binary=${1:?usage: check-hardening.sh /path/to/binary}
file "$binary"
readelf -hW "$binary" | grep 'Type:'
readelf -lW "$binary" | grep -E 'INTERP|GNU_STACK|GNU_RELRO'
readelf -dW "$binary" | grep -E 'NEEDED|BIND_NOW|FLAGS.*NOW'
readelf -Ws "$binary" | grep -E '__stack_chk_fail|__.*_chk' || true
readelf -nW "$binary" | grep -E 'IBT|SHSTK' || true
These commands are an audit entry point, not a complete scanner. Static linking, stripped symbols, LTO, and different libc implementations can change the output. When evidence is ambiguous, return to the link command, linker Map file, and disassembly.
Production build checklist
- Prefer
-fhardenedwhen supported, and record the output ofgcc --help=hardened. - Otherwise enable
-fstack-protector-strong, PIE, Full RELRO, No-exec stack, and Stack Clash Protection explicitly. - Use
-Wformat -Werror=format-securityin CI to reject statically detectable format-string mistakes. - Use AddressSanitizer, UndefinedBehaviorSanitizer, and fuzzing in test builds; do not treat Sanitizers as production hardening.
- Inspect the final binary inside the release image. A Makefile or base-image claim is not artifact evidence.
- Fix the out-of-bounds write. Hardening lowers exploit reliability and narrows the failure window; it does not make memory corruption acceptable.