Safe Penetration Testing Labs · intermediate · ~13 min

Detect C2 beaconing

Recognize regular call-home intervals in connection timestamps and report the beacon period.

Overview

Implants call home on a schedule, and that regularity is what betrays them. Human-driven traffic is bursty and irregular; an automated beacon produces connections separated by nearly constant intervals. Detecting it means looking at the gaps between sorted timestamps rather than the timestamps themselves: compute consecutive differences, then ask whether they are all close to a common value. This lesson works on a fixed array of timestamps — it analyses recorded connection metadata, not live traffic.

Why it matters

Beacon detection is one of the highest-value analytics in network defence, because it finds compromise from metadata alone — no payload inspection, no decryption. It also generalises: the same "is this sequence evenly spaced?" question detects polling loops, scheduled jobs and automated scrapers. And it is a good lesson in tolerance, because exact equality is useless against real data that always contains jitter.

Core concepts

Work on the gaps. Convert n timestamps into n-1 consecutive differences. Every property you care about lives in those gaps.

Jitter tolerance is mandatory. Real beacons deliberately randomise their interval slightly, and networks add delay regardless. Comparing gaps for exact equality finds nothing; each gap must be within +/- jitter of the reference.

Choose a reference deliberately. Comparing every gap to the first one is simple but sensitive to an unlucky first sample. Comparing to the median or the mode is more robust — worth knowing even when the simple version is what you implement.

The mode of the gaps. The most frequently occurring interval is the beacon period, and returning it tells the analyst how often the implant calls home, which is far more actionable than a boolean.

Need enough samples. Two timestamps produce one gap, which is trivially "regular". Require at least three or four before claiming a pattern; fewer is not evidence.

Sorted, positive gaps. The input must be sorted, and a non-positive gap means duplicate or out-of-order data — reject it rather than producing a nonsense verdict.

Regular is not malicious. Software updaters, monitoring agents and NTP clients all beacon perfectly. The signal narrows the field; attribution needs more evidence.

Syntax notes

/* all consecutive gaps within +/- jitter of the first one? */
int is_beaconing(const int *ts_sorted, int n, int jitter) {
    if (n < 3 || jitter < 0) return 0;          /* too few samples to claim a pattern */
    int d0 = ts_sorted[1] - ts_sorted[0];
    if (d0 <= 0) return 0;                       /* unsorted or duplicate timestamps */
    for (int i = 1; i < n - 1; i++) {
        int d = ts_sorted[i+1] - ts_sorted[i];
        if (d <= 0) return 0;
        int diff = d - d0;
        if (diff < 0) diff = -diff;              /* absolute difference */
        if (diff > jitter) return 0;             /* one bad gap breaks the pattern */
    }
    return 1;
}

/* the most common gap - the beacon period */
int dominant_interval(const int *ts_sorted, int n) {
    if (n < 2) return -1;
    int m = n - 1, best = 0, best_count = -1;
    for (int i = 0; i < m; i++) {
        int d = ts_sorted[i+1] - ts_sorted[i], c = 0;
        for (int j = 0; j < m; j++) if (ts_sorted[j+1] - ts_sorted[j] == d) c++;
        if (c > best_count) { best_count = c; best = d; }
    }
    return best;
}

Key points:

  • n < 3 is a deliberate evidence threshold, not an arithmetic guard.
  • The absolute difference is computed manually to avoid pulling in <stdlib.h> just for abs.
  • d <= 0 rejects unsorted or duplicated input rather than silently misreporting.

Lesson

Implanted malware 'beacons' to its command-and-control server on a schedule. Human traffic is bursty and irregular; a beacon is metronomic — even when the author adds random jitter.

The run below tests a series of connection times for regular spacing within a jitter tolerance, and reports the most common interval (the likely beacon period).

Code examples

#include <stdio.h>
static int is_beaconing(const int*ts,int n,int j){if(n<3||j<0)return 0;int d0=ts[1]-ts[0];if(d0<=0)return 0;for(int i=1;i<n-1;i++){int d=ts[i+1]-ts[i];int df=d-d0;if(df<0)df=-df;if(df>j)return 0;}return 1;}
static int dominant_interval(const int*ts,int n){if(n<2)return -1;int m=n-1,best=0,bc=-1;for(int i=0;i<m;i++){int di=ts[i+1]-ts[i],c=0;for(int j=0;j<m;j++)if(ts[j+1]-ts[j]==di)c++;if(c>bc||(c==bc&&di<best)){bc=c;best=di;}}return best;}
int main(void){
    int conns[] = {0, 61, 119, 182, 240, 301};    /* call-home times (s), ~60s apart */
    int n = (int)(sizeof conns/sizeof conns[0]);
    printf("beaconing (jitter 3)? : %s\n", is_beaconing(conns,n,3)?"YES":"no");
    printf("dominant interval     : %d s\n", dominant_interval(conns,n));
    return 0;
}

Line by line

Step Line What happens
1 n < 3 Two timestamps give one gap, which is trivially regular — that is not evidence of beaconing.
2 d0 = ts[1] - ts[0] The reference interval every other gap is compared against.
3 d0 <= 0 A zero or negative first gap means the data is unsorted or duplicated; refuse to judge it.
4 diff = d - d0, then negate if negative The absolute deviation of this gap from the reference.
5 diff > jitter A single gap outside tolerance ends it — beaconing requires every interval to be regular.
6 dominant_interval Counts how often each gap value occurs and returns the most common, giving the analyst the actual period.

Common mistakes

Comparing each gap to the previous gap instead of to a fixed baseline; ignoring the need for at least three timestamps.

Debugging tips

Compiler errors and warnings:

  • -Wsign-compare if the count and index types are mixed.
  • No warning for forgetting the absolute value — it just makes the test one-sided.

Runtime symptoms:

  • Only gaps that are too long are rejected. You compared d - d0 > jitter without taking the absolute value, so gaps that are too short pass.
  • Everything is flagged as beaconing. The sample threshold is missing (n < 3), so a pair of timestamps always qualifies.
  • Nothing is ever flagged. The jitter tolerance is 0, and real data always deviates slightly.
  • Nonsense results on unsorted input. The function assumes sorted timestamps; negative gaps must be rejected, not tolerated.
  • dominant_interval returns a rare gap. You compared counts with >= so later ties overwrite earlier ones, or you counted occurrences of the wrong value.
  • Legitimate software is flagged. Expected — updaters and monitoring agents beacon too. This is a lead, not a verdict.

Technique: build three arrays — perfectly regular, regular with small jitter, and clearly irregular — and confirm the verdicts. Then feed an unsorted array and check it is rejected rather than misjudged.

Memory safety

  • The sorted precondition is unchecked by the array itself. The d <= 0 test is what turns "unsorted input" from a silently wrong answer into an explicit rejection — a defensive habit worth generalising.
  • Index bounds. The loop reads ts[i+1], so it must stop at n - 2. Running to n - 1 reads one element past the end.
  • Subtraction overflow. Timestamp differences of int values can overflow if the range is large; use a wider type or validate the range for real capture data.
  • Minimum sample count is a correctness guard, not just a policy: with n < 2 the difference loop has no valid iterations at all.
  • No allocation and read-only inputconst int * keeps the captured metadata unmodified.
  • Metadata only. This analysis reads timestamps; it does not inspect payloads, contact any host, or act on the finding — the detection stays entirely offline.

Real-world uses

Concrete uses: Network-security platforms score internal hosts for beaconing behaviour using exactly this metadata analysis — connection times to an external address, gaps, and jitter tolerance. It is one of the most reliable ways to find command-and-control channels without decrypting traffic. The same technique identifies polling loops in application logs, detects automated scrapers by request cadence, and spots misconfigured agents retrying on a fixed timer.

Professional best practices:

Beginner:

  • Take the absolute deviation, and require a minimum number of samples.
  • Reject unsorted input rather than judging it.

Intermediate:

  • Compare against the median or modal gap rather than the first one, so a single unlucky sample cannot skew the verdict.
  • Report the period and a confidence measure, not just a boolean — analysts triage by strength of evidence.
  • Maintain an allowlist of known-good beacons (update services, monitoring agents); without one the false-positive rate makes the signal unusable.

Practice tasks

1. (Beginner) Regularity verdict. Implement is_beaconing with absolute deviation and a minimum of three samples. Example: gaps of 60, 61, 59 with jitter 2 -> 1; gaps of 60, 300, 61 -> 0. Concepts: tolerance, evidence thresholds.

2. (Beginner) Dominant interval. Implement dominant_interval returning the most common gap. Example: -> 60. Concepts: mode of a sequence.

3. (Intermediate) Reject bad input. Add explicit rejection of unsorted and duplicated timestamps, and test both. Concepts: defensive preconditions.

4. (Intermediate) Median reference. Compare every gap against the median rather than the first, and construct a case where the two disagree. Concepts: robust statistics.

Summary

Beacon detection works on the gaps between sorted timestamps, not the timestamps themselves: compute consecutive differences and test whether all of them sit within a jitter tolerance of a reference interval. The tolerance is essential — real beacons randomise slightly and networks add delay, so exact equality finds nothing — and the deviation must be an absolute value or only over-long gaps get rejected. Require at least three samples before claiming a pattern, reject non-positive gaps as unsorted or duplicated input rather than judging them, and report the dominant interval so the analyst learns the period. Remember that updaters and monitoring agents beacon perfectly too, so this is a lead rather than a verdict.

Practice with these exercises