guides / check-binary-hardening scenariosupdated 2026-10-25 readelf -h ./app

How to check PIE, RELRO, NX and stack canaries without installing checksec

2026-10-25 By luckyray Debian 12 · Ubuntu 24.04 · Alpine 3.20

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:

OutputMeaning
GNU_STACK ... RWno execute bit → NX enabled
GNU_STACK ... RWEexecutable stack → NX disabled for the stack
no GNU_STACK linelinker 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_STACK because 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, so readelf -s finds __stack_chk_fail — but if the binary is fully static and stripped, the symbol may be local and unreachable via -s. Use strings ./app | grep stack_chk_fail as a fallback.
  • Kernel-level protections are not in the file. ASLR (randomize_va_space), mmap_min_addr, yama/ptrace_scope and 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.

Related

guides
  1. How to tell whether a binary is stripped (and what it costs you) What strip removes, what it leaves behind, how to confirm it with file and readelf, and how to get usable stack traces back from a stripped production binary.
  2. How to check whether a binary is 32-bit or 64-bit (and for which CPU) Four commands that tell you the word size, instruction set and ABI of any executable — plus why file's one-line answer is sometimes all you need.
  3. cannot execute binary file: Exec format error — causes and fixes Why Linux refuses to run a binary with Exec format error, how to confirm the cause in three commands, and the five fixes that actually apply.