43 min read

> Where you are: Part III, Chapter 23 of 40. Chapter 22 pulled live network connections out of a RAM image — sockets, listening ports, and the remnants of a C2 channel that the process list tried to hide. That told you what the endpoint believed...

Chapter 23: Network Forensics — Packet Captures, Log Analysis, and Tracing Activity Across the Wire

Where you are: Part III, Chapter 23 of 40. Chapter 22 pulled live network connections out of a RAM image — sockets, listening ports, and the remnants of a C2 channel that the process list tried to hide. That told you what the endpoint believed about the network at one instant. This chapter goes to the network itself: the packets on the wire, the flow records in the collector, and the logs the firewall, proxy, DNS resolver, IDS, and Zeek sensor wrote while the traffic was still moving. It is the home chapter for the network side of anchor case #2 — the engineer who exfiltrated trade secrets — because the disk proved which files were opened from a thumb drive, and the network proves the 2.1 gigabytes that went out to a personal cloud account at 3 a.m.

Learning paths: This is the densest chapter for 🛡️ Incident Response — network security monitoring (NSM), command-and-control detection, lateral-movement tracing, and exfiltration hunting all live here. 🔍 Forensic Examiners must treat a packet capture as evidence: acquired, hashed, and admissible, with the same rigor as a disk image. 📜 Legal/eDiscovery practitioners should read the legal section twice — capturing the content of communications in transit is governed by the Wiretap Act, a far higher bar than reading a log at rest, and the difference between content and metadata decides what you may lawfully collect. 💾 Data Recovery gets the least here, but file extraction from a capture and the repair of a corrupted PCAP are genuine recovery work, and they teach a sharp inversion of this book's first theme.


The wire forgets: why network evidence is different

Open with the inversion, because it reorganizes everything you have learned so far. The foundational theme of this book is deleted ≠ destroyed: on storage media, removing a file deletes the pointer, not the bytes, and the data persists until something overwrites it. That theme does not hold on the network. A packet exists on the wire for microseconds. Once it has been delivered — or dropped — it is gone, with no slack space to carve, no journal to replay, no shadow copy to mount. On the network the default state of all evidence is destroyed, and the only data you will ever have is the data that something, somewhere, decided to write down as it passed. Network forensics is therefore the discipline of capture, retention, and the reconstruction of an ephemeral event from whatever records survived it.

That single fact drives the order of volatility you met in Chapter 15 — Live Response and Triage. Network state sits near the very top of the volatility ladder, above memory and far above disk, because you cannot image the past. If no sensor was capturing when the exfiltration happened, the packets are not "deleted" — they never persisted at all. The recovery technician's instinct ("the bytes are still down there somewhere") is wrong here. The network examiner's instinct must be different: what was watching, what did it record, how long does it keep that, and how fast is it aging out?

And yet the network is not silent. The same physics that makes a packet ephemeral makes it observable in transit — and observation, by design, happens at many points. A single HTTP upload from a workstation to a cloud account may be witnessed by, and leave a durable record in, half a dozen independent systems: the host's own firewall, the perimeter firewall, the forward proxy, the DNS resolver that turned www.dropbox.com into an IP, the NetFlow exporter on the core router, an inline IDS, a Zeek sensor on a SPAN port, and — if anyone was running a full packet capture — the capture file itself. This is Locard's exchange principle, the bedrock of all forensic science, expressed in packets: every contact leaves a trace. The contact between two hosts leaves traces in the infrastructure between them, even when neither endpoint keeps a copy. Your job is to know where those traces live and how to read them.

Consider the anchor. The same senior engineer from Chapter 16 — Windows Forensics — the one whose disk you proved had files opened from a removable device and a "PC cleaner" run to hide it — also had to get the data out of the building. A thumb drive is one path; the network is the other, and it is the path corporate monitoring is built to see. Counsel's question shifts slightly from the disk version: did proprietary data leave the corporate network to an external destination, and can you tie it to this user, this workstation, and this time? On the wire, that question has answers the suspect could not clean, because they were never written to the suspect's machine. They were written to the proxy, the flow collector, and the DNS log — systems the engineer had no access to and, very likely, no idea existed.

Why This Matters. On disk, evidence persists until overwritten; on the network, evidence exists only if it was captured. This reverses the first instinct of recovery work. It also makes capture coverage the single most consequential decision in network forensics, and it is a decision made long before any incident — by whoever designed the monitoring. When you arrive after the fact, you do not get to choose what was recorded. You inventory what was, and you build the strongest defensible account the surviving records support. The absence of a record is not necessarily the absence of an event — it may simply be the absence of a sensor. State that distinction explicitly; it is theme #5, know your limitations, applied at the level of the whole investigation.

Where network evidence lives

Before any tool, build the map. These are the sources you will inventory at the start of every network investigation, arranged from richest-but-rarest to leanest-but-longest-retained:

NETWORK EVIDENCE SOURCES — richness vs. retention
┌───────────────────────────────────────────────────────────────────────────┐
│  RICH / SHORT RETENTION        full packet capture (PCAP/PCAPNG)            │
│   ▲   every byte, every header  → content; huge; hours–days of storage      │
│   │                                                                         │
│   │   IDS/IPS alerts (Snort/Suricata)  → signatures fired; leads, not proof │
│   │   Zeek logs (conn/dns/http/ssl/files) → structured metadata + hashes    │
│   │   proxy logs            → URLs, users, bytes, user-agent (HTTP/CONNECT) │
│   │   DNS query logs        → what names were resolved (beaconing/DGA/tunnel)│
│   │   firewall logs         → 5-tuple + allow/deny + bytes                   │
│   │                                                                         │
│   ▼   NetFlow / IPFIX / sFlow → 5-tuple + counts, NO payload                │
│  LEAN / LONG RETENTION         → who-talked-to-whom-and-how-much, for months │
└───────────────────────────────────────────────────────────────────────────┘
   + endpoint corroboration: netstat/Get-NetTCPConnection (Ch.15),
     network artifacts recovered from RAM (Ch.22), browser & proxy
     history on the host (Ch.18), and the ISP/provider records you
     subpoena (Ch.25, Ch.31).

Read the map as a strategy. You almost never have full packet capture for the window you care about, because nobody can afford to keep it. You frequently have flow data, because it is cheap and kept for months. You usually have firewall, proxy, and DNS logs, because they are operationally useful. You may have IDS alerts and Zeek logs if a security team built an NSM stack. The art is to start from the lean-but-long sources to find the event in time, then pivot to any rich-but-short source that happens to cover that window, and finally corroborate against the endpoint artifacts from the rest of Part III. One source is a lead; sources that agree are a finding — the same correlation discipline that won the Windows chapter, now stretched across the network.


Capturing packets: tcpdump, dumpcap, and the snaplen trap

When you can capture — during an active incident, on a tap you control, under proper authority — you must capture correctly, because a flawed capture is evidence you cannot trust and may not be able to redo. The two workhorses are tcpdump (ubiquitous on Unix-like systems, scriptable, tiny) and dumpcap (Wireshark's dedicated capture engine, more stable for long or high-rate captures because it does no live dissection and spills straight to disk).

tcpdump in the field

The minimum viable forensic capture writes raw packets to a file, captures the whole packet, and does not waste time resolving names:

# Capture full frames on eth0 to a file; do not resolve names/ports.
#   -i eth0      interface to capture on
#   -w cap.pcap  write raw packets (do NOT use -v/-X here; -w is binary)
#   -s 0         snapshot length = unlimited (capture the ENTIRE packet)
#   -nn          do not resolve hostnames OR port names (faster, no DNS leakage)
sudo tcpdump -i eth0 -s 0 -nn -w cap.pcap

# Targeted capture with a BPF capture filter (only this host's traffic):
sudo tcpdump -i eth0 -s 0 -nn host 10.20.4.51 -w jrivera-ws.pcap

# Long-running capture with a ring buffer: 200 MB files, keep newest 50
#   (-C size in MB, -W file count, -G seconds for time-based rotation)
sudo tcpdump -i eth0 -s 0 -nn -C 200 -W 50 -w /evidence/cap.pcap

Three field facts that separate a usable capture from a wasted one. First, -s 0 (snaplen) is not optional for forensics. The snapshot length is how many bytes of each packet tcpdump keeps. For years the historical default was a truncating value (68, then 96 bytes) that captured headers but threw away payload; modern builds default to 262144, but you do not gamble on the default. If you capture with a small snaplen, every packet is truncated at that boundary, you lose the application data permanently, and no later analysis can recover what was never written. A capture with incl_len < orig_len on its packets is a capture that silently amputated your evidence. Set -s 0 and confirm.

Second, resolve nothing during capture (-nn). Name resolution while capturing both slows you down and injects your own DNS lookups into the very network you are observing — a contamination of the evidence and, occasionally, a tip-off to an adversary watching their own traffic. Resolve names later, offline, from a copy.

Third, rotate, do not run one giant file. A single multi-hundred-gigabyte PCAP is fragile and unwieldy. -C/-W (size-based ring) or -G (time-based, e.g., one file per hour) give you manageable, individually hashable evidence units. For multi-day captures, dumpcap is the better engine:

# dumpcap: Wireshark's capture engine — preferred for sustained/high-rate capture.
#   -b filesize:200000  rotate at ~200 MB   -b files:50  keep 50 (ring buffer)
dumpcap -i eth0 -b filesize:200000 -b files:50 -w /evidence/cap.pcapng

On a Windows host where you cannot install Wireshark, you are not stuck — modern Windows ships a built-in capture facility, pktmon, that can emit a PCAPNG directly, and PowerShell can snapshot the live connection table for triage:

# Built-in Windows packet capture (Win10 1809+ / Win11), no install required.
pktmon start --capture --pkt-size 0 --file C:\evidence\host.etl   # 0 = full packet
# ...reproduce / wait...
pktmon stop
pktmon etl2pcap C:\evidence\host.etl --out C:\evidence\host.pcapng

# Live connection table for triage (volatile — capture it before it changes):
Get-NetTCPConnection | Where-Object State -eq 'Established' |
  Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,OwningProcess |
  Sort-Object RemoteAddress | Format-Table -Auto

Chain of Custody. A packet capture is acquired evidence, and the second theme — the original is sacred — applies exactly as it does to a disk image. The capture file is the original; you hash it the moment capture stops and you work only on copies. Record the acquisition the way you would record imaging a drive: capturing host and its clock source (NTP-synced? to what?), interface and how it was fed (SPAN/mirror port, hardware TAP, or host NIC in promiscuous mode), start/stop times in UTC, the operator, the snaplen, any capture filter applied (because a filter is a deliberate exclusion of evidence and must be disclosed), and the file hash. capinfos produces both the statistics and the hashes in one step:

text capinfos -H cap.pcap File name: cap.pcap File type: Wireshark/tcpdump/... - pcap Number of packets: 4,812,337 Capture duration: 86,400.12 seconds SHA256: 9f2c4a...e71b ← record this in the chain-of-custody form SHA1: a3f1... RIPEMD160: 7c0d...

A defensible note reads: "Captured on TAP-02 (passive hardware tap, core-dist link), host ir-sensor-1 (NTP-synced to time.nist.gov, offset < 5 ms), 2024-06-28 00:00–24:00 UTC, snaplen 262144, no capture filter; file cap.pcap, SHA-256 9f2c4a…e71b." Templates live in Appendix F — Chain of Custody and Report Templates.

Legal Note. This is the chapter where a forensic action can become a federal crime if you get the authority wrong, so internalize the line. Capturing the content of communications in transit — the payload, the message, the file — is governed in the United States by the Wiretap Act (Title III of ECPA, 18 U.S.C. §§ 2510–2522), which generally forbids real-time interception of electronic communications without a court order meeting a high standard. Capturing non-content metadata in real time — the addressing and signaling information, i.e., who connected to whom, when, on what port — falls under the lower-bar Pen Register / Trap-and-Trace statute (18 U.S.C. §§ 3121–3127). Reading communications and records already stored at rest (logs, archived mail) is governed by the Stored Communications Act (18 U.S.C. §§ 2701–2712). For a corporate internal investigation, two Wiretap Act exceptions usually make on-network capture lawful: the provider exception (§ 2511(2)(a)(i)), which lets an operator monitor to protect its own network and rights, and consent (§ 2511(2)(c)–(d)) — which is exactly why enterprises put a monitoring banner on login and a clause in the acceptable-use policy. None of this is your call to make at the keyboard. Confirm scope and authority with counsel before you turn on capture; the full framework is Chapter 25 — The Legal Framework and Appendix E — Legal Frameworks Reference. The recurring lesson from Chapter 16 holds: note what you can do; do only what you are authorized to do.

The PCAP file format

You should be able to recognize a capture file by its first four bytes and read enough of its header to know what you are holding. The classic libpcap format begins with a 24-byte global header; the first four bytes are a magic number whose byte order tells you the host endianness and the timestamp resolution:

PCAP global header (classic libpcap, 24 bytes) — little-endian, microsecond
Offset    00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F   meaning
00000000  D4 C3 B2 A1 02 00 04 00  00 00 00 00 00 00 00 00
          └ magic ─┘ └ver┘ └minor┘ └ thiszone ┘ └ sigfigs ┘
          magic 0xA1B2C3D4 stored LE ⇒ µs resolution, little-endian host
          version 2.4
00000010  00 00 04 00 01 00 00 00
          └ snaplen ┘ └ linktype ┘
          snaplen = 0x00040000 = 262144 (FULL capture — good)
          linktype = 1 = LINKTYPE_ETHERNET

Each packet that follows has a 16-byte record header:
00000018  C0 A5 7E 66 20 A1 07 00  4A 00 00 00 4A 00 00 00
          └ ts_sec ─┘ └ ts_usec ┘ └ incl_len ┘ └ orig_len ┘
          ts_sec  = 0x667EA5C0 → 2024-06-28 12:00:00 UTC
          ts_usec = 0x0007A120 = 500,000 µs (.5 s)
          incl_len = 74  (bytes actually captured)
          orig_len = 74  (bytes on the wire) → EQUAL ⇒ not truncated ✓

The magic number is your quick-identify key, and the variants are worth memorizing because they appear in Appendix A — File Signatures Reference:

On disk (first 4 bytes)   Format / meaning
D4 C3 B2 A1               classic pcap, microsecond, little-endian host
A1 B2 C3 D4               classic pcap, microsecond, big-endian host
4D 3C B2 A1               classic pcap, NANOsecond resolution (0xA1B23C4D)
0A 0D 0D 0A               PCAPNG — a Section Header Block; next look for the
                          byte-order magic 4D 3C 2B 1A (0x1A2B3C4D)

PCAPNG (the modern "next generation" format Wireshark writes by default) is block-structured rather than a flat header-plus-records, which lets it carry multiple interfaces, per-interface metadata, name-resolution records, and capture comments — including an operator annotation block you can use to embed chain-of-custody notes directly in the evidence file. The forensic takeaway is small but real: incl_len versus orig_len on each packet is your truncation check. When they are equal, you have the whole packet; when incl_len is smaller, the snaplen clipped it and the payload beyond that point is gone forever.

Capture filters versus display filters

This is the single most common confusion for newcomers, and it has forensic consequences, so it gets its own subsection. There are two completely different filter languages, applied at two different times, with two different risk profiles:

CAPTURE FILTER (BPF)              vs.   DISPLAY FILTER (Wireshark)
applied AT capture time                 applied AFTER, to a saved file
syntax: host/port/net/tcp/udp           syntax: dotted protocol fields
  host 10.20.4.51                         ip.addr == 10.20.4.51
  tcp port 443                            tcp.port == 443
  udp port 53                             dns.qry.name contains "dropbox"
  src net 10.0.0.0/8                      http.request.method == "POST"
DISCARDS non-matching packets            HIDES non-matching packets
  → excluded data is GONE                 → excluded data is still in the file

A capture filter uses Berkeley Packet Filter syntax and is applied by the kernel as packets arrive — anything that does not match is never written. That is efficient, and sometimes legally necessary (you may be authorized to capture only one host's traffic), but it is destructive: filtered-out packets are not recoverable. A display filter uses Wireshark's rich dotted-field syntax (ip.addr, tcp.flags.syn, http.host) and is applied to an already-captured file — it only hides packets from view; everything is still in the PCAP and one click restores it. The forensic rule follows directly from theme #2: when you have the authority and the storage, capture broadly with no (or a wide) capture filter, then narrow non-destructively with display filters. You can always filter a capture down later; you can never un-filter what you refused to record.


Full capture versus flow: the storage tradeoff

You will rarely have full packet capture for the window you need, and the reason is arithmetic. A fully saturated 1 Gbps link carries 125 megabytes per second — roughly 450 GB per hour, about 10.8 TB per day. Even at a realistic 10 percent average utilization that is over a terabyte a day for one link, and enterprises have dozens. Nobody keeps full PCAP for months. They keep it for hours or, on a well-funded NSM platform, a few days in a rolling buffer that ages out continuously.

The economical alternative is flow data: a summary record per conversation, with no payload at all. NetFlow (Cisco's original, with the widely deployed v5 fixed format and the template-based v9), its IETF standardization IPFIX (RFC 7011), and the sampled variant sFlow all express the same idea. A flow record is built around the canonical 5-tuple plus counters:

A FLOW RECORD (NetFlow/IPFIX) — metadata only, NO payload
┌──────────────────────────────────────────────────────────────┐
│ src IP        10.20.4.51        ┐                              │
│ dst IP        162.125.248.18    │ the 5-TUPLE                  │
│ src port      51344             │ (identifies the conversation)│
│ dst port      443               │                              │
│ protocol      6 (TCP)           ┘                              │
│ start / end timestamps          duration                      │
│ packets       1,402,118         bytes  2,147,500,032          │  ← 2.1 GB out
│ TCP flags (cumulative OR)       input/output interface, ToS   │
└──────────────────────────────────────────────────────────────┘
   NO URLs, NO content, NO filenames — but it survives for MONTHS.

A flow record is on the order of tens of bytes, so a day that would be terabytes of PCAP is a few gigabytes of flow — roughly a thousandth to a ten-thousandth of the size. That is why flow is kept for 90 days or a year while PCAP is kept for hours. The tradeoff is exactly the content-versus-metadata line from the legal note: flow tells you who talked to whom, for how long, and how many bytes moved — but never what was said. For most exfiltration and C2 investigations, that is enough to find the event and bound it in time; you reserve any surviving full capture for proving content once flow has told you where to look.

# Mine flow with nfdump: top talkers by bytes, then drill into one host.
nfdump -R /flow/2024/06/28 -s ip/bytes -n 20            # top 20 IPs by volume
nfdump -R /flow/2024/06/28 'src ip 10.20.4.51 and dst port 443' -s record/bytes
# SiLK equivalent (rwfilter -> rwstats) for the same question:
rwfilter --start-date=2024/06/28 --saddr=10.20.4.51 --proto=6 --pass=stdout \
  | rwstats --fields=dIP --values=bytes --top --count=20

Limitation. Flow's blindness to content is a feature for storage and a wall for proof. Flow can show a 2.1 GB upload from a workstation to a cloud IP at 3 a.m. — strong evidence something large left the network — but it cannot tell you the upload was the stolen CAD files rather than, say, a legitimate backup or a video call's aggregate. Sampled flow (sFlow, or NetFlow with a sampling rate like 1-in-1000) is worse: it statistically estimates volume and will miss low-and-slow transfers entirely. Treat flow as a powerful targeting signal that tells you where to point the richer sources, never as the last word on what moved. This is theme #5 wearing a network uniform.


Reading a PCAP in Wireshark

When you do have a capture covering the window, Wireshark is the microscope. Three skills carry most of the work: filtering to the relevant packets, reassembling conversations into something human-readable, and extracting the files that crossed the wire.

Display filters that earn their keep

Display filters narrow millions of packets to the handful that matter, non-destructively. The ones you will reach for constantly:

DISPLAY FILTER                                    finds
ip.addr == 10.20.4.51                             all traffic to/from a host
ip.src == 10.20.4.51 && ip.dst == 162.125.248.18  one direction of one pair
tcp.flags.syn == 1 && tcp.flags.ack == 0          connection ATTEMPTS (SYN only)
tcp.analysis.retransmission                       loss / instability
http.request.method == "POST"                     uploads / form submissions
http.host contains "dropbox"                       requests to a destination
dns.qry.name contains "dropbox"                    name lookups for a destination
dns.flags.rcode == 3                              NXDOMAIN responses (DGA tell)
dns.qry.type == 16                                TXT queries (tunneling tell)
tls.handshake.type == 1                           TLS Client Hello (new sessions)
tls.handshake.extensions_server_name              the SNI — destination even in TLS
frame contains "password"                         a byte string anywhere in payload
tcp.stream == 3                                   isolate one TCP conversation
frame.len > 1400                                  large frames (bulk transfer)

Two of these deserve emphasis because they survive encryption. tls.handshake.extensions_server_name exposes the SNI — the server name the client requested in cleartext during the TLS handshake — so even when the payload is encrypted, the intended destination is usually visible. And dns.qry.name shows the name lookups that precede the connection. Encryption hides what was said; it rarely hides who was contacted, which is why theme #3 holds even here: every action leaves a trace, and even an encrypted action leaves a metadata trace.

Following streams and reading the conversation

A TCP conversation is split across hundreds of packets, possibly reordered, with retransmissions and acknowledgments interleaved. Wireshark's Follow → TCP Stream reassembles the byte stream of one conversation into the actual application dialogue — the HTTP request and response, the SMTP envelope, the FTP commands — and Follow → HTTP Stream decodes a single web transaction. Every conversation gets a stream index you can pivot on with tcp.stream == N. The Statistics menu turns the whole file into summaries: Protocol Hierarchy shows the mix of protocols by bytes (a fast way to spot, say, an unexpected glut of DNS that signals tunneling), Conversations ranks every host pair by bytes in each direction (the exfiltration view — look for internal→external pairs where the outbound byte count dwarfs the inbound), and Endpoints ranks individual hosts.

For encrypted streams, Follow → TLS Stream shows only ciphertext unless you possess the session keys — typically via an SSLKEYLOGFILE captured from a cooperating endpoint, occasionally via the server's private key for older RSA key-exchange. Without keys, you read the metadata (SNI, certificate, timing, volume) and stop there; we return to this hard wall under "Limitations."

Extracting transferred files

This is where network forensics borrows a recovery technique. Wireshark's File → Export Objects reconstructs complete files that were transferred over HTTP, SMB, FTP-DATA, TFTP, or IMF (email), reassembling them from the packets and writing them out as standalone files. It is, in effect, file carving (from Chapter 7 — File Carving) applied to a byte stream instead of a byte device: the same idea of reconstructing a file from a contiguous run of bytes identified by protocol structure rather than disk layout.

# tshark (Wireshark's CLI) can do the same headless, for scripting/automation:
tshark -r cap.pcap --export-objects http,./extracted/     # carve HTTP file objects
tshark -r cap.pcap --export-objects smb,./extracted-smb/  # and SMB transfers
ls -l ./extracted/      # each reconstructed file written out, ready to hash

Recovery vs. Forensics. Exporting a transferred file from a capture is the clearest dual-lens moment in this chapter. For the 💾 recovery technician, a file pulled from a PCAP is a restored artifact: a document that exists nowhere else — deleted from the sender, never saved by the receiver — can be reconstructed in full from the only place it persisted, the bytes that crossed the wire. For the 🔍 forensic examiner, that same reconstructed file is proof of transmission: you can hash the carved object, show it matches the stolen original, and demonstrate it traversed the network from this source to this destination at this timestamp. One reassembly; one discipline restores the file, the other proves it moved. (The same logic recovers VoIP audio: Wireshark's Telephony → RTP reassembles RTP streams into a playable call — a recording reconstructed entirely from captured packets.)

Tool Tip. For repeatable, scriptable analysis, drive tshark from the command line and let it emit columns you can pipe into anything. The next section turns those columns into evidence at scale; for a corrupted capture that Wireshark refuses to open, pcapfix repairs the file header and salvages intact records, and editcap/mergecap slice and combine captures non-destructively — the recovery-engineer's toolkit applied to evidence files. The broader tool landscape (Wireshark, NetworkMiner, Zeek, Arkime) is surveyed in Chapter 36 — The Forensic Toolkit and Appendix C — Tool Reference.


tshark and Python: packets into evidence at scale

Clicking through Wireshark proves a point about one conversation; answering "which internal host sent the most data to external destinations, and when?" across a multi-gigabyte capture needs automation. The pattern is tshark -T fields to flatten packets into CSV, then pandas to aggregate.

# Flatten a capture into CSV: one row per packet, chosen fields as columns.
tshark -r cap.pcap -T fields \
  -e frame.time_epoch -e ip.src -e ip.dst \
  -e tcp.srcport -e tcp.dstport -e _ws.col.Protocol -e frame.len \
  -E header=y -E separator=, -E quote=d > packets.csv

# DNS-only view for the beaconing/tunneling analysis later:
tshark -r cap.pcap -Y "dns.flags.response == 0" -T fields \
  -e frame.time_epoch -e ip.src -e dns.qry.name -e dns.qry.type \
  -E header=y -E separator=, > dns_queries.csv
# Find exfiltration candidates: internal hosts pushing bytes OUTBOUND.
# Illustrative reference code (never executed in the sandbox); structurally valid.
import ipaddress
import pandas as pd

df = pd.read_csv("packets.csv")
df = df.dropna(subset=["ip.src", "ip.dst"])

def is_internal(ip: str) -> bool:
    try:
        return ipaddress.ip_address(ip).is_private
    except ValueError:
        return False

# Keep only internal -> external traffic, then sum bytes per pair.
out = df[df["ip.src"].map(is_internal) & ~df["ip.dst"].map(is_internal)]
by_pair = (out.groupby(["ip.src", "ip.dst"])["frame.len"]
              .agg(bytes_out="sum", packets="count")
              .sort_values("bytes_out", ascending=False)
              .reset_index())

# Convert epoch to UTC and bucket by hour to expose OFF-HOURS transfers.
out = out.copy()
out["hour"] = pd.to_datetime(out["frame.time_epoch"], unit="s", utc=True).dt.hour
off_hours = out[(out["hour"] >= 0) & (out["hour"] < 6)]   # 00:00–06:00 UTC

print(by_pair.head(10))                 # top outbound talkers
print(off_hours.groupby("ip.src")["frame.len"].sum().sort_values(ascending=False))

The output of that script — 10.20.4.51 → 162.125.248.18, 2,147,500,032 bytes_out, concentrated in the 3 a.m. hour — is the network equivalent of finding the suspect's fingerprints. It does not yet prove what moved, but it tells you precisely which conversation to reconstruct and which proxy and DNS records to pull. A reusable, hardened version of this and the DNS analyzers below lives in Appendix B — Python Forensics Toolkit.


DNS: the protocol that narrates the investigation

Almost every network action begins with a name lookup. Before a workstation connects to a cloud account, it asks "what is the IP for www.dropbox.com?"; before malware phones home, it resolves its C2 domain. DNS is therefore a running narration of intent, and because the queries are small and operationally valuable, DNS logs are frequently retained when full capture is not. Three malicious patterns hide in DNS, and you must be able to spot each.

Beaconing

Command-and-control malware "checks in" with its operator on a schedule. That regularity is the tell: a host querying the same domain (or hitting the same IP) at a fixed interval — every 60 seconds, every 5 minutes — with low variance, for hours, is beaconing. Humans browse in bursts and pauses; a machine on a timer produces a near-constant interval. You quantify it with the coefficient of variation of the inter-arrival times: low variation plus high count equals beacon.

# Beaconing score: low coefficient of variation of inter-arrival deltas = regular.
import statistics

def beacon_score(timestamps):
    ts = sorted(timestamps)
    deltas = [b - a for a, b in zip(ts, ts[1:])]
    if len(deltas) < 5:
        return None                       # too few events to judge
    mean = statistics.mean(deltas)
    if mean == 0:
        return None
    cv = statistics.pstdev(deltas) / mean # coefficient of variation
    return {"events": len(ts), "interval_s": round(mean, 1), "cv": round(cv, 3)}

# A C2 beacon looks like: {'events': 480, 'interval_s': 60.0, 'cv': 0.004}
# Normal human traffic to a domain looks like: cv > 0.5, irregular.

In the ransomware anchor (Chapter 12 — Ransomware Recovery), the intrusion that preceded the encryption beaconed for days before the operators pulled the trigger. The DNS log held a steady 60-second heartbeat to a single domain — the kind of metronome no human produces — which, had anyone been watching, would have exposed the foothold long before the ransom note.

Domain Generation Algorithms (DGA)

To make C2 takedown harder, malware families generate a stream of pseudo-random domain names from a seeded algorithm (e.g., kq3v9zxr1p7m.com, xn4f8wq2lt6b.net), trying each until one resolves to the live server the operator registered that day. The forensic signatures are twofold: the queried names have high character entropy (they look like keyboard mashing, not words), and most of them fail to resolve, producing a burst of NXDOMAIN (rcode 3) responses as the malware walks its list looking for the one live domain.

# Shannon entropy of the queried label: high entropy ⇒ algorithmic/random.
import math
from collections import Counter

def shannon_entropy(s: str) -> float:
    if not s:
        return 0.0
    n = len(s)
    return -sum((c / n) * math.log2(c / n) for c in Counter(s).values())

# "google"        -> ~2.6 bits/char  (low; real word)
# "kq3v9zxr1p7m"  -> ~3.6 bits/char  (high; DGA candidate)
# Combine with: many distinct subdomains/SLDs per host + many NXDOMAIN = strong DGA.

A single high-entropy domain is noise; dozens of high-entropy domains queried by one host in a minute, most returning NXDOMAIN, is a DGA infection with very little ambiguity.

DNS tunneling

DNS can be abused not just to find a destination but to be the exfiltration channel. In DNS tunneling, data is encoded into the subdomain labels of queries to an attacker-controlled domain (<base32-encoded-chunk>.tunnel.evil-c2.net), and responses (often TXT or NULL records) carry data back. Because DNS is almost always allowed outbound even on locked-down networks, it is a favored covert channel — tools like iodine and dnscat2 implement it. The signatures: an unusually high query volume to a single domain, abnormally long query names (humans do not type 200-character hostnames), high-entropy labels, unusual record types (a flood of TXT or NULL), and large response sizes. You can spot it in a Wireshark protocol hierarchy as a suspicious bulge of DNS traffic, or quantify it: bytes-per-domain and unique-subdomains-per-domain that dwarf every other resolved name.

NORMAL DNS                            DNS TUNNELING (exfiltration)
www.dropbox.com           A           M3J2HGQZ1A.... .tunnel.evil-c2.net  TXT
api.github.com            A           K9F2LP0WX8.... .tunnel.evil-c2.net  TXT
outlook.office365.com     A           Q7Z1V4NB6T.... .tunnel.evil-c2.net  TXT
  short names, A/AAAA,                  200-char labels, TXT/NULL, ONE domain,
  many domains, low volume               thousands of queries, high entropy

Limitation: the encrypted-DNS blind spot. The plaintext DNS analysis above assumes you can see the queries. Increasingly you cannot. DNS over HTTPS (DoH) and DNS over TLS (DoT) encrypt the queries themselves — DoH rides ordinary port-443 HTTPS to a resolver like cloudflare-dns.com or dns.google, hiding the lookups inside a TLS session indistinguishable from web browsing. When a host uses DoH, your DNS log goes dark; you no longer see which names were resolved, only that the host talked to a known DoH provider. The pivots that remain are the TLS SNI of the actual connections that follow, endpoint-side DNS client logs (if the OS records them), and detecting DoH usage itself as a policy signal. This is theme #4 in action — technology changes, principles don't: the protocol evolved and blinded one sensor, so you move to the sensors that still see, and the method (find the lookup, then the connection, then the content) is unchanged even though the tool must change.


Firewall, proxy, and server logs

Captures are rare; logs are common. Three log families answer most network questions, in ascending order of detail.

Firewall logs record connection-level decisions: a timestamp, the 5-tuple, the action (allow/deny), often the byte count and the rule that fired. They are flow data with a verdict attached. They tell you a connection from 10.20.4.51 to 162.125.248.18:443 was allowed at 03:14, and how much data it carried — but nothing about the URL or content. Their special value is the deny records: a burst of denied outbound connections can reveal malware trying to reach blocked C2, and the absence of expected deny entries can confirm a path was open.

Proxy logs are the exfiltration goldmine, because a forward proxy operates at the URL layer and usually authenticates the user. Even for HTTPS, where the proxy cannot see inside the TLS tunnel, the CONNECT method records the destination host, the port, the authenticated username, the user-agent, and the byte counts in each direction. If the proxy performs SSL inspection (a sanctioned man-in-the-middle), it sees the full URL and content too. A native Squid log line for our anchor reads:

# Squid native access.log:  time  elapsed  client  result/code  bytes  method  URL  user  hierarchy  type
1719545647.231  2891088  10.20.4.51  TCP_TUNNEL/200  2147500032  CONNECT  www.dropbox.com:443  jrivera  HIER_DIRECT/162.125.248.18  -
                ^epoch 2024-06-28 03:14:07 UTC        ^2.1 GB up   ^HTTPS    ^destination host    ^USER   ^resolved to this IP

Read that one line. At 03:14:07 UTC, the authenticated user jrivera, from workstation 10.20.4.51, opened an HTTPS tunnel to www.dropbox.com:443 (resolved to 162.125.248.18) and pushed 2.1 GB through it over 48 minutes. The proxy could not read the encrypted bytes, but it recorded who, to where, how much, and when — and it attributed the activity to a named user account, the same way MountPoints2 attributed the USB device in the Windows chapter. That is attribution the suspect's "PC cleaner" never had the reach to touch.

Web server logs (Apache/NGINX/IIS) matter when your server is the subject — a defaced site, a web-app compromise, a data-scraping incident. They use the Common Log Format or, more usefully, the Combined Log Format, which adds referrer and user-agent:

# Combined Log Format:  host  ident  user  [time]  "request"  status  bytes  "referer"  "user-agent"
203.0.113.77 - - [28/Jun/2024:03:11:52 +0000] "POST /upload.php HTTP/1.1" 200 4194304 "-" "python-requests/2.31"
                                                ^POST upload  ^4 MB   ^a scripted client, not a browser

The python-requests user-agent on a POST at 3 a.m. is the kind of detail that separates automated abuse from human browsing — a small inconsistency that, exactly like a timestomped file's $SI`/`$FN contradiction, points one direction.

Recovery vs. Forensics. Logs serve both disciplines, though the recovery use is narrower. A 🔍 examiner reads proxy and DNS logs to prove where data went and who sent it. A 💾 recovery technician occasionally reads the same logs to locate lost data's new home: a client who deleted the only copy of a document but uploaded it to a cloud service can be pointed — via the proxy log's destination host and the DNS resolution — to exactly which service and account to recover it from. Same records; one proves the transfer, the other follows it to restore the file.


IDS/IPS and Zeek: from alerts to structured network logs

A security team that invested in network monitoring gives you two more evidence classes that turn raw packets into meaning: signature alerts and structured protocol logs.

Snort and Suricata

An Intrusion Detection System inspects traffic against a ruleset and raises an alert when traffic matches a known-bad pattern; an Intrusion Prevention System does the same inline and can block the match. Snort and Suricata are the dominant open engines (Suricata is multi-threaded and additionally emits rich JSON), both driven by signatures like the Emerging Threats ruleset. A rule has an action, a protocol, a source/direction/destination, and options:

alert tcp $HOME_NET any -> $EXTERNAL_NET 443 ( \
    msg:"ET POLICY Dropbox.com Offsite File Backup in Use"; \
    flow:established,to_server; \
    tls.sni; content:"dropbox.com"; \
    classtype:policy-violation; sid:2014726; rev:3; )

Suricata's EVE JSON output (eve.json) is the format you will actually parse, because it records not only alerts but flows, DNS, HTTP, TLS, and file events in one stream keyed by a shared flow_id:

{"timestamp":"2024-06-28T03:14:07.512+0000","flow_id":884412,
 "src_ip":"10.20.4.51","src_port":51344,"dest_ip":"162.125.248.18","dest_port":443,
 "proto":"TCP","event_type":"alert",
 "alert":{"signature_id":2014726,"rev":3,
          "signature":"ET POLICY Dropbox.com Offsite File Backup in Use",
          "category":"Potential Corporate Privacy Violation","severity":2},
 "tls":{"sni":"www.dropbox.com","version":"TLS 1.3"}}

The non-negotiable discipline: an IDS alert is a lead, not a finding. Signatures generate false positives, fire on benign traffic that resembles bad traffic, and miss anything novel. The alert tells you where to look; you confirm by examining the actual traffic — the flow record, the proxy log, and any surviving capture — before you write a word in a report. An examiner who testifies "the IDS alerted, therefore exfiltration occurred" will be dismantled on cross-examination (Chapter 27 — Expert Testimony). An examiner who testifies "the IDS alerted, and the proxy log shows a 2.1 GB upload by the named user to that destination at that time, and the flow record confirms the volume" has corroboration.

Zeek: the network's structured logbook

Zeek (formerly Bro) is not primarily a signature engine; it is a protocol analyzer that watches traffic and writes a structured, durable log of everything it understands, whether or not anything is "bad." For the examiner this is often the single most valuable network source, because it is compact (metadata, not payload — so it is kept for a long time) yet semantically rich. Zeek emits many logs; the ones you will live in:

ZEEK LOGS (the join key is the connection UID — same uid across every log)
conn.log    every connection: 5-tuple, service, duration, bytes each way,
            conn_state, history  ← the master flow log
dns.log     every query/response: query, qtype, rcode, answers
http.log    every HTTP transaction: method, host, uri, user_agent, status,
            request/response body lengths, resp_mime_types
files.log   every file seen on the wire: mime type, MD5/SHA1/SHA256, size, source
            (Zeek can EXTRACT the file too)
ssl.log     every TLS session: SNI (server_name), version, cipher, JA3/JA3S,
            cert subject/issuer, validation_status
x509.log    certificate details        notice.log / weird.log  anomalies Zeek flagged
smtp.log ftp.log smb_files.log ntlm.log kerberos.log ssh.log dhcp.log ...

The genius of Zeek for forensics is the uid: every connection gets a unique identifier, and that same uid appears in every log line about that connection. So a single TLS upload is one row in conn.log (with the byte volume), one row in ssl.log (with the SNI and certificate), one row in dns.log (the lookup that preceded it), and — if a file was transferred in cleartext — a row in files.log with the file's hash. Pivot on the uid and you assemble the whole story of one connection across protocols. A conn.log excerpt for the anchor:

#fields  ts  uid  id.orig_h  id.orig_p  id.resp_h  id.resp_p  proto  service  duration  orig_bytes  resp_bytes  conn_state  history
1719545647.231  CwXmn21H8aT  10.20.4.51  51344  162.125.248.18  443  tcp  ssl  2891.09  2147483648  18422  SF  ShADadDfF
                ^03:14:07 UTC  ^join key   ^workstation     ^Dropbox IP          ^TLS  ^48 min  ^2 GiB OUT  ^18 KB in  ^clean  ^orig pushed data

Two fields turn this row into evidence. orig_bytes (2,147,483,648) versus resp_bytes (18,422) is a screaming asymmetry: a workstation that sent two gigabytes and received eighteen kilobytes is uploading, not browsing — normal web traffic is the opposite ratio. And the history field (ShADadDfF) is Zeek's compact connection narrative: capital letters are the originator (the workstation), lowercase the responder; S=SYN, h=SYN-ACK, A/a=ack, D/d=data, F/f=fin. The string says the workstation initiated, the handshake completed, the workstation sent the bulk of the data (the repeated capital D), and both sides closed cleanly (conn_state SF = normal establishment and finish). That is a successful, complete, originator-driven bulk upload, described in eleven characters.

# Select the columns you want from Zeek's TSV with zeek-cut, then analyze in pandas.
cat conn.log | zeek-cut id.orig_h id.resp_h id.resp_p service orig_bytes resp_bytes conn_state \
  > conn_slim.tsv
# Upload-asymmetry hunt over Zeek conn.log: orig_bytes >> resp_bytes to external IPs.
import ipaddress
import pandas as pd

cols = ["orig_h","resp_h","resp_p","service","orig_bytes","resp_bytes","conn_state"]
df = pd.read_csv("conn_slim.tsv", sep="\t", names=cols, na_values="-")
for c in ("orig_bytes","resp_bytes"):
    df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0)

def external(ip):
    try:    return not ipaddress.ip_address(str(ip)).is_private
    except ValueError: return False

cand = df[df["resp_h"].map(external) & (df["orig_bytes"] > 50_000_000)].copy()
cand["ratio_out_in"] = cand["orig_bytes"] / cand["resp_bytes"].clip(lower=1)
cand = cand[cand["ratio_out_in"] > 10]            # 10x more sent than received
print(cand.sort_values("orig_bytes", ascending=False)
          [["orig_h","resp_h","resp_p","orig_bytes","resp_bytes","ratio_out_in"]]
          .head(20))

Tool Tip. When you have a PCAP but no pre-existing logs, you can generate them: run the capture through Zeek (zeek -r cap.pcap) and Suricata (suricata -r cap.pcap) offline, and you get the full structured log set and alert set from the raw packets, after the fact, on your analysis workstation — no live sensor required. This is the standard way to turn a seized capture into queryable evidence, and it is entirely repeatable, which matters for validation under Daubert. NetworkMiner complements this with a content-centric GUI that auto-extracts files, images, credentials, and a host inventory from a PCAP with no query language at all — excellent for triage and for showing a non-technical audience what crossed the wire.


Detecting data exfiltration

Pull the threads together, because exfiltration detection is the synthesis skill of this chapter and the heart of the anchor case. There is no single "exfiltration" signature; there is a constellation of signals, and the finding is built by stacking them:

  • Volume asymmetry. Clients normally download far more than they upload. An internal host whose outbound bytes to an external destination dwarf its inbound bytes is the primary signal — the orig_bytes >> resp_bytes you just hunted in Zeek.
  • Off-hours timing. Bulk transfers at 3 a.m., on weekends, or during the suspect's notice period (as in the anchor) are temporally anomalous. Bucket traffic by hour and let the outliers surface.
  • Destination reputation — with care. Connections to known-bad IPs from threat intelligence are damning when present, but sophisticated exfiltration uses legitimate services — Dropbox, Google Drive, Mega, pastebin, even a personal Gmail draft folder — precisely because they are allow-listed and blend in. Do not assume a "clean" destination means innocent traffic.
  • Beaconing and long-lived connections. The C2 heartbeat (regular interval, low variance) and connections that stay open for hours both deviate from normal short web sessions.
  • DNS tunneling. Covered above — exfiltration hidden inside DNS itself.
  • Protocol/port mismatch. TLS on a non-standard port, or — the inverse — a port-443 connection that is not TLS, or SSH tunneling over 443 to evade filtering. Zeek's service field (which reflects the protocol Zeek actually detected, not the port number) versus the port exposes these.

In the ransomware anchor, this constellation appears in its now-standard "double extortion" form: before encrypting anything, the operators staged and exfiltrated gigabytes of the victim's data so they could threaten to publish it. Had the victim's flow or Zeek logs been reviewed, the days-long beacon plus a large off-hours outbound transfer to a hosting-provider IP would have exposed both the foothold and the theft before the encryption ever ran — turning an unrecoverable disaster (no backups, Chapter 12) into a contained incident. The network record is often the only place the exfiltration half of a double-extortion attack is visible at all, because the staging files are deleted from disk and the malware is built to be quiet on the endpoint. Malware C2 analysis goes deeper in Chapter 32 — Malware Forensics.

EXFILTRATION SIGNAL STACK — none alone is proof; together they are a finding
┌────────────────────────────┬──────────────────────────────────────────────┐
│ Signal                     │ Where you see it                             │
├────────────────────────────┼──────────────────────────────────────────────┤
│ outbound >> inbound bytes  │ Zeek conn.log, flow, firewall, proxy bytes   │
│ off-hours / notice-period  │ timestamps across all sources (normalize UTC)│
│ destination (good or bad)  │ DNS log, proxy host, TLS SNI, threat intel   │
│ beacon / long duration     │ DNS interval analysis, conn.log duration     │
│ DNS tunneling              │ dns.log: long/high-entropy labels, TXT flood │
│ protocol≠port              │ Zeek service field vs. resp_p                 │
│ attributed user            │ proxy username, endpoint correlation (Ch.16) │
└────────────────────────────┴──────────────────────────────────────────────┘

The network timeline: correlating the wire with the endpoint

A network finding becomes court-grade when it is fused with the endpoint evidence into one timeline — and the technical prerequisite for fusion is time normalization. Every source stamps events with its own clock: the capture host, the proxy, the DNS server, the firewall, and the suspect's workstation may each be off by seconds or, if NTP failed, by minutes or hours, and they may report local time in different zones. Convert everything to UTC, record each source's clock offset, and account for skew explicitly; an uncorrected clock difference can make a cause look like it followed its effect, and opposing counsel will find it. This is the same discipline you will formalize in Chapter 21 — Timeline Analysis, where plaso/log2timeline ingests both host artifacts and many network log formats into a single super-timeline.

The payoff is a chain no single source could carry. The disk side (Chapter 16) proved which CAD files were opened from the thumb drive and that a cleaner ran to hide it. The network side proves where the copies went. Fused and normalized to UTC, the engineer's exfiltration reads as one continuous story:

ANCHOR #2 — FUSED TIMELINE (UTC), user jrivera / workstation 10.20.4.51
Fri 19:04  Opened TurbineHousing_v7.sldprt from E: (USB)        [DISK: LNK + Jump List, Ch.16]
Fri 19:18  DNS A query: www.dropbox.com → 162.125.248.18        [NET: dns.log]
Fri 19:19  TLS Client Hello, SNI www.dropbox.com                [NET: ssl.log, uid CwXmn21H8aT]
Sat 03:14  Proxy CONNECT www.dropbox.com:443, user jrivera,
           2,147,500,032 bytes uploaded over 48 min            [NET: proxy access.log]
Sat 03:14  Zeek conn.log: orig_bytes 2.0 GiB, resp_bytes 18 KB,
           conn_state SF (clean bulk upload)                    [NET: conn.log, same uid]
Sat 03:14  IDS alert sid 2014726 "Offsite File Backup in Use"   [NET: Suricata eve.json]
Sat 09:12  TurbineHousing_v7.sldprt sent to Recycle Bin         [DISK: $I metadata, Ch.16]
Sat 09:14  CCleaner run #3 (wipe free space)                    [DISK: Prefetch, Ch.16]
           — the cleaner erased the DISK trail; it could not
             reach the PROXY, DNS, flow, or Zeek records.       [theme #3]

Every line is sourced; the network lines are stamped by infrastructure the suspect never controlled; and the contradictions all point one way. That is the chapter's central lesson made literal: anti-forensics on the endpoint cannot reach the network's witnesses.


Worked example: tracing the exfiltration

Walk the network investigation end to end, as you would run it, each step using a skill from this chapter. You receive: 90 days of NetFlow, 30 days of Squid proxy logs, 30 days of Zeek logs, 7 days of Suricata eve.json, and — by luck — a 48-hour rolling full capture that happens to cover the suspect weekend. Counsel's question: did proprietary data leave the network to an external destination, attributable to user jrivera?

1 — Find the event in the lean-but-long source (flow). Start where retention is longest. nfdump over the suspect's notice period, filtered to src ip 10.20.4.51, sorted by bytes, surfaces one enormous outbound conversation: 2.1 GB to 162.125.248.18 on port 443, Saturday 03:14. Flow cannot say what moved, but it has told you the exact conversation, destination, and minute to pursue.

2 — Identify the destination (DNS + TLS SNI). Zeek's dns.log shows 10.20.4.51 resolved www.dropbox.com to 162.125.248.18 Friday 19:18; ssl.log shows a TLS session to that IP with SNI www.dropbox.com. The destination is a personal-cloud service — a classic exfiltration endpoint that no IP-reputation list would flag.

3 — Attribute to a user (proxy). The Squid access.log line for that connection carries the authenticated username jrivera, the destination host, and 2,147,500,032 bytes uploaded. This is the attribution leap: not "workstation 10.20.4.51," but the named account driving it — and the proxy recorded it because corporate policy routes all web traffic through an authenticated proxy.

4 — Confirm the shape of the transfer (Zeek conn.log). The conn.log row (same uid as the ssl.log session) shows orig_bytes 2.0 GiB, resp_bytes 18 KB, conn_state SF, history ShADadDfF — a clean, complete, originator-driven bulk upload. The asymmetry alone distinguishes upload from browsing.

5 — Corroborate with the alert (Suricata). eve.json holds alert sid 2014726 ("Offsite File Backup in Use") on that flow at 03:14. The alert is not the proof — it is the fifth independent source agreeing with the first four.

6 — Recover content from the capture (Export Objects). Because the 48-hour rolling capture covers the window, you reconstruct the transferred objects with tshark --export-objects. The connection is TLS, so without session keys you cannot read the encrypted payload directly — but the Friday 19:04 file-open evidence from the disk, the Friday 19:18 DNS lookup, and the Saturday 03:14 upload of a volume consistent with the CAD project's size, all fused, support the inference. Where an unencrypted transfer exists in the capture, you carve the file, hash it, and match it to the stolen original outright.

7 — Fuse and normalize. Merge the network timeline with the disk timeline from Chapter 16, every event in UTC, every source's clock offset recorded. The result is the fused timeline shown above: file opened from USB, destination resolved, 2.1 GB uploaded under the user's credentials at 3 a.m., IDS confirming, and — six hours later — the disk-side cleanup that erased the local trail but never reached the network's records.

Ethics Note. Reconstruct only what the records support, and state every inference as an inference. "2,147,500,032 bytes were uploaded by authenticated user jrivera to www.dropbox.com at 2024-06-28 03:14 UTC" is a finding, sourced to the proxy log. "jrivera stole the turbine-housing design" is a legal conclusion you do not get to assert — and "the upload was the CAD files" is an inference that must be labeled as one unless you carved and hash-matched the content. The sixth theme is never louder than here: behind this timeline is a person whose career and liberty may turn on your precision. Keep findings, inferences, and conclusions in three separate boxes; Chapter 26 — The Forensic Report shows you how, and Chapter 28 — Ethics holds the duty.

War Story. A real insider case turned on the gap between two clocks. The examiner's first timeline appeared to show the cloud upload happening before the file was opened on the endpoint — an impossibility that the defense seized on to argue the whole analysis was unreliable. The "impossibility" was a 47-second NTP drift on the proxy that no one had reconciled to UTC. Once both clocks were normalized and the offset documented, the sequence was correct and intuitive, and the timeline held. The lesson the examiner carried forward — and you should adopt now — is that in network forensics, the clock is evidence too. Record every source's time base and offset before you fuse anything; an unstated skew is a finding waiting to be impeached.


Common mistakes

  • Capturing with a truncating snaplen. Forgetting -s 0 (or trusting a small default) clips every packet at the snapshot boundary and throws away the payload permanently. Verify incl_len == orig_len; a capture that amputated the application data cannot be re-run after the traffic is gone.
  • Confusing capture filters with display filters. A BPF capture filter (host 10.0.0.5) permanently discards non-matching packets; a Wireshark display filter (ip.addr == 10.0.0.5) only hides them. Over-narrow at capture time and the evidence you needed is never written.
  • Treating an IDS alert as a finding. Signatures false-positive and false-negative constantly. An alert tells you where to look; you confirm with the flow, proxy, Zeek, and any capture before you conclude anything. "The IDS fired" is a lead, not proof.
  • Equating an IP address with a person. NAT/CGNAT, shared workstations, VPNs, Tor, spoofed source addresses, and open Wi-Fi all sever the IP→person link. An IP identifies (at best) a network interface at a moment; tying it to a human needs subscriber records, internal NAT logs, and endpoint corroboration — not assumption.
  • Ignoring clock skew. Fusing sources without normalizing to UTC and recording each clock's offset produces timelines where effects precede causes. The clock is evidence; document every source's time base.
  • Forgetting that flow has no content. Flow/NetFlow proves volume and endpoints, never what moved. Presenting "2.1 GB to a cloud IP" as "the stolen files were uploaded" overstates what flow can carry; reserve content claims for captures you actually reconstructed.
  • Letting the capture file go un-hashed. The PCAP is acquired evidence. Hash it at acquisition (capinfos -H), work on copies, and log the chain of custody exactly as for a disk image. An un-hashed capture is an un-authenticated exhibit.
  • Missing encrypted-DNS and TLS blind spots. Concluding "no malicious domains were queried" when the host used DoH, or "the destination is unknown" when the SNI was right there in the TLS handshake, are symmetric errors. Know which sensor went dark and pivot to the ones that still see.

Limitations: knowing when to stop

Network forensics has hard walls, and a professional report names them as plainly as its findings.

The first and largest is encryption. The overwhelming majority of traffic is now TLS, and TLS 1.3 encrypts more of the handshake than its predecessors. Without session keys you can read the metadata — SNI, certificate, timing, direction, and volume — but never the content. You can prove that 2.1 GB went to Dropbox; you cannot, from the wire alone, prove the bytes were the stolen design rather than a backup. QUIC (HTTP/3 over UDP/443) and Encrypted Client Hello push even the SNI out of reach on some connections. The honest finding is "an encrypted transfer of N bytes occurred to destination D at time T," with the content characterized only by inference or by endpoint corroboration.

The second wall is capture coverage. You cannot analyze packets that were never captured. If no sensor watched the relevant segment during the relevant window, the packets are gone — not deleted, never persisted. Flow may survive where full capture does not, but flow is metadata only, and sampled flow (sFlow, 1-in-N NetFlow) statistically misses low-and-slow exfiltration entirely. "No evidence of exfiltration was observed in the available records" is not the same statement as "no exfiltration occurred," and you must write the one the data supports.

The third wall is attribution, and it is where overreach ends careers. An IP address is a network locator, not an identity. Carrier-grade NAT puts thousands of subscribers behind one address; VPNs and Tor relocate the apparent source; a shared family or office machine has many users; source addresses can be spoofed for one-way traffic. Tying network activity to a person requires layering subscriber records (via legal process to the provider — Chapter 25), internal NAT and DHCP logs to translate public to internal, an authenticated proxy or directory record to name the account, and endpoint artifacts to put the person at the keyboard. This caution is at its most consequential in the court anchor (case #4): network and ISP records can place a download at a particular subscriber's IP at a particular time — a vital investigative lead — but the device, the account, and ultimately the person responsible must be established by the disk, the home-network topology, and the totality of evidence, never by the IP alone. Handled here strictly as procedure and law: the network locates; it does not, by itself, identify. The ethical weight of that distinction is Chapter 28's to carry.

Smaller but real: retention windows age out the source you need before you arrive; DoH/DoT blind the DNS log; NAT and load balancers rewrite addresses between sensors so the same flow looks like different conversations at different points; and clock skew corrupts fusion unless corrected. When the surviving records cannot answer the question, the fifth theme is the professional answer: "the available network evidence is insufficient to determine whether X occurred" is a valid, defensible finding. Forcing the packets to say more than they recorded is how a network examiner is impeached.


Progressive project: analyze the network evidence for your case

Continue building your Forensic Case File (introduced in Chapter 5 — The Forensic Process). Your case package includes a network evidence set — a PCAP plus exported proxy, DNS, and flow logs (see Appendix J — Practice Images and Lab Setup for where to obtain practice captures). This chapter you add the network layer:

  1. Acquire and authenticate. Hash the provided PCAP with capinfos -H and record it in your chain-of-custody worksheet (Appendix F). Note the snaplen, the capture's start/stop times, and whether any packets are truncated (incl_len < orig_len). Work only on a copy.
  2. Generate the structured logs. Run the capture through Zeek (zeek -r cap.pcap) and Suricata (suricata -r cap.pcap) to produce conn.log, dns.log, http.log, ssl.log, files.log, and eve.json. Hash and log each output.
  3. Hunt the exfiltration. Using tshark/zeek-cut and the Python sketches from this chapter, answer four questions and cite the exact source (log + field) behind each: (a) Which internal host sent the most data to an external destination, and how much? (b) What was that destination (DNS query + TLS SNI), and who was the authenticated user (proxy log)? (c) Is there evidence of beaconing or DNS tunneling (interval/entropy analysis of dns.log)? (d) Did any IDS signatures fire on the conversation, and did the flow/proxy/Zeek records corroborate them?
  4. Extract and hash any transferred files. Run tshark --export-objects http,./out (and smb) on the capture; hash every reconstructed object and note whether it matches a known file in the case.
  5. Build the network timeline, normalized to UTC. Record each source's clock offset. Then fuse it with your Windows-artifact timeline from Chapter 16, flagging any finding whose limits you must state in the report — content-unknown-because-encrypted, IP-not-person, no-coverage-gaps.

Save every log, CSV, extracted object, and timeline into the case-file folder. The capstone in Chapter 38 — The Capstone Investigation assembles the whole file; do not let this chapter's evidence live only in your head.


Summary

Network forensics inverts the book's founding theme: on the wire, the default state of evidence is destroyed, and the only data you will ever have is what some sensor wrote down as the packets passed. That single fact drives the whole discipline — you inventory what was watching and what it kept, you start from the lean-but-long sources (flow, DNS, firewall, proxy logs) to find and bound an event in time, and you pivot to the rich-but-short sources (Zeek, IDS, and any surviving full capture) to characterize it. You learned to capture correctly with tcpdump/dumpcap — full snaplen (-s 0), no name resolution during capture, rotation rather than one fragile giant file — and to treat the resulting PCAP as acquired evidence: hashed with capinfos, worked on copies, documented with the same chain-of-custody rigor as a disk image. You can recognize a capture by its magic bytes (D4 C3 B2 A1 classic µs, 0A 0D 0D 0A PCAPNG), read its header, and check incl_len against orig_len for truncation. You separated the two filter languages — destructive BPF capture filters versus non-destructive Wireshark display filters — and learned to capture broadly and narrow later. You weighed full capture against flow: content versus metadata, terabytes-per-day versus a thousandth of that, hours of retention versus months. In Wireshark and tshark you followed TCP/HTTP streams, read conversation statistics for upload asymmetry, and exported transferred files — file carving applied to a byte stream, a technique that restores a file for recovery and proves transmission for forensics. You read DNS as the investigation's narration: beaconing (low-variance intervals), DGA (high entropy plus NXDOMAIN floods), and DNS tunneling (long high-entropy labels, TXT/NULL floods to one domain) — while naming the DoH/DoT blind spot. You mined firewall, proxy, and web-server logs, and saw the proxy attribute a 2.1 GB HTTPS upload to a named user even through TLS. You turned alerts into structured evidence with Snort/Suricata (leads, never findings) and Zeek (conn.log/dns.log/http.log/files.log/ssl.log, joined by the uid, with orig_bytes/resp_bytes asymmetry and the history string telling the connection's story). You assembled the exfiltration signal stack, fused the network timeline with the endpoint timeline under a strict UTC clock discipline, and watched anti-forensics defeat itself again — the suspect cleaned the disk but could not reach the proxy, DNS, flow, or Zeek records that infrastructure kept on his behalf. And you marked the walls: encryption hides content, missing coverage means missing packets, and an IP address is a locator, not a person. The protocols will keep changing — TLS 1.3, QUIC, DoH, ECH — but the method does not: know what was watching, acquire it cleanly, correlate across independent sources, normalize the clocks, and state every finding with its limits.

You can now: - Capture traffic forensically with tcpdump/dumpcap (full snaplen, no live resolution, rotation), hash the result, and document its chain of custody; and recognize PCAP/PCAPNG by magic bytes and header fields. - Distinguish destructive BPF capture filters from non-destructive Wireshark display filters, and choose full capture versus NetFlow/IPFIX flow by the content-versus-metadata and storage tradeoff. - Analyze a PCAP in Wireshark/tshark — follow streams, read conversation statistics for upload asymmetry, decode protocols, and export/reconstruct transferred files. - Analyze DNS for beaconing, DGA, and tunneling, and read firewall, proxy, and web-server logs to attribute activity (including to a named user) even across TLS. - Turn IDS/IPS alerts (Snort/Suricata) into corroborated findings and mine Zeek logs (conn/dns/http/files/ssl) joined by uid, using orig_bytes/resp_bytes and the history field. - Detect data exfiltration by stacking signals, fuse a UTC-normalized network timeline with endpoint artifacts, and state the limits encryption, coverage gaps, and IP-not-person impose.

What's next. Chapter 24 — Mobile Device Forensics — takes the investigation off the wire and into the pocket: the acquisition methods (logical, file-system, physical, and the cloud-backup path), the SQLite databases and plists where iOS and Android keep messages, location, and app data, and why the most personal device a person owns is also the most locked-down — another proof that technology changes, principles don't.


Practice in exercises.md, test yourself with the quiz, apply it in the case studies, review the key takeaways, and go deeper with further reading.