How to check PIE, RELRO, NX and stack canaries without installing checksec
checksec is a convenience wrapper around readelf. On a locked-down host you cannot install it, and
on a suspicious binary you may not want to run a Python script against it anyway. The underlying
checks take five commands.
The five checks
# 1. PIE and type
readelf -h ./app | grep -E 'Type:'
# 2. NX: is the stack executable?
readelf -lW ./app | grep -E 'GNU_STACK'
# 3. RELRO: is there a GNU_RELRO segment, and is binding immediate?
readelf -lW ./app | grep -E 'GNU_RELRO'
readelf -dW ./app | grep -E 'BIND_NOW|FLAGS'
# 4. Stack canary: does the binary reference the canary handler?
readelf -sW ./app | grep -c '__stack_chk_fail'
# 5. FORTIFY: how many checked libc wrappers are in use?
readelf -sW ./app | grep -cE '__[a-z_]+_chk'
A hardened build looks like this:
Type: DYN (Position-Independent Executable file)
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x0b8e10 0x00000000000b9e10 0x00000000000b9e10 0x0001f0 0x0001f0 R 0x1
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
Reading each one honestly
PIE — position independent executable
e_type == ET_DYN is necessary but not sufficient: shared libraries are also ET_DYN. An
executable is PIE when it is ET_DYN and it has either DF_1_PIE in DT_FLAGS_1 or a
PT_INTERP segment.
$ readelf -dW ./app | grep FLAGS_1
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
What it buys: the whole image can be mapped at a randomised base address, so code addresses vary per run. It does not randomise the heap, the stack or library layout on its own — those are kernel and loader jobs that happen regardless.
NX — non-executable stack
GNU_STACK records the stack’s required permissions:
| Output | Meaning |
|---|---|
GNU_STACK ... RW | no execute bit → NX enabled |
GNU_STACK ... RWE | executable stack → NX disabled for the stack |
no GNU_STACK line | linker did not emit one; the kernel applies its default |
That third case is why the ELF Inspector reports “unknown” rather than “enabled” when the segment is absent. A static analysis cannot assert what an unstated default will be on the target kernel.
RELRO — relocations read-only
GNU_RELRO marks a region that becomes read-only after relocation processing. Partial RELRO
still leaves the GOT writable until the first lazy binding; full RELRO forces all bindings at
startup and then protects the whole GOT.
Full RELRO requires both the segment and immediate binding:
$ readelf -lW ./app | grep GNU_RELRO # segment present
$ readelf -dW ./app | grep -E 'BIND_NOW|FLAGS'
0x0000000000000018 (BIND_NOW)
Equivalent signals: DT_BIND_NOW, DF_BIND_NOW in DT_FLAGS, or NOW in DT_FLAGS_1.
Linking with -Wl,-z,relro,-z,now produces all of them.
Stack canary — a symbol, not a proof
__stack_chk_fail appearing in the symbol table proves the compiler was invoked with
-fstack-protector (or a distro hardening spec). It does not prove every function is protected:
the default -fstack-protector only instruments functions with arrays or alloca, and
-fstack-protector-strong broadens but does not universalise it.
An executable with zero references to __stack_chk_fail is worth asking about. One with many is
reassuring, not conclusive.
FORTIFY — checked wrappers
__memcpy_chk, __printf_chk, __snprintf_chk and friends appear when the code was built with
-D_FORTIFY_SOURCE=2 (or 3) at an optimisation level above -O0. Counting them is a crude but
useful signal: a binary with zero _chk symbols was almost certainly built without it.
Where these checks mislead
- Go binaries are static, non-PIE by default, and have no
GNU_STACKbecause they manage their own goroutine stacks. A checksec report will show a wall of red for a binary that is memory-safe by construction. Rust binaries are similar for canaries. - Static binaries have no
PT_INTERP, so “PIE” heuristics that rely on the interpreter path report wrongly. Static-PIE exists but is rare. - Stripped binaries still carry
.dynsym, soreadelf -sfinds__stack_chk_fail— but if the binary is fully static and stripped, the symbol may be local and unreachable via-s. Usestrings ./app | grep stack_chk_failas a fallback. - Kernel-level protections are not in the file. ASLR (
randomize_va_space),mmap_min_addr,yama/ptrace_scopeand seccomp profiles are host configuration. A perfect ELF on a host with ASLR disabled is not protected. - No flag here speaks to code quality. Most exploited vulnerabilities are logic errors with every mitigation enabled.
Doing it in a browser
The ELF Inspector runs all five checks and prints them in one block, with the rule for each verdict documented on the methodology page so you can reproduce the result with the commands above. That matters when the answer is going into a report: a reviewer will ask how you derived “full RELRO”, and the answer should be a command, not a tool name.
Related: is the binary stripped — stripping is orthogonal to hardening, and confusing the two is common. And static vs dynamic linking for why the static case makes several of these checks meaningless.
Every command in this guide was run against the distribution named in the output. If your output differs, the mechanism section explains why — and the ELF Inspector shows the same fields without needing a shell.