Chapter 20

pyshark

pyshark is Python bindings for Wireshark's tshark — it uses tshark under the hood to dissect every protocol that Wireshark knows, then exposes each packet as a Python object with named fields accessible by layer and field name. It gives you the full power of Wireshark's protocol dissectors in Python without manually parsing packet bytes.

Scenario

You need to analyze 50,000 TLS ClientHello packets and compute the JA3 hash for each one, then group by JA3 and identify which source IPs are using non-browser TLS fingerprints. With pyshark, you access pkt.tls.handshake_extensions_server_name and the full handshake extension list directly — no manual byte parsing required. Wireshark does the dissection; Python does the analysis logic.

FileCapture — Analyzing PCAP Files

Pythonpyshark-file-analysis.py
import pyshark
from collections import defaultdict

# Open a PCAP file
cap = pyshark.FileCapture(
    "capture.pcap",
    display_filter="tls.handshake.type == 1",  # ClientHello only
    keep_packets=False  # Don't keep packets in memory (streaming mode)
)

# Collect TLS fingerprint data
tls_stats = defaultdict(list)

for pkt in cap:
    try:
        # Access TLS layer fields
        sni = getattr(pkt.tls, "handshake_extensions_server_name", "")
        src_ip = pkt.ip.src
        dst_ip = pkt.ip.dst
        dst_port = pkt.tcp.dstport

        # TLS version from record layer
        version = getattr(pkt.tls, "record_version", "")
        # Cipher suites offered (comma-separated if multiple)
        ciphers = getattr(pkt.tls, "handshake_ciphersuite", "")

        tls_stats[src_ip].append({
            "sni": sni,
            "dst": f"{dst_ip}:{dst_port}",
            "version": version,
            "ciphers_count": len(ciphers.split(",")) if ciphers else 0
        })

    except AttributeError:
        pass  # Packet missing expected fields

cap.close()

# Report: sources with many different SNIs (possible C2 probing)
print("Sources with 10+ unique SNI destinations:")
for src, sessions in tls_stats.items():
    unique_sni = set(s["sni"] for s in sessions)
    if len(unique_sni) >= 10:
        print(f"  {src}: {len(unique_sni)} unique SNIs, {len(sessions)} total sessions")

# Report: sources connecting with no SNI (possible C2 — real browsers always send SNI)
print("\nConnections with no SNI (suspicious):")
for src, sessions in tls_stats.items():
    no_sni = [s for s in sessions if not s["sni"]]
    if no_sni:
        dsts = set(s["dst"] for s in no_sni)
        print(f"  {src} → {dsts} ({len(no_sni)} connections without SNI)")

LiveCapture — Real-Time Analysis

Pythonpyshark-live-capture.py
import pyshark
import signal, sys
from datetime import datetime
from collections import Counter

# Real-time analysis for incident response — alert on C2 indicators
dns_counts = Counter()
suspicious_uas = []
running = True

def signal_handler(sig, frame):
    global running
    running = False
    print("\nStopping capture...")

signal.signal(signal.SIGINT, signal_handler)

# Live capture with BPF pre-filter
cap = pyshark.LiveCapture(
    interface="eth0",
    bpf_filter="tcp port 443 or udp port 53",
    display_filter="dns or tls.handshake.type==1"
)

print("Starting live analysis... (Ctrl+C to stop)")
cap.sniff(timeout=5)  # Collect 5 seconds worth of packets

for pkt in cap.sniff_continuously(packet_count=10000):
    if not running:
        break
    try:
        ts = datetime.now().strftime("%H:%M:%S")

        # DNS monitoring
        if hasattr(pkt, "dns") and pkt.dns.flags_response == "0":
            name = pkt.dns.qry_name
            dns_counts[name] += 1
            # Alert on high-frequency queries to same domain (potential tunneling)
            if dns_counts[name] > 20:
                print(f"[{ts}] ALERT: High DNS frequency: {name} ({dns_counts[name]} queries)")

        # TLS SNI monitoring
        if hasattr(pkt, "tls"):
            sni = getattr(pkt.tls, "handshake_extensions_server_name", "")
            src = pkt.ip.src if hasattr(pkt, "ip") else ""
            # Alert on connections with no SNI from internal hosts
            if not sni and src.startswith("10."):
                print(f"[{ts}] ALERT: No SNI from internal host {src} → {pkt.ip.dst}:443")

    except AttributeError:
        pass

cap.close()

print(f"\nTop 10 DNS queries:")
for name, count in dns_counts.most_common(10):
    print(f"  {count:5d}  {name}")
Mental model: pyshark trades speed for readability

pyshark is the slowest of the Python pcap libraries because it runs tshark as a subprocess for every packet — the overhead of launching and communicating with tshark adds latency. For a PCAP with 1 million packets, pyshark might take 10–30 minutes; tshark directly might take 30 seconds. Use pyshark when: you need Wireshark-quality protocol dissection in Python (complex protocols like SMB2, Kerberos, QUIC), you prefer readable pkt.dns.qry_name field access over byte indexing, or you're building a proof-of-concept. Use tshark subprocess + field extraction for production pipelines where speed matters. The two are complementary: use tshark for bulk pre-filtering and field extraction, use pyshark for per-packet analysis of the filtered subset.

Q & A

Q: pyshark raises AttributeError on some packets when accessing field names. How do I handle this cleanly?

AttributeError in pyshark means the packet doesn't have the layer or field you're accessing. This is completely normal — not every TCP packet has TLS, not every IP packet has DNS. Two patterns for clean handling: (1) hasattr check: if hasattr(pkt, 'tls') and hasattr(pkt.tls, 'handshake_extensions_server_name') — verbose but explicit. (2) getattr with default: sni = getattr(pkt.tls, 'handshake_extensions_server_name', None) — returns None if the field doesn't exist. The most robust pattern for batch processing is a try/except AttributeError around the entire per-packet block — this catches any unexpected missing field without needing to pre-check every field individually. Add except Exception as e: pass for truly robust processing that ignores malformed packets. Never let a single malformed packet abort processing of 1 million packets.