
From the hunt desk. If your SOC still alerts on “STDDEV(beacon_interval) < X”, you are running a 2018 playbook against 2026 operators. modern command-and-control frameworks, and commercial offensive toolkits all ship jitter on by default — the rule fires zero times in a real intrusion and burns analyst cycles on every legitimate health check. This post is the replacement playbook: detection-engineering at the FFT level, MITRE ATT&CK TA0011 (Command and Control) coverage, and purple-team-validated atomic tests. The deliverable is a Lambda + Athena pipeline you can deploy this week.
Modern command-and-control (C2) frameworks have quietly broken every “average inter-arrival time” beacon detection rule sitting in production SIEMs today. modern command-and-control frameworks, and commercial offensive toolkits all ship with jitter built into the agent — a 60-second beacon configured with 40% jitter produces real-world intervals anywhere from 36 to 84 seconds. To a threshold-based detector watching the average and standard deviation, that looks indistinguishable from a chatty web client checking for updates. The beacon is invisible. The operator keeps shells.
This playbook walks through a detection pipeline that does see those beacons — by stepping out of the time domain entirely and into the frequency domain. We apply Fast Fourier Transform (FFT) to the inter-arrival-time (IAT) series extracted from AWS VPC Flow Logs, then cluster the spectral output with DBSCAN to pull out one or more beacon frequencies per host — including the sleep-transition pattern where a C2 operator flips the agent from fast (5 s) to slow (3600 s) on demand.
The pipeline runs end-to-end against any AWS account that already streams VPC Flow Logs to S3, requires no agent on the endpoint, and produces detections with confidence scores tunable for your false-positive budget. Read it as a sequel to our earlier deep-dive on attack hunting using AWS VPC Flow Logs, our broader threat hunting for cloud attacks playbook, and the AWS Bedrock CloudTrail playbook we published last week.
Why Traditional Beacon Detection Fails Against Modern C2
Pre-2020, beacon hunting was a one-liner: group by (src_ip, dst_ip, dst_port), count flows, compute standard deviation of inter-arrival times, alert when the result is suspiciously small. commercial C2 frameworks beacons configured with a flat 60-second sleep would surface immediately — they were almost perfectly periodic.
C2 operators responded the way they always do: they raised the cost of detection. Four behaviours specifically broke every simple rule:
- Random jitter: A configurable percentage that randomises the sleep interval per beacon. commercial C2 frameworks’s
sleep 60 40command produces a 36–84 s interval. open-source C2 frameworks’s--jitterflag does the same. open-source C2 frameworks agents carry per-profile jitter. commercial offensive toolkits async beacons add jitter by default. - Sleep transitions: The operator switches the agent between fast and slow modes during hands-on-keyboard work. A typical operator session shows 5 s beacons during exploitation, then 3600 s once persistence is in place. Average IAT becomes meaningless across that boundary.
- Multi-stage callbacks: A single compromised host may run two or more beacons — one to a short-lived staging server, one to a long-lived C2. The IAT distribution becomes bimodal or multimodal and the global average lands somewhere in the middle that looks like a busy desktop user.
- Payload-size camouflage: Modern frameworks randomise beacon payload sizes so even
STDDEV(bytes)trends look noisy.
The maths is unforgiving. With a 60-second beacon at 40% jitter, the coefficient of variation (CV = σ/μ) sits around 0.23 — well inside the band a normal HTTPS client would produce. With sleep transitions layered on top, the CV easily climbs above 0.5, which is the band most teams whitelist as “human noise.” The signal is there; the metric being applied to it can no longer see it.
Stepping Into the Frequency Domain
The insight behind the pipeline is simple. The beacon’s periodic structure is preserved even when its instantaneous intervals are randomised — it is a sinusoidal signal buried in additive noise. Spectral analysis exists precisely to recover that kind of signal. Apply an FFT to the IAT series, and the dominant frequency peak reveals the true beacon interval. Jitter spreads out the noise floor but does not move the peak.
Formally, given an IAT series of length N, the discrete Fourier transform is:
F(k) = Σ[n=0 .. N-1] IAT(n) · e^(-j 2π k n / N)
And the dominant beacon interval is recovered as:
f_dominant = argmax | F(k) |
beacon_interval = 1 / f_dominant
You do not need a DSP background to run this — numpy.fft.fft() does the heavy lifting in five lines of Python. What matters is the conceptual move: stop asking “is this regular?” in the time domain and start asking “is there a strong periodic component?” in the frequency domain.
Jitter survives this test poorly. A 40 %-jittered 60-second beacon over 100 callbacks produces a clearly dominant peak at f ≈ 1/60 Hz, with the jitter energy distributed across the broader spectrum. A genuinely random human browsing pattern produces a flat or near-flat spectrum — no dominant peak at all. The two are now trivial to separate.
DBSCAN for Multi-Stage and Sleep-Transition Beacons
A single FFT peak is good, but a single host may carry multiple beacons — that is the multi-stage and sleep-transition problem. The fix is to cluster the spectral output. Each (frequency, amplitude) pair from the FFT becomes a point. We run DBSCAN on those points and treat each dense cluster as one distinct beacon pattern. Anything that fails to join a cluster (noise points) is dismissed as legitimate traffic.
DBSCAN was originally published in Ester et al., 1996, and it has two parameters worth tuning to your environment:
eps = 0.1— the neighbourhood radius. Smaller values demand tighter peak alignment, which suits low-jitter operators; larger values catch noisier campaigns at the cost of more false positives. 0.1 is a solid starting point.min_samples = 3— minimum points to form a cluster. Lower means more sensitivity to short-lived beacons; higher means fewer alerts on transient anomalies. Three is enough to surface multi-stage callbacks without firing on every browser keepalive.
What you get back is one or more labelled clusters per host. A vanilla commercial C2 frameworks beacon produces a single cluster. A sleep-transition beacon produces two — one tight cluster near the fast frequency, one tight cluster near the slow frequency. A real multi-stage operation produces three or more.
Feature Engineering from VPC Flow Log Attributes
The full feature set we compute per (srcAddr, dstAddr, dstPort) tuple:
| Feature | Source attributes | Formula | What it captures |
|---|---|---|---|
| IAT series | start | start[i] − start[i−1] | Time between consecutive flows — the raw signal |
| CV (coefficient of variation) | start | STDDEV(IAT) / AVG(IAT) | Jitter level — primary time-domain signal |
| Spectral power ratio | start | max_fft_peak / total_power | How concentrated the periodicity is — primary frequency-domain signal |
| Byte consistency | bytes | 1 − (STDDEV(bytes) / AVG(bytes)) | Payload uniformity — beacons trend toward fixed-size callbacks |
| Session duration | start, end | MAX(end) − MIN(start) | Campaign length — short C2 sessions tighten thresholds |
| Packet ratio | packets, bytes | AVG(bytes / packets) | Protocol fingerprint — beacons have tight, repeatable packet shapes |
| Flow regularity | start | autocorrelation(IAT, lag=1) | Self-similarity — a secondary regularity signal that complements FFT |
None of these features are exotic — they all come straight from the standard VPC Flow Logs schema (v2 or higher). If you have already implemented the hunts in our network threat hunting with outbound traffic guide, your data lake is ready for this pipeline as-is.
The Beacon Score: Combining Time and Frequency Domain Evidence
Each feature carries its own weight. We combine them into a single confidence score:
Beacon Score = spectral_power_ratio
× (1 − CV_iat)
× log(flow_count)
× (1 − CV_bytes)
Reading the formula:
- spectral_power_ratio drives the score upward when the FFT peak is sharp — strong periodicity.
- (1 − CV_iat) rewards low jitter, but does not require it — jittered beacons still pass thanks to the spectral term.
- log(flow_count) filters out noise from low-volume tuples and rewards long-running campaigns without letting one extremely chatty service dominate.
- (1 − CV_bytes) adds payload-consistency evidence — beacons have stereotyped payload sizes even when intervals are jittered.
Empirically, scores fall into well-separated bands:
- CV < 0.1 — no jitter. Classic naïve beacon. Probably a misconfigured red-team exercise or a developer testing C2.
- 0.1 ≤ CV ≤ 0.3 — low jitter. The bulk of in-the-wild commercial C2 frameworks and open-source C2 frameworks beacons live here.
- 0.3 < CV ≤ 0.5 — high jitter. Operators who have done their homework. Detection rests almost entirely on the spectral term.
- CV > 0.5 — human or event-driven. Almost certainly benign — but check the spectral_power_ratio anyway, because very long beacons (3600 s) can show high CV across short windows.
Our alerting threshold is score > 0.7 for “confirmed beacon” — calibrate to your environment. We tune this initially with a two-week historical backtest against known clean traffic, then adjust upward if the analyst queue gets noisy.
Athena SQL — Pre-Processing for the ML Pipeline
Before any FFT runs, Athena (or any equivalent SQL engine on top of your VPC Flow Logs) takes care of the heavy lifting: window functions to compute IATs, filtering to egress-only traffic, and aggregation to compress the dataset before it hits Python.
WITH ordered_flows AS (
SELECT srcaddr, dstaddr, dstport, start, bytes, packets,
LAG(start) OVER (PARTITION BY srcaddr, dstaddr, dstport ORDER BY start) AS prev_start,
LAG(bytes) OVER (PARTITION BY srcaddr, dstaddr, dstport ORDER BY start) AS prev_bytes,
ROW_NUMBER() OVER (PARTITION BY srcaddr, dstaddr, dstport ORDER BY start) AS seq
FROM central_vpc_flow_logs
WHERE action = 'ACCEPT'
AND srcaddr LIKE '10.%'
AND dstaddr NOT LIKE '10.%'
AND dstaddr NOT LIKE '172.%'
AND dstaddr NOT LIKE '192.168.%'
AND day BETWEEN '2026/03/19' AND '2026/03/23'
),
iat_features AS (
SELECT srcaddr, dstaddr, dstport,
start - prev_start AS iat,
bytes, packets, seq
FROM ordered_flows
WHERE prev_start IS NOT NULL
)
SELECT srcaddr, dstaddr, dstport,
COUNT(*) AS flow_count,
AVG(iat) AS avg_iat,
STDDEV(iat) AS stddev_iat,
STDDEV(iat) / NULLIF(AVG(iat), 0) AS cv_iat,
AVG(bytes) AS avg_bytes,
STDDEV(bytes) / NULLIF(AVG(bytes), 0) AS cv_bytes,
MIN(iat) AS min_iat,
MAX(iat) AS max_iat,
APPROX_PERCENTILE(iat, 0.5) AS median_iat
FROM iat_features
GROUP BY srcaddr, dstaddr, dstport
HAVING COUNT(*) > 20
ORDER BY cv_iat ASC;
A few notes on tuning this for production:
- Egress-only filter: The
WHEREclause restricts to traffic leaving your private RFC 1918 space toward the public internet. Adjust to match your IP ranges (some teams operate 100.64/10 or other carrier-grade NAT — add those). - HAVING COUNT(*) > 20: 20 callbacks is the absolute minimum for a useful FFT — fewer than that and the spectral peak is statistically unreliable. Most teams settle on 30–50.
- Window function partitioning: The five-tuple key here is
(srcaddr, dstaddr, dstport)— we deliberately ignore source port because ephemeral source ports rotate on every flow. If your VPC Flow Logs are v3+ and include traffic path metadata, narrow further to direction = egress. - Date window: Five days is a good baseline — long enough to surface low-and-slow beacons, short enough to keep Athena scans cheap. Re-run daily with a sliding window.
- Output volume: Expect 50,000–500,000 candidate tuples per day in a mid-size enterprise. The pipeline downstream filters this aggressively.
Putting It Into Production
The end-to-end architecture is short and inexpensive:
- VPC Flow Logs → S3 (already enabled in most accounts; if not, see our VPC Flow Logs hunting primer).
- Athena partition the bucket by year/month/day for query speed.
- EventBridge schedule kicks off a daily Lambda at 03:00 local time.
- Lambda issues the Athena query above, paginates results to S3, then invokes a Python step that runs FFT and DBSCAN over each tuple. For high-volume environments, swap Lambda for an SageMaker Processing job — Lambda’s 15-minute ceiling will bite once you exceed a few hundred thousand tuples.
- Output goes back to S3 (Parquet) and is shipped to your SIEM via Kinesis Firehose or SQS for analyst triage.
The core Python is intentionally boring:
import numpy as np
from sklearn.cluster import DBSCAN
def beacon_score(iat: np.ndarray, byte_sizes: np.ndarray) -> dict:
"""Return spectral & clustering verdict for one (src, dst, port) tuple."""
if len(iat) < 20:
return {"verdict": "insufficient_data"}
# FFT
spectrum = np.abs(np.fft.fft(iat - iat.mean()))
half = spectrum[: len(spectrum) // 2]
peak_power = half.max()
total = half.sum()
spr = peak_power / total # spectral_power_ratio
f_dom = np.argmax(half) / len(iat)
beacon_int = 1 / f_dom if f_dom else None
# Time-domain stats
cv_iat = iat.std() / iat.mean() if iat.mean() else 1.0
cv_bytes = byte_sizes.std() / byte_sizes.mean() if byte_sizes.mean() else 1.0
# Multi-stage clustering
pairs = np.vstack([
np.fft.fftfreq(len(iat))[: len(iat) // 2],
half / total,
]).T
clusters = DBSCAN(eps=0.1, min_samples=3).fit(pairs).labels_
n_clusters = len(set(clusters)) - (1 if -1 in clusters else 0)
score = spr * (1 - cv_iat) * np.log(max(2, len(iat))) * (1 - cv_bytes)
return {
"beacon_score": float(score),
"beacon_interval_sec": beacon_int,
"spectral_power_ratio": spr,
"cv_iat": cv_iat,
"cv_bytes": cv_bytes,
"clusters": int(n_clusters),
}
That function, fed by the Athena output, is the entire detection engine. Wrap it in pagination logic, an alert sink, and an allow-list — production-ready in a single afternoon.
Detection Coverage Matrix
Where this pipeline lands across the modern C2 inventory:
| Framework | Default config | Jitter / sleep behaviour | Detection |
|---|---|---|---|
| commercial C2 frameworks | sleep 60 0 |
Flat 60 s beacon | Trivial — both time and frequency domain. |
| commercial C2 frameworks | sleep 60 40 |
36–84 s random jitter | Frequency domain — clear peak at 1/60 Hz. |
| commercial C2 frameworks | sleep 3600 50 |
Long-and-slow with high jitter | Requires 5–7 day window; spectral peak still resolves. |
| open-source C2 frameworks | --jitter 30s |
30 s random jitter | Frequency domain — DBSCAN catches dual-stage when used. |
| open-source C2 frameworks | Custom Apollo / Athena profiles | Variable per profile | Depends on profile; default Apollo profile resolves cleanly. |
| commercial offensive toolkits | Async beacons | Event-driven, longer baseline | Partial — needs longer observation window and tighter min_samples. |
| Manual / custom | Polling HTTPS | Anything from 1 s to 24 h | Resolves at any interval ≥ 2× sample rate of the VPC Flow Log timestamps. |
| Pure event-driven | WebSocket / long-poll | No periodicity | Uncovered — needs different telemetry (DNS, TLS metadata, EDR). |
The honest gap is event-driven C2 — agents that wait for a server push rather than poll. For those you need to layer a different signal (DNS query rate, TLS JA3/JA4 fingerprinting, EDR process-tree analysis). Pair this pipeline with the patterns from our AWS identity attack hunt and authentication-event hunting guides for full coverage.
Limits and False-Positive Sources
Periodicity is everywhere on a production network. The pipeline will alert on:
- NTP and time sync — almost always allow-listed by destination IP/port (UDP 123).
- Cloud telemetry agents — CloudWatch agent, commercial observability vendors agent, New Relic, your SIEM Universal Forwarder. Allow-list by destination domain or known agent ASN.
- Health checks — internal load balancer probes, ECS task health checks. The Athena egress filter handles most of these, but cross-VPC traffic may slip through.
- Software update polling — Windows Update, Linux package managers, OS phone-home. Allow-list by destination FQDN.
- SaaS sync clients — Dropbox, OneDrive, Outlook IDLE connections. These are particularly nasty because they often share characteristics with beacons. Build an explicit allow-list of approved corporate SaaS endpoints.
The cleanest operational pattern is a layered allow-list: an “always allow” list of known periodic services, then a per-host tolerance score that decays over time if the host stops triggering. Whatever you do, do not silence alerts by raising the threshold — that just shifts the problem to a different score band and erodes your detection coverage.
Where This Sits in a Mature Threat Hunting Programme
Adaptive C2 detection is a single corner of a comprehensive network-detection programme. The pipeline above pairs naturally with:
- VPC Flow Log attack hunting for the volumetric and access-pattern hunts.
- Outbound network threat hunting for the destination-side enrichment.
- Cloud attack threat hunting for identity and resource-level evidence.
- AWS Bedrock CloudTrail playbook for the GenAI service attack surface.
- Authentication-event threat hunting for the identity side of the kill chain.
- Linux threat hunting using CUT, SORT, UNIQ, DIFF for the host-side investigation once an alert lands.
- Weekly Threat Advisory for the upstream IOCs you should baseline-block on the same VPC traffic.
None of these detections work in isolation, and the analyst who reaches for one of them in a real incident reaches for all of them. The output of the FFT pipeline tells you which (src, dst, port) tuples deserve a deeper look; the other hunts tell you what else that host has been doing.
MITRE ATT&CK Techniques Covered by This Detection
This pipeline maps to the Command and Control (TA0011) tactic. Treat the table below as your hunt-coverage worksheet: full = the detection fires reliably, partial = the technique surfaces under certain configurations and needs companion telemetry, out of scope = adjacent technique that the SOC should cover with a different hunt. The companion posts in this VPC Flow Log series fill most of the gaps.
| ATT&CK ID | Technique / sub-technique | Coverage | Hunter notes |
|---|---|---|---|
| T1071 | Application Layer Protocol (parent) | Full | FFT surfaces periodicity regardless of L7 protocol — HTTP/S, DNS, custom binary all show up |
| T1071.001 | Web Protocols (HTTP/HTTPS C2) | Full | Default surface — modern command-and-control frameworks all default to 443 |
| T1071.004 | DNS C2 | Partial | Catches periodic DNS resolver beacons; pair with the Data Exfiltration post for DNS-tunnel volumetric signal |
| T1573 | Encrypted Channel (parent) | Full | Encryption hides payload but not timing — FFT sees through TLS |
| T1573.001 | Symmetric Cryptography | Full | — |
| T1573.002 | Asymmetric Cryptography | Full | — |
| T1029 | Scheduled Transfer | Full | Direct match for sleep + jitter behaviour |
| T1008 | Fallback Channels | Full | DBSCAN multi-cluster output reveals fallback beacons on same host |
| T1090 | Proxy (parent) | Partial | Proxy hops on a periodic schedule still detectable; tunnelled-through-proxy is harder |
| T1090.002 | External Proxy | Partial | — |
| T1090.003 | Multi-hop Proxy (Tor, etc.) | Partial | Tor exit-node IPs surface even when payload is opaque |
| T1095 | Non-Application Layer Protocol | Partial | Raw-socket and ICMP-based beacons surface in IAT analysis |
| T1102 | Web Service (cloud dead-drops) | Partial | Beacons polling cloud storage show periodicity; pair with destination reputation enrichment |
| T1102.001 | Dead Drop Resolver | Partial | — |
| T1102.002 | Bidirectional Communication | Full | Polling pattern is the same as direct C2 |
| T1132 | Data Encoding | Out of scope | Encoding affects payload not timing; ignored by FFT |
| T1568 | Dynamic Resolution (DGA) | Out of scope | Pair with DNS-tunnel detection (post #3) and threat-intel domain feeds |
| T1041 | Exfiltration Over C2 Channel | Out of scope | Volumetric concern — see post #3 (Isolation Forest + LSTM) |
Adversary emulation / purple-team validation. public adversary-emulation atomics tests T1071.001 and T1573 are the obvious entry points. For full-framework emulation, drop a commercial C2 frameworks beacon with sleep 60 40, a open-source C2 frameworks beacon with --jitter 20s, and a modular open-source agents on a lab host instrumented with this pipeline — all three should surface within one hour of telemetry. For broader purple-team exercises, the MITRE open-source adversary-emulation frameworks command-and-control plugin lets you script the whole sequence.
Sigma / SIEM detection-as-code. The detection logic above is straightforward to express as a Sigma rule that fires on the post-FFT score field rather than raw events. Once your pipeline emits beacon-score events into the SIEM, the Sigma rule is a one-liner threshold check, which is exactly the right separation of concerns: heavyweight ML in the pipeline, lightweight threshold logic in the rule.
STIX / TAXII enrichment. Beacon destinations surfaced by this pipeline are excellent candidates for ingestion into your TIP (MISP, OpenCTI, a threat-intelligence platform). Output the (src, dst, dst_port, beacon_interval) tuple as a STIX 2.1 indicator object, tag with x-mitre-tactic-id = TA0011, and the broader CTI community gets the benefit of your detection without any code-sharing.
Closing Thoughts: Make Spectral Analysis Part of Your Hunting Cadence
If your SOC still relies on STDDEV-based beacon rules, you are missing every modern C2 framework’s default-on jitter setting. The fix is not more rules — it is a different kind of mathematics. Frequency-domain analysis has been a standard technique in radio engineering, audio processing, and seismology for decades; the only reason we are not using it on network telemetry yet is that nobody plugged in numpy.fft.fft(). The Athena SQL and Python in this post are everything you need to run a meaningful pilot this week.
Tune the parameters. Backtest against your own VPC Flow Logs. Add the allow-lists your environment needs. If you have war stories from rolling this out — successes, embarrassing false positives, or operational quirks — please get in touch via the contact page. We will fold the most useful lessons into a follow-up post and credit the contributors.
Happy threat hunting.
#threathunting #c2detection #beacondetection #cobaltstrike #sliver #mythic #bruteratel #FFT #DBSCAN #vpcflowlogs #awssecurity #cloudsecurity #networkdetection #SOC #blueteam #ml #anomalydetection #signalprocessing #cyberdefense #ttps #mitreattack #networktrafficanalysis #infosec









