Skip to content

Wireless IDS

A Wireless IDS/IPS (WIDS/WIPS) is the monitoring layer for the radio estate your APs can’t self-defend: distributed sensors (dedicated radios in monitor mode, or APs scanning off-channel) observing the 802.11 spectrum, and a detection layer classifying what it sees — rogue APs (unauthorized infrastructure on your network), evil twins (spoofed SSIDs luring your clients — WI4’s attack), deauthentication floods (management-frame abuse), ad-hoc/bypass networks, and client misbehavior (misassociated devices, hotspot bridges, Wi-Fi Direct leaks). The classification problem is the discipline: the air is full of neighbors, guests, phones hot-spotting, Miracast casts, and the coffee-shop next door — and every one of them looks like an “unauthorized AP” to a naive detector, which is why WIDS programs live or die on authorized-asset baselines (BSSID allowlists wired to your actual AP inventory) and verified classification (wireless is physical: the verdict that matters — “is this AP plugged into MY network?” — comes from correlation with the wired side, DHCP/spanning-tree/switch-port data, not from radio signal strength). The craft’s center: the WIDS is a correlation engine between two worlds — the radio you can’t fully control and the wire you can — and its detections are only as honest as the baseline of what’s supposed to be there.

The wireless attacks in this handbook (WI2’s cracking, WI4’s evil twin, WI7’s evasion) all produce radio signatures — and without a WIDS, the detection story for the physical layer is essentially nothing: the evil twin that harvests corporate credentials leaves no endpoint artifact, no SIEM log, no EDR event until the credential fires somewhere else. The 802.11 threat model is also physical adjacency — an attacker needs parking-lot proximity, not network access — which makes wireless the one layer where “we’re air-gapped from the internet” means nothing. The deauth class deserves special mention: management frames were historically unauthenticated (anyone can transmit a deauth claiming your AP’s MAC), and while 802.11w PMF (Protected Management Frames) cryptographically protects deauth/disassoc for capable clients [verify client fleet PMF support], the estate without PMF is deauth-able at will — which breaks sessions, forces reconnects onto the attacker’s stronger fake AP, and enables the handshake captures WI2 cracks.

The failure modes are detection-shaped. The rogue swamp: a WIDS without an authoritative AP-inventory baseline flags 400 “rogues” a day (neighbors, guests, phones) — and the SOC mutes the channel; the one real rogue (a contractor’s travel router bridging your LAN) drowns. Signal-strength confidence: classifying an AP as “internal” because RSSI is strong — attackers use directional antennas and can look closer than the lobby. PMF assumptions: assuming deauth attacks are solved because the new APs support 802.11w — clients without PMF capability remain deauth-able, and mixed-mode estates are half-protected. Sensor placement by coverage maps: RF coverage maps describe the service network, not the monitoring need — the parking lot, the executive floor’s glass conference rooms, and the loading dock each need sensors for different reasons. And alert-only rogue response: finding the rogue is half the job; the other half is the wired-side correlation that locates its switch port and kills it — the WIDS that alerts without a takedown workflow produces findings, not outcomes.

RADIO PLANE WIRED PLANE
+------------------+ +----------------------+
| SENSORS | BSSID/ESSID | AP CONTROLLER / WLC |
| monitor mode, | inventory | authorized AP list |
| off-channel scan |<--------------->| switch port map |
| or scan-APs | | DHCP snooping tables |
+--------+---------+ | 802.1X/NAC state |
| detections +----------+-----------+
v |
+----------------------------+ |
| CLASSIFICATION |<-----------------+
| authorized (in baseline) | "is it on MY wire?"
| neighbor (external, benign)| = wired correlation,
| rogue (unauthorized + | not RSSI
| wire-connected) |
| evil twin (authorized ESSID|
| + foreign BSSID) |
| adhoc / hotspot / direct |
+-------------+--------------+
|
v
+----------------------------+ deauth storm -> WI2 handshake
| ALERTS -> BT1/BT3 pipeline | theft correlation
| verdicts, FP-rate tracking | evil twin -> WI4 incident path
+----------------------------+

The authorized baseline is the product. Every BSSID your controller manages, every ESSID you broadcast (corporate, guest, IoT), and the known-external list (the coffee shop, the hotel next door — discovered and annotated, not just tolerated) — this is the reference set every detection compares against. The baseline is generated from the WLC/switch side (the source of truth), not hand-typed, and it drifts: new APs provisioned, SSIDs added for a project, MAC randomized phones — the weekly diff between radio-observed and wire-authorized is the program’s health check.

Classification runs the wired-correlation rule. An AP broadcasting your guest ESSID from a foreign BSSID is an evil twin candidate; correlation that says “this BSSID’s traffic appears on switch port 34” makes it a rogue with an address to kill. An unknown ESSID with strong signal but no wired presence is a neighbor (annotate, monitor, move on). The false-alarm classes are catalog-worthy: phones hot-spotting (their own SSID, no wire), Miracast/Wi-Fi Direct sessions (ad-hoc-ish P2P), a client bridging (a laptop with a hotspot bridging the corporate LAN — the nastiest class, because it is on your wire), and the visitor’s MiFi. Each has a signature once you correlate the wired side.

Deauth detection counts management frames by (AP BSSID, client MAC, rate): a deauth storm (hundreds/minute) is attack; scattered deauths are RF noise. The response correlation matters more than the alert: deauths against your clients immediately followed by a new AP association means the forced-reconnect play (WI4’s evil twin onboarding) is running — that sequence, not the deauth alone, is the detection that saves the credentials. PMF status per client class (which devices support 802.11w, which SSIDs mandate it) turns “we’re deauth-able” from an assumption into a measured register.

Deep dive: the bridge is the finding — why client-side telemetry outranks AP-side

Programs fixate on classifying access points, but the incident lives on the station side: an evil twin nobody found yet has still harvested nothing until a client associates — and the association event is far easier to see authoritatively than the AP itself. The wire knows things the radio can’t spoof: which clients hold corporate DHCP leases, which authenticated via 802.1X in the last hour, which EDR agent checked in after a suspicious association. The station-side hunt (the Station section of any capture, the WLC’s client table, the NAC session log) answers three questions the AP-side view can’t: which of our devices are on foreign APs right now, which are probing for SSIDs they shouldn’t know (probe requests leak saved networks — the recon tell), and which roamed onto something foreign in the last day. A WIDS program that watches clients as primary and APs as context catches the incident class that matters — the harvested credential — even when the rogue AP itself evaded classification.

TechniqueWhat it doesWhere it failsNotes
Authorized BSSID baselineWire-side truth vs radio viewHand-typed, rotsGenerate from WLC
Wired-side correlation“On my network?” answeredMissing switch-port dataThe rogue verdict
Evil-twin detectionAuthorized ESSID + foreign BSSIDSSID string spoofingWI4 incident path
Deauth flood detectionManagement-frame rate alarmsRF noise FPsRate + sequence logic
PMF coverage registerWho supports 802.11w per SSIDMixed-mode blind spotMeasured, not assumed
Rogue containment (deauth of rogue)WIPS strips fake APsLegal/regulatory cautionJurisdiction-dependent [verify]
Hotspot/direct classificationPhone P2P noise catalogedConsumer device churnWeekly annotation
Sensor coverage planMonitor the perimeter + floorsCoverage maps misusedParking lot counts
Client-behavior checksMisassociated/probe-happy devicesBYOD volumeJoins WI4 policy
FP-rate per classNeighbor class tuned over timeMuted channel deathBT1 verdict join

rogueap — the rogue-AP classifier: parses airodump-ng CSV exports (the de facto field format — AP section and Station section), scores every observed AP against an authorized baseline (BSSID allowlist + ESSID inventory), classifies (authorized / evil-twin-candidate / neighbor / unknown), and flags wire-bridge suspects from the Station section (clients with IPs on your corporate range connected to foreign APs).

#!/usr/bin/env python3
"""rogueap — classify observed APs against the authorized baseline.
Input: airodump-ng CSV export (-w file --output-format csv):
AP section: BSSID, First, Last, channel, Speed, Privacy, Cipher,
Authentication, Power, #beacons, #IV, LAN-IP, ID-len,
ESSID, Key
blank line, then Station section: MAC, First, Last, ..., BSSID (AP assoc),
LAN-IP, Rate, Power, #frames, ..., Probe-SSIDs...
Baseline: authorized_ap.csv (bssid,essid) + corporate CIDR (stations with
corporate IPs attached to foreign BSSIDs = wire-bridge suspects).
"""
import argparse
import csv
import sys
from pathlib import Path
REQUIRED_AP_COLS = 15 # airodump AP section column count
def parse_airodump(path: Path):
"""Split the export into (aps, stations) sections."""
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
try:
blank = next(i for i, l in enumerate(lines) if not l.strip())
except StopIteration:
sys.exit("fail-loud: no Station section (no blank separator) — "
"is this really an airodump CSV?")
ap_rows = list(csv.reader(lines[:blank]))
st_rows = list(csv.reader(lines[blank + 1:]))
if len(ap_rows) < 2:
sys.exit("fail-loud: AP section empty — nothing observed is not a "
"clean airspace, it's a broken capture")
return ap_rows[1:], [r for r in st_rows[1:] if len(r) > 13]
def norm_bssid(b):
return b.strip().upper()
def main():
ap = argparse.ArgumentParser(description="rogue AP classifier")
ap.add_argument("capture", type=Path, help="airodump-ng CSV export")
ap.add_argument("--baseline", type=Path, required=True,
help="authorized_ap.csv: bssid,essid")
ap.add_argument("--corp-cidr", required=True,
help="corporate IP range prefix, e.g. 10.40.")
ap.add_argument("--out", type=Path, default=Path("rogueap_out"))
args = ap.parse_args()
if not args.capture.exists():
sys.exit(f"fail-loud: capture not found: {args.capture}")
if not args.baseline.exists():
sys.exit(f"fail-loud: baseline not found: {args.baseline}")
base_rows = list(csv.DictReader(args.baseline.open(encoding="utf-8")))
authorized = {}
for r in base_rows:
authorized[norm_bssid(r.get("bssid", ""))] = \
(r.get("essid") or "").strip()
aps, stations = parse_airodump(args.capture)
classes = []
for row in aps:
if len(row) < REQUIRED_AP_COLS:
continue
bssid = norm_bssid(row[0])
channel = row[3].strip()
privacy = row[5].strip()
power = int(row[8]) if row[8].strip().lstrip("-").isdigit() else -100
essid = row[13].strip()
lan_ip = row[11].strip()
if not bssid or bssid in ("BSSID",):
continue
# classification: the wired-correlation cascade
if bssid in authorized:
klass, note = "authorized", ""
exp_essid = authorized[bssid]
if essid and exp_essid and essid != exp_essid:
klass, note = "MISMATCH", f"bssid authorized for {exp_essid!r}"
elif essid and any(essid == e for e in authorized.values()):
klass = "EVIL-TWIN-CANDIDATE"
note = f"authorized ESSID {essid!r} on foreign BSSID"
elif lan_ip and lan_ip.startswith(args.corp_cidr):
klass = "ROGUE-ON-WIRE"
note = f"corporate IP {lan_ip} — bridge suspect"
else:
klass = "NEIGHBOR-UNKNOWN"
note = f"open-classification: privacy={privacy}"
classes.append({"class": klass, "bssid": bssid, "essid": essid,
"channel": channel, "privacy": privacy,
"power_dbm": power, "lan_ip": lan_ip, "note": note})
# station-side: corporate clients associated to foreign BSSIDs
bridged = []
for s in stations:
station_mac, assoc_bssid, ip = norm_bssid(s[0]), norm_bssid(s[5]), s[6].strip()
if assoc_bssid in ("(not associated)", ""):
continue
if assoc_bssid not in authorized and ip.startswith(args.corp_cidr):
bridged.append({"station": station_mac, "ap": assoc_bssid,
"ip": ip,
"note": "corporate client on unauthorized AP"})
order = {"ROGUE-ON-WIRE": 0, "EVIL-TWIN-CANDIDATE": 1, "MISMATCH": 2,
"NEIGHBOR-UNKNOWN": 3, "authorized": 4}
classes.sort(key=lambda c: order[c["class"]])
args.out.mkdir(parents=True, exist_ok=True)
with (args.out / "classification.csv").open("w", newline="",
encoding="utf-8") as fh:
w = csv.DictWriter(fh, fieldnames=["class", "bssid", "essid",
"channel", "privacy", "power_dbm",
"lan_ip", "note"])
w.writeheader()
w.writerows(classes)
counts = {}
for c in classes:
counts[c["class"]] = counts.get(c["class"], 0) + 1
print(f"APs observed={len(classes)} stations={len(stations)} "
f"baseline={len(authorized)}")
for k in ("ROGUE-ON-WIRE", "EVIL-TWIN-CANDIDATE", "MISMATCH",
"NEIGHBOR-UNKNOWN", "authorized"):
if counts.get(k):
print(f" {k:<22} {counts[k]}")
print(f"bridged corporate clients: {len(bridged)}")
for b in bridged[:10]:
print(f" [BRIDGE] {b['station']} -> {b['ap']} ip={b['ip']}")
print(f"register: {args.out / 'classification.csv'}")
if __name__ == "__main__":
main()

parse_airodump() (lines 25-38) splits the export at the blank line into AP and Station sections — airodump’s real format — and fails loud on the two silent-death modes: a file with no Station section (wrong tool output) and an empty AP section (“nothing observed” must mean broken capture, not clean airspace). The classification cascade (lines 68-90) encodes the module’s doctrine in priority order: BSSID in the wire-generated baseline → authorized (with an MISMATCH flag if the ESSID drifted — a re-provisioned AP is baseline drift to fix, not a threat); authorized ESSID on a foreign BSSID → EVIL-TWIN-CANDIDATE (the WI4 attack signature); corporate IP in the LAN-IP column → ROGUE-ON-WIRE (airodump fills LAN-IP from the wire — this is the field-correlation the doctrine demands, harvested from the capture itself); everything else → NEIGHBOR-UNKNOWN (annotate, don’t panic).

The Station-section pass (lines 93-104) is the bridge hunt: a station associated to an unauthorized BSSID while carrying a corporate-range IP is a client bridging your network to a hostile AP — the nastiest class in the taxonomy, because the wire and the radio are joined inside your own device. Sorting (line 107) puts the actionable classes first, and the summary (lines 116-126) prints the five-class distribution plus the bridge list — the one-glance airspace verdict. Note what the tool deliberately does not do: no RSSI-based “how close is it” scoring — power is recorded (line 79) as context, but classification rides the wire-correlation cascade exclusively, because signal strength is the one signal the attacker controls with a $30 antenna.

The weekly airspace sweep runs rogueap against each sensor’s airodump export; the baseline diff lands in the program’s register. The evil-twin scenario plays out end to end: WI4’s attacker deploys Corp-WiFi from a battery router in the lobby — the sensor sees the authorized ESSID on a foreign BSSID (EVIL-TWIN-CANDIDATE), the alert joins the BT3 pipeline, and the response workflow checks the wired side (spanning-tree/DHCP-snooping tables — is this BSSID bridged anywhere? no: it’s standalone) and the clients list (are our devices associating to it?). Before the takedown completes, two laptops associate — the bridged-clients check catches them mid-incident, BT1 opens cases, BT4’s IR path runs (BT15’s session revocation for the harvested credentials), and the post-incident action is PMF enforcement on the corporate SSID [verify client fleet support first — the mixed-mode reality].

The deauth scenario correlates rather than alarms: a deauth storm against conference-room clients followed by new associations to a foreign BSSID is the forced-reconnect sequence — the WIDS alert (rate alarm) plus the association telemetry (the same capture’s Station section) produce the two-step detection that matters, and the incident joins the WI2/WI4 playbook. The neighbor-class tuning closes the loop: each week’s NEIGHBOR-UNKNOWN entries get annotated (the coffee shop, the new tenant’s printer), the FP-rate per class feeds BT1’s verdict history, and the register that leadership sees is actionable classes only — which is why the WIDS channel never dies of noise like its unmaintained cousins.

  1. Baseline — generate authorized BSSID/ESSID set from the WLC/switch side; annotate known neighbors
  2. Sense — sensors sweep off-channel; airodump-class exports land weekly (or continuously to the pipeline)
  3. Classify — rogueap’s cascade: authorized / mismatch / evil-twin-candidate / rogue-on-wire / neighbor
  4. Correlate — wired side answers “is it ours”: switch-port, DHCP-snooping, spanning-tree data
  5. Detect — evil twins and rogues alert into BT3; deauth storms rate-alarm with PMF status attached
  6. Respond — locate bridge suspects (corporate clients on hostile APs), BT4 IR path, BT15 revocation
  7. Tune — neighbor annotations compound; FP-rate per class tracked; baseline diffs weekly
  8. Harden — PMF enforcement where the client fleet allows; sensor placement per coverage plan

Rogue classification needs the wire; deauth analysis needs the timeline. deauthmon reads a frame-log CSV (timestamp, type, src, dst, reason — exportable from tshark/Wireshark or sensor tooling) and answers the operational question: is this RF noise or an attack — by rate analysis, target clustering, and the follow-on association join that turns deauth counts into incident detections.

Rate analysis with human windows: deauths bucketed per (AP, client) pair per minute — storms are bursts, noise is scatter; the threshold is a configured rate (default 30/min — tune to your RF). The sequence detection is the tool’s real value: deauth bursts followed within 60s by a new association from the same client (join from the association log) is the forced-reconnect signature (WI4’s onboarding moment) — classified separately from raw floods. Reason codes recorded: reason 3 (deauth: station leaving) vs reason 7 (class 3 timeout) differ diagnostically; the tool reports the distribution rather than guessing. Fail-loud on schema drift; output is the incident register.

#!/usr/bin/env python3
"""deauthmon — deauthentication attack analysis from frame logs.
Input CSV: timestamp(ISO), frame_type(deauth|assoc), src, dst, reason
Output: per-(src,dst) rate analysis + forced-reconnect sequence register."""
import argparse
import csv
import sys
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path
STORM_PER_MIN = 30
RECONNECT_WINDOW = 60 # seconds: deauth burst -> assoc = sequence
def parse_ts(raw):
try:
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
sys.exit(f"fail-loud: unparseable timestamp {raw!r}")
def main():
ap = argparse.ArgumentParser(description="deauth attack analyzer")
ap.add_argument("framelog", type=Path)
ap.add_argument("--storm-rate", type=int, default=STORM_PER_MIN)
ap.add_argument("--out", type=Path, default=Path("deauthmon_out"))
args = ap.parse_args()
if not args.framelog.exists():
sys.exit(f"fail-loud: framelog not found: {args.framelog}")
events = []
with args.framelog.open(encoding="utf-8", errors="replace") as fh:
for n, row in enumerate(csv.DictReader(fh), 2):
if not all(k in row for k in ("timestamp", "frame_type",
"src", "dst")):
sys.exit(f"fail-loud: line {n}: missing columns")
events.append({"ts": parse_ts(row["timestamp"]),
"type": row["frame_type"].strip().lower(),
"src": row["src"].strip().upper(),
"dst": row["dst"].strip().upper(),
"reason": row.get("reason", "").strip()})
deauths = [e for e in events if e["type"] == "deauth"]
assocs = [e for e in events if e["type"] == "assoc"]
if not deauths and not assocs:
sys.exit("fail-loud: no deauth/assoc events — wrong log?")
# rate analysis per (src, dst) per minute
buckets = defaultdict(list)
for e in deauths:
minute = e["ts"].replace(second=0, microsecond=0)
buckets[(e["src"], e["dst"])].append(minute)
storms = []
for pair, minutes in buckets.items():
per_min = defaultdict(int)
for m in minutes:
per_min[m] += 1
peak = max(per_min.values())
if peak >= args.storm_rate:
storms.append({"src": pair[0], "dst": pair[1],
"peak_per_min": peak,
"reasons": sorted({e["reason"] for e in deauths
if (e["src"], e["dst"]) == pair})})
# forced-reconnect: deauth burst -> assoc same dst within window
sequences = []
deauth_by_dst = defaultdict(list)
for e in deauths:
deauth_by_dst[e["dst"]].append(e["ts"])
for a in assocs:
recent = [t for t in deauth_by_dst.get(a["dst"], [])
if timedelta(0) <= a["ts"] - t <=
timedelta(seconds=RECONNECT_WINDOW)]
if len(recent) >= 3:
sequences.append({"client": a["dst"],
"new_assoc_to": a["src"],
"deauths_before": len(recent),
"note": "forced-reconnect signature (WI4 path)"})
storms.sort(key=lambda s: -s["peak_per_min"])
args.out.mkdir(parents=True, exist_ok=True)
with (args.out / "register.csv").open("w", newline="",
encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["kind", "src", "dst", "metric", "detail"])
for s in storms:
w.writerow(["deauth-storm", s["src"], s["dst"],
s["peak_per_min"], f"reasons={','.join(s['reasons'])}"])
for q in sequences:
w.writerow(["forced-reconnect", q["new_assoc_to"], q["client"],
q["deauths_before"], q["note"]])
print(f"deauth events={len(deauths)} assoc events={len(assocs)} "
f"pairs={len(buckets)}")
print(f"storms (>={args.storm_rate}/min): {len(storms)}")
for s in storms[:8]:
print(f" [STORM] {s['src']} -> {s['dst']} peak {s['peak_per_min']}/min")
print(f"forced-reconnect sequences: {len(sequences)}")
for q in sequences[:8]:
print(f" [SEQ] {q['client']} -> reassociated to {q['new_assoc_to']} "
f"after {q['deauths_before']} deauths")
print(f"register: {args.out / 'register.csv'}")
if __name__ == "__main__":
main()

The rate analysis (lines 46-56) buckets deauths per (source, destination) per wall-clock minute — storms appear as peaks, RF noise as scatter — and the threshold is a CLI knob because conference-room RF and warehouse RF have different normals. The sequence detector (lines 59-73) is where the incident hides: for every association, look back 60 seconds for a deauth burst against the same client — three or more deauths followed by reassociation is the forced-reconnect signature that WI4’s evil twin produces, and it’s classified separately from raw floods because the response differs (credential revocation vs takedown). Reason codes (line 58) ride along in the storm register — the deauth reason distribution distinguishes “station leaving” spam from class-timeout noise diagnostically.

The register (lines 75-85) writes both classes with kind labels — the BT1 pipeline routes them differently (storms → RF/ops check; sequences → security case), which is the whole point of separating them at analysis time rather than at triage time. The fail-loud gates (lines 33-44) cover the two silent failures: wrong log format and empty logs — an empty register must mean “quiet airspace”, never “broken exporter”.

capture management frames (monitor mode, observe your test AP + client)

Section titled “capture management frames (monitor mode, observe your test AP + client)”

$ tshark -i wlan0mon -T fields -e frame.time_epoch -e wlan.fc.type_subtype
-e wlan.sa -e wlan.da -e wlan.fixed.reason_code > frames.raw

(convert to the tool’s CSV with your usual shim — 10 lines of awk/python)

Section titled “(convert to the tool’s CSV with your usual shim — 10 lines of awk/python)”

analyze a quiet window: no storms, no sequences

Section titled “analyze a quiet window: no storms, no sequences”

$ python3 deauthmon.py frames.csv deauth events=12 assoc events=8 pairs=6 storms (>=30/min): 0 forced-reconnect sequences: 0 register: deauthmon_out/register.csv

then run a deauth burst against the test client (lab only!) and re-run

Section titled “then run a deauth burst against the test client (lab only!) and re-run”

$ python3 deauthmon.py frames_attack.csv deauth events=612 assoc events=3 pairs=2 storms (>=30/min): 1 [STORM] AA:BB:CC:DD:EE:FF -> 11:22:33:44:55:66 peak 412/min forced-reconnect sequences: 1 [SEQ] 11:22:33:44:55:66 -> reassociated to 22:33:44:55:66:77 after 9 deauths

The register feeds BT1/BT3 with class labels attached, and the deltas matter: a new persistent storm source (same pair across hours) is infrastructure, not noise — pivot the source MAC against the rogue baseline (rogueap’s output) and the OUI (a device claiming to be your AP vendor from an unknown MAC is the evil twin’s costume). The PMF register joins here: sequences against PMF-capable clients should be impossible — if your tool sees them, either the client fleet register is wrong or PMF isn’t actually enabled on that SSID, and both are findings. Wire the weekly run into the same review that reads rogueap’s classification — airspace posture is one story, not two dashboards.

  1. Ingest the airodump CSV directly (share the parser with rogueap) so one capture feeds both tools.
  2. Add beacon-rate analysis: an AP whose beacons stop mid-window (deauthed or jammed) is its own signal.
  3. Correlate association targets with the authorized baseline to flag client roams to foreign APs automatically.
  4. Emit a PMF-coverage report from association capabilities (RSN IE parsing) to drive the enforcement roadmap.
  5. Alert on reason-code anomalies: mass reason-2 (prev auth no longer valid) from one AP is different ops trouble.
SignalSourceIDNotes
Evil-twin candidatesrogueapweeklyForeign BSSID + our ESSID
Rogue-on-wire APsrogueap + switchper eventBridge verdict
Bridged corporate clientsrogueap stationsper eventNastiest class
Deauth stormsdeauthmonper minuteRate alarm
Forced-reconnect sequencesdeauthmonper eventWI4 signature
PMF coverage gapsClient registerquarterlyEnforcement roadmap
Baseline driftWLC diffweeklyNew APs, ESSID changes
Neighbor annotationsWeekly reviewongoingFP compound
Sensor outageSensor heartbeatper dayCoverage honesty
Client roam-to-foreignStation telemetryper eventClient-side WIDS

Adversaries evade the WIDS as deliberately as they evade EDR. MAC mimicry: the evil twin clones not just the ESSID but a vendor OUI matching your AP fleet (a $5 MAC change) — the baseline’s answer is BSSID-level inventory (every legitimate radio’s exact MAC), not vendor-class checks. Channel/time discipline: operating the rogue on rarely-swept channels or during sensor-off windows — continuous off-channel scanning and sensor heartbeat monitoring (the coverage honesty row above) close the gap. Low-and-slow phishing APs: broadcasting at whisper power to a handful of victims instead of lobby-loud — sensor placement (near glass, near parking, near exec floors) beats power thresholds. Client-side evasion: attacks that skip AP impersonation entirely — deauth-only sessions to harvest handshakes (WI2) leave no rogue AP at all, so the WIDS must detect the deauth pattern itself (deauthmon’s rate analysis), not just fake infrastructure. Frame spoofing noise: deliberate deauth spam from randomized MACs to flood the WIDS into noise-death — rate analysis per persistent pair and the FP-rate-per-class metric keep the channel alive; the attacker’s goal is your mute button, and the register that separates actionable classes from annotated noise is what keeps that button unpressed. Wired-side evasion: the true rogue bridging via a wall port in an unmonitored IDF closet never broadcasts loudly — 802.1X/NAC on every port (the wired layer’s own discipline) is the control that meets it, which is why BT13’s segmentation work is in this module’s related set.

⚠️ What caught me: our WIDS console showed 340 “rogue” devices and I wore the noise as a badge of thoroughness — until the quarterly review asked how many rogues we’d removed (one, eight months prior) and what the other 339 were (the neighbor’s mesh system, forty phones, a printer, and the coffee shop — annotated in exactly zero places). The real rogue we eventually caught was a contractor’s travel router bridging a production VLAN, and we only found it because a station in an airodump export carried a corporate DHCP lease — the wire told us what the radio couldn’t. Since then the program reports three numbers: rogues-on-wire (target zero), evil-twin candidates (investigated same-day), and neighbor annotations (growing, which is good — it means someone is reading the noise instead of muting it).

Build a two-radio airspace lab. Baseline: run one authorized AP (test SSID); generate the baseline CSV from its BSSID; run rogueap on a capture — confirm the authorized class. Evil twin: stand up a second AP broadcasting the same SSID from a different radio; re-run — confirm EVIL-TWIN-CANDIDATE fires. Bridge: connect a laptop to the rogue AP and assign it a corporate-range IP in the capture environment; re-run — confirm both ROGUE-ON-WIRE and the bridged-client list fire. Deauth: from the lab, send a deauth burst at your own test client (your air, your rules [verify local legality]); run deauthmon and confirm both the storm and the forced-reconnect sequence if your client reassociates. PMF: enable 802.11w on the authorized AP, repeat the deauth against a PMF-capable client, and observe the difference. Debrief: which classification needed the wired data, and what does that mean for where your real WIDS program should spend its integration effort?

  • Technical: Walk the rogue classification cascade and explain why wired-side correlation — not RSSI — produces the verdict that matters.
  • Technical: What is 802.11w PMF, which attack does it kill, and why does a mixed-mode client fleet leave your estate half-deauth-able?
  • Scenario: rogueap flags an AP broadcasting your guest ESSID from a foreign BSSID, and the wired correlation comes back clean (not bridged). Walk the response: is this an incident, and what do you check about your clients?
  • Scenario: Deauth storms hit the executive floor every Tuesday 9-11am. Walk the diagnosis: what patterns (rate, pair persistence, reason codes) separate RF ops trouble from attack?
  • Conceptual: Why does an un-annotated neighbor list kill a WIDS program within a quarter?
  • Deep-dive: Design the WIDS program for a 6-building campus: sensor placement, baseline generation, the classification pipeline, takedown workflow, and the metrics for the quarterly airspace review.
  • Practical: Your WIDS fires 400 alerts/day and the SOC muted it last month. What do you rebuild first — baseline, classification, or the alert routing — and how do you prove the rebuild worked?

BT2 (IDS Rules) · WI1 (Wireless Fundamentals & Recon) · WI4 (Enterprise Wi-Fi & Evil Twin) · WI7 (Wireless IDS Evasion) · E17 (Network Detection)