C2 extraction from PCAP is a detective process — you rarely have the C2 IP in advance. The approach is to find what looks statistically abnormal in the traffic, then prove or disprove the hypothesis by examining the conversation content.
Identifying Beaconing Without Prior IOCs
# Step 1: Find all external connection destinations (not internal RFC 1918)
tshark -r capture.pcap \
-T fields \
-e ip.dst \
| grep -vE "^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.|0\.)" \
| sort | uniq -c | sort -rn > external_destinations.txt
# Step 2: Find destinations with many evenly-spaced connections (beaconing)
# Count connections per minute to each external IP
tshark -r capture.pcap \
-Y "not ip.dst in {10.0.0.0/8 192.168.0.0/16 172.16.0.0/12}" \
-T fields \
-e frame.time_epoch \
-e ip.dst \
| awk '{print strftime("%Y-%m-%d %H:%M", $1), $2}' \
| sort | uniq -c > connections_per_minute.txt
# Step 3: Identify unusually regular connection intervals
# Python analysis of inter-connection timing (coefficient of variation)
python3 << 'EOF'
import sys
from collections import defaultdict
import statistics
# Parse tshark timestamp output
# tshark -r capture.pcap -Y "ip.dst==X" -T fields -e frame.time_epoch | sort
timestamps = [float(line.strip()) for line in open("target_timestamps.txt")]
intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)]
mean_interval = statistics.mean(intervals)
stdev_interval = statistics.stdev(intervals)
cv = stdev_interval / mean_interval # Coefficient of Variation
print(f"Mean interval: {mean_interval:.1f}s")
print(f"StdDev: {stdev_interval:.1f}s")
print(f"CV: {cv:.3f}")
print("Likely beacon" if cv < 0.3 else "Likely human/variable")
EOF
Cobalt Strike HTTP Beacon Analysis
# Filter for Cobalt Strike HTTP beacon characteristics
# 1. Connections to non-browser destinations with browser User-Agents
tshark -r capture.pcap \
-Y "http.request and http.user_agent contains MSIE" \
-T fields \
-e ip.dst \
-e http.host \
-e http.user_agent \
-e http.request.uri
# 2. Cookie-based C2 (command in cookie, response in Set-Cookie)
tshark -r capture.pcap \
-Y "ip.addr == 185.220.101.9" \
-T fields \
-e http.request.method \
-e http.host \
-e http.request.uri \
-e http.cookie \
-e http.set_cookie
# Decode base64-encoded cookie value:
echo "SGVsbG8gV29ybGQ=" | base64 -d
# 3. Default Cobalt Strike URIs (from malleable profile analysis)
tshark -r capture.pcap \
-Y "http.request.uri in {/jquery-3.3.1.min.js /updates.rss /pixel.gif /submit.php}" \
-T fields \
-e frame.time \
-e ip.src \
-e ip.dst \
-e http.request.uri \
-e http.content_length_header
DNS Tunneling Detection in PCAP
# 1. Find long DNS query names (tunneled data in subdomain)
tshark -r capture.pcap \
-Y "dns.flags.response == 0 and dns.qry.name.len > 50" \
-T fields \
-e frame.time \
-e ip.src \
-e dns.qry.name \
-e dns.qry.type
# 2. Count queries per base domain (tunnel queries cluster under one domain)
tshark -r capture.pcap \
-Y "dns.flags.response == 0" \
-T fields \
-e dns.qry.name \
| awk -F. '{print $(NF-1)"."$NF}' \
| sort | uniq -c | sort -rn | head -20
# A base domain with thousands of unique subdomain queries = DNS tunnel
# 3. TXT record queries (primary DNS exfiltration record type)
tshark -r capture.pcap \
-Y "dns.qry.type == 16" \
-T fields \
-e frame.time \
-e ip.src \
-e dns.qry.name \
-e dns.resp.txt
# 4. Calculate subdomain entropy (high entropy = encoded data)
# Normal: api.company.com (entropy ~2.0)
# Tunnel: 4a3f7e2c8b1d5e.c2.evil.com (entropy ~4.5)
python3 << 'EOF'
import math
def entropy(s):
p = {c: s.count(c)/len(s) for c in set(s)}
return -sum(v * math.log2(v) for v in p.values())
domains = open("dns_queries.txt").read().splitlines()
for d in domains:
subdomain = '.'.join(d.split('.')[:-2]) # Remove TLD
if subdomain:
e = entropy(subdomain)
if e > 4.0:
print(f"HIGH ENTROPY {e:.2f}: {d}")
EOF
Exfiltration Detection in PCAP
# 1. Large outbound data transfers (HTTP POST / PUT)
tshark -r capture.pcap \
-Y "http.request.method == POST and ip.dst != [internal_subnets]" \
-T fields \
-e frame.time \
-e ip.src \
-e ip.dst \
-e http.host \
-e http.request.uri \
-e http.content_length
# 2. Data volume by destination (find large outbound transfers)
tshark -r capture.pcap \
-T fields \
-e ip.dst \
-e tcp.len \
| awk '{sum[$1]+=$2} END {for (ip in sum) print sum[ip], ip}' \
| sort -rn | head -10
# 3. ICMP exfiltration (data hidden in ICMP payload)
tshark -r capture.pcap \
-Y "icmp.type == 8 and data.len > 64" \
-T fields \
-e frame.time \
-e ip.src \
-e ip.dst \
-e data.len \
-e data
# ICMP ping should have 32 or 64 bytes of padding
# ICMP with 1400 bytes of payload = data being tunneled
# 4. SMB lateral movement / file sharing detection
tshark -r capture.pcap \
-Y "smb2.cmd == 5" \
-T fields \
-e frame.time \
-e ip.src \
-e ip.dst \
-e smb2.filename
# smb2.cmd == 5 = SMB2 Create (file open/create)
# Shows files accessed via SMB (lateral movement, data staging)