Introduction / Overview
Disclaimer: This article is for education and authorized security testing only. Write, run, and analyze shellcode exclusively in an isolated lab you own or have explicit written permission to test. Executing payloads against systems you do not control is illegal in most jurisdictions.
Shellcode is the rawest form of an exploit payload: a self-contained blob of machine code with no headers, no imports, and no loader to lean on. To run anywhere it lands, it must be position independent and resolve its own dependencies at runtime. On Windows, that means two classic techniques every analyst should understand cold: walking the PEB to find kernel32.dll, and API hashing to locate functions without storing readable strings.
This walkthrough builds a small x64 payload that pops MessageBoxA, explains every trick it uses, and then flips to the blue-team side: how to emulate, debug, and detect this code.
How it works / Background
A normal PE relies on the loader to populate the Import Address Table. Shellcode gets none of that, so it bootstraps itself:
- Find the PEB. On x64 the Process Environment Block is always reachable via the
GSsegment register at offset0x60(GS:[0x30]on x86 viaFS). The PEB is process-relative, so no hardcoded address is needed. - Walk the loader lists.
PEB->Ldr->InMemoryOrderModuleListis a doubly linked list of loaded modules.ntdll.dllandkernel32.dllare present in every user-mode process, so the shellcode walks the list to grabkernel32's base address. - Parse the export table. From the base address it reads the PE header, finds the
IMAGE_EXPORT_DIRECTORY, and iteratesAddressOfNames. - API hashing. Instead of comparing against strings like
"LoadLibraryA"(which AV/EDR signature on, and which bloat the payload), it hashes each export name with a small rolling hash (ROR-13 add is the canonical one popularized by Metasploit'sblock_api) and compares against a precomputed 4-byte constant. A match yields the function RVA, which is added to the base to get a callable pointer.
With LoadLibraryA and GetProcAddress resolved this way, the shellcode can pull in anything else (user32!MessageBoxA, ws2_32 sockets for a reverse shell, etc.).
Prerequisites / Lab setup
Use a disposable Windows VM (or a Linux box for assembly/emulation) with no network bridge to anything you care about.
- NASM to assemble:
apt install nasm/choco install nasm - scdbg (from David Zimmer / sandsprite) — a libemu-based shellcode emulator
- x64dbg for live debugging on Windows
- Ghidra or radare2 for static analysis
- Optionally Metasploit to compare against a known-good
msfvenompayload
A quick reference payload for comparison:
msfvenom -p windows/x64/messagebox TEXT="lab" -f raw -o msgbox.bin
# Inspect length and bytes
ls -l msgbox.bin
xxd msgbox.bin | headBashWalkthrough / PoC (step by step)
1. PEB walk + hashed kernel32 resolution (x64 NASM skeleton)
The snippet below is the core bootstrap. It is position independent: no absolute addresses, only register-relative access. Comments mark each stage.
bits 64
default rel
start:
mov rax, gs:[0x60] ; RAX = PEB
mov rax, [rax + 0x18] ; PEB->Ldr
mov rsi, [rax + 0x20] ; Ldr->InMemoryOrderModuleList (first entry)
next_module:
mov rbx, [rsi + 0x20] ; module DllBase
mov rdi, [rsi + 0x50] ; BaseDllName.Buffer (UNICODE)
mov rsi, [rsi] ; follow Flink to next module
test rbx, rbx
jz not_found
; (compare hashed module name to find kernel32, then parse exports)
; ... export-table walk + ROR13 hash compare omitted for brevity ...ASMThe full version parses IMAGE_DOS_HEADER -> IMAGE_NT_HEADERS -> Export Directory, iterates AddressOfNames, computes the ROR-13 hash of each ASCII name, and matches against constants such as LoadLibraryA = 0xEC0E4E8E.
Assemble and extract raw bytes:
nasm -f bin shellcode.asm -o shellcode.bin
xxd -i shellcode.bin | head # C array form for a test harnessBash2. Emulate without executing: scdbg
scdbg runs the bytes against an emulated Windows environment and logs every API call — the fastest, safest first look:
scdbg /f shellcode.bin # auto-find entry, emulate
scdbg /f shellcode.bin /s -1 # try every offset as an entry point
scdbg /f shellcode.bin /api /foff 0 # verbose API logging from offset 0BashExpected output names the resolved APIs in call order, e.g.:
LoadLibraryA(user32)
MessageBoxA(text="lab", caption=..., type=0)PlaintextThis is exactly how you confirm what an unknown blob does before ever letting it touch a real CPU.
3. Live debugging with a harness
To debug on Windows, drop the bytes into a tiny loader that marks the buffer executable and jumps to it:
// loader.c (build: cl loader.c) — LAB ONLY
#include <windows.h>
unsigned char sc[] = { /* xxd -i output here */ };
int main(void) {
void *m = VirtualAlloc(0, sizeof sc, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(m, sc, sizeof sc);
((void(*)())m)();
return 0;
}COpen the compiled binary in x64dbg, set a breakpoint on VirtualAlloc's return, then single-step into the buffer to watch the PEB walk (gs:[0x60]) and the export-table loop resolve hashes in real time.
Mermaid diagram

The diagram shows the runtime bootstrap: from the GS-relative PEB read, through the module-list walk and export parsing, to hash-matched API resolution and the final payload call.
Detection & Defense (Blue Team)
Shellcode behavior is noisy if you know where to look. Weight these defenses as seriously as the offense above.
Static and emulation triage
- Treat any executable buffer reached via
gs:[0x60]/fs:[0x30]access plus an export-table walk as high-confidence shellcode. Run unknown blobs throughscdbg /f file.bin /s -1in a sandbox before anything else. - Hunt for API hashing: the ROR-13 constant table and the
0x0Drotate are well-known signatures. YARA rules from Florian Roth and theblock_apiMetasploit stub catch a large share of real-world payloads.
Runtime / EDR detections (map to MITRE ATT&CK)
- T1055 Process Injection and T1620 Reflective Code Loading: alert on
VirtualAlloc/VirtualProtectcreatingPAGE_EXECUTE_READWRITEregions, especially in non-image, private memory. - Memory scanning: tools like
Moneta,PE-sieve, andHollows Hunterflag executable private regions with no backing module — the hallmark of in-memory shellcode. - ETW + call-stack telemetry: a
kernel32!MessageBoxA(orWS2_32connect) whose return address points into unbacked memory is a strong injection signal. Modern EDRs flag unbacked-stack-frame syscalls.
Hardening
- Enable CFG (Control Flow Guard), CET shadow stacks, and ACG/Arbitrary Code Guard where supported to block jumps into unsigned RWX memory.
- Apply Windows Defender Exploit Guard and Attack Surface Reduction (ASR) rules (e.g., "Block executable code from being created by Office/scripts") to cut common shellcode delivery paths.
- Deny W^X: minimize processes that allocate RWX. Many legitimate apps never need it, so RWX allocation is itself a useful hunting predicate.
For deeper static workflows see Ghidra for malware analysis and reading PE export tables. For the injection side, see process injection techniques.
Conclusion
Position-independent Windows shellcode is a compact lesson in the whole Windows loader: PEB-relative addressing makes it relocatable, list walking finds kernel32 without imports, and API hashing hides intent from naive string scanners. The same structure that makes it powerful also makes it detectable — RWX allocations, unbacked call stacks, and the ROR-13 hash signature are reliable tells. Understanding both halves is what separates an analyst from a script user.
References
- MITRE ATT&CK — T1055 Process Injection: https://attack.mitre.org/techniques/T1055/
- MITRE ATT&CK — T1620 Reflective Code Loading: https://attack.mitre.org/techniques/T1620/
- scdbg (sandsprite): http://sandsprite.com/blogs/index.php?uid=7&pid=152
- HackTricks — Windows shellcode & PEB: https://book.hacktricks.xyz/
- Microsoft Docs — PEB structure: https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb
- PE-sieve / Hollows Hunter (hasherezade): https://github.com/hasherezade/pe-sieve
- Metasploit
block_apistub: https://github.com/rapid7/metasploit-framework
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments