Chapter 222

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.

Scenario

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

JA3 FINGERPRINT CONSTRUCTION ═══════════════════════════════════════════════════════════════════════ TLS ClientHello fields used (in order): 1. TLS version (e.g., 771 = TLSv1.2) 2. Cipher suites list (comma-separated decimal values, excluding GREASE) 3. Extensions list (decimal, excluding GREASE) 4. Elliptic curves list 5. Elliptic curve point formats JA3 = MD5("version,ciphers,extensions,curves,formats") Example ClientHello from Cobalt Strike: Version: 771 Ciphers: 49196,49195,49200,49199,... (specific CS default set) Extensions: 0,23,35,... JA3: 1cac09fdb8f57df7d8c4d9741dc57a8a (CS default profile, unmodified) JA3S = MD5 of ServerHello fields (server's response) JA3-pair detection: (JA3 → JA3S) combination is more unique than either alone DETECTION USE: - Known-bad JA3 hashes: Cobalt Strike, Metasploit, Sliver have known defaults - Anomalous JA3: generic TLS library with non-browser cipher order - Malleable C2 can change JA3 by modifying TLS profile settings ═══════════════════════════════════════════════════════════════════════
# 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 frameworkNetwork indicatorDetection method
Cobalt Strike (default)JA3 1cac09..., URI /jquery-3.3.1.min.js, X-Cache-Id headerJA3 hash match, URI path regex
Metasploit MeterpreterJA3 a0e9f5..., specific TLS cipher orderingJA3 hash, connection to non-standard ports
Sliver (default)mTLS with certificate CN="operator", specific QUIC ALPNCertificate subject inspection, QUIC fingerprint
DNS C2 (generic)High-entropy subdomains, regular query intervals, NXDOMAIN ratioNXDomain rate, subdomain entropy, query interval analysis
EmotetHTTP POST to IP:port (no SNI), RC4-encrypted body, no RefererHTTP 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.