How to detect beaconing (C2 traffic) in a PCAP
Malware that has a foothold usually checks in with its controller on a timer. That rhythm survives encryption, so you can often see it in a capture even when you can't read a single payload. The hard part is telling it apart from the many legitimate programs that also run on a timer.
In PCAPNG Analyzer Pro, this whole guide happens on upload. Every host pair’s check-ins are measured; regular ones are flagged with the client, server, port and interval; keepalives on long-lived connections are told apart; and you can see at a glance whether the destination is new to your network.
See a sample incident report →1. What beaconing looks like
A beacon is one host contacting the same destination again and again at a roughly fixed interval: every 30 seconds, every 5 minutes, every hour. Each check-in is usually small and similar in size, and the pattern continues whether or not anyone is using the machine.
Two things make it harder to spot than that description suggests:
- Jitter. Most command-and-control frameworks randomise the sleep time, for example 60 seconds ±20%, precisely so the timing isn't perfectly regular.
- Long sleeps. A beacon that checks in hourly shows only once in a 10-minute capture. You need enough capture time to see at least five or six check-ins, so for slow beacons that means hours of traffic.
The useful measurement is therefore not "is it regular?" but "how regular is it, relative to its average interval?" The coefficient of variation (CoV) answers that: the standard deviation of the intervals divided by their mean. A perfect timer scores 0. Uniform ±20% jitter scores about 0.12, and even ±50% jitter stays below 0.3. Ordinary human browsing usually scores well above 0.5.
2. List connection starts, not packets
Measure the time between check-ins, not between packets. A check-in over HTTPS is a burst of 10 or more packets: handshake, request, response, teardown. Packet-to-packet gaps inside that burst are milliseconds, while the gap between bursts is the beacon interval. Mixing the two produces a meaningless average.
For TCP, each new connection starts with a SYN. List every initial SYN with its time, client, server and port, leaving out retried SYNs so one connection counts once:
tshark -r capture.pcapng \
-Y "tcp.flags.syn == 1 && tcp.flags.ack == 0 && !tcp.analysis.retransmission" \
-T fields -e frame.time_epoch -e ip.src -e ip.dst -e tcp.dstport \
> starts.tsv
Some beacons reuse one long-lived connection, and CDN-hosted ones change IP address. Two other event lists catch those:
# One row per DNS lookup, by client and name
tshark -r capture.pcapng -Y "dns.flags.response == 0" \
-T fields -e frame.time_epoch -e ip.src -e dns.qry.name > lookups.tsv
# One row per TLS or QUIC ClientHello, by client and server name
tshark -r capture.pcapng -Y "tls.handshake.type == 1" \
-T fields -e frame.time_epoch -e ip.src -e tls.handshake.extensions_server_name > hellos.tsv
Grouping by server name rather than IP address keeps a beacon together when its destination moves between CDN addresses. Recent Wireshark versions decrypt QUIC Initial packets, so the ClientHello list includes HTTP/3 connections too.
3. Measure the intervals
Save this short awk script as intervals.awk. It reads a timestamp followed by the fields that identify a pair, then prints the CoV, the number of events, the average interval and the pair:
# Input: a timestamp, then the fields that identify the pair, sorted by
# pair and then time. Prints CoV, count, mean interval and the pair.
# Set -v min=SECONDS to skip pairs whose average gap is shorter.
{
t = $1; sub(/^[^ \t]+[ \t]+/, ""); key = $0
if (key != prev) { report(); prev = key; n = 0; sum = 0; sumsq = 0; last = t; next }
gap = t - last; last = t; n++; sum += gap; sumsq += gap * gap
}
END { report() }
function report( mean, var) {
if (n < 5) return
mean = sum / n; if (mean < min) return
var = sumsq / n - mean * mean; if (var < 0) var = 0
printf "%.2f\t%d\t%.1fs\t%s\n", sqrt(var) / mean, n + 1, mean, prev
}
Sort each list by pair and time, run the script, and sort the result so the most regular pairs come first:
sort -k2 -k1,1n starts.tsv | awk -v min=5 -f intervals.awk | sort -n | head -20
Pairs need at least six events to be reported. min=5 drops pairs whose average gap is under five seconds: a browser opening several connections to one site at once otherwise looks perfectly regular. A beacon stands out like this:
0.06 20 29.8s 192.168.1.30 198.51.100.23 443
Twenty connections, one every 29.8 seconds on average, with a CoV of 0.06. In the same kind of list, ordinary browsing typically scores 0.5 to 2.
A low CoV says a program is running on a timer. It doesn't say which program, or whether it's unwanted. Treat each low-CoV pair as a question to answer, not a finding to report.
4. Validate a candidate in Wireshark
Take one pair from the list and look at it directly. Filter to its connection starts:
tcp.flags.syn == 1 && tcp.flags.ack == 0 && ip.src == 192.168.1.30 && ip.dst == 198.51.100.23
Then choose View → Time Display Format → Seconds Since Previous Displayed Packet. The Time column now shows each interval directly, so you can see the rhythm and any outliers. Then check:
- Size consistency. With the pair as the display filter, open Statistics → Conversations, tick Limit to display filter and read the TCP tab. Check-ins of near-identical size, with a small request and an equally small reply, are typical of an idle beacon. A sudden large transfer can mean a task was issued.
- Persistence. Does the rhythm run through the whole capture, including periods when the user was away?
- Scope. Is it one host, or every host on the network? A pattern on every machine is far more likely to be standard software.
- Identity. Check the destination's DNS name and TLS server name, and the client's JA3 or JA4 fingerprint. Compare the fingerprint with the browsers and agents you know are deployed.
5. Rule out legitimate pollers
Most regular traffic on a healthy network is legitimate. Expect to find:
- NTP time synchronisation (UDP 123), and DHCP lease renewals.
- Update and licence checks from operating systems, browsers and applications.
- Endpoint security, monitoring and management agents reporting in.
- Push notification and chat services holding connections open with keepalives.
- Cloud sync clients, and printers or IoT devices polling a local controller.
A beacon becomes more interesting when several of these hold together:
- The destination is new to your network, or reached by bare IP address with no DNS lookup.
- The client fingerprint doesn't match any deployed software.
- Only one host shows the pattern.
- It persists over days, at all hours.
The capture can't tell you which process made the connections. That needs the endpoint: EDR telemetry, Sysmon network events, or netstat -ano on the host while the pattern is live.
Mind the capture point, too. Behind NAT or a proxy, many clients share one address, so the "client" in your list may be a gateway. And DNS-over-HTTPS hides lookups entirely, leaving only the connection pattern.
Manual method vs PCAPNG Analyzer
Manual method
- Capture for long enough to see several check-ins.
- Export connection starts, lookups and ClientHellos with tshark.
- Compute intervals and CoV per pair.
- Validate candidates in Wireshark.
- Rule out known pollers, then confirm the process on the endpoint.
With PCAPNG Analyzer
- Upload the capture, or let a watched folder ingest captures continuously (Pro).
- Review the beaconing finding: who, where, how often and how regular, with the usual harmless causes and what to check next.
- Click the destination to see every capture it appears in, and check the client’s JA4 and the destination’s first-seen status.
The detector groups each host pair’s packets into check-ins (bursts separated by a quiet gap) and flags pairs whose check-ins come at regular intervals. By default that means at least six check-ins, at least five seconds apart on average, with the interval varying by less than 30%; all three are adjustable in Settings → Detection Rules. Check-ins that reuse one long-lived connection, the usual shape of an application keepalive, and timers between two private hosts are reported as informational. High severity is kept for very regular timers to external addresses. History adds what timing can’t: whether the destination is new, and whether the pattern repeats across captures.
Keep the context beacons need
Run PCAPNG Analyzer on your own computer or server, keep a history of captures, and see when a destination first appeared. Your packet data never leaves infrastructure you control.
Compare Free and ProSelf-host it