Network Protocol Analysis
Network forensics reconstructs attacker activity from packet captures and NetFlow data. Even with TLS encryption obscuring payload content, protocol metadata — TLS handshake parameters, JA3/JA3S fingerprints, flow timing, certificate details, server name indication (SNI) — leaks distinctive patterns that identify C2 communication, lateral movement, and exfiltration. This chapter covers the analytical workflow from raw PCAP to actionable detection rules.
You receive a PCAP from a suspected compromised host. The host is sending HTTPS traffic to an IP address at regular 60-second intervals. Your task: identify whether this is C2 beaconing, extract the TLS fingerprint, determine the C2 domain (or confirm direct IP usage), and build a network-based detection rule.
PCAP Analysis Fundamentals
# Wireshark/tshark workflow for initial PCAP triage:
# 1. Top talkers (bytes)
tshark -r sample.pcap -q -z conv,ip | head -20
# 2. DNS queries (extract all domains)
tshark -r sample.pcap -Y "dns.flags.response == 0" -T fields -e dns.qry.name | sort -u
# 3. HTTP requests (unencrypted C2 or staging)
tshark -r sample.pcap -Y "http.request" -T fields \
-e frame.time -e ip.src -e ip.dst -e http.host -e http.request.uri
# 4. TLS SNI (HTTPS destinations even without decryption)
tshark -r sample.pcap -Y "tls.handshake.type == 1" -T fields \
-e ip.src -e ip.dst -e tls.handshake.extensions_server_name
# 5. Extract all unique IPs contacted
tshark -r sample.pcap -T fields -e ip.dst | sort | uniq -c | sort -rn | head -30
# 6. Detect beaconing by connection interval (Python analysis):
import pyshark, statistics, datetime
cap = pyshark.FileCapture('sample.pcap',
display_filter='ip.dst == 185.220.101.55 and tcp.flags.syn == 1')
times = [float(pkt.sniff_timestamp) for pkt in cap]
cap.close()
if len(times) > 5:
intervals = [times[i+1]-times[i] for i in range(len(times)-1)]
mean = statistics.mean(intervals)
stddev = statistics.stdev(intervals)
print(f"Mean interval: {mean:.1f}s StdDev: {stddev:.1f}s")
if stddev / mean < 0.2:
print("LOW JITTER: likely automated C2 beacon")
TLS Traffic Analysis — JA3/JA3S Fingerprinting
# Extract JA3 from PCAP:
# ja3 tool (Salesforce): python ja3.py sample.pcap
# tshark plugin: tshark -r sample.pcap -q -z expert (shows TLS details)
# Python ja3 computation:
from hashlib import md5
import json
def compute_ja3(client_hello):
# client_hello = parsed TLS ClientHello dict
version = str(client_hello['version'])
ciphers = ','.join(str(c) for c in client_hello['ciphers']
if c not in [0x0a0a, 0x6a6a]) # exclude GREASE
extensions = ','.join(str(e) for e in client_hello['extensions']
if e not in [0x0a0a, 0x6a6a])
curves = ','.join(str(c) for c in client_hello.get('elliptic_curves', [])
if c not in [0x0a0a])
points = ','.join(str(p) for p in client_hello.get('ec_point_formats', []))
raw = f"{version},{ciphers},{extensions},{curves},{points}"
return md5(raw.encode()).hexdigest(), raw
# Known Cobalt Strike JA3 hashes (default profiles):
CS_JA3 = {
"1cac09fdb8f57df7d8c4d9741dc57a8a": "Cobalt Strike default (pre-4.0)",
"6bea3f851e1a462f3bf1c3ebadff69e6": "Cobalt Strike 4.x malleable default",
"a0e9f5d64349fb13191bc781f81f42e1": "Meterpreter default",
}
SMB Protocol Forensics
# SMB traffic analysis: lateral movement, credential relay, share enumeration
# Wireshark SMB filter examples:
# All SMB2 session setups (authentication attempts):
# smb2.cmd == 1 (NEGOTIATE) or smb2.cmd == 3 (SETUP)
# Failed auth attempts (brute force):
tshark -r sample.pcap -Y "smb2.cmd == 1 and smb2.flags.response == 1" \
-T fields -e frame.time -e ip.src -e ip.dst -e smb2.nt_status | grep -v SUCCESS
# Extract all accessed UNC paths:
tshark -r sample.pcap -Y "smb2.cmd == 5" -T fields \
-e ip.src -e ip.dst -e smb2.filename | sort -u
# Detect PsExec-style service creation via SVCCTL pipe:
tshark -r sample.pcap -Y "smb2.filename == 'svcctl'" -T fields \
-e frame.time -e ip.src -e ip.dst
# NetBIOS Name Service poisoning (LLMNR/NBNS):
tshark -r sample.pcap -Y "nbns.flags.opcode == 5" -T fields \
-e frame.time -e ip.src -e nbns.name
# Legitimate query: from broadcast source; MITM: unicast response to ALL queries
C2 Traffic Fingerprinting
| C2 framework | Network indicator | Detection method |
|---|---|---|
| Cobalt Strike (default) | JA3 1cac09..., URI /jquery-3.3.1.min.js, X-Cache-Id header | JA3 hash match, URI path regex |
| Metasploit Meterpreter | JA3 a0e9f5..., specific TLS cipher ordering | JA3 hash, connection to non-standard ports |
| Sliver (default) | mTLS with certificate CN="operator", specific QUIC ALPN | Certificate subject inspection, QUIC fingerprint |
| DNS C2 (generic) | High-entropy subdomains, regular query intervals, NXDOMAIN ratio | NXDomain rate, subdomain entropy, query interval analysis |
| Emotet | HTTP POST to IP:port (no SNI), RC4-encrypted body, no Referer | HTTP without SNI, specific URI path patterns |
Detection Engineering
title: Known C2 JA3 TLS Fingerprint
logsource:
product: zeek
service: ssl
detection:
selection:
ja3|contains:
- '1cac09fdb8f57df7d8c4d9741dc57a8a' # Cobalt Strike default
- 'a0e9f5d64349fb13191bc781f81f42e1' # Meterpreter
condition: selection
level: critical
tags: [attack.command_and_control, T1071.001]
title: LLMNR/NBNS Poisoning — Unicast Response to Broadcast Query
logsource:
product: zeek
service: dns
detection:
selection:
qtype: 'A'
answers|contains: '.'
src_addr|not_cidr: '224.0.0.0/8' # multicast range
dst_addr|cidr: '0.0.0.0/0' # any unicast — compare src to query origin
condition: selection
level: medium
-- MDE KQL: TLS connections without SNI (direct IP HTTPS — unusual for browsers)
DeviceNetworkEvents
| where RemotePort == 443
| where isempty(RemoteUrl) or RemoteUrl == RemoteIP // no hostname, just IP
| where InitiatingProcessFileName !in~ (
"MsMpEng.exe","svchost.exe","WaaSMedicSVC.exe"
)
| project Timestamp, DeviceName, RemoteIP, InitiatingProcessFileName
-- MDE KQL: Zeek-style beaconing detection from connection logs
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemotePort in (80, 443, 8080, 8443)
| summarize
count = count(),
first = min(Timestamp),
last = max(Timestamp),
bytes_sent = sum(SentBytes)
by DeviceName, RemoteIP, InitiatingProcessFileName
| extend duration_min = datetime_diff('minute', last, first)
| where count > 20 and duration_min > 60
| extend rate = todouble(count) / todouble(duration_min)
| where rate between (0.5 .. 2.5) // ~1 connection/min ± 60s interval
| order by rate asc
Q&A
JA3 fingerprinting is widely used to detect Cobalt Strike and other C2 tools. Why is JA3 increasingly unreliable as a standalone detection method, and what complementary network indicator is more resilient to evasion?
JA3 is computed from the TLS ClientHello message — specifically the TLS version, cipher suites, extensions, elliptic curves, and point formats. Modern Cobalt Strike (version 4.x+) and most other commercial C2 frameworks allow operators to configure a Malleable C2 profile that controls which cipher suites, extensions, and TLS settings the beacon uses. By specifying a TLS profile that mimics a common browser (Chrome, Firefox, Edge), the attacker generates a JA3 hash identical to millions of legitimate endpoints. Since JA3 is entirely a ClientHello artifact and the ClientHello is sent before any authentication or payload exchange, it is trivial to spoof: just configure the C2 framework to advertise the same cipher list as Chrome. The result is that JA3 detection provides good coverage against default/unmodified C2 deployments but zero coverage against any competent red team or sophisticated attacker who configures their profile.
The more resilient complementary indicator is the JA3/JA3S pair combined with certificate analysis. JA3S fingerprints the ServerHello, which is determined by the C2 server's TLS implementation — and while the client can spoof its ClientHello arbitrarily, the server's TLS stack is harder to change. More importantly, the server's X.509 certificate is a rich source of fingerprinting: self-signed C2 certificates have characteristic issuer/subject fields (often generic organization names, short validity periods, no SANs, or hostnames that don't match the IP), validity periods of exactly 1 or 2 years (common C2 defaults), and specific RSA key sizes. Tools like JARM (from Salesforce) actively fingerprint TLS servers by sending multiple synthetic ClientHellos and analyzing how the server responds — the JARM fingerprint is determined by the TLS library on the server side, which the attacker cannot easily change without rebuilding their C2 framework. Cobalt Strike's JARM fingerprint is well-known and has remained stable across versions that changed the client-side JA3 profile. The JARM fingerprint, combined with flow timing analysis and certificate characteristic scoring, provides detection resilience that JA3 alone cannot achieve.