Chapter 32

C2 Traffic Analysis

C2 (Command and Control) channels have distinct traffic signatures: regular beacon intervals, specific URI patterns, distinctive TLS fingerprints, and unusual user agents. This chapter covers identifying C2 traffic in PCAPs using behavioral patterns, JA3 fingerprinting, and known framework signatures.

Scenario

Memory analysis identified a Cobalt Strike beacon process. The PCAP covers 72 hours. You need to find: when C2 communication started, what commands were sent (or infer them from timing), and whether any data was exfiltrated via the C2 channel. The beacon config says sleep=60 with 10% jitter — every 54-66 seconds the beacon should check in. Looking at the packet timestamps for connections to the known C2 IP confirms exactly this pattern: 847 connections over 72 hours, average interval 61.2 seconds.

Beacon Pattern Detection

Bashbeacon-detection.sh
PCAP="/cases/CASE-2026-009/network/filtered-suspect.pcap"
SUSPECT_IP="185.220.101.47"

# Extract all connection timestamps to the suspect IP
tshark -r $PCAP \
    -Y "ip.dst == $SUSPECT_IP && tcp.flags.syn == 1 && tcp.flags.ack == 0" \
    -T fields -e frame.time_epoch \
    | sort -n > /tmp/connection-times.txt

# Calculate inter-connection intervals (Python)
python3 << 'EOF'
import sys

times = [float(t) for t in open('/tmp/connection-times.txt').read().split()]
if len(times) < 2:
    print("Not enough connections")
    sys.exit()

intervals = [times[i+1] - times[i] for i in range(len(times)-1)]
avg = sum(intervals) / len(intervals)
stdev = (sum((x - avg)**2 for x in intervals) / len(intervals)) ** 0.5
low_cv = stdev / avg  # coefficient of variation — low = highly regular

print(f"Total connections: {len(times)}")
print(f"Average interval: {avg:.1f} seconds ({avg/60:.1f} minutes)")
print(f"Std deviation:    {stdev:.1f} seconds")
print(f"Min interval:     {min(intervals):.1f}s  Max: {max(intervals):.1f}s")
print(f"Coefficient of variation: {low_cv:.3f}")
print()
if low_cv < 0.15:
    print("HIGH CONFIDENCE BEACON: Very regular interval (CV < 0.15)")
    print(f"Possible sleep setting: {avg:.0f}s with ~{stdev/avg*100:.0f}% jitter")
elif low_cv < 0.30:
    print("MODERATE CONFIDENCE BEACON: Somewhat regular interval")
else:
    print("Low regularity — may not be beacon, or high jitter setting")
EOF

JA3/JA3S TLS Fingerprinting

JA3 is an MD5 hash of specific TLS ClientHello fields. Different C2 frameworks have distinctive JA3 values because they use different TLS libraries with different default cipher suite selections:

  JA3 Fingerprint Construction
  ═══════════════════════════════════════════════════════════════════

  JA3 input fields (from TLS ClientHello — all cleartext):
    TLS Version, Cipher Suites, Extensions, Elliptic Curves, EC Point Formats

  Concatenated as: "Version,CipherSuites,Extensions,Groups,PointFormats"
  MD5 hashed → 32-hex JA3 fingerprint

  Known C2 JA3 signatures (as of 2026):
    a0e9f5d64349fb13191bc781f81f42e1 → Cobalt Strike default profile
    e7d705a3286e19ea42f587b6a804f1f5 → Metasploit meterpreter
    72a589da586844d7f0818ce684948eea → Havoc C2
    (These rotate as operators change profiles — treat as starting hints)

  JA3S (server response fingerprint):
    Hash of TLS ServerHello fields
    Specific C2 servers have characteristic server-side TLS configurations

  Detection workflow:
    1. Extract all JA3 hashes from PCAP
    2. Compare against known bad + known good databases
    3. Investigate unusual JA3 hashes from internal hosts to external IPs
    4. Focus on JA3 hashes seen on many internal hosts → potential C2 spread
Bashja3-analysis.sh
PCAP="/cases/CASE-2026-009/network/full-capture.pcap"

# Extract JA3 fingerprints using zeek (formerly known as Bro)
# Zeek processes the PCAP and generates logs including ssl.log with ja3 fields
zeek -r $PCAP /opt/zeek/share/zeek/policy/protocols/ssl/ja3.zeek

# Extract JA3 hashes from zeek ssl.log
awk -F'\t' '{print $8, $9, $4}' ssl.log | sort | uniq -c | sort -rn | head -30
# Fields: ja3, ja3s, server_name

# Alternative: use ja3 Python package directly against PCAP
pip install pyshark
python3 << 'EOF'
import pyshark

cap = pyshark.FileCapture(
    '/cases/CASE-2026-009/network/full-capture.pcap',
    display_filter='tls.handshake.type == 1'
)

ja3_counts = {}
for pkt in cap:
    try:
        # ja3 is available if tshark has ja3 dissector enabled
        ja3 = pkt.tls.handshake_ja3
        dst = pkt.ip.dst
        sni = pkt.tls.handshake_extensions_server_name
        key = f"{ja3}|{dst}|{sni}"
        ja3_counts[key] = ja3_counts.get(key, 0) + 1
    except:
        pass

cap.close()
for k, v in sorted(ja3_counts.items(), key=lambda x: -x[1])[:20]:
    ja3, dst, sni = k.split("|")
    print(f"{v:4d}  JA3:{ja3}  dst:{dst}  SNI:{sni}")
EOF

Cobalt Strike HTTP C2 Signatures

IndicatorDefault valuetshark filter
Default URI patterns/ca, /dpixel, /____utm.gif, /jquery-3.3.1.min.jshttp.request.uri matches "(ca|dpixel|utm.gif)"
Default user agentMozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; 125LA; MS-RTC LM 8)http.user_agent contains "125LA"
Default HTTP header orderAccept, Host, Cookie (distinctive ordering)Visual inspection via Follow Stream
Beacon cookieEncrypted session data in Cookie header — base64-encoded ~80 charshttp.cookie matches "^[A-Za-z0-9+/]{70,100}={0,2}$"
HTTPS default profileDomain fronting-capable; server cert often self-signedCheck certificate issuer in TLS ServerHello
Mental Model: Attacker Profile Customization

Sophisticated attackers customize their Cobalt Strike profiles (Malleable C2 profiles) to change every observable indicator: URIs, headers, user agents, sleep intervals, and jitter. Default indicator lists miss these. The more reliable detection methods are: (1) JA3 hash — harder to change because it reflects the underlying TLS library, not just the configuration; (2) beacon interval pattern — the mathematical regularity of beaconing is behavioral and survives profile changes; (3) data size pattern — each beacon check-in has a characteristic payload size; (4) named pipe patterns (covered in Ch21 for memory analysis). Combine behavioral analysis with indicator-based analysis for better coverage.

Domain Fronting Detection

Domain fronting routes C2 traffic through a legitimate CDN (Cloudflare, AWS CloudFront, Azure CDN). The TLS SNI shows the front domain (legitimate), but the HTTP Host header shows the real C2 domain:

Bashdomain-fronting-detect.sh
PCAP="/cases/CASE-2026-009/network/full-capture.pcap"

# If traffic is decrypted (SSLKEYLOGFILE available):
# Compare TLS SNI (in ClientHello) vs HTTP Host header
# These should match for legitimate traffic
# Mismatch = domain fronting

tshark -r $PCAP \
    -Y "http.host && tls.handshake.extensions_server_name" \
    -T fields \
    -e ip.dst \
    -e tls.handshake.extensions_server_name \
    -e http.host | \
    awk '$2 != $3 && $2 != "" && $3 != ""' | \
    head -20
# SNI != Host header = domain fronting indicator

Q & A

Q: The PCAP shows connections to a Cloudflare IP — millions of legitimate sites use Cloudflare. How do you determine if it's C2 vs normal web traffic?

You can't determine from the IP alone — Cloudflare hosts millions of sites. The discriminators are: (1) SNI from TLS ClientHello: even through Cloudflare, the SNI field shows the destination domain. An unknown, recently-registered, or high-entropy domain behind Cloudflare is suspicious — benign sites have recognizable names. (2) Beacon pattern: calculate inter-connection intervals. Normal browser traffic is irregular (user-driven). C2 through Cloudflare still shows the regular beacon interval. (3) Initiating process: cross-reference the connection with NetBIOS/SMB logs or endpoint data to identify which process made the connection. If `explorer.exe` is making regular connections to a CDN (shouldn't make any), that's C2. (4) HTTP headers after decryption: if you have the session key log, check whether the HTTP Host header differs from the SNI (domain fronting). (5) Volume over time: C2 traffic is typically small and regular. A 60-byte HTTP GET every 60 seconds for 72 hours is not normal browser behavior — even to a CDN-fronted domain.