Skip to content

Wireless IDS Evasion

Wireless IDS evasion is the study of how adversary operations stay inside (or outside) the detection envelope that Wireless Intrusion Detection Systems define — and, run in reverse, it is the most effective methodology for testing that envelope. WIDS products key on specific frame shapes and statistics: deauth/disassociation storm rates, rogue-beacon signatures, probe-response anomalies (KARMA/MANA fingerprints), association-rate bursts, and profile mismatches against known APs. Each detection class has an evasion shape: pacing (a single deauth instead of a storm — below rate thresholds), positioning (operating on channels or in positions the sensor plan doesn’t cover), cloning depth (replicating the legitimate beacon’s full IE profile so profile-diff detection sees nothing), identity reuse (spoofing known AP MACs so rogue detection attributes frames to your own fleet), and client-side preference (attacks that happen at the client — evil-twin harvesting — where the only network-side evidence is an association the sensors may never correlate). The module’s discipline is dual: for the assessor, building evasion-shaped drills that measure which WIDS rules actually fire; for the defender, understanding which of your detections are rate-based (pacing-evasive), signature-based (cloning-evasive), coverage-based (position-evasive), or correlation-based (the only class that survives all three).

A WIDS that has never been evasion-tested is a compliance checkbox with unknown coverage: vendors publish detection categories, but your deployment’s sensor density, channel plan, and alert thresholds determine what actually fires — and the gap between “the product detects X” and “our deployment detects X” is exactly where real adversaries live. The failure modes are systematic and worth naming: rate-based deauth detection dies to single-deauth pacing (which is all WI2’s handshake forcing needs); rogue detection dies to cloning depth and MAC spoofing (the twin that looks like your fleet until the inventory diff catches it); coverage dies to channel and position gaps (the sensor plan that watches 1/6/11 while the twin sits on 44); and everything dies to low-volume patience — an adversary who does one thing per hour produces a stream of single events that no rate rule aggregates. Running evasion drills against your own WIDS converts the product’s promise into a measured coverage map (which shapes fire, which don’t, at which sensor), and that map drives the three real fixes: sensor density/placement (coverage), baseline correlation (pacing), and inventory joins (cloning). For the assessor, the evasion drill is also the honest way to test client-side attacks: the evil twin whose beacon is never flagged, whose harvest produces only EAP failures on the RADIUS side, is the scenario that proves your detection story’s weak edge — and it is better discovered by your own drill than by a report from an incident.

Each WIDS detection class maps to an evasion shape and a defender counter:

WIDS DETECTION CLASSES vs EVASION vs COUNTERS
[deauth storm alert] rate-based
evasion: single deauth, minutes apart, rotating source
counter: correlation (any unexplained deauth from
non-BSS MAC = event, not just storms)
[rogue AP alert] signature/identity-based
evasion: clone legit AP MAC + full IE profile
counter: inventory join (BSSID in survey, not in
inventory list) + physical RF localization
[KARMA/probe anomaly] behavior signature
evasion: targeted responses (only probed SSIDs),
low volume, quiet modes
counter: client-side posture (probing policy) +
response-frame rate baselines per area
[association flood] rate-based
evasion: one association per client, spread over time
counter: per-client history (association never seen
before from this MAC/position = event)
[channel jamming] RF-level
evasion: n/a (jamming is loud by nature)
counter: spectrum monitoring, noise-floor alerts
THE PATTERN: every single-signal rule dies to pacing or
cloning; the surviving detections are CORRELATIONS —
frame x inventory x position x time.

The pacing evasion is the most operationally important: WI2’s handshake forcing needs exactly one deauth per attempt, and “one deauth” is indistinguishable from a roaming glitch unless the defender correlates source MAC against AP inventory — a frame from a MAC that is not one of your APs, targeting your client, is an event at any volume. The cloning evasion escalates the rogue-detection arms race: a twin that copies the legitimate BSSID and the full IE set defeats every frame-level check, and detection moves to physics (the same BSSID on two channels simultaneously — a physical impossibility for one radio, trivially detectable by two sensors seeing it on different channels) and to inventory (BSSID seen where your AP isn’t installed). The position/coverage evasion is deployment-shaped: sensors are placed where cabling is easy, attackers operate where coverage is thin — the counter is the coverage map (the tool below) that turns sensor density from a deployment accident into a design artifact. The client-side split is the structural asymmetry: evil-twin harvesting produces its only network-side evidence on the RADIUS (failure bursts) and endpoint (association to unknown AP) — WIDS sees only the twin’s beacon, which cloning can hide — so the mature detection architecture pairs WIDS with RADIUS analytics and endpoint posture, exactly the join the T7/WI4 workflows validate. The legal line stays load-bearing: jamming (RF DoS) is illegal in nearly every jurisdiction and is not an assessment technique — detection of it (noise-floor monitoring) is defensive coverage, not a red-team tool.

Evasion shapeDefeatsHowDefender counter
Single-deauth pacingRate-based deauth alerts1 frame, minutes apart, rotating sourcesSource-inventory correlation
Full-profile cloningSignature rogue detectionCopy BSSID + complete IE setSame-BSSID-two-channels physics check
MAC spoofing of own APRogue detectionTransmit as known BSSIDInventory join + localization
Channel-gap operationCoverage-based detectionSensor-blind channels (chanstat)Coverage map + sensor adds
Position-gap operationCoverage-based detectionWeak-signal corners, floorsDensity design, not accidents
Quiet MANAProbe-anomaly signaturesRespond only to selected probesClient posture (probing policy)
Long-horizon patienceAll rate-based rulesOne event per hourTrending over rate alerting
Encrypted-transport opsContent inspectionAll C2 inside legit TLSEndpoint + volume stats
Sensor-area DoS (jamming)The sensors themselvesRF floodingNoise-floor alerts; ILLEGAL to do
Twin-at-client harvestingNetwork-side WIDS entirelyNo rogue frame neededRADIUS + endpoint joins

widsdiffer.py — the WIDS coverage differentiator: joins drill-shaped capture events (the evasion-shaped frames you transmitted in an authorized drill) against the alerts your WIDS/SIEM recorded for the same window, and computes per-shape detection coverage — which evasion shapes fired which rules, which were missed, and which sensor (if any) saw them. The drill-and-measure loop from T1/T9, applied to the wireless detection estate. stdlib only.

#!/usr/bin/env python3
"""widsdiffer.py — WIDS drill-coverage differentiator.
Usage:
python widsdiffer.py --drill drill_events.json \
--alerts wids_alerts.csv [--window 120] [--out cov.md]
drill_events.json: the frames your authorized drill sent
[{"shape": "single-deauth", "t": 1710000000.0,
"src": "emu:01", "dst": "client:07", "channel": 6}]
wids_alerts.csv: timestamp,rule,sensor,details
stdlib only.
"""
import argparse, csv, json, sys
from collections import defaultdict
def load_drill(path):
try:
return json.load(open(path, encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
sys.exit(f"drill file error: {e}")
def load_alerts(path):
alerts = []
try:
with open(path, encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
try:
alerts.append({
"t": float(row["timestamp"]),
"rule": row.get("rule", "?"),
"sensor": row.get("sensor", "?"),
"details": row.get("details", "")})
except (KeyError, ValueError):
continue
except OSError as e:
sys.exit(f"alerts file error: {e}")
return alerts
def correlate(drill_events, alerts, window):
"""For each drill shape: any alert within window?"""
per_shape = defaultdict(lambda: {
"sent": 0, "detected": 0, "sensors": set(),
"rules": set(), "missed": 0})
alerts.sort(key=lambda a: a["t"])
for ev in drill_events:
shape = per_shape[ev.get("shape", "unknown")]
shape["sent"] += 1
t0 = ev.get("t")
hit = None
for a in alerts:
if abs(a["t"] - t0) <= window:
hit = a
break
if hit:
shape["detected"] += 1
shape["sensors"].add(hit["sensor"])
shape["rules"].add(hit["rule"])
else:
shape["missed"] += 1
return per_shape
def report(per_shape):
lines = ["# WIDS drill coverage", "",
"| shape | sent | detected | missed | "
"rules | sensors |",
"|---|---|---|---|---|---|"]
rows = []
for shape, s in sorted(per_shape.items(),
key=lambda kv: kv[1]["missed"],
reverse=True):
cov = s["detected"] / s["sent"] if s["sent"] else 0
rows.append((cov, shape, s))
lines.append(
f"| {shape} | {s['sent']} "
f"| {s['detected']} | {s['missed']} "
f"| {', '.join(sorted(s['rules'])) or '-'} "
f"| {', '.join(sorted(s['sensors'])) or '-'} |")
lines += ["", "## Coverage summary", ""]
total_sent = sum(s["sent"] for s in per_shape.values())
total_det = sum(s["detected"] for s in
per_shape.values())
lines.append(f"- overall: {total_det}/{total_sent} "
f"drill events detected "
f"({total_det/total_sent:.0%})"
if total_sent else "- no events")
blind = [(sh, s) for cov, sh, s in rows if cov == 0]
if blind:
lines.append("- **blind shapes** (never detected):")
for sh, s in blind:
lines.append(f" - {sh} ({s['sent']} sent)")
return "\n".join(lines) + "\n"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--drill", required=True)
ap.add_argument("--alerts", required=True)
ap.add_argument("--window", type=int, default=120)
ap.add_argument("--out")
args = ap.parse_args()
drill = load_drill(args.drill)
alerts = load_alerts(args.alerts)
if not drill:
sys.exit("empty drill events")
per_shape = correlate(drill, alerts, args.window)
text = report(per_shape)
print(text)
if args.out:
open(args.out, "w").write(text)
print(f"[+] {args.out}")
if __name__ == "__main__":
main()
  • The correlation window (lines 42-55) is the join’s heart: each drill event is matched against alerts within ±window seconds (default 120 — telemetry latency between your transmission and the sensor’s alert is real, and the window is a documented parameter, not an assumption). First-match scoring keeps the per-shape counters honest about events rather than alerts.
  • Per-shape aggregation (lines 34-41) builds the coverage view: sent vs detected vs missed per evasion shape, with the sensor and rule names captured — the report’s columns tell you not just whether a shape was caught but which sensor and which rule did it, which is the operational detail that drives placement and tuning.
  • The blind-shapes summary (lines 66-73) is the report’s punchline: shapes with zero detection are the estate’s wireless blind spots, named and counted — the artifact that turns “we have WIDS” into “these four shapes sail through.”
  • The drill-event contract (lines 9-14) deserves emphasis: the JSON records what you transmitted (shape, timestamp, source, channel) — the ground truth the correlation needs. Without drill-side ground truth, coverage measurement degrades to alert counting, which measures sensor volume rather than detection coverage.

The drill reaches the WIDS estate exactly as the T-series’ other drills reach their targets: authorized, windowed, emu-marked frames transmitted into your own airspace, with the drill manifest as ground truth. Each shape reaches a different detection layer: paced deauths reach the rate rules (and should be caught only by correlation), cloned beacons reach signature rules (and should be caught only by inventory joins), channel-gap operations reach the coverage map (and should be caught by nothing — which is the finding), and twin-at-client harvests reach the RADIUS logs (which is where the join to endpoint telemetry lives). The differentiator’s output reaches the program as the coverage matrix’s wireless rows: shapes detected (validated), shapes missed (gaps with the exact evasion shape named), and sensors that saw nothing (placement findings). Those rows reach three owners: detection engineering gets the correlation-rule backlog (source-inventory deauth correlation, same-BSSID-two-channel checks), facilities/infra gets the sensor-placement findings (channel and position gaps), and the SOC gets the drill-window alert review (which of your alerts were emu artifacts, and how fast were they classified). The trend reaches leadership as the wireless coverage line — validated shapes per quarter, rising as correlations and sensors land — the same honest metric shape the whole program runs on. And the absence of detections during a drill reaches the report as its most valuable row: the evasion shape that sailed through is exactly the one an adversary will use, now known to you first.

  1. Plan — authorized drill: 5 evasion shapes, own airspace, WIDS + SIEM logging confirmed
  2. Baseline — 24h quiet capture: alert noise floor recorded (what does normal look like)
  3. Execute — paced deauth (1/5min), cloned beacon (full IE), ch-44 op, quiet MANA, twin harvest
  4. Capture — drill manifest records every frame: shape, time, channel, source
  5. Correlate — widsdiffer.py joins drill events vs WIDS alerts within 120s window
  6. Measure — 2/5 shapes detected (deauth by correlation, twin by RADIUS join), 3 blind
  7. Diagnose — blind shapes map to sensor gaps (ch 44) + missing inventory-join rule
  8. Fix — sensor added to ch 44, correlation rules deployed; re-drill next quarter: 4/5

sensorcover.py — the sensor coverage analyzer: from sensor positions (floor-plan coordinates) and capture data (per-channel signal visibility from WI1/WI6 captures), computes the per-channel/per-position coverage grid, flags blind spots, and recommends sensor additions — turning sensor placement from cable-convenience into a coverage design.

  • Channel × position grid, not floor-plan art. The deliverable is a grid (which channels are visible from which positions) because WIDS coverage is per-channel: a sensor at position P that doesn’t hop to channel 44 provides zero coverage there regardless of its floor position.
  • Captures as truth. Coverage comes from what sensors actually saw in drill captures (sensor alert metadata + channel), not from radio-propagation models — measured coverage survives building changes that models don’t.
  • Recommendations as gaps, not placements. The tool reports where coverage is missing (channel + position cells) and lets humans choose placements — placement optimization is a facilities conversation with constraints the tool doesn’t know.
  • WIDS-vendor-agnostic. The input is the drill/alert CSV shape from widsdiffer — any vendor’s alerts normalize into the same coverage math.
#!/usr/bin/env python3
"""sensorcover.py — WIDS sensor coverage grid from drill captures.
Usage:
python sensorcover.py --sensors sensors.json \
--alerts drill_alerts.csv [--channels 1,6,11,36,44,149]
sensors.json: [{"name":"s1","x":10,"y":5,"channels":[1,6,11]}]
alerts CSV: widsdiffer shape (timestamp,rule,sensor,details)
stdlib only.
"""
import argparse, csv, json, sys
from collections import defaultdict
def load_sensors(path):
try:
return json.load(open(path, encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
sys.exit(f"sensor file error: {e}")
def alert_channels(alerts):
"""Extract channels seen per sensor from alert details."""
per_sensor = defaultdict(set)
for a in alerts:
s = a.get("sensor", "?")
details = a.get("details", "")
# convention: details contain 'ch=<n>' from drill tags
if "ch=" in details:
try:
ch = int(details.split("ch=")[1]
.split()[0].strip(","))
per_sensor[s].add(ch)
except (ValueError, IndexError):
continue
else:
per_sensor[s].add(-1) # unknown channel marker
return per_sensor
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--sensors", required=True)
ap.add_argument("--alerts", required=True)
ap.add_argument("--channels", default="1,6,11,36,44,149")
ap.add_argument("--out")
args = ap.parse_args()
sensors = load_sensors(args.sensors)
alerts = []
with open(args.alerts, encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
alerts.append(row)
seen = alert_channels(alerts)
channels = [int(c) for c in args.channels.split(",")]
print("== sensor channel coverage ==")
print(f"{'sensor':<12} {'pos':<10} "
f"{'channels seen (of plan)':<26} gaps")
print("-" * 70)
grid = defaultdict(set) # channel -> sensors covering
for s in sensors:
saw = seen.get(s["name"], set())
planned = set(s.get("channels", channels))
observed = saw & set(channels)
gaps = sorted(planned - observed)
for ch in observed:
grid[ch].add(s["name"])
print(f"{s['name']:<12} "
f"({s.get('x','?')},{s.get('y','?')}) "
f"{sorted(observed) if observed else 'NONE':<26} "
f"{gaps if gaps else '-'}")
print("\n== channel-level coverage ==")
for ch in channels:
cov = grid.get(ch, set())
state = "covered" if cov else "BLIND"
print(f" ch {ch:<4} {state:<8} "
f"sensors: {sorted(cov) or '-'}")
blind = [ch for ch in channels if not grid.get(ch)]
if blind:
print(f"\n[!] blind channels: {blind} — "
f"add sensors or hop plans; drills on these "
f"channels are invisible by construction")
if args.out:
json.dump({"channels": {str(c): sorted(
grid.get(c, set())) for c in channels},
"blind": blind},
open(args.out, "w"), indent=2)
print(f"[+] {args.out}")
if __name__ == "__main__":
main()
  • The channel extraction (lines 21-35) parses the drill-tag convention (ch=<n> in alert details) — the drill manifest writes the channel into its frames, the WIDS echoes it in alert details, and the coverage math gets per-sensor channel truth without vendor API dependencies.
  • The grid (lines 55-63) inverts the per-sensor view into per-channel coverage: for each planned channel, which sensors actually observed it — the WIDS coverage question, answered from measured drill traffic rather than vendor datasheets.
  • The blind-channel summary (lines 66-69) is the deliverable: channels no sensor saw during the drill are invisible by construction, and the finding text says exactly that — drills on blind channels are the estate’s undetectable airspace, known now by name.
  • The per-sensor gaps column (line 59) distinguishes planned-but-unobserved channels (hop-plan or hardware issues) from observed coverage — the operational detail that separates “add a sensor” from “fix the hop config”.

run the authorized drill across shapes and channels

Section titled “run the authorized drill across shapes and channels”

$ python drillkit.py —shapes paced-deauth,cloned-beacon
—channels 6,44 —window 300 # (from your lab toolkit)

$ cat > sensors.json <<‘EOF’ [{“name”:“s1”,“x”:10,“y”:5,“channels”:[1,6,11]}, {“name”:“s2”,“x”:30,“y”:5,“channels”:[36,149]}] EOF

$ python widsdiffer.py —drill drill_events.json
—alerts wids_alerts.csv —out cov.md

shapesentdetectedmissedrulessensors
paced-deauth1248corr-rules1
cloned-beacon606--
  • blind shapes (never detected):
    • cloned-beacon (6 sent)

$ python sensorcover.py —sensors sensors.json
—alerts wids_alerts.csv —channels 1,6,11,36,44 == channel-level coverage == ch 1 covered sensors: [‘s1’] ch 44 BLIND sensors: ’-’ [!] blind channels: [44] — add sensors or hop plans

The drill-and-differentiate loop is the wireless version of T9’s atomic suite: scheduled, measured, trended — and its findings feed the three real fixes (correlation rules, sensor adds, inventory joins) rather than threshold tweaking. Your own tooling hygiene: the differentiator and coverage tools read CSV/JSON only; the transmission steps are the drill kit’s, gated by the same authorization architecture as every T-series exercise. The vendor relationship matters: share the blind-shape findings with your WIDS vendor (their rules may need your deployment’s specifics) and require the drill as an acceptance test at procurement — “detects rogue APs” is a claim your clone-drill verifies in an afternoon. Wazuh/SIEM ingestion of WIDS alerts [verify per vendor] is the join that makes RADIUS + endpoint + WIDS correlation possible — the only detection class that survives pacing and cloning. The standing metrics: drill coverage percentage per shape, blind-channel count, and mean time-to-classify drill alerts (the SOC’s tag-and-close speed from T2’s convention) — three numbers that make the wireless detection estate as measurable as the malware one.

  1. Physical localization join: multi-sensor RSSI triangulation of drill sources — coverage and position finding in one drill.
  2. Alert-latency profiling: distribution of transmit-to-alert delay per shape/sensor — the tuning data for the correlation window.
  3. Client-side posture join: correlate drill twin-associations with MDM/endpoint network-join logs (the client-side half of the story).
  4. Continuous drills: lightweight scheduled probe-shape drills (passive-only) feeding the coverage trend weekly.
  5. WIDS-rule mapping: vendor rule IDs → ATT&CK-ish wireless technique mapping in the inventory CSV, completing the program matrix.
SignalSourceIDNotes
Deauth from non-inventory MACCorrelation rule (SIEM)source-inventory joinSurvives pacing evasion
Same BSSID on two channelsMulti-sensor correlationphysics checkSurvives cloning
Drill shapes detectedwidsdiffercoverage matrixValidated vs blind
Blind channelssensorcovercoverage gridPlacement findings
Alert latency distributionwidsdifferper-shape timingCorrelation-window tuning
RADIUS failure bursts (drift)RADIUS analyticsper-BSSIDTwin-harvest signature
Client assoc to unknown APMDM/EDR joinsendpoint postureClient-side half
Noise-floor anomaliesSpectrum monitorjam detectionPrecursor signature
Alert classification latencySOC drill windowstag-and-close timingMarker discipline check
Wazuh wireless rule coverageHIDS/SIEM[verify rule IDs per deployment]The correlation layer

This module’s evasion catalogue is the point — but the meta-evasion, the one that outlasts every specific shape, is operating below the aggregation floor: every WIDS rule has an implicit volume assumption (storms, floods, bursts), and an adversary who accepts patience — one frame, one association, one harvest per hour — presents the telemetry with single events that only correlation can score. The defender’s counter-evolution is structural: rate rules are the cheapest to write and the cheapest to evade, so the mature WIDS architecture moves value into joins — frame × inventory (source MACs that aren’t yours), frame × physics (same identity, two channels), frame × position (signal from where nothing should be), frame × identity-history (client associated to an AP it has never seen). The cloning arms race converges to the same place: frame-level checks die to depth-cloning, but the physics check (one radio, two channels) and the inventory check (BSSID where your AP isn’t) survive any clone. The coverage asymmetry is the defender’s structural weakness: sensors sit where cabling allows, adversaries operate where coverage doesn’t — countered by making coverage measured (the grid) instead of assumed, and by client-side telemetry (RADIUS, MDM network-join logs) that covers the air your sensors don’t. And the legal asymmetry runs the defender’s way at the extremes: jamming is loud and illegal, so the jam-based evasion class is bounded — which is worth knowing when threat-modeling: the adversary who stays legal is the adversary your correlations can catch.

⚠️ What caught me: our first WIDS drill scored 5/5 shapes detected, and I nearly filed the coverage matrix green — then I noticed all five detections came from one sensor and one rule (the deauth storm rule), and the “detected” clones were caught by the legacy ‘MAC OUI mismatch’ heuristic, not by anything that would survive a fleet-matched clone. The drill had measured that our sensors see the airspace; it hadn’t measured that our rules understand it. The redesign split the score: per-shape × per-detection-class coverage (rate / signature / correlation / coverage), and the honest number — correlation-class coverage — started at 20%. That number, not the 5/5, is what got the sensor budget approved; a green matrix that hides which class caught what is just noise with formatting.

  1. Run the authorized 5-shape drill against your lab WIDS (or a SIEM-fed sensor): paced deauth, cloned beacon, blind-channel op, quiet MANA, twin harvest — then widsdiffer.py for the coverage matrix.
  2. Implement the correlation rules the drill exposes (source-inventory deauth join, same-BSSID-two-channel check) in your SIEM and re-drill — the coverage delta is the correlation class’s value, measured.
  3. Build the sensor grid with sensorcover.py from your sensor inventory + drill alerts; identify the blind channels and physically verify one (walk there with a client and confirm the invisibility).
  4. Clone-depth experiment: beacon-clone your lab AP at two depths (SSID-only, full-IE) and observe which detector layer catches each — the arms race, calibrated on your own estate.
  5. Trend it: schedule a lightweight monthly drill (passive shapes only) and trend the coverage line for a quarter — the wireless version of the T9 regression loop.
  • Technical: For each WIDS detection class (rate, signature, coverage, correlation), name its evasion shape and the defender counter that survives it.
  • Technical: Why does the same-BSSID-on-two-channels check survive full beacon cloning, and what deployment requirement does it impose?
  • Scenario: Your drill scores 5/5 detected, all by one rate rule. What does that actually measure, and how do you redesign the score?
  • Conceptual: Why is correlation-based detection the only class that survives pacing and cloning — and what data joins does it require?
  • Conceptual: Where does client-side telemetry (RADIUS, MDM) cover the gaps that WIDS coverage cannot, and what join makes that real?
  • Deep-dive: Design the annual wireless detection validation: drill shapes, sensor grid, per-class scoring, and the procurement acceptance test.
  • Practical: Leadership sees “WIDS: deployed” on the compliance matrix. What does your drill coverage number say, and how do you present the gap without triggering vendor-defensive pushback?

WI1 (Wireless Fundamentals & Recon (802.11)) · WI2 (WPA2-PSK Cracking (Handshake Capture)) · WI4 (Enterprise Wi-Fi & Evil Twin) · WI6 (RF & SDR Fundamentals) · BT12 (Wireless IDS)