Packet Capture Triage: tcpdump, BPF Filters, and Reading a Capture for Malicious Traffic
- 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
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,
tcpdumpis 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.
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.
tcpdumpdoes two jobs: capture live traffic to a.pcapfile, and read a.pcapback 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 anycaptures 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.pcap12: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.5on a high random port); right is the destination (45.77.8.9on port 8443);lengthis 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?
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, andss) 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, net10.0.0.0/8— an address or subnet. - port
443, portrange1-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 8443is "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?
- host
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 | head40 104.244.72.115 8 45.77.8.9 3 203.0.113.20 2 198.51.100.7 1 10.0.0.42Strip 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?
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 | head58400 104.244.72.115 4096 45.77.8.9 2632 203.0.113.20 1345 198.51.100.7
104.244.72.115received ~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?
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.
pcaptells you what crossed the wire; the logs tell you who and how on the host./var/log/auth.logmay 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.
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 sameawk/sortpipeline you'd point at a real decoded pcap.tcpdump --versionConfirm 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 -rnCount contacts per destination on a stand-in capture. 45.77.8.9appears 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.
- 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.
| Behaviour | Signal | How you find it in a capture |
|---|---|---|
| C2 beacon | Regular timing, constant small size, one host | Count contacts per dest, then check timestamp spacing |
| Exfiltration | High volume to one host, bursty | Sum the length field per destination |
| Normal session | Varied sizes, irregular, sustained | One conversation, not a repeat or a burst |
