C2 Infrastructure Emulation
Definition
Section titled “Definition”C2 infrastructure emulation is the construction and operation of lab-range command-and-control architecture that mirrors adversary infrastructure design — listeners, redirector chains, CDN-fronted fronts, DNS channels, malleable traffic profiles — for the purpose of testing detections (M7’s analysis, deployed) and rehearsing the operational discipline of running C2 without ever touching untrusted tooling or production networks. The emulation builds each resilience layer adversaries use: a direct listener (the simplest shape), redirectors that decouple implant from server, TLS fronting that moves reputation to a CDN, DNS channels that ride resolver infrastructure, and legitimate-service channels that hide inside trust. Everything runs on lab-ranged infrastructure with marker-marked traffic and hard network boundaries, because the craft being taught — infrastructure design under the assumption that defenders are watching — is exactly what makes the blue side’s network detections testable against realistic, well-run adversary behavior rather than naive first-draft beacons.
Why It Matters
Section titled “Why It Matters”Most network detections are validated against the easiest adversary shape — a direct beacon to a bare listener — and die the first time they meet infrastructure that resembles real operations. Emulating C2 infrastructure properly closes that gap: the blue team’s beacon periodicity hunts, JA3 fingerprints, URI-template rules, and DNS-channel statistics get tested against redirector chains, fronted TLS, and malleable profiles, and the resulting coverage picture is honest in a way single-listener testing can never be. For the red team, the lab range is where infrastructure skills are built safely: standing redirectors, generating TLS with the right properties, designing profiles, and — most importantly — rehearsing the operational security cycle (burnable infra, rotation drills, and the discipline of never mixing exercise and production assets) in an environment where mistakes cost a VM, not an engagement. The module also produces an asset most teams lack entirely: a map of what their own estate sees when C2 is well-designed — which hops are visible, which telemetry each shape generates, and where the detection story degrades from “we see everything” to “we see a periodic DNS query”. Finally, C2 emulation is the natural bridge between the M-series analysis and live defense validation: the protocol templates decoded in M7 become traffic replayed through this module’s ranges, closing the loop from decode to detect.
Detailed Explanation
Section titled “Detailed Explanation”Adversary C2 architecture is a resilience stack, and each layer exists to answer a specific defensive move:
C2 RESILIENCE STACK (lab emulation shapes) L0 DIRECT: implant -> listener (one hop) telemetry: full — IP, timing, protocol, TLS dies to: first blocklist hit L1 REDIRECTOR: implant -> redirector(s) -> listener socat/nginx relay; listener IP never exposed telemetry: implant sees redirector; backend hidden dies to: redirector takedown (cheap, replaceable) L2 FRONTING: implant -> CDN/legit host -> (hidden route) -> listener reputation borrowed; SNI/Host games; TLS at edge telemetry: only CDN egress visible dies to: JA3/profile correlation, provider policy L3 TRUST-CHANNEL: implant -> legit service (cloud storage, paste sites, DNS TXT) -> operator reads tasking telemetry: looks like SaaS traffic dies to: process-context correlation (EDR side)
LAB VALIDATION LOOPS for each layer L: beacon drill -> hop telemetry capture -> detection rule check -> coverage noteThe redirector layer is the core lab exercise: a redirector is a relay that forwards traffic to the listener while hiding its address — socat TCP relays for the raw shape, nginx with TLS termination for the HTTPS shape, SSH tunnels for the ad-hoc shape, and CDN workers for the fronted shape. The design decisions mirror real ones: redirectors are burnable (replaceable in minutes), so implants are configured with multiple redirector addresses; traffic shaping (malleable profiles) is applied at the implant, so the redirector must forward blind — it should never need to understand the protocol, which is also why redirectors make poor detection points. The profile layer decides what the traffic looks like: URI paths, header order and values, cookie placement, body encoding, jitter and sleep schedules — all parameters the emulation makes explicit, because M7’s analysis showed that the set of choices is the fingerprint. The DNS layer exercises the quiet channel: TXT-record tasking and subdomain-label exfil against a lab authoritative server, validated against the dnstunnel statistics from M7. Two disciplines keep the lab honest. Network boundaries: the range resolves lab domains only (split-horizon DNS or hosts-file pinning), egress is denied by default, and every beacon URL/domain carries the exercise marker — a beacon that resolves a public name is a finding about the range, not a badge. Attribution cleanliness: lab infrastructure uses reserved documentation domains (RFC 2606 .test/.lab.local conventions) and lab-only certificates from your own CA, so no exercise traffic is ever indistinguishable from real infrastructure — the blue team should be able to classify every packet’s origin in seconds.
Common Techniques
Section titled “Common Techniques”| Infrastructure shape | Implementation (lab) | What it tests | Telemetry visible |
|---|---|---|---|
| Direct listener | Python/http.server class listener | Baseline detections | Everything |
| socat TCP redirector | socat TCP-LISTEN:8443,fork TCP:listener:8080 | Redirector-chain hunting | Redirector hop only |
| nginx TLS redirector | TLS terminate + proxy_pass | JA3/cert at edge | Edge TLS + forwarded |
| CDN fronting shape | Lab CDN worker → lab listener | Reputation-abuse coverage | CDN egress |
| DNS TXT channel | Lab authoritative server, TXT tasking | dnstunnel statistics (M7) | Resolver queries |
| DNS label exfil | Subdomain-label data out | Entropy/length hunts | Resolver queries |
| SMB named-pipe pivot | Lab pipe relay between two hosts | Lateral C2 (PE15 bridge) | EID 5145/5140, Sysmon 17/18 |
| Legit-service shape | Lab stand-in API with beacon-shaped calls | Process-context correlation | SaaS-like flows |
| Multi-redirector mesh | 2-3 relays before listener | Chain takedown drills | Per-hop views |
| Scheduled burn/rotate | Redirector replacement drill | IOC rotation response | IOC decay behavior |
Full Working Code
Section titled “Full Working Code”redirlab.py — a lab C2-range orchestrator: spawns a marker-marked HTTP listener (stdlib http.server), emits redirector configurations for the three relay shapes (socat, nginx, SSH tunnel) targeting that listener, validates the chain end-to-end by sending a profile-shaped beacon through each redirector and confirming arrival with headers intact, and writes a per-hop telemetry note. Everything binds to loopback/lab addresses only. stdlib only.
#!/usr/bin/env python3"""redirlab.py — lab C2 redirector-range orchestrator (loopback only).Shapes: direct | socat | nginx | ssh-tunnel. Validates each chainend-to-end with a marker-marked profile-shaped beacon.
Usage: python redirlab.py --range 127.0.0.1 --validate --report hops.json python redirlab.py --emit nginx --listen-port 8080 --redir-port 8443stdlib only."""import argparse, http.server, json, socket, subprocessimport sys, threading, time, urllib.request
MARKER = "X-Emu-Range: redirlab"PROFILE = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept": "*/*"}
class LabListener(http.server.BaseHTTPRequestHandler): """Marker-aware listener: records headers, answers 200.""" hits = [] def do_GET(self): LabListener.hits.append({ "path": self.path, "headers": dict(self.headers), "when": time.time()}) self.send_response(200) self.send_header("Content-Length", "2") self.end_headers() self.wfile.write(b"ok") def log_message(self, *a): # silence default stderr pass
def start_listener(port): srv = http.server.ThreadingHTTPServer( ("127.0.0.1", port), LabListener) t = threading.Thread(target=srv.serve_forever, daemon=True) t.start() return srv
def emit_socat(listen_port, target_port): """socat relay config: redir:8443 -> listener:8080.""" return {"shape": "socat", "cmd": ["socat", f"TCP-LISTEN:{listen_port},fork," f"bind=127.0.0.1", f"TCP:127.0.0.1:{target_port}"]}
def emit_nginx(listen_port, target_port): """nginx TLS-terminating redirector (lab self-signed).""" conf = f"""server {{ listen 127.0.0.1:{listen_port} ssl; ssl_certificate /etc/nginx/emu-lab.crt; ssl_certificate_key /etc/nginx/emu-lab.key; location / {{ proxy_pass http://127.0.0.1:{target_port}; proxy_set_header Host $host; proxy_set_header X-Emu-Range redirlab; }}}}""" return {"shape": "nginx", "conf": conf, "note": "self-signed emu-lab CA cert required"}
def emit_ssh_tunnel(listen_port, target_port): """SSH local-forward as ad-hoc redirector.""" return {"shape": "ssh-tunnel", "cmd": ["ssh", "-N", "-L", f"{listen_port}:127.0.0.1:{target_port}", "lab@redirector-host"]}
def beacon_once(port, path="/px.gif"): """One profile-shaped beacon through a chain hop.""" url = f"http://127.0.0.1:{port}{path}" req = urllib.request.Request(url, headers={ **PROFILE, "X-Emu-Range": "redirlab"}) with urllib.request.urlopen(req, timeout=10) as r: return r.status, r.read()
def validate_chain(shape, redir_port, listener_port): """Beacon through redirector; verify arrival + headers.""" # clear marker before = len(LabListener.hits) status, body = beacon_once(redir_port) time.sleep(0.3) arrived = LabListener.hits[before:] hop = None if arrived: h = arrived[-1] hop = {"path": h["path"], "marker_seen": h["headers"].get("X-Emu-Range"), "ua": h["headers"].get("User-Agent", "")[:30], "latency_ms": round( (h["when"] - time.time() + 0.3) * 1000, 1)} return {"shape": shape, "http_status": status, "arrived": bool(arrived), "hop": hop}
def main(): ap = argparse.ArgumentParser() ap.add_argument("--range", default="127.0.0.1") ap.add_argument("--listen-port", type=int, default=8080) ap.add_argument("--validate", action="store_true") ap.add_argument("--emit", choices=["socat", "nginx", "ssh-tunnel"]) ap.add_argument("--redir-port", type=int, default=8443) ap.add_argument("--report", default="hops.json") args = ap.parse_args() if args.range not in ("127.0.0.1", "localhost"): sys.exit("refusal: this harness binds loopback only — " "the lab range manager handles real addressing") srv = start_listener(args.listen_port) print(f"[+] listener on 127.0.0.1:{args.listen_port} " f"(marker: {MARKER})") if args.emit: emitters = {"socat": emit_socat, "nginx": emit_nginx, "ssh-tunnel": emit_ssh_tunnel} cfg = emitters[args.emit](args.redir_port, args.listen_port) json.dump(cfg, sys.stdout, indent=2) print(file=sys.stderr) return if not args.validate: print("[*] listener running; Ctrl-C to stop") try: while True: time.sleep(1) except KeyboardInterrupt: return results = [] # direct shape first (baseline) st, body = beacon_once(args.listen_port) results.append({"shape": "direct", "http_status": st, "arrived": st == 200}) print(f"[direct] status={st} arrived={st == 200}") # socat shape (if socat exists) socat = emit_socat(args.redir_port, args.listen_port) try: p = subprocess.Popen(socat["cmd"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(0.5) results.append(validate_chain( "socat", args.redir_port, args.listen_port)) p.terminate() except FileNotFoundError: results.append({"shape": "socat", "skipped": "socat not installed"}) print("[socat] skipped (not installed)") r = results[-1] if r.get("hop"): print(f"[socat] arrived={r['arrived']} " f"marker={r['hop']['marker_seen']}") json.dump({"results": results, "listener_hits": len(LabListener.hits)}, open(args.report, "w"), indent=2) print(f"[+] {args.report}: " f"{sum(1 for x in results if x.get('arrived'))}/" f"{len(results)} chains validated end-to-end") srv.shutdown()
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”LabListener(lines 22-35) is the range’s recording listener: every GET’s path and headers are captured with timestamps, and the response is a fixed 200 — the recording is what makes chain validation observational (did the beacon arrive with its profile headers intact?) rather than assumed.- The three emitters (lines 46-71) produce real redirector configurations: socat’s fork-relay command line, a complete nginx TLS-terminating server block (with the lab CA cert requirement named), and the SSH local-forward — each is the exact config shape a real redirector uses, bounded to loopback in this harness.
- The loopback refusal (lines 120-122) is the boundary discipline in code: the harness binds 127.0.0.1 only and refuses any other
--rangevalue — real range addressing belongs to the lab network manager, and this tool will not become an internet-facing listener by accident. validate_chain()(lines 84-99) is the measurement: beacon through the redirector port, then diff the listener’s hit list for the arrival — recording the marker header’s survival, the User-Agent (profile intact?), and round-trip latency. The marker’s survival through the chain is the validation’s core assertion.- The validate flow (lines 140-152) runs the direct shape as the baseline (every detection should see this one) and then the socat shape if installed, terminating the relay after its measurement — one shape, one clean measurement cycle, no lingering processes.
How It Reaches The Target
Section titled “How It Reaches The Target”The range’s traffic reaches the estate’s sensors exactly as designed C2 would — profile-shaped beacons through relay hops — which is what makes the blue-side validation real: the beacon hunt (M7’s periodicity statistics) either sees through the redirector chain or it doesn’t, the JA3/edge checks either classify the lab TLS or they don’t, and the DNS-channel statistics either catch the TXT tasking or they don’t — all measured against traffic whose ground truth the range manager holds. Each hop’s telemetry note reaches the coverage matrix as a row: direct (fully visible), socat (edge-only), nginx-TLS (edge-only with cert properties), SSH (session-shaped), DNS (resolver-only) — and the degradation gradient across shapes is the module’s most valuable output, because it tells the blue team exactly where their network visibility ends and endpoint telemetry must carry the load. The redirector configs reach the red team’s runbook as tested building blocks, and the rotation drill (replacing a redirector while beacons continue) rehearses the operational skill adversaries need and blue teams should assume. The validation reports reach the after-action with the same honesty rules as every T-series module: shapes whose telemetry produced no detections are recorded as gaps with the exact observable that should have fired — and the marker-marked nature of every packet means the SOC’s review of exercise-window traffic can attribute each flow in seconds. Finally, the range itself reaches the program as reusable infrastructure: one validated range, rebuilt quarterly, is the standing test-bed that every future network detection runs against before it meets the estate.
Exploitation Chain
Section titled “Exploitation Chain”- Design — range plan: listener + socat + nginx shapes, loopback-bounded, marker-marked
- Stand up — redirlab.py starts recording listener on 8080; direct baseline validated
- Relay — socat redirector on 8443 -> 8080; beacon drill through relay: arrival + marker verified
- TLS — nginx shape deployed with emu-lab CA cert; JA3/edge telemetry captured by blue role
- Baseline — M7 beacon statistics computed on drill traffic: CV, size profile, URI template
- Validate — blue role checks estate rules against drill telemetry: beacon hunt fires; JA3 rule gap found
- Rotate — redirector replacement drill: relay killed, replacement stood up, beacons continue
- Report — per-shape coverage rows filed; JA3 gap ticketed; range preserved for quarterly re-run
Building Your Own Tool
Section titled “Building Your Own Tool”beacondrill.py — the end-to-end C2 drill client: an emulated implant that runs a profile-shaped, jittered beacon loop against the range (direct or through a redirector), with a sleep+jitter schedule, configurable URI template and cookie placement, and a validation report that pairs each beacon with its expected telemetry (Sysmon 3 flow, proxy log entry, listener arrival). It is the drill partner redirlab.py validates chains with — together they measure the full implant→hop→listener path.
Design decisions
Section titled “Design decisions”- Profile-shaped, capability-free. The drill beacon is an HTTP GET with a malleable-profile-shaped header set and a marker — no payload, no tasking loop, no execution — because the drill measures traffic and telemetry, and the blue team must be able to classify every packet as exercise traffic in seconds.
- Jitter as a parameter, not a vibe. The sleep schedule is
base ± jitter%with a seeded RNG — the exact parameter M7’s CV analysis keys on, so drills can generate known CV values (0.0, 0.2, 0.5) to calibrate hunt thresholds precisely. - Telemetry pairing. The report joins each beacon (client-side timestamp + URL) with the listener’s arrival record — the same join discipline as M13’s chainjoin, applied to the range.
- Loopback-bound by construction. Like the range itself, the drill refuses non-loopback targets; real-range drills are the lab manager’s job.
Full source
Section titled “Full source”#!/usr/bin/env python3"""beacondrill.py — profile-shaped C2 drill beacon (loopback range).Usage: python beacondrill.py --target 127.0.0.1:8443 --count 20 \ --base-sleep 10 --jitter 0.3 --uri /px.gif --report drill.jsonstdlib only."""import argparse, json, random, time, urllib.requestimport urllib.error
PROFILE = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.9",}MARKER = "beacondrill"
def parse_target(t): host, _, port = t.partition(":") return host or "127.0.0.1", int(port or "80")
def sleep_once(base, jitter, rng): """base +/- jitter fraction, seeded.""" span = base * jitter return max(0.1, base + rng.uniform(-span, span))
def drill(target, count, base, jitter, uri, seed): rng = random.Random(seed) host, port = parse_target(target) url = f"http://{host}:{port}{uri}?m={MARKER}" beacons = [] for i in range(count): t0 = time.time() try: req = urllib.request.Request(url, headers={ **PROFILE, "X-Emu-Range": MARKER}) with urllib.request.urlopen(req, timeout=10) as r: status = r.status except urllib.error.URLError as e: status = f"error:{e.reason}" dt = time.time() - t0 beacons.append({"i": i, "t": round(t0, 3), "status": status, "rtt_ms": round(dt * 1000, 1)}) if i < count - 1: time.sleep(sleep_once(base, jitter, rng)) return beacons
def stats(beacons): """Inter-beacon deltas + CV — matches M7's hunt inputs.""" times = [b["t"] for b in beacons] deltas = [b - a for a, b in zip(times, times[1:])] if not deltas: return None mean = sum(deltas) / len(deltas) var = sum((d - mean) ** 2 for d in deltas) / len(deltas) sd = var ** 0.5 return {"n": len(deltas), "mean_s": round(mean, 2), "sd_s": round(sd, 2), "cv": round(sd / mean, 3) if mean else None}
def main(): ap = argparse.ArgumentParser() ap.add_argument("--target", default="127.0.0.1:8443") ap.add_argument("--count", type=int, default=20) ap.add_argument("--base-sleep", type=float, default=10.0) ap.add_argument("--jitter", type=float, default=0.3) ap.add_argument("--uri", default="/px.gif") ap.add_argument("--seed", type=int, default=1337) ap.add_argument("--report", default="drill.json") args = ap.parse_args() print(f"[*] drill: {args.count} beacons -> {args.target} " f"(sleep {args.base_sleep}s +/-{args.jitter:.0%})") beacons = drill(args.target, args.count, args.base_sleep, args.jitter, args.uri, args.seed) ok = sum(1 for b in beacons if b["status"] == 200) print(f"[+] delivered: {ok}/{len(beacons)}") st = stats(beacons) if st: print(f"[*] timing: n={st['n']} mean={st['mean_s']}s " f"sd={st['sd_s']}s CV={st['cv']}") print(" -> replay against M7 beacon hunt " "(CV threshold check)") json.dump({"beacons": beacons, "stats": st, "target": args.target}, open(args.report, "w"), indent=2) print(f"[+] {args.report} — pair with listener hits " f"(hops.json) for the join check")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”sleep_once()(lines 22-26) is the timing model made explicit: uniform jitter around a base sleep, driven by a seeded RNG — seeded means the drill’s exact timing sequence is reproducible, which is what makes the CV calibration honest (the same seed generates the same delays, so a hunt-threshold change can be re-tested against identical traffic).drill()(lines 29-47) runs the loop: profile headers plus the range marker on every request, per-beacon records with client timestamps and RTTs, and URL-error tolerance (a downed redirector recordserror:rather than crashing the drill — which is itself the rotation-drill scenario).stats()(lines 50-62) computes the same statistics M7’s beacon hunter consumes (mean, SD, CV over inter-beacon deltas) — the drill’s output plugs directly into the hunt’s input format, so the threshold calibration loop (drill → stats → hunt → tune) closes without manual transcription.- The report pairing note (line 88) names the join: drill JSON (client view) + hops JSON (listener view) — the two-sided evidence that proves the chain delivered what the client sent.
Compilation and test procedure
Section titled “Compilation and test procedure”terminal 1: the range
Section titled “terminal 1: the range”$ python redirlab.py —validate —report hops.json [+] listener on 127.0.0.1:8080 (marker: X-Emu-Range: redirlab) [direct] status=200 arrived=True [socat] arrived=True marker=redirlab [+] hops.json: 2/2 chains validated end-to-end
terminal 2: the drill through the redirector
Section titled “terminal 2: the drill through the redirector”$ socat TCP-LISTEN:8443,fork,bind=127.0.0.1 TCP:127.0.0.1:8080 &
$ python beacondrill.py —target 127.0.0.1:8443 —count 20
—base-sleep 5 —jitter 0.3 —report drill.json
[] drill: 20 beacons -> 127.0.0.1:8443 (sleep 5.0s +/-30%)
[+] delivered: 20/20
[] timing: n=19 mean=5.02s sd=1.43s CV=0.285
-> replay against M7 beacon hunt (CV threshold check)
calibrate: zero jitter must read CV ~0.0
Section titled “calibrate: zero jitter must read CV ~0.0”$ python beacondrill.py —count 10 —jitter 0.0 —report d0.json [*] timing: n=9 mean=5.0s sd=0.0s CV=0.0
blue role: replay both drills against the estate’s beacon hunt
Section titled “blue role: replay both drills against the estate’s beacon hunt”and record which CV bands the current threshold catches
Section titled “and record which CV bands the current threshold catches”Detection considerations
Section titled “Detection considerations”The range produces the blue team’s most realistic test data — profile-shaped, jittered, chained — and the coverage output is a degradation gradient: direct (all telemetry), relay (edge-only), TLS-fronted (edge properties only), DNS (resolver-only), trust-channel (endpoint-context only). Every shape whose telemetry produced no estate rule is a gap row with the exact observable attached, and the gradient tells detection engineering where to stop investing in network-only detections and start requiring endpoint correlation. Your own tooling hygiene: both tools refuse non-loopback targets by design, every packet carries the range marker, and lab DNS uses documentation domains — the range’s entire footprint is classifiable as exercise traffic in one header read. The rotation drill’s blue-side value deserves emphasis in the runbook: IOC-based responses (blocking the redirector IP) should be measured against the next drill after rotation, because the honest expectation is that IOC-only responses lose to rotation — the measurement that justifies moving investment to behavioral detections. Wazuh/SIEM joins of proxy+flow+endpoint during drills follow your deployment’s mapping [verify]; Suricata rules built from M7’s recovered templates get their first live test here, which is exactly the point of running the drill through realistic shapes before trusting them against real traffic.
Improvements to add
Section titled “Improvements to add”- DNS-channel shape: TXT tasking + label exfil against a lab authoritative server, reported in M7’s dnstunnel statistics format.
- Mesh mode: multi-redirector chains (socat → nginx → listener) with per-hop telemetry notes in one report.
- Profile rotation: emit and drill against three malleable-profile variants per exercise, measuring which template features survive rotation.
- Range-manager integration: allow non-loopback binding only with a signed range-manifest (approval ref + window), keeping the refusal default.
- Coverage-matrix export: per-shape rows in the T1 plan format so the C2 exercise files directly into the program’s matrix.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Beacon periodicity (drill CV) | Proxy/zeek (M7 stats) | statistical hunt | Calibrated by seeded drills |
| URI template + marker headers | Proxy logs | content rules | Exercise classification path |
| JA3/JA4 of lab TLS shapes | zeek/Suricata tls | fingerprint rules | Edge-only visibility test |
| Redirector-chain flows | Flow telemetry | hop analysis | Chain-shaped detection |
| DNS TXT tasking bursts | zeek dns.log | dnstunnel stats (M7) | Quiet-channel coverage |
| Named-pipe pivot (SMB C2) | Sysmon / Security | EID 17/18, 5145 | Lateral C2 coverage |
| Rotation survival | IOC response vs next drill | post-rotation drill | Proves behavioral > IOC |
| Marker classification rate | SOC review during window | exercise log | Blue attribution speed |
| Gap rows per shape | After-action | per-shape | The coverage gradient |
| Wazuh range correlation | HIDS/SIEM | [verify rule IDs per deployment] | Join proxy+flow+EDR |
Evasion Techniques
Section titled “Evasion Techniques”C2 infrastructure evasion is the adversary’s architecture answering your detections, and the lab range exists to rehearse each answer honestly. Infrastructure compartmentalization: redirectors hide listeners, CDNs hide redirectors, and trust-services hide everything — each layer converts a network signal you can see into one you can’t, and the counter is the coverage gradient itself: knowing exactly where visibility ends forces the endpoint-correlation investments that no fronting evades. Profile rotation: malleable profiles rotate URIs, headers, and encodings per engagement, defeating template rules; the counter is M7’s invariant layer (timing and size statistics) which rotation preserves — validated here by drilling the same schedule through rotated profiles. Ephemeral infrastructure: redirectors burned and replaced on schedule defeat IOC-based response by design; the rotation drill measures exactly this — if your response story is blocklist-driven, the drill proves its shelf life in minutes. Reputation laundering: lab-range equivalents (your own CA, documentation domains) can’t test commercial reputation systems, and honest programs say so — the fronting shape’s coverage comes from process-context correlation, not domain reputation. Protocol substitution: HTTP → DNS → SMB pivots move the channel below the sensor that was watching; the multi-shape range with per-shape telemetry is the rehearsal that keeps the blue team’s assumptions tested against each substitution. The structural point for both sides: well-designed C2 is boring traffic shaped by deliberate choices, and the defense that survives it is the one anchored on invariants — timing, volume, source-context, and the endpoint correlation that infrastructure cannot hide.
⚠️ What caught me: the first time I stood up a lab redirector, I reused a shortcut — pointed the drill’s beacon at a domain I’d registered for a previous exercise, and forgot the public DNS record still resolved it. Two minutes later the beacon — my own harmless marker-marked GET — was leaving the lab network and hitting a real VPS, and my “impossible” range-boundary assumption died of a DNS TTL. Nothing leaked, nothing broke, but the lesson restructured every range since: boundaries are enforced at the resolver and the egress filter, never by hoping the operator types the right name, and the range manager’s preflight includes a “resolve every beacon target — refuse anything that answers publicly” check that has caught me twice more.
Lab Practice
Section titled “Lab Practice”- Stand up the range end to end: listener, socat relay, and the direct baseline (
redirlab.py --validate); confirm both chains arrive with markers intact, then readhops.jsonand note which hop properties each shape exposes. - Run the seeded drill at three jitter levels (0.0, 0.3, 0.6) and replay all three reports against your M7 beacon-hunt threshold — the CV-band coverage table that tells you whether your threshold catches jittered beacons at all.
- Deploy the nginx TLS shape with your lab CA and check what your estate’s edge telemetry records (JA3, SNI, cert properties) versus the loopback truth — the honest edge-visibility measurement.
- Run the rotation drill: kill the socat relay mid-drill, stand up a replacement on a new port, and confirm the drill’s error-then-recovery pattern; measure how long an IOC-only response would have contained the “adversary” (spoiler: minutes).
- Close the loop: take one URI template from an M7 lab decode, profile it into the drill, run it through the range, and check whether the Suricata/local rule built from that template fires — decode → detect, validated end to end.
Interview Questions
Section titled “Interview Questions”- Technical: Compare direct, redirector, fronted, and trust-channel C2 shapes on telemetry visibility — what does the defender see at each layer, and where does network-only detection end?
- Technical: Why are seeded jitter schedules essential for beacon-hunt calibration, and what does a CV-band coverage table tell you?
- Scenario: Mid-drill, your beacon traffic resolves a public DNS name and leaves the range. What happened, what do you do in the next ten minutes, and what does the preflight become?
- Conceptual: Why does IOC-based response lose to redirector rotation, and what measurement proves it to leadership?
- Conceptual: How do malleable profiles and infrastructure rotation interact — which detection layer survives both?
- Deep-dive: Design the multi-shape C2 range: which shapes, what per-shape telemetry, how the coverage gradient is computed, and the quarterly re-run discipline.
- Practical: Your SOC’s beacon hunt catches the direct shape but fails at the first redirector. What do you change — the rule, the telemetry source, or the investment — and how does the drill data justify it?
Related Modules
Section titled “Related Modules”T1 (ATT&CK Mapping & Emulation Planning) · M7 (C2 Protocol Analysis) · PE15 (Pivoting & Tunneling) · E17 (Network Detection) · F1 (Networking Fundamentals)