Chapter 26

TLS Deep Dive

TLS encrypts the application payload but leaves substantial forensic evidence in the handshake: the TLS version negotiated, cipher suites offered and selected, the SNI extension (the hostname the client wants to connect to), the server's X.509 certificate (subject, issuer, SANs, validity period), and the TLS extensions list which forms the basis for JA3 fingerprinting. Together these fields characterize TLS sessions without decrypting a single byte of payload.

Scenario

You're analyzing HTTPS traffic from a suspected Cobalt Strike infection. Every connection is TLS-encrypted but you notice: all ClientHello packets offer the exact same cipher suites in the exact same order, with extensions in an unusual sequence that doesn't match any browser. The JA3 hash computed from these fields matches known Cobalt Strike signatures in public threat intel databases. You've identified the C2 traffic without decrypting anything.

TLS 1.2 Handshake — Forensic Anatomy

  TLS 1.2 Handshake — What Each Message Reveals
  ═══════════════════════════════════════════════════════════════════

  Client → Server: ClientHello                    [FORENSIC GOLDMINE]
  ├── TLS version: 0x0303 (TLS 1.2) / 0x0301 (TLS 1.0, old/malware)
  ├── Random: 32 bytes (not useful directly)
  ├── Session ID: resumes previous session (length = 0 if new)
  ├── Cipher Suites: ordered list the client supports
  │     Normal browser: 15-25 suites in specific order
  │     Malware: often 1-5 suites or unusual order
  ├── Compression Methods: 0x00 (none) — any other value = old/broken
  └── Extensions:
        server_name (0x0000): the SNI — hostname client wants
        supported_groups (0x000a): EC curves supported
        session_ticket (0x0023): enable TLS session tickets
        supported_versions (0x002b): TLS 1.3 if offered
        signature_algorithms (0x000d): hash+sig combos accepted
        extended_master_secret (0x0017): security extension
        encrypt_then_mac (0x0016): security extension

  Server → Client: ServerHello
  ├── Selected TLS version
  ├── Selected cipher suite
  └── Selected extensions

  Server → Client: Certificate                    [FORENSIC GOLDMINE]
  ├── Subject CN: who the cert claims to be
  ├── Subject Alternative Names (SANs): all valid hostnames
  ├── Issuer CN: which CA issued the cert
  ├── Serial number: unique identifier
  ├── Not Before / Not After: validity window
  │     Newly issued cert for a session = possible C2
  │     Self-signed = no CA verification = C2 indicator
  └── Public key algorithm and size

  ServerHello + Certificate → Client is visible even in encrypted TLS 1.3
  (Certificate is encrypted in TLS 1.3 but ServerHello is plaintext)

TLS Metadata Extraction

bashtls-analysis.sh
#!/bin/bash
PCAP="$1"

echo "=== TLS ClientHello analysis ==="
tshark -r "$PCAP" -n -Y "tls.handshake.type==1" -T fields \
  -E separator="\t" \
  -e frame.time_epoch \
  -e ip.src \
  -e ip.dst \
  -e tcp.dstport \
  -e tls.handshake.extensions_server_name \
  -e tls.handshake.version \
  -e tls.handshake.ciphersuite \
  | head -50

echo ""
echo "=== Server certificates (subject + issuer) ==="
tshark -r "$PCAP" -n -Y "tls.handshake.type==11" -T fields \
  -E separator="\t" \
  -e ip.src \
  -e ip.dst \
  -e tls.handshake.certificate_length \
  -e x509ce.dNSName \
  -e tls.handshake.extensions_server_name \
  | head -30

echo ""
echo "=== Self-signed certificates (issuer == subject) ==="
tshark -r "$PCAP" -n \
  -Y "tls.handshake.type==11" \
  -T json \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for pkt in data:
    try:
        layers = pkt['_source']['layers']
        tls = layers.get('tls', {})
        # Look for self-signed indicator: same issuer and subject RDN
        cert = str(tls)
        if 'tls.x509' in cert.lower():
            print(str(pkt['_source']['layers'].get('ip', {}).get('ip.dst',''))[:50])
    except:
        pass
" 2>/dev/null | sort -u | head -20

echo ""
echo "=== TLS versions negotiated (old versions = red flag) ==="
tshark -r "$PCAP" -n -Y "tls.handshake.type==2" -T fields \
  -e tls.record.version -e tls.handshake.version \
  | sort | uniq -c | sort -rn

echo ""
echo "=== Connections with no SNI (C2 indicator) ==="
tshark -r "$PCAP" -n \
  -Y "tls.handshake.type==1 and not tls.handshake.extensions_server_name" \
  -T fields \
  -E separator="\t" \
  -e ip.src \
  -e ip.dst \
  -e tcp.dstport \
  | sort | uniq -c | sort -rn | head -20

Certificate Forensics

Certificate AttributeLegitimateC2 / Malicious
IssuerKnown CA (DigiCert, Let's Encrypt, Sectigo)Self-signed, unknown CA, or attacker-controlled CA
Subject CNMatches SNI sent by clientGeneric (example.com), mismatched, or IP address
SANsMultiple relevant hostnamesOne or none; wildcard for single domain
Validity period1–2 years typicalVery long (10 years) or very short (days/hours)
Issue dateStable infrastructure, weeks to months oldRecently issued (last 7 days) for a session
Public key size2048+ RSA or 256-bit ECMay use 1024-bit RSA (old, weak)
Let's Encrypt certsLegitimate domains passing ACME validationAttackers also use LE — free, automated, trusted by all
Common mistake: treating Let's Encrypt as safe

Let's Encrypt is used extensively by attackers because it's free, automated, and trusted by every browser and OS in the world. An attacker who registers microsoft-update-service.com and gets a Let's Encrypt cert has a perfectly valid HTTPS connection that will never trigger a certificate error. The issuer being Let's Encrypt is not a signal of legitimacy — it's table stakes for any competent attacker. What matters is the domain name, the SNI, and whether the IP hosting the cert matches what you expect. A Let's Encrypt cert on an IP with no rDNS, registered 3 days ago, hosted on a VPS provider, is suspicious regardless of who issued the cert.

Q & A

Q: In TLS 1.3, the certificate is encrypted. What forensic metadata is still visible?

TLS 1.3 encrypts more of the handshake than TLS 1.2, but significant metadata remains visible: (1) ClientHello is always plaintext: the full list of cipher suites, extensions, supported groups, and the SNI are visible unless the client uses Encrypted Client Hello (ECH), which is not yet widely deployed. (2) ServerHello is plaintext: the selected cipher suite and server random are visible. (3) Certificate in the handshake: in TLS 1.3, the Certificate and CertificateVerify messages are encrypted in the EncryptedExtensions record. However, many TLS 1.3 implementations still respond to OCSP stapling requests which reveal the certificate serial number. (4) JA3 still works: the ClientHello fields used for JA3 are present and visible in TLS 1.3 — the same fingerprinting technique applies. (5) Network metadata: source/destination IP, port, SNI, timing, and data volume remain visible. The payload is encrypted, but the connection metadata is not.