Improve capture performance: shorter RX lock, PMKID-first save, channel sweep
- 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
This commit is contained in:
@@ -12,6 +12,8 @@ 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 ───
|
||||
|
||||
@@ -29,10 +31,11 @@ CapturePhase capturePhase = PHASE_OBSERVING;
|
||||
#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 2 // total observe→deauth→capture cycles
|
||||
#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.
|
||||
@@ -195,6 +198,24 @@ int getBestChannel() {
|
||||
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);
|
||||
@@ -327,8 +348,9 @@ static void sendDeauthBurstAll() {
|
||||
void startMultiCapture() {
|
||||
if (is_capturing) return;
|
||||
|
||||
current_channel = getBestChannel();
|
||||
current_channel = getNextChannelSweep();
|
||||
if (current_channel == 0) return;
|
||||
last_channel = current_channel;
|
||||
|
||||
memset(slots, 0, sizeof(slots));
|
||||
int filled = 0;
|
||||
@@ -444,10 +466,11 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
|
||||
|
||||
// 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;
|
||||
uint16_t mhl = 24;
|
||||
mhl = 24;
|
||||
if (to_ds && from_ds) mhl += 6; // WDS 4-addr
|
||||
if (is_qos) mhl += 2; // QoS Control
|
||||
|
||||
@@ -462,46 +485,134 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
CaptureSlot* s = &slots[i];
|
||||
if (!s->active || pending_save_slots[i] >= 0) continue;
|
||||
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);
|
||||
|
||||
// Beacon capture
|
||||
if (is_beacon && !s->beacon_captured && len >= 36 &&
|
||||
memcmp(&payload[10], s->bssid, 6) == 0) {
|
||||
// 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);
|
||||
continue;
|
||||
}
|
||||
portEXIT_CRITICAL(&capture_mux);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_data) continue;
|
||||
// Data frame
|
||||
if (phase_snapshot == PHASE_OBSERVING) {
|
||||
const uint8_t* client_mac = hit_match_addr2 ? &payload[4] : &payload[10];
|
||||
addClientToSlot(s, client_mac);
|
||||
}
|
||||
|
||||
// Check if this data frame involves this slot's BSSID
|
||||
bool match_addr1 = (memcmp(&payload[4], s->bssid, 6) == 0);
|
||||
bool match_addr2 = (memcmp(&payload[10], s->bssid, 6) == 0);
|
||||
if (!match_addr1 && !match_addr2) continue;
|
||||
if (is_eapol) {
|
||||
s->eapol_count++;
|
||||
pcapAppendSlot(s, payload, len);
|
||||
latest_eapol_count = s->eapol_count;
|
||||
|
||||
// Phase 2: map client MAC from all data frames (including encrypted)
|
||||
// FromDS: Addr1=DA(client), Addr2=BSSID | ToDS: Addr1=BSSID, Addr2=SA(client)
|
||||
if (capturePhase == PHASE_OBSERVING) {
|
||||
const uint8_t* client_mac = match_addr2 ? &payload[4] : &payload[10];
|
||||
addClientToSlot(s, client_mac);
|
||||
}
|
||||
|
||||
// EAPOL capture (active during all phases)
|
||||
if (is_eapol) {
|
||||
s->eapol_count++;
|
||||
pcapAppendSlot(s, payload, len);
|
||||
latest_eapol_count = s->eapol_count;
|
||||
|
||||
if (s->eapol_count >= 2) {
|
||||
pending_save_slots[i] = i;
|
||||
}
|
||||
break;
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user