QUIC-Based C2
QUIC-based C2 is an emerging evasion technique that leverages the QUIC protocol (HTTP/3 transport, UDP 443) to blend C2 traffic with legitimate web browsing. Because QUIC is UDP-based, it bypasses many TCP-focused network monitoring tools. QUIC's TLS 1.3 is integrated directly into the protocol, making decryption harder without endpoint access. As Chrome, Firefox, and major web services increasingly adopt HTTP/3 over QUIC, C2 traffic hidden in QUIC becomes progressively harder to distinguish from normal traffic.
A new implant variant uses a Go-based QUIC library to communicate C2 data on UDP 443. Your existing detection stack — which inspects TCP flows — completely misses it because QUIC is UDP. Your Zeek instance doesn't have the QUIC package installed. tshark shows UDP datagrams to port 443 but doesn't decode the QUIC structure. You need to retrofit your detection stack to handle QUIC traffic.
QUIC Forensics Fundamentals
QUIC Packet Structure — What's Visible Without Decryption
═══════════════════════════════════════════════════════════════════
QUIC runs over UDP (port 443 or arbitrary)
Initial Packets (unencrypted connection establishment):
├── QUIC version (e.g., 0x00000001 = QUIC v1 RFC 9000)
├── Destination Connection ID (variable length, 0-20 bytes)
├── Source Connection ID (variable length)
└── CRYPTO frames containing TLS 1.3 ClientHello
└── SNI visible here, same as TCP TLS
Cipher suites, extensions, ALPN ("h3") all visible
After Initial: Handshake + 1-RTT packets (partially/fully encrypted)
├── Handshake packets: encrypted with handshake keys
├── 1-RTT packets: encrypted with 1-RTT keys (application data)
└── QUIC version, connection IDs remain in packet header (plaintext)
Key forensic metadata (without decryption):
├── UDP src/dst port (typically 443)
├── QUIC version (version 1 = RFC QUIC, version 0 = GQUIC/legacy)
├── Connection IDs (can track sessions across IP changes)
├── SNI from Initial packet CRYPTO frame (ClientHello)
├── ALPN from ClientHello ("h3" = HTTP/3, custom = suspicious)
└── Packet sizes and timing (behavioral analysis still works)
QUIC C2 indicators:
├── ALPN not "h3" (legitimate web browsers always use h3 or h2)
├── SNI absent or custom string (C2 implants often omit SNI)
├── Non-standard QUIC version (custom implementation)
└── Beaconing on QUIC (same CV-based analysis as TCP)
QUIC Traffic Analysis
#!/bin/bash
PCAP="$1"
echo "=== QUIC connections (UDP 443) ==="
tshark -r "$PCAP" -n -Y "quic" -T fields \
-E separator="\t" \
-e ip.src -e ip.dst -e udp.dstport -e quic.version \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== QUIC SNI (from ClientHello CRYPTO frames) ==="
tshark -r "$PCAP" -n -Y "quic.tls.handshake.extensions_server_name" -T fields \
-E separator="\t" \
-e ip.src -e ip.dst \
-e quic.tls.handshake.extensions_server_name \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== QUIC ALPN (h3=legit, other=suspicious) ==="
tshark -r "$PCAP" -n -Y "quic.tls.handshake.extensions_alpn_str" -T fields \
-E separator="\t" \
-e ip.src -e ip.dst \
-e quic.tls.handshake.extensions_alpn_str \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== QUIC connections without SNI (C2 indicator) ==="
tshark -r "$PCAP" -n \
-Y "quic.version and not quic.tls.handshake.extensions_server_name" \
-T fields \
-E separator="\t" \
-e ip.src -e ip.dst -e udp.dstport \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== Non-standard QUIC versions ==="
# Standard QUIC v1 = 0x00000001, GQUIC variations = 0x51xxxxxx
tshark -r "$PCAP" -n -Y "quic.version != 0x00000001" -T fields \
-E separator="\t" \
-e ip.src -e ip.dst -e quic.version \
| sort | uniq -c | sort -rn | head -10
echo ""
echo "=== QUIC beaconing detection ==="
tshark -r "$PCAP" -n -Y "quic.version" -T fields \
-E separator="\t" \
-e frame.time_epoch -e ip.src -e ip.dst \
| python3 -c "
import sys, math
from collections import defaultdict
timestamps = defaultdict(list)
for line in sys.stdin:
parts = line.strip().split('\t')
if len(parts) >= 3:
try:
ts, src, dst = float(parts[0]), parts[1], parts[2]
timestamps[(src, dst)].append(ts)
except:
pass
for (src, dst), ts_list in timestamps.items():
if len(ts_list) < 10:
continue
ts_list.sort()
intervals = [ts_list[i+1]-ts_list[i] for i in range(len(ts_list)-1)]
intervals = [iv for iv in intervals if 0 < iv < 3600]
if len(intervals) < 5:
continue
mean = sum(intervals)/len(intervals)
if mean < 1:
continue
variance = sum((x-mean)**2 for x in intervals)/len(intervals)
cv = math.sqrt(variance)/mean
if cv < 0.2:
print(f'QUIC BEACON cv={cv:.3f} interval={mean:.0f}s n={len(ts_list)} {src}→{dst}')
" | sort | head -10
QUIC is a UDP protocol. Many detection tools — Suricata rules written for TCP, Snort signatures, even some Zeek packages — only inspect TCP flows. If your Zeek installation doesn't have the zeek-quic package, all QUIC traffic passes through as raw UDP without any Zeek metadata (no conn.log entry, no ssl.log entry for the TLS inside QUIC). Before you can write detection rules for QUIC C2, you need: (1) Zeek with the zeek-quic package installed and loaded, (2) Suricata 6.0+ which has native QUIC dissection, (3) Updated detection rules that look at QUIC-specific fields. The behavioral analysis (beaconing, data volume, timing) works identically for QUIC and TCP — the numbers are the same, just over UDP instead of TCP. The toolchain update is the prerequisite; the detection logic is the same.
Q & A
Q: Should I block all QUIC (UDP 443) at the firewall to force C2 back to TCP where I can see it?
This is a common defensive tradeoff and many organizations do it: block outbound UDP 443, forcing clients to fall back to TCP 443. Chrome, Firefox, and most clients automatically fall back to HTTP/2/TLS when QUIC is blocked. The detection benefit is significant — you get full TCP visibility, Zeek ssl.log entries, and JA3 fingerprints. The cost: slightly slower page loads for some users (QUIC's connection establishment is faster than TCP+TLS), and breakage for any application that requires QUIC (Zoom, some gaming applications, some IoT devices). For most enterprise environments, this tradeoff favors blocking QUIC: the detection improvement is significant, the user experience impact is small, and the added visibility into what was previously opaque UDP traffic is worth it. If you block QUIC, monitor Zeek for any hosts that stop generating TLS metadata for destinations they previously contacted — those may be QUIC-only applications that now silently fail.