Signal9Signal9 — home
online
all lessons

Packet Capture Triage: tcpdump, BPF Filters, and Reading a Capture for Malicious Traffic

Intermediate40 min7 steps
What you'll learn
  • Capture traffic to a pcap and read it back as text with tcpdump -nnr, and read a capture line
  • Write BPF (Berkeley Packet Filter) expressions to scope a capture by host, net, port, protocol and direction
  • Hunt C2 beaconing by regularity: the same external host, constant small packets, on a fixed interval
  • Hunt exfiltration by volume — and know why the loudest host is the exfil, not the C2
  1. Why read the wire, and the two shapes of bad traffic

    Frame the task: a capture is the tape of what crossed the wire — replay it for attacker behaviour.

    In Active Connections you read the live table with ss — who is talking to whom right now. But an incident is rarely happening the instant you log in. A packet capture is the tape: every packet that crossed an interface, timestamped, so you can replay what happened — including the short beacon that fired an hour ago and the transfer that already finished.

    On a server with no GUI, tcpdump is your Wireshark. Malware traffic, stripped of the packets' contents, still leaves two shapes you can see:

    • Command & control (C2 beaconing) — an implant phoning home on a timer. The tell is regularity: the same external host, small same-size packets, a fixed interval. It's quiet — low volume by design, so it hides under normal traffic.
    • Exfiltration — data leaving. The tell is volume: a burst of full-size packets to one destination.

    The rest of this lesson is finding each one in a capture, and not confusing them.

  2. tcpdump: capture to a pcap, read it back as text

    Record traffic to a file and decode it, and learn to read a single capture line.

    tcpdump does two jobs: capture live traffic to a .pcap file, and read a .pcap back and print it as text. In an investigation you usually receive a pcap from a sensor and read it; knowing how it was captured tells you what you can trust.

    Example — not runnable on your lab box

    tcpdump -nni eth0 -w /evidence/cap.pcap -c 5000 'tcp'
    tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    5000 packets captured
    -n / -nn
    Don't resolve names (-n) or port numbers (-nn). You want raw IPs and numeric ports so nothing hides behind a friendly name.
    -i eth0
    Capture on this interface. -i any captures on all of them.
    -w file
    Write raw packets to a .pcap (binary) instead of printing — the file you keep as evidence.
    -c 5000
    Stop after N packets — bound the capture so it can't run forever.
    This is how the evidence file is made: a bounded capture of TCP traffic on eth0, written to a pcap. As a responder you're usually handed this file rather than making it.

    Example — not runnable on your lab box

    tcpdump -nnr /evidence/cap.pcap
    12:00:00.000000 IP 10.0.0.5.49500 > 45.77.8.9.8443: Flags [P.], seq 1:513, ack 1, length 512
    12:00:03.114201 IP 10.0.0.5.51022 > 203.0.113.20.443: Flags [P.], length 812
    12:01:00.007000 IP 10.0.0.5.49501 > 45.77.8.9.8443: Flags [P.], length 512
    -r file
    Read packets from a saved pcap instead of a live interface — no root, no traffic needed. This is the read-back that turns a binary pcap into greppable text.
    Read one line as: TIME IP SRC.PORT > DST.PORT: Flags [..], length N. Left of the > is the source (10.0.0.5 on a high random port); right is the destination (45.77.8.9 on port 8443); length is the payload bytes. Once it's text, grep and awk do the rest.

    Check yourself

    Which tcpdump flag reads packets from a saved .pcap file instead of a live interface?

  3. BPF: the capture filter language

    Scope a capture with Berkeley Packet Filter expressions so you see only what matters.

    A busy link is a firehose. BPF — the Berkeley Packet Filter — is the tiny expression language tcpdump (and Wireshark, and ss) use to say which packets you care about. Applied in the kernel at capture time, so non-matching packets are dropped before they ever reach userspace — cheap and precise.

    The primitives compose:

    • host 10.0.0.5, net 10.0.0.0/8 — an address or subnet.
    • port 443, portrange 1-1024 — a service port.
    • tcp / udp / icmp — a protocol.
    • src / dst — direction: src host X, dst port 53.
    • combine with and, or, not (and parentheses).

    So host 45.77.8.9 and tcp port 8443 is "only TCP traffic to or from that host on 8443."

    Example — not runnable on your lab box

    tcpdump -nni eth0 'host 45.77.8.9 and tcp port 8443'
    12:00:00.000000 IP 10.0.0.5.49500 > 45.77.8.9.8443: Flags [P.], length 512
    12:01:00.007000 IP 10.0.0.5.49501 > 45.77.8.9.8443: Flags [P.], length 512
    Once you have a suspect IP, this zooms the capture to just its traffic — the beacon in isolation, nothing else on the wire.

    Example — not runnable on your lab box

    tcpdump -nnr /evidence/cap.pcap 'src 10.0.0.5 and not port 22'
    12:00:00.000000 IP 10.0.0.5.49500 > 45.77.8.9.8443: Flags [P.], length 512
    12:45:00.000000 IP 10.0.0.5.52000 > 104.244.72.115.443: Flags [P.], length 1460
    The same BPF language works at read time with -r: "everything the victim sent, except my own SSH session (port 22)." Filtering out your admin traffic is often the first move so it doesn't drown the signal.

    Check yourself

    Which BPF primitive restricts a capture to a single TCP service port?

  4. Hunt 1 — beaconing, found by regularity

    Surface a C2 beacon: the same external host, contacted again and again on a timer.

    A beacon is quiet and regular. It won't top a bytes chart — each check-in is tiny. What gives it away is repetition to one external host at a steady interval. Two moves: count contacts per destination, then look at the timestamps of the top external host.

    Example — not runnable on your lab box

    tcpdump -nnr /evidence/cap.pcap | awk '{print $5}' | sed 's/:$//' | sed -E 's/\.[0-9]+$//' | sort | uniq -c | sort -rn | head
         40 104.244.72.115
          8 45.77.8.9
          3 203.0.113.20
          2 198.51.100.7
          1 10.0.0.42
    Strip each line to its destination IP (field 5, drop the trailing : and the .port) and count. 104.244.72.115 (40) is a burst — that's the exfil, next step. 45.77.8.9 (8) is the quiet repeat talker: too regular to be a human, too small to be a transfer. That's the beacon candidate.

    Example — not runnable on your lab box

    tcpdump -nnr /evidence/cap.pcap 'host 45.77.8.9' | awk '{print $1}'
    12:00:00.000000
    12:01:00.007000
    12:02:00.014000
    12:03:00.021000
    12:04:00.028000
    Isolate the suspect host and look at when it was contacted. One packet every 60 seconds, like clockwork. A human session is bursty and irregular; a fixed interval is a machine on a timer — a beacon. The interval (60s) is itself an indicator to record and to hunt for on other hosts.

    Check yourself

    Repeated, same-size contacts to one external IP at a fixed time interval are the fingerprint of what?

  5. Hunt 2 — exfil, found by volume (and it isn't the C2)

    Find the bulk transfer by summing bytes per destination, and separate it from the beacon.

    Exfiltration is the opposite shape: loud and bursty. Many full-size packets (length 1460, a full Ethernet payload) to one destination in a short window. You find it by summing bytes per destination, not counting packets.

    Example — not runnable on your lab box

    tcpdump -nnr /evidence/cap.pcap | awk '$2=="IP"{ dst=$5; sub(/:$/,"",dst); split(dst,a,"."); ip=a[1]"."a[2]"."a[3]"."a[4]; for(i=1;i<=NF;i++) if($i=="length") b[ip]+=$(i+1) } END{ for(k in b) print b[k], k }' | sort -rn | head
    58400 104.244.72.115
     4096 45.77.8.9
     2632 203.0.113.20
     1345 198.51.100.7
    104.244.72.115 received ~58 KB in a burst — orders of magnitude more than anything else. That's data leaving: the exfil destination. Note it's a different host from the beacon (45.77.8.9).

    Check yourself

    You sum bytes per destination and one host dwarfs the rest. Is that host your C2?

  6. Corroborate: the baseline and the logs

    Turn two suspect IPs into an incident by cross-referencing the allow-list and host logs.

    A capture rarely stands alone. Two things turn "two odd IPs" into a finding:

    • The baseline / allow-list. Your asset inventory or proxy allow-list says which external hosts a server is supposed to talk to. The beacon and exfil destinations are the ones not on it — that's what makes them suspect rather than just busy.
    • Host logs. pcap tells you what crossed the wire; the logs tell you who and how on the host. /var/log/auth.log may show the brute-force or the new sudo session that preceded the beacon; DNS logs may show the lookups that resolved the C2 domain; ss -tanp (the live table) ties a peer IP to the owning process.
  7. Try it in your live terminal

    Confirm the tool and run the triage pipeline on a sample capture yourself.

    Your sandbox has tcpdump. An idle lab box won't have interesting traffic to sniff, so here you'll confirm the tool and run the exact triage moves on a small sample capture you create — the same awk/sort pipeline you'd point at a real decoded pcap.

    tcpdump --version
    Confirm tcpdump (and its libpcap) are present. On a real host this is the tool you'd use to read the evidence pcap with tcpdump -nnr file.pcap.
    printf '12:00:00 IP 10.0.0.5.5000 > 45.77.8.9.8443: Flags [P.], length 512\n12:01:00 IP 10.0.0.5.5001 > 45.77.8.9.8443: Flags [P.], length 512\n12:02:00 IP 10.0.0.5.5002 > 45.77.8.9.8443: Flags [P.], length 512\n12:00:05 IP 10.0.0.5.5003 > 203.0.113.20.443: Flags [P.], length 900\n' > /tmp/cap.txt && awk '{print $5}' /tmp/cap.txt | sed 's/:$//' | sed -E 's/\.[0-9]+$//' | sort | uniq -c | sort -rn
    Count contacts per destination on a stand-in capture. 45.77.8.9 appears three times to one busy web host's once — the repeat external talker is your beacon lead. This is the count step from Hunt 1, on data you can run right now.
    grep '45.77.8.9' /tmp/cap.txt | awk '{print $1}'
    Isolate the beacon's packets and look at the timestamps: 12:00:00, 12:01:00, 12:02:00 — a steady 60-second interval. Fixed spacing is the beacon signature you'd record as the interval.
Recap
  • A pcap is the tape of what crossed the wire; tcpdump captures it (-w) and reads it back as text (-nnr) so grep/awk can triage it.
  • BPF (Berkeley Packet Filter) expressions — host, net, port, tcp/udp, src/dst, and/or/not — scope a capture to the traffic that matters.
  • C2 beaconing is found by REGULARITY: the same external host, small same-size packets, a fixed interval — it's quiet, so volume charts miss it.
  • Exfil is found by VOLUME: a bulk transfer to one host — and it is NOT the C2. Control and exfil are usually different hosts; report both.
Two behaviours, two signals
BehaviourSignalHow you find it in a capture
C2 beaconRegular timing, constant small size, one hostCount contacts per dest, then check timestamp spacing
ExfiltrationHigh volume to one host, burstySum the length field per destination
Normal sessionVaried sizes, irregular, sustainedOne conversation, not a repeat or a burst