diff --git a/application.fam b/application.fam index 455d2dd..d941edc 100644 --- a/application.fam +++ b/application.fam @@ -15,6 +15,7 @@ App( sources=[ "src/app.c", "src/screen.c", + "src/screen_boot.c", "src/screen_blackout.c", "src/screen_bluetooth.c", "src/screen_deauth.c", @@ -45,6 +46,7 @@ App( "src/screen_deauth_client.c", "src/screen_arp_from_creds.c", "src/screen_mitm_pcap.c", + "src/screen_nmap.c", "src/screen_beacon_spam.c", "src/uart_comm.c", ], diff --git a/dist/c5lab_dev.fap b/dist/c5lab_dev.fap index 4f13ba2..ad0e678 100644 Binary files a/dist/c5lab_dev.fap and b/dist/c5lab_dev.fap differ diff --git a/dist/debug/c5lab_dev_d.elf b/dist/debug/c5lab_dev_d.elf index 258ae36..f8158bb 100644 Binary files a/dist/debug/c5lab_dev_d.elf and b/dist/debug/c5lab_dev_d.elf differ diff --git a/src/app.c b/src/app.c index 1831ed3..a99f5a8 100644 --- a/src/app.c +++ b/src/app.c @@ -1,20 +1,77 @@ #include "app.h" #include "uart_comm.h" #include "screen_main_menu.h" +#include "screen_boot.h" #include "screen.h" #include #include #include #include +// ============================================================================ +// Custom event handler +// +// Boot screen worker thread posts BOOT_EVENT_* to drive the post-boot +// transition. We MUST NOT call screen_pop / push directly from inside the +// boot worker thread - those manipulate the global view stack and the GUI +// dispatcher state, so we route the work through the dispatcher's custom +// event queue which runs callbacks on the GUI thread. +// ============================================================================ + +static void app_push_main_menu(WiFiApp* app) { + void* main_menu_data = NULL; + View* main_menu = screen_main_menu_create(app, &main_menu_data); + if(!main_menu) { + view_dispatcher_stop(app->view_dispatcher); + return; + } + screen_push_with_cleanup(app, main_menu, main_menu_cleanup_internal, main_menu_data); +} + +static bool app_custom_event_handler(void* context, uint32_t event) { + WiFiApp* app = (WiFiApp*)context; + if(!app) return false; + + switch(event) { + case BOOT_EVENT_DONE: + // Push main menu first (becomes the active view), THEN remove the + // boot screen from the bottom of the stack. screen_pop is a no-op + // when the stack has only one entry, hence the dedicated remove_first. + app_push_main_menu(app); + screen_remove_first(app); + return true; + + case BOOT_EVENT_CONTINUE: + // User chose to continue without a connected board. + app->board_connected = false; + app_push_main_menu(app); + screen_remove_first(app); + return true; + + case BOOT_EVENT_FAILED: + // Boot worker is now waiting for user input. Nothing to do here - + // the screen draws its own "OK=continue BACK=exit" footer. + return true; + + case BOOT_EVENT_CANCELLED: + // BACK pressed during/after boot - tear down the app. + // screen_pop_all in cleanup releases the boot view; here we just stop. + view_dispatcher_stop(app->view_dispatcher); + return true; + + default: + return false; + } +} + int32_t wifi_attacks_app(void* p) { UNUSED(p); - + WiFiApp* app = (WiFiApp*)malloc(sizeof(WiFiApp)); if(!app) return -1; memset(app, 0, sizeof(WiFiApp)); - - // Initialize GUI + + // GUI plumbing app->gui = furi_record_open(RECORD_GUI); if(!app->gui) { free(app); @@ -27,55 +84,44 @@ int32_t wifi_attacks_app(void* p) { return -1; } app->view_stack = view_stack_alloc(); - + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); - - // Enable 5V power output on GPIO to power the ESP32 board - if(furi_hal_power_is_otg_enabled()) { - // Already enabled, nothing to do - } else { - furi_hal_power_enable_otg(); - } - - // Initialize UART - uart_comm_init(app); - - // Check initial board connection - furi_delay_ms(500); // Give board time to initialize - app->board_connected = uart_check_board_connection(app); - - // If board connected, check SD card - if(app->board_connected) { - app->sd_card_ok = uart_check_sd_card(app); - app->sd_card_checked = true; - } - - // Initialize app state + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, app_custom_event_handler); + + // CRITICAL: Disable expansion service BEFORE the boot worker acquires USART. + // Expansion service owns USART by default (waiting for an expansion module); + // skipping this triggers furi_check failures inside furi_hal_serial when we + // try to take the port - especially visible on battery power. + app->expansion = furi_record_open(RECORD_EXPANSION); + expansion_disable(app->expansion); + + // App state init (5V, UART, board check are deferred to the boot screen + // worker thread so the user sees live progress instead of a frozen UI). app->networks = NULL; app->network_count = 0; memset(app->selected_networks, 0, sizeof(app->selected_networks)); app->selected_count = 0; - - // Initialize scanning state + app->scan_results = NULL; app->scan_result_count = 0; app->scan_result_capacity = 0; app->scanning_in_progress = false; app->scan_bytes_received = 0; app->last_scan_line = furi_string_alloc(); - + app->attack_status = furi_string_alloc(); app->attack_log = furi_string_alloc(); app->current_ssid = furi_string_alloc(); app->current_password = furi_string_alloc(); app->attack_in_progress = false; - + app->sniffer_packet_count = 0; app->evil_twin_html_selection = 0; app->html_files = NULL; app->html_file_count = 0; app->evil_twin_password = furi_string_alloc(); - + // Load red team mode from persistent storage app->red_team_mode = false; { @@ -91,59 +137,69 @@ int32_t wifi_attacks_app(void* p) { storage_file_free(file); furi_record_close(RECORD_STORAGE); } - - // Create and push main menu with cleanup - void* main_menu_data = NULL; - View* main_menu = screen_main_menu_create(app, &main_menu_data); - if(!main_menu) { - uart_comm_deinit(app); + + // Push the boot screen as the first view. Its worker thread powers up the + // board, probes UART, and posts BOOT_EVENT_DONE / BOOT_EVENT_FAILED via + // the custom event handler above. + void* boot_data = NULL; + View* boot_view = screen_boot_create(app, &boot_data); + if(!boot_view) { + if(app->expansion) { + expansion_enable(app->expansion); + furi_record_close(RECORD_EXPANSION); + } view_dispatcher_free(app->view_dispatcher); view_stack_free(app->view_stack); furi_record_close(RECORD_GUI); free(app); return -1; } - - // Use screen_push_with_cleanup for main menu too - screen_push_with_cleanup(app, main_menu, main_menu_cleanup_internal, main_menu_data); - + screen_push_with_cleanup(app, boot_view, screen_boot_cleanup_internal, boot_data); + // Run the ViewDispatcher event loop view_dispatcher_run(app->view_dispatcher); - + // Cleanup - remove all views first screen_pop_all(app); - + // Disable 5V GPIO power output if(furi_hal_power_is_otg_enabled()) { furi_hal_power_disable_otg(); } - + uart_comm_deinit(app); - + + // Restore expansion service ownership of USART per SDK contract. + if(app->expansion) { + expansion_enable(app->expansion); + furi_record_close(RECORD_EXPANSION); + app->expansion = NULL; + } + furi_string_free(app->attack_status); furi_string_free(app->attack_log); furi_string_free(app->current_ssid); furi_string_free(app->current_password); furi_string_free(app->evil_twin_password); furi_string_free(app->last_scan_line); - + if(app->scan_results) free(app->scan_results); - + for(uint32_t i = 0; i < app->network_count; i++) { free(app->networks[i]); } if(app->networks) free(app->networks); - + for(uint32_t i = 0; i < app->html_file_count; i++) { free(app->html_files[i]); } if(app->html_files) free(app->html_files); - + view_dispatcher_free(app->view_dispatcher); view_stack_free(app->view_stack); furi_record_close(RECORD_GUI); - + free(app); - + return 0; } diff --git a/src/app.h b/src/app.h index 0ab727e..722dbc5 100644 --- a/src/app.h +++ b/src/app.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,14 @@ typedef struct { #define MAX_SCAN_RESULTS 64 +// Cached password entry (populated from `show_pass evil` and from successful captures) +#define MAX_CACHED_PASSWORDS 32 + +typedef struct { + char ssid[33]; + char password[65]; +} CachedPassword; + // Screen context structure typedef struct { WiFiApp* app; @@ -92,6 +101,18 @@ struct WiFiApp { // WiFi connection status (set by wifi_connect success in ARP/wpasec screens) bool wifi_connected; + + // Cache of known WPA passwords keyed by SSID. Populated lazily from `show_pass evil` + // and also after successful captures (Evil Twin, Portal, Karma, Rogue AP). + // Prevents re-running `show_pass evil` on every attack screen. + CachedPassword password_cache[MAX_CACHED_PASSWORDS]; + uint8_t password_cache_count; + bool password_cache_loaded; + + // Expansion service handle. We must call expansion_disable() before acquiring + // USART (otherwise the expansion service races us on the same port and + // causes furi_check failures), and expansion_enable() on shutdown. + Expansion* expansion; }; // App entry point diff --git a/src/screen.c b/src/screen.c index afcad54..7e257b3 100644 --- a/src/screen.c +++ b/src/screen.c @@ -104,6 +104,18 @@ void screen_pop(WiFiApp* app) { } } +void screen_remove_first(WiFiApp* app) { + if(!app || screen_view_stack_size == 0) return; + ScreenStackEntry first = screen_view_stack[0]; + for(uint8_t i = 0; i + 1 < screen_view_stack_size; i++) { + screen_view_stack[i] = screen_view_stack[i + 1]; + } + screen_view_stack_size--; + view_dispatcher_remove_view(app->view_dispatcher, first.view_id); + if(first.cleanup) first.cleanup(first.view, first.cleanup_data); + if(first.view) view_free(first.view); +} + void screen_pop_to_main(WiFiApp* app) { // Pop all views except the first one (main menu) while(screen_view_stack_size > 1) { diff --git a/src/screen.h b/src/screen.h index 455ba2e..7678825 100644 --- a/src/screen.h +++ b/src/screen.h @@ -14,6 +14,8 @@ void screen_push_with_cleanup(WiFiApp* app, View* view, void (*cleanup)(View*, v void screen_pop(WiFiApp* app); void screen_pop_to_main(WiFiApp* app); void screen_pop_all(WiFiApp* app); +// Remove the bottom-most entry from the stack (e.g. boot screen after success). +void screen_remove_first(WiFiApp* app); // Helper drawing functions void screen_draw_title(Canvas* canvas, const char* title); diff --git a/src/screen_arp_from_creds.c b/src/screen_arp_from_creds.c index 0c83113..cf01a4e 100644 --- a/src/screen_arp_from_creds.c +++ b/src/screen_arp_from_creds.c @@ -17,13 +17,16 @@ #include "screen_attacks.h" #include "uart_comm.h" #include "screen.h" +#include #include #include #include #include #define TAG "ARPCreds" -#define ARP_CREDS_MAX_HOSTS 32 +#define ARP_CREDS_MAX_HOSTS 32 +#define ARP_CREDS_PASSWORD_MAX 64 +#define ARP_CREDS_TEXT_INPUT_ID 995 // ============================================================================ // Data Structures @@ -38,13 +41,15 @@ typedef struct { WiFiApp* app; volatile bool attack_finished; uint8_t state; - // 0 = connecting to WiFi - // 1 = scanning hosts - // 2 = host list display - // 3 = ARP poisoning active + // 0 = connecting to WiFi + // 1 = scanning hosts + // 2 = host list display + // 3 = ARP poisoning active + // 10 = confirm saved password (OK=use, Right=enter new) + // 11 = waiting for password input (TextInput) char ssid[33]; - char password[65]; + char password[ARP_CREDS_PASSWORD_MAX + 1]; ArpCredsHostEntry hosts[ARP_CREDS_MAX_HOSTS]; uint8_t host_count; @@ -55,6 +60,12 @@ typedef struct { bool connect_failed; FuriThread* thread; + TextInput* text_input; + bool text_input_added; + bool password_entered; + volatile bool password_choice_made; + volatile bool password_use_saved; + View* main_view; } ArpFromCredsData; typedef struct { @@ -76,10 +87,47 @@ void arp_from_creds_cleanup_internal(View* view, void* data) { furi_thread_join(d->thread); furi_thread_free(d->thread); } + + if(d->text_input) { + if(d->text_input_added) { + view_dispatcher_remove_view(d->app->view_dispatcher, ARP_CREDS_TEXT_INPUT_ID); + } + text_input_free(d->text_input); + } + free(d); FURI_LOG_I(TAG, "ARP from Creds cleanup complete"); } +// ============================================================================ +// TextInput callback +// ============================================================================ + +static void arp_creds_password_callback(void* context) { + ArpFromCredsData* data = (ArpFromCredsData*)context; + if(!data || !data->app) return; + + FURI_LOG_I(TAG, "Password entered: %s", data->password); + password_cache_put(data->app, data->ssid, data->password); + data->password_entered = true; + data->state = 0; // proceed to connecting + + uint32_t main_view_id = screen_get_current_view_id(); + view_dispatcher_switch_to_view(data->app->view_dispatcher, main_view_id); +} + +static void arp_creds_show_text_input(ArpFromCredsData* data) { + if(!data || !data->text_input) return; + + if(!data->text_input_added) { + View* ti_view = text_input_get_view(data->text_input); + view_dispatcher_add_view(data->app->view_dispatcher, ARP_CREDS_TEXT_INPUT_ID, ti_view); + data->text_input_added = true; + } + + view_dispatcher_switch_to_view(data->app->view_dispatcher, ARP_CREDS_TEXT_INPUT_ID); +} + // ============================================================================ // Drawing // ============================================================================ @@ -92,7 +140,30 @@ static void arp_from_creds_draw(Canvas* canvas, void* model) { canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); - if(data->state == 0) { + if(data->state == 10) { + screen_draw_title(canvas, "ARP Poisoning"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 22, "Saved password found:"); + + char pw_line[22]; + size_t pw_len = strlen(data->password); + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password); + canvas_draw_str(canvas, 2, 35, pw_line); + if(pw_len > 21) { + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password + 21); + canvas_draw_str(canvas, 2, 44, pw_line); + } + + canvas_draw_str(canvas, 2, 62, "OK:use Right:new"); + + } else if(data->state == 11) { + screen_draw_title(canvas, "ARP Poisoning"); + canvas_set_font(canvas, FontSecondary); + screen_draw_centered_text(canvas, "Enter password", 32); + // TextInput is added by the worker thread (calling it from draw + // callback would deadlock on the ViewModel mutex). + + } else if(data->state == 0) { screen_draw_title(canvas, "ARP Poisoning"); canvas_set_font(canvas, FontSecondary); char line[48]; @@ -182,7 +253,42 @@ static bool arp_from_creds_input(InputEvent* event, void* context) { return false; } - if(data->state == 0 || data->state == 1) { + if(data->state == 10) { + if(event->key == InputKeyOk) { + data->password_use_saved = true; + data->password_choice_made = true; + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyRight) { + data->password[0] = '\0'; + data->state = 11; + data->password_choice_made = true; + arp_creds_show_text_input(data); + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyBack) { + data->attack_finished = true; + view_commit_model(view, false); + screen_pop(data->app); + return true; + } + + } else if(data->state == 11) { + if(event->key == InputKeyOk) { + arp_creds_show_text_input(data); + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyBack) { + data->attack_finished = true; + view_commit_model(view, false); + screen_pop(data->app); + return true; + } + + } else if(data->state == 0 || data->state == 1) { // Connecting / scanning - only back works if(event->key == InputKeyBack) { data->attack_finished = true; @@ -242,6 +348,23 @@ static int32_t arp_from_creds_thread(void* context) { FURI_LOG_I(TAG, "Thread started for SSID: %s", data->ssid); + // Step 0: Confirm saved password (always present, came from compromised list) + FURI_LOG_I(TAG, "Awaiting user confirmation for saved password"); + data->state = 10; + while(!data->password_choice_made && !data->attack_finished) { + furi_delay_ms(50); + } + if(data->attack_finished) return 0; + + if(!data->password_use_saved) { + FURI_LOG_I(TAG, "User chose to enter a new password"); + // Input handler already set state=11 and showed TextInput + while(!data->password_entered && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + } + // Step 1: Connect to WiFi data->state = 0; char cmd[128]; @@ -295,8 +418,8 @@ static int32_t arp_from_creds_thread(void* context) { start = furi_get_tick(); uint32_t last_rx = start; - while((furi_get_tick() - last_rx) < 3000 && - (furi_get_tick() - start) < 20000 && + while((furi_get_tick() - last_rx) < 10000 && + (furi_get_tick() - start) < 30000 && !data->attack_finished) { const char* line = uart_read_line(app, 500); if(line) { @@ -308,13 +431,22 @@ static int32_t arp_from_creds_thread(void* context) { continue; } - if(in_host_section && data->host_count < ARP_CREDS_MAX_HOSTS) { - // Parse " IP -> MAC" + if(in_host_section) { + if(strstr(line, "========================") || strstr(line, "Found ")) { + in_host_section = false; + continue; + } + + if(data->host_count >= ARP_CREDS_MAX_HOSTS) continue; + + // Skip (MAC unknown) hosts - can't arp_ban without MAC + if(strstr(line, "(MAC unknown)")) continue; + + // Parse " IP -> MAC [ARP]" char ip[16] = {0}; char mac[18] = {0}; const char* arrow = strstr(line, "->"); if(arrow) { - // Extract IP (before ->) const char* p = line; while(*p == ' ') p++; size_t ip_len = 0; @@ -323,7 +455,6 @@ static int32_t arp_from_creds_thread(void* context) { } ip[ip_len] = '\0'; - // Extract MAC (after ->) p = arrow + 2; while(*p == ' ') p++; size_t mac_len = 0; @@ -385,11 +516,15 @@ View* screen_arp_from_creds_create( memset(data, 0, sizeof(ArpFromCredsData)); data->app = app; data->attack_finished = false; - data->state = 0; + data->state = 10; // start on confirm-saved-password screen data->host_count = 0; data->selected_host = 0; data->hosts_loaded = false; data->connect_failed = false; + data->password_entered = false; + data->text_input_added = false; + data->password_choice_made = false; + data->password_use_saved = false; strncpy(data->ssid, ssid, sizeof(data->ssid) - 1); data->ssid[sizeof(data->ssid) - 1] = '\0'; @@ -401,6 +536,7 @@ View* screen_arp_from_creds_create( free(data); return NULL; } + data->main_view = view; view_allocate_model(view, ViewModelTypeLocking, sizeof(ArpFromCredsModel)); ArpFromCredsModel* m = view_get_model(view); @@ -411,6 +547,20 @@ View* screen_arp_from_creds_create( view_set_input_callback(view, arp_from_creds_input); view_set_context(view, view); + // Create TextInput for re-entering password + data->text_input = text_input_alloc(); + if(data->text_input) { + text_input_set_header_text(data->text_input, "Enter Password:"); + text_input_set_result_callback( + data->text_input, + arp_creds_password_callback, + data, + data->password, + ARP_CREDS_PASSWORD_MAX, + true); + FURI_LOG_I(TAG, "TextInput created"); + } + // Start thread data->thread = furi_thread_alloc(); furi_thread_set_name(data->thread, "ARPCreds"); diff --git a/src/screen_arp_poisoning.c b/src/screen_arp_poisoning.c index c03896a..c9ec090 100644 --- a/src/screen_arp_poisoning.c +++ b/src/screen_arp_poisoning.c @@ -44,12 +44,13 @@ typedef struct { WiFiApp* app; volatile bool attack_finished; uint8_t state; - // 0 = checking password (thread) - // 1 = waiting for password input (TextInput) - // 2 = connecting to WiFi - // 3 = scanning hosts - // 4 = host list display - // 5 = ARP poisoning active + // 0 = checking password (thread) + // 1 = waiting for password input (TextInput) + // 2 = connecting to WiFi + // 3 = scanning hosts + // 4 = host list display + // 5 = ARP poisoning active + // 10 = confirm saved password (OK=use, Right=enter new) // Network info char ssid[33]; @@ -71,6 +72,8 @@ typedef struct { TextInput* text_input; bool text_input_added; bool password_entered; + volatile bool password_choice_made; + volatile bool password_use_saved; View* main_view; } ArpPoisoningData; @@ -119,6 +122,7 @@ static void arp_password_callback(void* context) { if(!data || !data->app) return; FURI_LOG_I(TAG, "Password entered: %s", data->password); + password_cache_put(data->app, data->ssid, data->password); data->password_entered = true; data->state = 2; // Move to connecting @@ -159,10 +163,25 @@ static void arp_poisoning_draw(Canvas* canvas, void* model) { screen_draw_title(canvas, "ARP Poisoning"); canvas_set_font(canvas, FontSecondary); screen_draw_centered_text(canvas, "Enter password", 32); - if(!data->text_input_added) { - arp_show_text_input(data); + // TextInput is added by the worker thread (calling it from draw + // callback would deadlock on the ViewModel mutex). + + } else if(data->state == 10) { + screen_draw_title(canvas, "ARP Poisoning"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 22, "Saved password found:"); + + char pw_line[22]; + size_t pw_len = strlen(data->password); + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password); + canvas_draw_str(canvas, 2, 35, pw_line); + if(pw_len > 21) { + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password + 21); + canvas_draw_str(canvas, 2, 44, pw_line); } + canvas_draw_str(canvas, 2, 62, "OK:use Right:new"); + } else if(data->state == 2) { screen_draw_title(canvas, "ARP Poisoning"); canvas_set_font(canvas, FontSecondary); @@ -264,6 +283,29 @@ static bool arp_poisoning_input(InputEvent* event, void* context) { return true; } + } else if(data->state == 10) { + if(event->key == InputKeyOk) { + data->password_use_saved = true; + data->password_choice_made = true; + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyRight) { + data->password[0] = '\0'; + data->state = 1; + data->password_choice_made = true; + arp_show_text_input(data); + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyBack) { + data->attack_finished = true; + uart_send_command(data->app, "stop"); + view_commit_model(view, false); + screen_pop(data->app); + return true; + } + } else if(data->state == 2 || data->state == 3) { // Connecting / scanning - only back works if(event->key == InputKeyBack) { @@ -314,62 +356,6 @@ static bool arp_poisoning_input(InputEvent* event, void* context) { return true; } -// ============================================================================ -// Password discovery helper -// ============================================================================ - -static bool arp_check_password(ArpPoisoningData* data) { - WiFiApp* app = data->app; - - uart_clear_buffer(app); - uart_send_command(app, "show_pass evil"); - furi_delay_ms(200); - - uint32_t start = furi_get_tick(); - uint32_t last_rx = start; - - while((furi_get_tick() - last_rx) < 1000 && - (furi_get_tick() - start) < 5000 && - !data->attack_finished) { - const char* line = uart_read_line(app, 300); - if(line) { - last_rx = furi_get_tick(); - FURI_LOG_I(TAG, "show_pass: %s", line); - - // Parse "SSID", "password" - const char* p = line; - while(*p == ' ' || *p == '\t') p++; - if(*p != '"') continue; - p++; - const char* ssid_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t ssid_len = p - ssid_start; - p++; - - while(*p == ',' || *p == ' ' || *p == '\t') p++; - - if(*p != '"') continue; - p++; - const char* pass_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t pass_len = p - pass_start; - - if(ssid_len == strlen(data->ssid) && - strncmp(ssid_start, data->ssid, ssid_len) == 0) { - if(pass_len < sizeof(data->password)) { - strncpy(data->password, pass_start, pass_len); - data->password[pass_len] = '\0'; - FURI_LOG_I(TAG, "Password found: %s", data->password); - return true; - } - } - } - } - return false; -} - // ============================================================================ // Attack Thread // ============================================================================ @@ -384,27 +370,45 @@ static int32_t arp_poisoning_thread(void* context) { char cmd[256]; snprintf(cmd, sizeof(cmd), "select_networks %lu", (unsigned long)data->net_index); uart_send_command(app, cmd); - furi_delay_ms(500); + furi_delay_ms(100); uart_clear_buffer(app); if(data->attack_finished) return 0; - // Step 2: Check if password is known + // Step 2: Check if password is known (cache + show_pass evil fallback) data->state = 0; - bool found = arp_check_password(data); + bool found = attack_resolve_password( + app, data->ssid, data->password, sizeof(data->password), &data->attack_finished); if(data->attack_finished) return 0; - if(!found) { + if(found) { + FURI_LOG_I(TAG, "Saved password found, awaiting user confirmation"); + data->state = 10; + while(!data->password_choice_made && !data->attack_finished) { + furi_delay_ms(50); + } + if(data->attack_finished) return 0; + + if(!data->password_use_saved) { + FURI_LOG_I(TAG, "User chose to enter a new password"); + // Input handler already set state=1 and showed TextInput + while(!data->password_entered && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + } + } else { data->state = 1; FURI_LOG_I(TAG, "Password unknown, requesting user input"); + // Show TextInput from worker thread (NOT from draw callback - that + // would deadlock on the ViewModelTypeLocking mutex). + arp_show_text_input(data); while(!data->password_entered && !data->attack_finished) { furi_delay_ms(100); } if(data->attack_finished) return 0; - } else { - data->state = 2; } // Step 3: Connect to WiFi @@ -459,8 +463,8 @@ static int32_t arp_poisoning_thread(void* context) { start = furi_get_tick(); uint32_t last_rx = start; - while((furi_get_tick() - last_rx) < 3000 && - (furi_get_tick() - start) < 20000 && + while((furi_get_tick() - last_rx) < 10000 && + (furi_get_tick() - start) < 30000 && !data->attack_finished) { const char* line = uart_read_line(app, 500); if(line) { @@ -472,13 +476,22 @@ static int32_t arp_poisoning_thread(void* context) { continue; } - if(in_host_section && data->host_count < ARP_MAX_HOSTS) { - // Parse " IP -> MAC" + if(in_host_section) { + if(strstr(line, "========================") || strstr(line, "Found ")) { + in_host_section = false; + continue; + } + + if(data->host_count >= ARP_MAX_HOSTS) continue; + + // Skip (MAC unknown) hosts - can't arp_ban without MAC + if(strstr(line, "(MAC unknown)")) continue; + + // Parse " IP -> MAC [ARP]" char ip[16] = {0}; char mac[18] = {0}; const char* arrow = strstr(line, "->"); if(arrow) { - // Extract IP (before ->) const char* p = line; while(*p == ' ') p++; size_t ip_len = 0; @@ -487,7 +500,6 @@ static int32_t arp_poisoning_thread(void* context) { } ip[ip_len] = '\0'; - // Extract MAC (after ->) p = arrow + 2; while(*p == ' ') p++; size_t mac_len = 0; diff --git a/src/screen_attacks.h b/src/screen_attacks.h index d8089a7..72412b8 100644 --- a/src/screen_attacks.h +++ b/src/screen_attacks.h @@ -50,6 +50,9 @@ View* screen_arp_poisoning_create(WiFiApp* app, void** out_data); // MITM PCAP Sniffer - connects to network, captures traffic to PCAP View* screen_mitm_pcap_create(WiFiApp* app, void** out_data); +// Nmap - connects to network, discovers hosts, port scans +View* screen_nmap_create(WiFiApp* app, void** out_data); + // Deauth Detector - monitors for deauthentication attacks View* screen_deauth_detector_create(WiFiApp* app, void** out_data); @@ -77,6 +80,7 @@ void sniffer_cleanup(View* view, void* data); void rogue_ap_cleanup_internal(View* view, void* data); void arp_poisoning_cleanup_internal(View* view, void* data); void mitm_pcap_cleanup_internal(View* view, void* data); +void nmap_cleanup_internal(View* view, void* data); void deauth_detector_cleanup_internal(View* view, void* data); void karma_probe_cleanup_internal(View* view, void* data); void deauth_client_cleanup_internal(View* view, void* data); diff --git a/src/screen_boot.c b/src/screen_boot.c new file mode 100644 index 0000000..208e27c --- /dev/null +++ b/src/screen_boot.c @@ -0,0 +1,453 @@ +/** + * Boot Screen + * + * Shows a live progress log while the application powers up the ESP32 board + * and verifies UART connectivity. Steps: + * 1. Enable 5V power (OTG) + * 2. Initialize UART + * 3. Wait for board boot (visible countdown) + * 4. Probe board (ping/pong with retries) + * 5. Board detected + * 6. Check SD card (best-effort, never blocks success) + * + * On full success the worker thread posts BOOT_EVENT_DONE through the + * ViewDispatcher custom event queue, which the app handler turns into a + * "pop boot, push main menu" transition. + * + * On failure the user can press OK to continue without the board (sends + * BOOT_EVENT_CONTINUE) or BACK to exit (BOOT_EVENT_CANCELLED). + */ + +#include "screen_boot.h" +#include "uart_comm.h" +#include "screen.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "BootScreen" + +#define BOOT_STEP_COUNT 6 +#define BOOT_BOOT_WAIT_MS 2000 +#define BOOT_PING_ATTEMPTS 6 +#define BOOT_PING_TIMEOUT_MS 700 +#define BOOT_PING_INTER_DELAY_MS 200 +#define BOOT_SUCCESS_DWELL_MS 400 + +typedef enum { + BootStepPending = 0, + BootStepInProgress, + BootStepDone, + BootStepFailed, +} BootStepState; + +typedef enum { + BootPhaseRunning = 0, + BootPhaseSuccess, + BootPhaseFailed, +} BootPhase; + +typedef struct { + WiFiApp* app; + + FuriThread* thread; + volatile bool cancel; // set by BACK / cleanup + volatile bool finished; // set by worker when done (success or failure) + + BootStepState step_state[BOOT_STEP_COUNT]; + char step_label[BOOT_STEP_COUNT][40]; + uint8_t spinner_phase; + BootPhase phase; + uint32_t ping_attempt; // 1-based current attempt for status display + + View* main_view; +} BootData; + +typedef struct { + BootData* data; +} BootModel; + +static const char* const BOOT_STEP_NAMES[BOOT_STEP_COUNT] = { + "5V power", + "Init UART", + "Wait boot", + "Probe board", + "Board detected", + "Check SD card", +}; + +// ============================================================================ +// Drawing helpers +// ============================================================================ + +static char boot_status_glyph(BootStepState s, uint8_t spinner_phase) { + switch(s) { + case BootStepDone: return 'v'; + case BootStepFailed: return 'x'; + case BootStepInProgress: { + static const char spin[] = {'|', '/', '-', '\\'}; + return spin[spinner_phase & 0x3]; + } + case BootStepPending: + default: return ' '; + } +} + +static void boot_draw(Canvas* canvas, void* model) { + BootModel* m = (BootModel*)model; + if(!m || !m->data) return; + BootData* data = m->data; + + canvas_clear(canvas); + canvas_set_font(canvas, FontPrimary); + screen_draw_title(canvas, "C5Lab boot"); + + canvas_set_font(canvas, FontSecondary); + + uint8_t y = 21; + for(uint8_t i = 0; i < BOOT_STEP_COUNT; i++) { + char glyph = boot_status_glyph(data->step_state[i], data->spinner_phase); + char line[64]; + const char* label = data->step_label[i][0] ? data->step_label[i] : BOOT_STEP_NAMES[i]; + snprintf(line, sizeof(line), "[%c] %s", glyph, label); + canvas_draw_str(canvas, 2, y, line); + y += 8; + } + + if(data->phase == BootPhaseFailed) { + canvas_draw_line(canvas, 0, 60, 128, 60); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 68, "OK=continue BACK=exit"); + } +} + +// ============================================================================ +// Input +// ============================================================================ + +static bool boot_input(InputEvent* event, void* context) { + View* view = (View*)context; + if(!view) return false; + BootModel* m = view_get_model(view); + if(!m || !m->data) { + view_commit_model(view, false); + return false; + } + BootData* data = m->data; + WiFiApp* app = data->app; + + if(event->type != InputTypeShort) { + view_commit_model(view, false); + return false; + } + + if(data->phase == BootPhaseRunning) { + if(event->key == InputKeyBack) { + data->cancel = true; + view_commit_model(view, false); + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_CANCELLED); + return true; + } + } else if(data->phase == BootPhaseFailed) { + if(event->key == InputKeyOk) { + view_commit_model(view, false); + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_CONTINUE); + return true; + } + if(event->key == InputKeyBack) { + data->cancel = true; + view_commit_model(view, false); + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_CANCELLED); + return true; + } + } else if(data->phase == BootPhaseSuccess) { + // Defensive: in normal flow the boot screen is removed from the stack + // immediately after BOOT_EVENT_DONE. If for any reason it is still + // reachable (e.g. user navigates back through everything), make sure + // BACK / OK still let the user exit instead of being trapped here. + if(event->key == InputKeyBack || event->key == InputKeyOk) { + view_commit_model(view, false); + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_CANCELLED); + return true; + } + } + + view_commit_model(view, false); + return true; +} + +// ============================================================================ +// Model mutation helpers (always go through view_commit_model so the GUI +// thread re-renders). +// ============================================================================ + +static void boot_set_step(BootData* data, uint8_t idx, BootStepState s, const char* label) { + if(idx >= BOOT_STEP_COUNT || !data->main_view) return; + BootModel* m = view_get_model(data->main_view); + if(m && m->data) { + m->data->step_state[idx] = s; + if(label) { + strncpy(m->data->step_label[idx], label, sizeof(m->data->step_label[idx]) - 1); + m->data->step_label[idx][sizeof(m->data->step_label[idx]) - 1] = '\0'; + } else { + m->data->step_label[idx][0] = '\0'; + } + } + view_commit_model(data->main_view, true); +} + +static void boot_set_phase(BootData* data, BootPhase phase) { + if(!data->main_view) return; + BootModel* m = view_get_model(data->main_view); + if(m && m->data) m->data->phase = phase; + view_commit_model(data->main_view, true); +} + +static void boot_set_ping_attempt(BootData* data, uint32_t attempt) { + if(!data->main_view) return; + BootModel* m = view_get_model(data->main_view); + if(m && m->data) m->data->ping_attempt = attempt; + view_commit_model(data->main_view, true); +} + +static void boot_tick_spinner(BootData* data) { + if(!data->main_view) return; + BootModel* m = view_get_model(data->main_view); + if(m && m->data) m->data->spinner_phase++; + view_commit_model(data->main_view, true); +} + +// Cancel-aware delay: sleeps in ~50ms slices, returns false if cancel was raised. +static bool boot_sleep(BootData* data, uint32_t ms) { + const uint32_t slice = 50; + uint32_t remaining = ms; + while(remaining > 0) { + if(data->cancel) return false; + uint32_t s = remaining > slice ? slice : remaining; + furi_delay_ms(s); + remaining -= s; + } + return !data->cancel; +} + +// ============================================================================ +// Worker thread +// ============================================================================ + +static int32_t boot_worker(void* context) { + BootData* data = (BootData*)context; + if(!data || !data->app) return -1; + WiFiApp* app = data->app; + + FURI_LOG_I(TAG, "Boot worker started"); + + // ---------------- Step 1: enable 5V power ---------------- + // Use power service (RECORD_POWER) instead of furi_hal directly: + // - On battery: it retries OTG enable up to 5x (BQ25896 boost converter + // often needs more than one attempt to lock in). + // - On USB cable (VBUS >= 4.5V): it intentionally skips OTG because the + // board is already powered from USB - we still treat that as success. + boot_set_step(data, 0, BootStepInProgress, "5V power..."); + bool was_on = furi_hal_power_is_otg_enabled(); + if(!was_on) { + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, true); + furi_record_close(RECORD_POWER); + + // Power service kicks retries asynchronously; poll up to ~600 ms for + // the actual hardware state. Done in 50 ms slices so BACK stays snappy. + for(int i = 0; i < 12; i++) { + if(data->cancel) goto cancelled; + if(furi_hal_power_is_otg_enabled()) break; + furi_delay_ms(50); + } + } + if(furi_hal_power_is_otg_enabled() || was_on) { + boot_set_step(data, 0, BootStepDone, was_on ? "5V power (already on)" : "5V power on"); + } else { + // OTG didn't lock in. Don't fail outright - on USB the board still has + // VBUS, and the ping in step 4 is the real authoritative liveness test. + boot_set_step(data, 0, BootStepFailed, "5V power: OTG not confirmed"); + } + if(data->cancel) goto cancelled; + + // ---------------- Step 2: init UART ---------------- + boot_set_step(data, 1, BootStepInProgress, "Init UART..."); + uart_comm_init(app); + if(!app->serial) { + boot_set_step(data, 1, BootStepFailed, "Init UART: serial busy"); + FURI_LOG_E(TAG, "Failed to acquire serial handle"); + boot_set_phase(data, BootPhaseFailed); + data->finished = true; + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_FAILED); + return 0; + } + boot_set_step(data, 1, BootStepDone, "Init UART (115200)"); + if(data->cancel) goto cancelled; + + // ---------------- Step 3: wait for board boot with countdown ---------------- + boot_set_step(data, 2, BootStepInProgress, "Wait boot 2.0s"); + { + const uint32_t step_ms = 100; + uint32_t elapsed = 0; + while(elapsed < BOOT_BOOT_WAIT_MS) { + if(data->cancel) goto cancelled; + uint32_t remaining = BOOT_BOOT_WAIT_MS - elapsed; + char buf[32]; + snprintf(buf, sizeof(buf), "Wait boot %lu.%lus", + (unsigned long)(remaining / 1000), + (unsigned long)((remaining % 1000) / 100)); + // Update label and tick spinner together to keep the UI alive. + BootModel* m = view_get_model(data->main_view); + if(m && m->data) { + strncpy(m->data->step_label[2], buf, sizeof(m->data->step_label[2]) - 1); + m->data->step_label[2][sizeof(m->data->step_label[2]) - 1] = '\0'; + m->data->spinner_phase++; + } + view_commit_model(data->main_view, true); + furi_delay_ms(step_ms); + elapsed += step_ms; + } + } + // Drain any boot-time noise the ESP printed during the wait. + uart_clear_buffer(app); + boot_set_step(data, 2, BootStepDone, "Wait boot 2.0s"); + + // ---------------- Step 4: probe board (ping/pong, retries) ---------------- + boot_set_step(data, 3, BootStepInProgress, "Probe board (1/6)"); + bool pong = false; + for(uint32_t attempt = 1; attempt <= BOOT_PING_ATTEMPTS && !data->cancel; attempt++) { + boot_set_ping_attempt(data, attempt); + { + char buf[40]; + snprintf(buf, sizeof(buf), "Probe board (%lu/%u)", + (unsigned long)attempt, BOOT_PING_ATTEMPTS); + boot_set_step(data, 3, BootStepInProgress, buf); + } + + if(uart_ping_once(app, BOOT_PING_TIMEOUT_MS)) { + pong = true; + break; + } + boot_tick_spinner(data); + if(!boot_sleep(data, BOOT_PING_INTER_DELAY_MS)) goto cancelled; + } + if(data->cancel) goto cancelled; + + if(!pong) { + boot_set_step(data, 3, BootStepFailed, "Probe board: no pong"); + boot_set_step(data, 4, BootStepFailed, "Board not detected"); + boot_set_step(data, 5, BootStepPending, NULL); + boot_set_phase(data, BootPhaseFailed); + data->finished = true; + FURI_LOG_W(TAG, "Board did not respond after %u attempts", BOOT_PING_ATTEMPTS); + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_FAILED); + return 0; + } + { + char buf[40]; + snprintf(buf, sizeof(buf), "Probe board OK (%lu/%u)", + (unsigned long)data->ping_attempt, BOOT_PING_ATTEMPTS); + boot_set_step(data, 3, BootStepDone, buf); + } + + // ---------------- Step 5: board detected ---------------- + boot_set_step(data, 4, BootStepDone, "Board detected"); + app->board_connected = true; + if(data->cancel) goto cancelled; + + // ---------------- Step 6: SD card check (best-effort, optional) ---------------- + boot_set_step(data, 5, BootStepInProgress, "Check SD card..."); + bool sd_ok = uart_check_sd_card(app); + app->sd_card_ok = sd_ok; + app->sd_card_checked = true; + boot_set_step(data, 5, sd_ok ? BootStepDone : BootStepFailed, + sd_ok ? "SD card OK" : "SD card not found"); + if(data->cancel) goto cancelled; + + // ---------------- Done ---------------- + boot_set_phase(data, BootPhaseSuccess); + data->finished = true; + boot_sleep(data, BOOT_SUCCESS_DWELL_MS); // let user read the final state + if(data->cancel) goto cancelled; + + FURI_LOG_I(TAG, "Boot complete, posting BOOT_EVENT_DONE"); + view_dispatcher_send_custom_event(app->view_dispatcher, BOOT_EVENT_DONE); + return 0; + +cancelled: + FURI_LOG_I(TAG, "Boot cancelled"); + data->finished = true; + return 0; +} + +// ============================================================================ +// Cleanup +// ============================================================================ + +void screen_boot_cleanup_internal(View* view, void* data) { + UNUSED(view); + BootData* d = (BootData*)data; + if(!d) return; + + FURI_LOG_I(TAG, "Boot cleanup starting"); + + d->cancel = true; + if(d->thread) { + furi_thread_join(d->thread); + furi_thread_free(d->thread); + d->thread = NULL; + } + + free(d); + FURI_LOG_I(TAG, "Boot cleanup complete"); +} + +// ============================================================================ +// Screen creation +// ============================================================================ + +View* screen_boot_create(WiFiApp* app, void** out_data) { + if(!app) return NULL; + + BootData* data = (BootData*)malloc(sizeof(BootData)); + if(!data) return NULL; + memset(data, 0, sizeof(BootData)); + data->app = app; + for(uint8_t i = 0; i < BOOT_STEP_COUNT; i++) { + data->step_state[i] = BootStepPending; + data->step_label[i][0] = '\0'; + } + data->phase = BootPhaseRunning; + + View* view = view_alloc(); + if(!view) { + free(data); + return NULL; + } + data->main_view = view; + + view_allocate_model(view, ViewModelTypeLocking, sizeof(BootModel)); + BootModel* m = view_get_model(view); + m->data = data; + view_commit_model(view, true); + + view_set_draw_callback(view, boot_draw); + view_set_input_callback(view, boot_input); + view_set_context(view, view); + + data->thread = furi_thread_alloc(); + furi_thread_set_name(data->thread, "C5LabBoot"); + furi_thread_set_stack_size(data->thread, 2048); + furi_thread_set_callback(data->thread, boot_worker); + furi_thread_set_context(data->thread, data); + furi_thread_start(data->thread); + + if(out_data) *out_data = data; + return view; +} diff --git a/src/screen_boot.h b/src/screen_boot.h new file mode 100644 index 0000000..b39968a --- /dev/null +++ b/src/screen_boot.h @@ -0,0 +1,13 @@ +#pragma once + +#include "app.h" + +// Custom event ids posted by the boot screen worker thread to the +// app-level ViewDispatcher custom event handler. +#define BOOT_EVENT_DONE 0xB0010001u +#define BOOT_EVENT_FAILED 0xB0010002u +#define BOOT_EVENT_CANCELLED 0xB0010003u +#define BOOT_EVENT_CONTINUE 0xB0010004u // user chose to continue without board + +View* screen_boot_create(WiFiApp* app, void** out_data); +void screen_boot_cleanup_internal(View* view, void* data); diff --git a/src/screen_evil_twin.c b/src/screen_evil_twin.c index 133c185..2efea6e 100644 --- a/src/screen_evil_twin.c +++ b/src/screen_evil_twin.c @@ -412,7 +412,16 @@ static int32_t evil_twin_thread(void* context) { } } - if(strstr(line, "Password verified!")) data->password_verified = true; + if(strstr(line, "Password verified!")) { + data->password_verified = true; + // Cache verified credentials so subsequent attacks (Rogue AP / ARP / + // MITM PCAP / Nmap) on this SSID skip the "Checking password..." step. + const char* cached_ssid = furi_string_get_cstr(app->current_ssid); + const char* cached_pass = furi_string_get_cstr(app->current_password); + if(cached_ssid && cached_ssid[0] && cached_pass && cached_pass[0]) { + password_cache_put(app, cached_ssid, cached_pass); + } + } if(strstr(line, "Evil Twin portal shut down")) { data->portal_shutdown = true; data->attack_finished = true; diff --git a/src/screen_mitm_pcap.c b/src/screen_mitm_pcap.c index 4b59993..afabdcf 100644 --- a/src/screen_mitm_pcap.c +++ b/src/screen_mitm_pcap.c @@ -30,11 +30,12 @@ typedef struct { WiFiApp* app; volatile bool attack_finished; uint8_t state; - // 0 = checking password - // 1 = waiting for password input (TextInput) - // 2 = connecting to WiFi - // 3 = starting PCAP capture - // 4 = capture active (status screen) + // 0 = checking password + // 1 = waiting for password input (TextInput) + // 2 = connecting to WiFi + // 3 = starting PCAP capture + // 4 = capture active (status screen) + // 10 = confirm saved password (OK=use, Right=enter new) char ssid[33]; char password[MITM_PASSWORD_MAX + 1]; @@ -49,6 +50,8 @@ typedef struct { TextInput* text_input; bool text_input_added; bool password_entered; + volatile bool password_choice_made; + volatile bool password_use_saved; View* main_view; } MitmPcapData; @@ -97,6 +100,7 @@ static void mitm_password_callback(void* context) { if(!data || !data->app) return; FURI_LOG_I(TAG, "Password entered: %s", data->password); + password_cache_put(data->app, data->ssid, data->password); data->password_entered = true; data->state = 2; @@ -138,6 +142,22 @@ static void mitm_pcap_draw(Canvas* canvas, void* model) { canvas_set_font(canvas, FontSecondary); screen_draw_centered_text(canvas, "Enter password", 32); + } else if(data->state == 10) { + screen_draw_title(canvas, "MITM PCAP Sniffer"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 22, "Saved password found:"); + + char pw_line[22]; + size_t pw_len = strlen(data->password); + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password); + canvas_draw_str(canvas, 2, 35, pw_line); + if(pw_len > 21) { + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password + 21); + canvas_draw_str(canvas, 2, 44, pw_line); + } + + canvas_draw_str(canvas, 2, 62, "OK:use Right:new"); + } else if(data->state == 2) { screen_draw_title(canvas, "MITM PCAP Sniffer"); canvas_set_font(canvas, FontSecondary); @@ -203,6 +223,23 @@ static bool mitm_pcap_input(InputEvent* event, void* context) { return true; } + if(data->state == 10) { + if(event->key == InputKeyOk) { + data->password_use_saved = true; + data->password_choice_made = true; + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyRight) { + data->password[0] = '\0'; + data->state = 1; + data->password_choice_made = true; + mitm_show_text_input(data); + view_commit_model(view, false); + return true; + } + } + if(event->key == InputKeyBack) { data->attack_finished = true; uart_send_command(data->app, "stop"); @@ -215,61 +252,6 @@ static bool mitm_pcap_input(InputEvent* event, void* context) { return true; } -// ============================================================================ -// Password discovery helper (same logic as ARP Poisoning) -// ============================================================================ - -static bool mitm_check_password(MitmPcapData* data) { - WiFiApp* app = data->app; - - uart_clear_buffer(app); - uart_send_command(app, "show_pass evil"); - furi_delay_ms(200); - - uint32_t start = furi_get_tick(); - uint32_t last_rx = start; - - while((furi_get_tick() - last_rx) < 1000 && - (furi_get_tick() - start) < 5000 && - !data->attack_finished) { - const char* line = uart_read_line(app, 300); - if(line) { - last_rx = furi_get_tick(); - FURI_LOG_I(TAG, "show_pass: %s", line); - - const char* p = line; - while(*p == ' ' || *p == '\t') p++; - if(*p != '"') continue; - p++; - const char* ssid_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t ssid_len = p - ssid_start; - p++; - - while(*p == ',' || *p == ' ' || *p == '\t') p++; - - if(*p != '"') continue; - p++; - const char* pass_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t pass_len = p - pass_start; - - if(ssid_len == strlen(data->ssid) && - strncmp(ssid_start, data->ssid, ssid_len) == 0) { - if(pass_len < sizeof(data->password)) { - strncpy(data->password, pass_start, pass_len); - data->password[pass_len] = '\0'; - FURI_LOG_I(TAG, "Password found: %s", data->password); - return true; - } - } - } - } - return false; -} - // ============================================================================ // Attack Thread // ============================================================================ @@ -284,18 +266,35 @@ static int32_t mitm_pcap_thread(void* context) { char cmd[256]; snprintf(cmd, sizeof(cmd), "select_networks %lu", (unsigned long)data->net_index); uart_send_command(app, cmd); - furi_delay_ms(500); + furi_delay_ms(100); uart_clear_buffer(app); if(data->attack_finished) return 0; - // Step 2: Check if password is known + // Step 2: Check if password is known (cache + show_pass evil fallback) data->state = 0; - bool found = mitm_check_password(data); + bool found = attack_resolve_password( + app, data->ssid, data->password, sizeof(data->password), &data->attack_finished); if(data->attack_finished) return 0; - if(!found) { + if(found) { + FURI_LOG_I(TAG, "Saved password found, awaiting user confirmation"); + data->state = 10; + while(!data->password_choice_made && !data->attack_finished) { + furi_delay_ms(50); + } + if(data->attack_finished) return 0; + + if(!data->password_use_saved) { + FURI_LOG_I(TAG, "User chose to enter a new password"); + // Input handler already set state=1 and showed TextInput + while(!data->password_entered && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + } + } else { data->state = 1; FURI_LOG_I(TAG, "Password unknown, requesting user input"); mitm_show_text_input(data); @@ -304,8 +303,6 @@ static int32_t mitm_pcap_thread(void* context) { furi_delay_ms(100); } if(data->attack_finished) return 0; - } else { - data->state = 2; } // Step 3: Connect to WiFi diff --git a/src/screen_nmap.c b/src/screen_nmap.c new file mode 100644 index 0000000..cb5bb36 --- /dev/null +++ b/src/screen_nmap.c @@ -0,0 +1,1035 @@ +/** + * Nmap Port Scanner Screen + * + * Connects to a WiFi network, discovers hosts, lets user pick target(s) + * and scan level, then runs start_nmap and displays results. + * Flow: + * 1. select_networks + * 2. show_pass evil -> check if password is known + * 3. (optional) TextInput for password + * 4. wifi_connect + * 5. list_hosts -> discover hosts + * 6. User selects host(s) (one or all) + * 7. User picks scan level (quick/medium/heavy) + * 8. start_nmap [IP] -> parse progress + results + * 9. Show results + */ + +#include "screen_attacks.h" +#include "uart_comm.h" +#include "screen.h" +#include +#include +#include +#include +#include + +#define TAG "Nmap" + +#define NMAP_PASSWORD_MAX 64 +#define NMAP_TEXT_INPUT_ID 996 +#define NMAP_MAX_HOSTS 32 +#define NMAP_MAX_PORTS_PER_HOST 20 + +typedef struct { + char ip[16]; + char mac[18]; + bool selected; +} NmapHostEntry; + +typedef struct { + int port; + char service[16]; +} NmapPort; + +typedef struct { + char ip[16]; + char mac[18]; + NmapPort ports[NMAP_MAX_PORTS_PER_HOST]; + uint8_t port_count; + bool no_open_ports; +} NmapResultHost; + +typedef struct { + WiFiApp* app; + volatile bool attack_finished; + uint8_t state; + // 0 = checking password + // 1 = waiting for password input (TextInput) + // 2 = connecting to WiFi + // 3 = scanning hosts (list_hosts) + // 4 = host list with selection + // 5 = scan level picker + // 6 = nmap scanning in progress + // 7 = results display + // 10 = confirm saved password (OK=use, Right=enter new) + + char ssid[33]; + char password[NMAP_PASSWORD_MAX + 1]; + uint32_t net_index; // 1-based + + // Host discovery + NmapHostEntry hosts[NMAP_MAX_HOSTS]; + uint8_t host_count; + uint8_t host_cursor; + bool all_hosts_selected; + volatile bool host_selection_confirmed; + + // Scan level + uint8_t scan_level_cursor; // 0=quick,1=medium,2=heavy + volatile bool scan_level_confirmed; + + // Results + NmapResultHost results[NMAP_MAX_HOSTS]; + uint8_t result_count; + uint16_t total_open_ports; + + // Progress + char progress_ip[16]; + uint8_t progress_pct; + uint8_t hosts_scanned; + uint8_t hosts_total; + + char status_text[64]; + bool connect_failed; + + // UI scroll for results + int16_t scroll_pos; + + FuriThread* thread; + TextInput* text_input; + bool text_input_added; + bool password_entered; + volatile bool password_choice_made; + volatile bool password_use_saved; + View* main_view; +} NmapData; + +typedef struct { + NmapData* data; +} NmapModel; + +// ============================================================================ +// Cleanup +// ============================================================================ + +static void nmap_cleanup_impl(View* view, void* data) { + UNUSED(view); + NmapData* d = (NmapData*)data; + if(!d) return; + + FURI_LOG_I(TAG, "Cleanup starting"); + + d->attack_finished = true; + if(d->thread) { + furi_thread_join(d->thread); + furi_thread_free(d->thread); + } + + if(d->text_input) { + if(d->text_input_added) { + view_dispatcher_remove_view(d->app->view_dispatcher, NMAP_TEXT_INPUT_ID); + } + text_input_free(d->text_input); + } + + free(d); + FURI_LOG_I(TAG, "Cleanup complete"); +} + +void nmap_cleanup_internal(View* view, void* data) { + nmap_cleanup_impl(view, data); +} + +// ============================================================================ +// TextInput callback +// ============================================================================ + +static void nmap_password_callback(void* context) { + NmapData* data = (NmapData*)context; + if(!data || !data->app) return; + + FURI_LOG_I(TAG, "Password entered: %s", data->password); + password_cache_put(data->app, data->ssid, data->password); + data->password_entered = true; + data->state = 2; + + uint32_t main_view_id = screen_get_current_view_id(); + view_dispatcher_switch_to_view(data->app->view_dispatcher, main_view_id); +} + +static void nmap_show_text_input(NmapData* data) { + if(!data || !data->text_input) return; + + if(!data->text_input_added) { + View* ti_view = text_input_get_view(data->text_input); + view_dispatcher_add_view(data->app->view_dispatcher, NMAP_TEXT_INPUT_ID, ti_view); + data->text_input_added = true; + } + + view_dispatcher_switch_to_view(data->app->view_dispatcher, NMAP_TEXT_INPUT_ID); +} + +// ============================================================================ +// Helper: count total result lines for scroll +// ============================================================================ + +static int16_t nmap_result_line_count(NmapData* data) { + int16_t lines = 0; + for(uint8_t i = 0; i < data->result_count; i++) { + lines++; // host header line + if(data->results[i].no_open_ports) { + lines++; // "(no open ports)" + } else { + lines += data->results[i].port_count; + } + } + return lines; +} + +// ============================================================================ +// Drawing +// ============================================================================ + +static const char* scan_level_names[] = {"Quick (20 ports)", "Medium (50 ports)", "Heavy (100 ports)"}; +static const char* scan_level_cmds[] = {"quick", "medium", "heavy"}; + +static void nmap_draw(Canvas* canvas, void* model) { + NmapModel* m = (NmapModel*)model; + if(!m || !m->data) return; + NmapData* data = m->data; + + canvas_clear(canvas); + canvas_set_font(canvas, FontPrimary); + + if(data->state == 0) { + screen_draw_title(canvas, "Nmap"); + canvas_set_font(canvas, FontSecondary); + screen_draw_centered_text(canvas, "Checking password...", 32); + + } else if(data->state == 1) { + screen_draw_title(canvas, "Nmap"); + canvas_set_font(canvas, FontSecondary); + screen_draw_centered_text(canvas, "Enter password", 32); + // TextInput is added by the worker thread (calling it from draw + // callback would deadlock on the ViewModel mutex). + + } else if(data->state == 10) { + screen_draw_title(canvas, "Nmap"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 22, "Saved password found:"); + + char pw_line[22]; + size_t pw_len = strlen(data->password); + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password); + canvas_draw_str(canvas, 2, 35, pw_line); + if(pw_len > 21) { + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password + 21); + canvas_draw_str(canvas, 2, 44, pw_line); + } + + canvas_draw_str(canvas, 2, 62, "OK:use Right:new"); + + } else if(data->state == 2) { + screen_draw_title(canvas, "Nmap"); + canvas_set_font(canvas, FontSecondary); + char line[48]; + char ssid_trunc[20]; + strncpy(ssid_trunc, data->ssid, sizeof(ssid_trunc) - 1); + ssid_trunc[sizeof(ssid_trunc) - 1] = '\0'; + snprintf(line, sizeof(line), "Connecting to %s...", ssid_trunc); + screen_draw_centered_text(canvas, line, 32); + if(data->connect_failed) { + screen_draw_centered_text(canvas, "Connection FAILED", 46); + screen_draw_centered_text(canvas, "Press Back", 58); + } + + } else if(data->state == 3) { + screen_draw_title(canvas, "Nmap"); + canvas_set_font(canvas, FontSecondary); + screen_draw_centered_text(canvas, "Discovering hosts...", 32); + if(data->status_text[0]) { + screen_draw_centered_text(canvas, data->status_text, 46); + } + + } else if(data->state == 4) { + // Host selection with checkboxes + screen_draw_title(canvas, "Select Hosts"); + canvas_set_font(canvas, FontSecondary); + + if(data->host_count == 0) { + screen_draw_centered_text(canvas, "No hosts found", 32); + } else { + // Item 0 = "All hosts", items 1..host_count = individual hosts + uint8_t total_items = data->host_count + 1; + uint8_t y = 22; + uint8_t max_visible = 4; + uint8_t start = 0; + if(data->host_cursor >= max_visible) { + start = data->host_cursor - max_visible + 1; + } + + for(uint8_t i = start; i < total_items && (i - start) < max_visible; i++) { + uint8_t display_y = y + ((i - start) * 9); + char line[42]; + + if(i == 0) { + snprintf(line, sizeof(line), "[%c] All hosts (%u)", + data->all_hosts_selected ? 'x' : ' ', data->host_count); + } else { + uint8_t hi = i - 1; + snprintf(line, sizeof(line), "[%c] %s", + data->hosts[hi].selected ? 'x' : ' ', + data->hosts[hi].ip); + } + + if(i == data->host_cursor) { + canvas_draw_box(canvas, 0, display_y - 7, 128, 9); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str(canvas, 1, display_y, line); + canvas_set_color(canvas, ColorBlack); + } else { + canvas_draw_str(canvas, 1, display_y, line); + } + } + + // Hint bar + canvas_draw_str(canvas, 2, 64, "ok:toggle"); + canvas_draw_str(canvas, 76, 64, "right:scan"); + } + + } else if(data->state == 5) { + // Scan level picker + screen_draw_title(canvas, "Scan Level"); + canvas_set_font(canvas, FontSecondary); + + for(uint8_t i = 0; i < 3; i++) { + uint8_t display_y = 24 + (i * 12); + if(i == data->scan_level_cursor) { + canvas_draw_box(canvas, 0, display_y - 8, 128, 11); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str(canvas, 4, display_y, scan_level_names[i]); + canvas_set_color(canvas, ColorBlack); + } else { + canvas_draw_str(canvas, 4, display_y, scan_level_names[i]); + } + } + + } else if(data->state == 6) { + // Scanning in progress + screen_draw_title(canvas, "Nmap Scanning"); + canvas_set_font(canvas, FontSecondary); + + if(data->hosts_total > 0) { + char line[48]; + snprintf(line, sizeof(line), "Host %u/%u", data->hosts_scanned + 1, data->hosts_total); + canvas_draw_str(canvas, 2, 22, line); + } + + if(data->progress_ip[0]) { + char line[48]; + snprintf(line, sizeof(line), "IP: %s", data->progress_ip); + canvas_draw_str(canvas, 2, 34, line); + } + + // Progress bar + canvas_draw_frame(canvas, 2, 40, 124, 8); + uint8_t fill = (data->progress_pct * 120) / 100; + if(fill > 120) fill = 120; + if(fill > 0) { + canvas_draw_box(canvas, 4, 42, fill, 4); + } + + { + char line[48]; + snprintf(line, sizeof(line), "Ports found: %u", data->total_open_ports); + canvas_draw_str(canvas, 2, 58, line); + } + + } else if(data->state == 7) { + // Results display + screen_draw_title(canvas, "Nmap Results"); + canvas_set_font(canvas, FontSecondary); + + if(data->result_count == 0) { + screen_draw_centered_text(canvas, "No results", 32); + } else { + // Flatten results into scrollable lines + int16_t total_lines = nmap_result_line_count(data); + uint8_t max_visible = 5; + int16_t y = 20; + int16_t line_idx = 0; + + for(uint8_t h = 0; h < data->result_count; h++) { + NmapResultHost* rh = &data->results[h]; + + // Host header line + if(line_idx >= data->scroll_pos && line_idx < data->scroll_pos + max_visible) { + uint8_t dy = y + ((line_idx - data->scroll_pos) * 9); + char line[42]; + if(rh->mac[0]) { + snprintf(line, sizeof(line), "%s (%s)", rh->ip, rh->mac); + } else { + snprintf(line, sizeof(line), "%s", rh->ip); + } + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 1, dy, line); + } + line_idx++; + + if(rh->no_open_ports) { + if(line_idx >= data->scroll_pos && line_idx < data->scroll_pos + max_visible) { + uint8_t dy = y + ((line_idx - data->scroll_pos) * 9); + canvas_draw_str(canvas, 8, dy, "(no open ports)"); + } + line_idx++; + } else { + for(uint8_t p = 0; p < rh->port_count; p++) { + if(line_idx >= data->scroll_pos && line_idx < data->scroll_pos + max_visible) { + uint8_t dy = y + ((line_idx - data->scroll_pos) * 9); + char line[42]; + snprintf(line, sizeof(line), " %d/%s %s", + rh->ports[p].port, "tcp", rh->ports[p].service); + canvas_draw_str(canvas, 6, dy, line); + } + line_idx++; + } + } + } + + // Summary at bottom + { + char line[48]; + snprintf(line, sizeof(line), "%u hosts, %u ports [%d/%d]", + data->result_count, data->total_open_ports, + data->scroll_pos + 1, total_lines); + canvas_draw_str(canvas, 2, 64, line); + } + } + } +} + +// ============================================================================ +// Input Handling +// ============================================================================ + +static bool nmap_input(InputEvent* event, void* context) { + View* view = (View*)context; + if(!view) return false; + + NmapModel* m = view_get_model(view); + if(!m || !m->data) { + view_commit_model(view, false); + return false; + } + NmapData* data = m->data; + + bool is_navigation = (event->key == InputKeyUp || event->key == InputKeyDown); + if(is_navigation) { + if(event->type != InputTypePress && event->type != InputTypeRepeat) { + view_commit_model(view, false); + return false; + } + } else { + if(event->type != InputTypeShort) { + view_commit_model(view, false); + return false; + } + } + + if(data->state == 0 || data->state == 1) { + if(event->key == InputKeyBack) { + data->attack_finished = true; + uart_send_command(data->app, "stop"); + view_commit_model(view, false); + screen_pop(data->app); + return true; + } + if(data->state == 1 && event->key == InputKeyOk) { + nmap_show_text_input(data); + view_commit_model(view, false); + return true; + } + + } else if(data->state == 10) { + if(event->key == InputKeyOk) { + data->password_use_saved = true; + data->password_choice_made = true; + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyRight) { + data->password[0] = '\0'; + data->state = 1; + data->password_choice_made = true; + nmap_show_text_input(data); + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyBack) { + data->attack_finished = true; + uart_send_command(data->app, "stop"); + view_commit_model(view, false); + screen_pop(data->app); + return true; + } + + } else if(data->state == 2 || data->state == 3) { + if(event->key == InputKeyBack) { + data->attack_finished = true; + uart_send_command(data->app, "stop"); + view_commit_model(view, false); + screen_pop_to_main(data->app); + return true; + } + + } else if(data->state == 4) { + // Host selection + uint8_t total_items = data->host_count + 1; // +1 for "All hosts" + if(event->key == InputKeyUp) { + if(data->host_cursor > 0) data->host_cursor--; + } else if(event->key == InputKeyDown) { + if(data->host_cursor + 1 < total_items) data->host_cursor++; + } else if(event->key == InputKeyOk) { + if(data->host_cursor == 0) { + // Toggle "All hosts" + data->all_hosts_selected = !data->all_hosts_selected; + for(uint8_t i = 0; i < data->host_count; i++) { + data->hosts[i].selected = data->all_hosts_selected; + } + } else { + uint8_t hi = data->host_cursor - 1; + data->hosts[hi].selected = !data->hosts[hi].selected; + // Update "all" state + bool all = true; + for(uint8_t i = 0; i < data->host_count; i++) { + if(!data->hosts[i].selected) { all = false; break; } + } + data->all_hosts_selected = all; + } + } else if(event->key == InputKeyRight) { + // Check if any host is selected + bool any_selected = false; + for(uint8_t i = 0; i < data->host_count; i++) { + if(data->hosts[i].selected) { any_selected = true; break; } + } + if(any_selected) { + data->host_selection_confirmed = true; + data->state = 5; + data->scan_level_cursor = 0; + } + } else if(event->key == InputKeyBack) { + data->attack_finished = true; + uart_send_command(data->app, "stop"); + view_commit_model(view, false); + screen_pop_to_main(data->app); + return true; + } + + } else if(data->state == 5) { + // Scan level picker + if(event->key == InputKeyUp) { + if(data->scan_level_cursor > 0) data->scan_level_cursor--; + } else if(event->key == InputKeyDown) { + if(data->scan_level_cursor < 2) data->scan_level_cursor++; + } else if(event->key == InputKeyOk) { + data->scan_level_confirmed = true; + } else if(event->key == InputKeyBack) { + data->state = 4; + } + + } else if(data->state == 6) { + // Scanning - only back + if(event->key == InputKeyBack) { + data->attack_finished = true; + uart_send_command(data->app, "stop"); + view_commit_model(view, false); + screen_pop_to_main(data->app); + return true; + } + + } else if(data->state == 7) { + // Results - scroll + int16_t total_lines = nmap_result_line_count(data); + int16_t max_visible = 5; + if(event->key == InputKeyUp) { + if(data->scroll_pos > 0) data->scroll_pos--; + } else if(event->key == InputKeyDown) { + if(data->scroll_pos + max_visible < total_lines) data->scroll_pos++; + } else if(event->key == InputKeyBack) { + data->attack_finished = true; + view_commit_model(view, false); + screen_pop_to_main(data->app); + return true; + } + } + + view_commit_model(view, true); + return true; +} + +// ============================================================================ +// Attack Thread +// ============================================================================ + +static int32_t nmap_thread(void* context) { + NmapData* data = (NmapData*)context; + WiFiApp* app = data->app; + + FURI_LOG_I(TAG, "Thread started for SSID: %s (index %lu)", + data->ssid, (unsigned long)data->net_index); + + // Step 1: select_networks + char cmd[256]; + snprintf(cmd, sizeof(cmd), "select_networks %lu", (unsigned long)data->net_index); + uart_send_command(app, cmd); + furi_delay_ms(100); + uart_clear_buffer(app); + + if(data->attack_finished) return 0; + + // Step 2: Check if password is known (cache + show_pass evil fallback) + data->state = 0; + bool found = attack_resolve_password( + app, data->ssid, data->password, sizeof(data->password), &data->attack_finished); + + if(data->attack_finished) return 0; + + if(found) { + FURI_LOG_I(TAG, "Saved password found, awaiting user confirmation"); + data->state = 10; + while(!data->password_choice_made && !data->attack_finished) { + furi_delay_ms(50); + } + if(data->attack_finished) return 0; + + if(!data->password_use_saved) { + FURI_LOG_I(TAG, "User chose to enter a new password"); + // Input handler already set state=1 and showed TextInput + while(!data->password_entered && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + } + } else { + data->state = 1; + FURI_LOG_I(TAG, "Password unknown, requesting user input"); + nmap_show_text_input(data); + + while(!data->password_entered && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + } + + // Step 3: Connect to WiFi + data->state = 2; + snprintf(cmd, sizeof(cmd), "wifi_connect %s %s", data->ssid, data->password); + FURI_LOG_I(TAG, "Sending: wifi_connect %s ***", data->ssid); + uart_clear_buffer(app); + uart_send_command(app, cmd); + + bool connected = false; + uint32_t start = furi_get_tick(); + while((furi_get_tick() - start) < 15000 && !data->attack_finished) { + const char* line = uart_read_line(app, 500); + if(line) { + FURI_LOG_I(TAG, "wifi_connect: %s", line); + strncpy(data->status_text, line, sizeof(data->status_text) - 1); + + if(strstr(line, "SUCCESS")) { + connected = true; + app->wifi_connected = true; + break; + } + if(strstr(line, "FAIL") || strstr(line, "Error") || strstr(line, "error")) { + data->connect_failed = true; + FURI_LOG_E(TAG, "Connection failed"); + break; + } + } + } + + if(data->attack_finished) return 0; + + if(!connected) { + data->connect_failed = true; + FURI_LOG_E(TAG, "Connection timed out or failed"); + while(!data->attack_finished) { + furi_delay_ms(100); + } + return 0; + } + + // Step 4: Scan hosts (list_hosts) + data->state = 3; + snprintf(data->status_text, sizeof(data->status_text), "Sending ARP requests..."); + FURI_LOG_I(TAG, "Scanning hosts"); + furi_delay_ms(200); + uart_clear_buffer(app); + uart_send_command(app, "list_hosts"); + + bool in_host_section = false; + start = furi_get_tick(); + uint32_t last_rx = start; + + while((furi_get_tick() - last_rx) < 10000 && + (furi_get_tick() - start) < 30000 && + !data->attack_finished) { + const char* line = uart_read_line(app, 500); + if(line) { + last_rx = furi_get_tick(); + FURI_LOG_I(TAG, "list_hosts: %s", line); + + if(strstr(line, "=== Discovered Hosts ===")) { + in_host_section = true; + continue; + } + + if(in_host_section) { + if(strstr(line, "========================") || strstr(line, "Found ")) { + in_host_section = false; + continue; + } + + if(data->host_count >= NMAP_MAX_HOSTS) continue; + + if(strstr(line, "(MAC unknown)")) { + // Store ICMP-only hosts (nmap can scan them by IP) + const char* arrow = strstr(line, "->"); + if(arrow) { + const char* p = line; + while(*p == ' ') p++; + size_t ip_len = 0; + char ip[16] = {0}; + while(p < arrow && *p != ' ' && ip_len < sizeof(ip) - 1) { + ip[ip_len++] = *p++; + } + ip[ip_len] = '\0'; + if(ip[0]) { + strncpy(data->hosts[data->host_count].ip, ip, + sizeof(data->hosts[0].ip) - 1); + strncpy(data->hosts[data->host_count].mac, "unknown", + sizeof(data->hosts[0].mac) - 1); + data->hosts[data->host_count].selected = false; + data->host_count++; + } + } + continue; + } + + // Parse " IP -> MAC [ARP]" + char ip[16] = {0}; + char mac[18] = {0}; + const char* arrow = strstr(line, "->"); + if(arrow) { + const char* p = line; + while(*p == ' ') p++; + size_t ip_len = 0; + while(p < arrow && *p != ' ' && ip_len < sizeof(ip) - 1) { + ip[ip_len++] = *p++; + } + ip[ip_len] = '\0'; + + p = arrow + 2; + while(*p == ' ') p++; + size_t mac_len = 0; + while(*p && *p != ' ' && *p != '\n' && *p != '\r' && + mac_len < sizeof(mac) - 1) { + mac[mac_len++] = *p++; + } + mac[mac_len] = '\0'; + + if(ip[0] && mac[0]) { + strncpy(data->hosts[data->host_count].ip, ip, + sizeof(data->hosts[0].ip) - 1); + strncpy(data->hosts[data->host_count].mac, mac, + sizeof(data->hosts[0].mac) - 1); + data->hosts[data->host_count].selected = false; + data->host_count++; + } + } + } + + snprintf(data->status_text, sizeof(data->status_text), + "Found %u hosts", data->host_count); + } + } + + FURI_LOG_I(TAG, "Found %u hosts", data->host_count); + + if(data->attack_finished) return 0; + + // Step 5: Wait for user to select hosts + data->state = 4; + data->host_cursor = 0; + + while(!data->host_selection_confirmed && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + + // Step 6: Wait for scan level selection + // (state is already set to 5 by the input handler) + while(!data->scan_level_confirmed && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + + // Step 7: Build and send nmap command + data->state = 6; + data->progress_pct = 0; + data->hosts_scanned = 0; + + // Count selected hosts and find if single + uint8_t selected_count = 0; + int8_t single_host_idx = -1; + for(uint8_t i = 0; i < data->host_count; i++) { + if(data->hosts[i].selected) { + selected_count++; + single_host_idx = i; + } + } + + if(selected_count == 1) { + snprintf(cmd, sizeof(cmd), "start_nmap %s %s", + scan_level_cmds[data->scan_level_cursor], + data->hosts[single_host_idx].ip); + } else { + snprintf(cmd, sizeof(cmd), "start_nmap %s", + scan_level_cmds[data->scan_level_cursor]); + } + + FURI_LOG_I(TAG, "Sending: %s", cmd); + uart_clear_buffer(app); + uart_send_command(app, cmd); + + // Step 8: Parse nmap output + bool scan_complete = false; + int8_t current_result_idx = -1; + start = furi_get_tick(); + + while(!scan_complete && !data->attack_finished && + (furi_get_tick() - start) < 300000) { // 5 min max + const char* line = uart_read_line(app, 500); + if(!line) continue; + + FURI_LOG_I(TAG, "nmap: %s", line); + + // Host discovery total + if(strstr(line, "Total:") && strstr(line, "hosts discovered")) { + int n = 0; + const char* tp = strstr(line, "Total:"); + if(tp) { + n = atoi(tp + 6); + } + data->hosts_total = (uint8_t)n; + FURI_LOG_I(TAG, "Total hosts to scan: %u", data->hosts_total); + continue; + } + + // Single-host mode + if(strstr(line, "Single-host mode")) { + data->hosts_total = 1; + continue; + } + + // Scan phase start + if(strstr(line, "host(s)") && strstr(line, "ports each")) { + int n = 0; + const char* sp = strstr(line, "Scanning "); + if(sp) { + n = atoi(sp + 9); + } + if(n > 0) data->hosts_total = (uint8_t)n; + continue; + } + + // New host block: "Host: IP (MAC)" or "Host: IP (MAC unknown)" + if(strncmp(line, "Host:", 5) == 0) { + if(data->result_count < NMAP_MAX_HOSTS) { + current_result_idx = data->result_count; + NmapResultHost* rh = &data->results[current_result_idx]; + memset(rh, 0, sizeof(NmapResultHost)); + + char ip_buf[16] = {0}; + char mac_buf[18] = {0}; + const char* p = line + 5; + while(*p == ' ') p++; + + size_t ip_len = 0; + while(*p && *p != ' ' && ip_len < sizeof(ip_buf) - 1) { + ip_buf[ip_len++] = *p++; + } + ip_buf[ip_len] = '\0'; + strncpy(rh->ip, ip_buf, sizeof(rh->ip) - 1); + + const char* paren = strchr(p, '('); + if(paren) { + paren++; + if(strncmp(paren, "MAC unknown", 11) != 0) { + size_t mac_len = 0; + while(*paren && *paren != ')' && mac_len < sizeof(mac_buf) - 1) { + mac_buf[mac_len++] = *paren++; + } + mac_buf[mac_len] = '\0'; + strncpy(rh->mac, mac_buf, sizeof(rh->mac) - 1); + } + } + + strncpy(data->progress_ip, rh->ip, sizeof(data->progress_ip) - 1); + data->result_count++; + data->hosts_scanned = data->result_count; + + FURI_LOG_I(TAG, "Result host #%u: %s (%s)", + data->result_count, rh->ip, rh->mac); + } + continue; + } + + // Progress: " Scanning IP ports from-to [current/total] ..." + if(strstr(line, "Scanning") && strstr(line, "ports") && strchr(line, '[')) { + const char* bracket = strchr(line, '['); + if(bracket) { + int current = 0, total = 0; + if(sscanf(bracket, "[%d/%d]", ¤t, &total) == 2 && total > 0) { + data->progress_pct = (uint8_t)((current * 100) / total); + } + } + continue; + } + + // Open port: " PORT/tcp open SERVICE" + { + const char* p = line; + while(*p == ' ') p++; + int port = 0; + char service[16] = {0}; + if(sscanf(p, "%d/tcp open %15s", &port, service) == 2 || + sscanf(p, "%d/tcp open %15s", &port, service) == 2) { + if(current_result_idx >= 0 && current_result_idx < NMAP_MAX_HOSTS) { + NmapResultHost* rh = &data->results[current_result_idx]; + if(rh->port_count < NMAP_MAX_PORTS_PER_HOST) { + rh->ports[rh->port_count].port = port; + strncpy(rh->ports[rh->port_count].service, service, + sizeof(rh->ports[0].service) - 1); + rh->port_count++; + data->total_open_ports++; + FURI_LOG_I(TAG, " Port %d/%s open", port, service); + } + } + continue; + } + } + + // No open ports + if(strstr(line, "(no open ports)")) { + if(current_result_idx >= 0 && current_result_idx < NMAP_MAX_HOSTS) { + data->results[current_result_idx].no_open_ports = true; + } + continue; + } + + // Completion: "Scanned N hosts, found M open ports" + if(strstr(line, "Scanned") && strstr(line, "open ports")) { + scan_complete = true; + data->progress_pct = 100; + FURI_LOG_I(TAG, "Scan complete: %u hosts, %u open ports", + data->result_count, data->total_open_ports); + continue; + } + + // Stopped by user + if(strstr(line, "(scan stopped by user)")) { + scan_complete = true; + continue; + } + } + + // Step 9: Show results + data->state = 7; + data->scroll_pos = 0; + + while(!data->attack_finished) { + furi_delay_ms(100); + } + + FURI_LOG_I(TAG, "Thread finished"); + return 0; +} + +// ============================================================================ +// Screen Creation +// ============================================================================ + +View* screen_nmap_create(WiFiApp* app, void** out_data) { + FURI_LOG_I(TAG, "Creating Nmap screen"); + + if(!app || app->selected_count != 1) { + FURI_LOG_E(TAG, "Exactly 1 network must be selected"); + return NULL; + } + + NmapData* data = (NmapData*)malloc(sizeof(NmapData)); + if(!data) return NULL; + + memset(data, 0, sizeof(NmapData)); + data->app = app; + data->attack_finished = false; + data->state = 0; + data->net_index = app->selected_networks[0]; + data->password_entered = false; + data->text_input_added = false; + data->connect_failed = false; + data->host_selection_confirmed = false; + data->scan_level_confirmed = false; + + uint32_t idx0 = data->net_index - 1; + if(idx0 < app->scan_result_count && app->scan_results) { + strncpy(data->ssid, app->scan_results[idx0].ssid, sizeof(data->ssid) - 1); + data->ssid[sizeof(data->ssid) - 1] = '\0'; + } else { + strncpy(data->ssid, "Unknown", sizeof(data->ssid) - 1); + } + + View* view = view_alloc(); + if(!view) { + free(data); + return NULL; + } + data->main_view = view; + + view_allocate_model(view, ViewModelTypeLocking, sizeof(NmapModel)); + NmapModel* m = view_get_model(view); + m->data = data; + view_commit_model(view, true); + + view_set_draw_callback(view, nmap_draw); + view_set_input_callback(view, nmap_input); + view_set_context(view, view); + + data->text_input = text_input_alloc(); + if(data->text_input) { + text_input_set_header_text(data->text_input, "Enter Password:"); + text_input_set_result_callback( + data->text_input, + nmap_password_callback, + data, + data->password, + NMAP_PASSWORD_MAX, + true); + FURI_LOG_I(TAG, "TextInput created"); + } + + data->thread = furi_thread_alloc(); + furi_thread_set_name(data->thread, "NmapScan"); + furi_thread_set_stack_size(data->thread, 2048); + furi_thread_set_callback(data->thread, nmap_thread); + furi_thread_set_context(data->thread, data); + furi_thread_start(data->thread); + + if(out_data) *out_data = data; + + FURI_LOG_I(TAG, "Nmap screen created"); + return view; +} diff --git a/src/screen_rogue_ap.c b/src/screen_rogue_ap.c index dea0af2..29bdbe8 100644 --- a/src/screen_rogue_ap.c +++ b/src/screen_rogue_ap.c @@ -38,10 +38,11 @@ typedef struct { WiFiApp* app; volatile bool attack_finished; uint8_t state; - // 0 = checking password (thread) - // 1 = waiting for password input (TextInput) - // 2 = select HTML file - // 3 = attack running + // 0 = checking password (thread) + // 1 = waiting for password input (TextInput) + // 2 = select HTML file + // 3 = attack running + // 10 = confirm saved password (OK=use, Right=enter new) // Network info char ssid[33]; @@ -65,6 +66,8 @@ typedef struct { TextInput* text_input; bool text_input_added; bool password_entered; + volatile bool password_choice_made; + volatile bool password_use_saved; View* main_view; } RogueApData; @@ -120,6 +123,7 @@ static void rogue_ap_password_callback(void* context) { if(!data || !data->app) return; FURI_LOG_I(TAG, "Password entered: %s", data->password); + password_cache_put(data->app, data->ssid, data->password); data->password_entered = true; data->state = 2; // Move to HTML selection @@ -158,14 +162,28 @@ static void rogue_ap_draw(Canvas* canvas, void* model) { screen_draw_centered_text(canvas, "Checking password...", 32); } else if(data->state == 1) { - // Waiting for password input - TextInput is shown + // Waiting for password input - TextInput already swapped in by the + // worker thread (calling it from here would deadlock on ViewModel lock). screen_draw_title(canvas, "Rogue AP"); canvas_set_font(canvas, FontSecondary); screen_draw_centered_text(canvas, "Enter password", 32); - if(!data->text_input_added) { - rogue_ap_show_text_input(data); + + } else if(data->state == 10) { + screen_draw_title(canvas, "Rogue AP"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 22, "Saved password found:"); + + char pw_line[22]; + size_t pw_len = strlen(data->password); + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password); + canvas_draw_str(canvas, 2, 35, pw_line); + if(pw_len > 21) { + snprintf(pw_line, sizeof(pw_line), "%.21s", data->password + 21); + canvas_draw_str(canvas, 2, 44, pw_line); } + canvas_draw_str(canvas, 2, 62, "OK:use Right:new"); + } else if(data->state == 2) { // HTML file selection screen_draw_title(canvas, "Select HTML"); @@ -259,6 +277,27 @@ static bool rogue_ap_input(InputEvent* event, void* context) { view_commit_model(view, false); return true; } + } else if(data->state == 10) { + if(event->key == InputKeyOk) { + data->password_use_saved = true; + data->password_choice_made = true; + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyRight) { + data->password[0] = '\0'; + data->state = 1; + data->password_choice_made = true; + rogue_ap_show_text_input(data); + view_commit_model(view, false); + return true; + } + if(event->key == InputKeyBack) { + data->attack_finished = true; + view_commit_model(view, false); + screen_pop(data->app); + return true; + } } else if(data->state == 2) { // HTML selection if(event->key == InputKeyUp) { @@ -292,65 +331,6 @@ static bool rogue_ap_input(InputEvent* event, void* context) { return true; } -// ============================================================================ -// Password discovery helper -// ============================================================================ - -static bool rogue_ap_check_password(RogueApData* data) { - WiFiApp* app = data->app; - - uart_clear_buffer(app); - uart_send_command(app, "show_pass evil"); - furi_delay_ms(200); - - uint32_t start = furi_get_tick(); - uint32_t last_rx = start; - - while((furi_get_tick() - last_rx) < 1000 && - (furi_get_tick() - start) < 5000 && - !data->attack_finished) { - const char* line = uart_read_line(app, 300); - if(line) { - last_rx = furi_get_tick(); - FURI_LOG_I(TAG, "show_pass: %s", line); - - // Parse "SSID", "password" - const char* p = line; - // Skip whitespace - while(*p == ' ' || *p == '\t') p++; - if(*p != '"') continue; - p++; // skip opening quote - const char* ssid_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t ssid_len = p - ssid_start; - p++; // skip closing quote - - // Skip separator - while(*p == ',' || *p == ' ' || *p == '\t') p++; - - if(*p != '"') continue; - p++; // skip opening quote - const char* pass_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t pass_len = p - pass_start; - - // Compare SSID - if(ssid_len == strlen(data->ssid) && - strncmp(ssid_start, data->ssid, ssid_len) == 0) { - if(pass_len < sizeof(data->password)) { - strncpy(data->password, pass_start, pass_len); - data->password[pass_len] = '\0'; - FURI_LOG_I(TAG, "Password found: %s", data->password); - return true; - } - } - } - } - return false; -} - // ============================================================================ // Attack Thread // ============================================================================ @@ -365,29 +345,49 @@ static int32_t rogue_ap_thread(void* context) { char cmd[256]; snprintf(cmd, sizeof(cmd), "select_networks %lu", (unsigned long)data->net_index); uart_send_command(app, cmd); - furi_delay_ms(500); + furi_delay_ms(100); uart_clear_buffer(app); if(data->attack_finished) return 0; - // Step 2: Check if password is known + // Step 2: Check if password is known (cache + show_pass evil fallback) data->state = 0; - bool found = rogue_ap_check_password(data); + bool found = attack_resolve_password( + app, data->ssid, data->password, sizeof(data->password), &data->attack_finished); if(data->attack_finished) return 0; - if(!found) { + if(found) { + FURI_LOG_I(TAG, "Saved password found, awaiting user confirmation"); + data->state = 10; + while(!data->password_choice_made && !data->attack_finished) { + furi_delay_ms(50); + } + if(data->attack_finished) return 0; + + if(!data->password_use_saved) { + FURI_LOG_I(TAG, "User chose to enter a new password"); + // Input handler already set state=1 and showed TextInput + while(!data->password_entered && !data->attack_finished) { + furi_delay_ms(100); + } + if(data->attack_finished) return 0; + } + data->state = 2; // proceed to HTML selection + } else { // Need user to enter password data->state = 1; FURI_LOG_I(TAG, "Password unknown, requesting user input"); + // Show TextInput from worker thread (NOT from draw callback - that + // would deadlock on the ViewModelTypeLocking mutex while we are + // already inside view_get_model() in the draw call). + rogue_ap_show_text_input(data); // Wait for password entry while(!data->password_entered && !data->attack_finished) { furi_delay_ms(100); } if(data->attack_finished) return 0; - } else { - data->state = 2; // skip to HTML selection } // Step 3: Load HTML files @@ -499,6 +499,7 @@ static int32_t rogue_ap_thread(void* context) { strncpy(data->last_password, colon, sizeof(data->last_password) - 1); data->last_password[sizeof(data->last_password) - 1] = '\0'; FURI_LOG_I(TAG, "Password captured: %s", data->last_password); + password_cache_put(app, data->ssid, data->last_password); } } } diff --git a/src/screen_wifi_scan.c b/src/screen_wifi_scan.c index 2ac8c53..6f4b272 100644 --- a/src/screen_wifi_scan.c +++ b/src/screen_wifi_scan.c @@ -353,8 +353,9 @@ static const AttackMenuItem all_attack_items[] = { {"Rogue AP", 5, true}, {"ARP Poisoning", 6, true}, {"MITM PCAP Sniffer", 7, true}, + {"Nmap", 8, true}, }; -#define ALL_ATTACK_ITEM_COUNT 8 +#define ALL_ATTACK_ITEM_COUNT 9 static uint8_t get_visible_attack_items(WiFiApp* app, AttackMenuItem* out, uint8_t max_out) { uint8_t count = 0; @@ -439,8 +440,8 @@ static bool attack_selection_input(InputEvent* event, void* context) { if(visual_idx >= item_count) return true; uint8_t attack_type = visible[visual_idx].attack_id; - // Rogue AP, ARP Poisoning, and MITM PCAP require exactly 1 selected network - if((attack_type == 5 || attack_type == 6 || attack_type == 7) && app->selected_count != 1) { + // Rogue AP, ARP Poisoning, MITM PCAP, and Nmap require exactly 1 selected network + if((attack_type == 5 || attack_type == 6 || attack_type == 7 || attack_type == 8) && app->selected_count != 1) { // Cannot launch - need exactly 1 network return true; } @@ -477,6 +478,9 @@ static bool attack_selection_input(InputEvent* event, void* context) { } else if(attack_type == 7) { attack_view = screen_mitm_pcap_create(app, &cleanup_data); cleanup_func = mitm_pcap_cleanup_internal; + } else if(attack_type == 8) { + attack_view = screen_nmap_create(app, &cleanup_data); + cleanup_func = nmap_cleanup_internal; } if(attack_view) { screen_push_with_cleanup(app, attack_view, cleanup_func, cleanup_data); diff --git a/src/screen_wpasec.c b/src/screen_wpasec.c index 776ffd3..93f9f57 100644 --- a/src/screen_wpasec.c +++ b/src/screen_wpasec.c @@ -115,6 +115,7 @@ static void wpasec_password_callback(void* context) { if(!data || !data->app) return; FURI_LOG_I(TAG, "Password entered: %s", data->password); + password_cache_put(data->app, data->ssid, data->password); data->password_entered = true; data->state = 7; // Move to connecting @@ -134,62 +135,6 @@ static void wpasec_show_text_input(WpasecData* data) { view_dispatcher_switch_to_view(data->app->view_dispatcher, WPASEC_TEXT_INPUT_ID); } -// ============================================================================ -// Password discovery helper (Evil Twin passwords) -// ============================================================================ - -static bool wpasec_check_password(WpasecData* data) { - WiFiApp* app = data->app; - - uart_clear_buffer(app); - uart_send_command(app, "show_pass evil"); - furi_delay_ms(200); - - uint32_t start = furi_get_tick(); - uint32_t last_rx = start; - - while((furi_get_tick() - last_rx) < 1000 && - (furi_get_tick() - start) < 5000 && - !data->should_exit) { - const char* line = uart_read_line(app, 300); - if(line) { - last_rx = furi_get_tick(); - FURI_LOG_I(TAG, "show_pass: %s", line); - - // Parse "SSID", "password" - const char* p = line; - while(*p == ' ' || *p == '\t') p++; - if(*p != '"') continue; - p++; - const char* ssid_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t ssid_len = (size_t)(p - ssid_start); - p++; - - while(*p == ',' || *p == ' ' || *p == '\t') p++; - - if(*p != '"') continue; - p++; - const char* pass_start = p; - while(*p && *p != '"') p++; - if(*p != '"') continue; - size_t pass_len = (size_t)(p - pass_start); - - if(ssid_len == strlen(data->ssid) && - strncmp(ssid_start, data->ssid, ssid_len) == 0) { - if(pass_len < sizeof(data->password)) { - strncpy(data->password, pass_start, pass_len); - data->password[pass_len] = '\0'; - FURI_LOG_I(TAG, "Password found via Evil Twin: %s", data->password); - return true; - } - } - } - } - return false; -} - // ============================================================================ // Drawing // ============================================================================ @@ -460,10 +405,11 @@ static int32_t wpasec_thread(void* context) { } if(data->should_exit) return 0; - // Try to get password from Evil Twin data + // Try to get password from cache / Evil Twin data data->state = 5; FURI_LOG_I(TAG, "Checking Evil Twin passwords for %s", data->ssid); - bool pw_found = wpasec_check_password(data); + bool pw_found = attack_resolve_password( + app, data->ssid, data->password, sizeof(data->password), &data->should_exit); if(data->should_exit) return 0; diff --git a/src/uart_comm.c b/src/uart_comm.c index b057637..3149c94 100644 --- a/src/uart_comm.c +++ b/src/uart_comm.c @@ -208,6 +208,30 @@ bool uart_check_board_connection(WiFiApp* app) { return false; } +bool uart_ping_once(WiFiApp* app, uint32_t timeout_ms) { + if(!app || !app->serial) return false; + + uart_clear_buffer(app); + uart_send_command(app, "ping"); + + uint32_t deadline = furi_get_tick() + timeout_ms; + FuriString* line = furi_string_alloc(); + bool got_pong = false; + + while(furi_get_tick() < deadline) { + if(uart_read_line_internal(app, line)) { + const char* line_str = furi_string_get_cstr(line); + if(strstr(line_str, "pong")) { + got_pong = true; + break; + } + } + } + + furi_string_free(line); + return got_pong; +} + //============================================================================= // SD Card Check //============================================================================= @@ -343,6 +367,133 @@ void uart_clear_buffer(WiFiApp* app) { furi_string_reset(app->uart_line_buffer); } +//============================================================================= +// Password cache (shared between Rogue AP / ARP / MITM PCAP / Nmap screens) +//============================================================================= + +#define PASSWORD_REFRESH_INITIAL_DELAY_MS 50 +#define PASSWORD_REFRESH_TOTAL_TIMEOUT_MS 1500 +#define PASSWORD_REFRESH_SILENCE_MS 250 +#define PASSWORD_REFRESH_LINE_TIMEOUT_MS 100 + +bool password_cache_lookup(WiFiApp* app, const char* ssid, char* out, size_t out_size) { + if(!app || !ssid || !out || out_size == 0) return false; + + for(uint8_t i = 0; i < app->password_cache_count; i++) { + if(strncmp(app->password_cache[i].ssid, ssid, sizeof(app->password_cache[i].ssid)) == 0) { + strncpy(out, app->password_cache[i].password, out_size - 1); + out[out_size - 1] = '\0'; + return true; + } + } + return false; +} + +void password_cache_put(WiFiApp* app, const char* ssid, const char* password) { + if(!app || !ssid || !password || ssid[0] == '\0') return; + + // Update existing entry if SSID is already cached. + for(uint8_t i = 0; i < app->password_cache_count; i++) { + if(strncmp(app->password_cache[i].ssid, ssid, sizeof(app->password_cache[i].ssid)) == 0) { + strncpy(app->password_cache[i].password, password, + sizeof(app->password_cache[i].password) - 1); + app->password_cache[i].password[sizeof(app->password_cache[i].password) - 1] = '\0'; + return; + } + } + + // Append, or overwrite oldest (slot 0) when full. + uint8_t slot; + if(app->password_cache_count < MAX_CACHED_PASSWORDS) { + slot = app->password_cache_count++; + } else { + // Shift left, drop oldest. + memmove(&app->password_cache[0], &app->password_cache[1], + sizeof(CachedPassword) * (MAX_CACHED_PASSWORDS - 1)); + slot = MAX_CACHED_PASSWORDS - 1; + } + + strncpy(app->password_cache[slot].ssid, ssid, sizeof(app->password_cache[slot].ssid) - 1); + app->password_cache[slot].ssid[sizeof(app->password_cache[slot].ssid) - 1] = '\0'; + strncpy(app->password_cache[slot].password, password, + sizeof(app->password_cache[slot].password) - 1); + app->password_cache[slot].password[sizeof(app->password_cache[slot].password) - 1] = '\0'; + + FURI_LOG_I(TAG, "Cache put: '%s' -> %u entries", ssid, app->password_cache_count); +} + +bool password_cache_refresh(WiFiApp* app, bool force, volatile bool* cancel) { + if(!app) return false; + if(!force && app->password_cache_loaded) return true; + if(cancel && *cancel) return false; + + uart_clear_buffer(app); + uart_send_command(app, "show_pass evil"); + furi_delay_ms(PASSWORD_REFRESH_INITIAL_DELAY_MS); + + uint32_t start = furi_get_tick(); + uint32_t last_rx = start; + + while((furi_get_tick() - last_rx) < PASSWORD_REFRESH_SILENCE_MS && + (furi_get_tick() - start) < PASSWORD_REFRESH_TOTAL_TIMEOUT_MS && + (!cancel || !*cancel)) { + + const char* line = uart_read_line(app, PASSWORD_REFRESH_LINE_TIMEOUT_MS); + if(!line) continue; + + last_rx = furi_get_tick(); + + // Skip command echo. + if(strncmp(line, "show_pass", 9) == 0) continue; + // Skip non-CSV lines (only "SSID","PASSWORD" entries start with a quote). + if(line[0] != '"') continue; + + const char* p = line; + char ssid[33] = {0}; + char password[65] = {0}; + + if(!csv_next_quoted_field(&p, ssid, sizeof(ssid))) continue; + if(!csv_next_quoted_field(&p, password, sizeof(password))) continue; + if(ssid[0] == '\0') continue; + + password_cache_put(app, ssid, password); + } + + if(cancel && *cancel) { + FURI_LOG_I(TAG, "Password cache refresh cancelled"); + return false; + } + + app->password_cache_loaded = true; + FURI_LOG_I(TAG, "Password cache refreshed: %u entries", app->password_cache_count); + return true; +} + +bool attack_resolve_password( + WiFiApp* app, + const char* ssid, + char* out, + size_t out_size, + volatile bool* cancel) { + if(!app || !ssid || !out || out_size == 0) return false; + + if(password_cache_lookup(app, ssid, out, out_size)) { + FURI_LOG_I(TAG, "Password cache hit for '%s'", ssid); + return true; + } + + if(!app->password_cache_loaded) { + password_cache_refresh(app, false, cancel); + if(cancel && *cancel) return false; + if(password_cache_lookup(app, ssid, out, out_size)) { + FURI_LOG_I(TAG, "Password found after refresh for '%s'", ssid); + return true; + } + } + + return false; +} + //============================================================================= // Scanning helpers //============================================================================= diff --git a/src/uart_comm.h b/src/uart_comm.h index 4f3c00a..5448460 100644 --- a/src/uart_comm.h +++ b/src/uart_comm.h @@ -11,8 +11,40 @@ void uart_clear_buffer(WiFiApp* app); bool uart_check_board_connection(WiFiApp* app); bool uart_check_sd_card(WiFiApp* app); +// Single ping/pong attempt with a configurable timeout. Used by the boot +// screen worker thread which retries on its own. Safe to call when +// `app->serial` is NULL (returns false). +bool uart_ping_once(WiFiApp* app, uint32_t timeout_ms); + // CSV parsing bool csv_next_quoted_field(const char** p, char* out, size_t out_size); // Scanning void uart_start_scan(WiFiApp* app); + +// ===================================================================== +// Password cache (shared between attack screens) +// ===================================================================== + +// Look up a password by SSID in the in-RAM cache. Returns true if found and +// writes it to `out`. Does NOT touch UART. +bool password_cache_lookup(WiFiApp* app, const char* ssid, char* out, size_t out_size); + +// Insert or update a (ssid, password) pair in the cache. Safe to call +// from any context. Older entries are overwritten when full. +void password_cache_put(WiFiApp* app, const char* ssid, const char* password); + +// Synchronize the cache with the firmware via `show_pass evil`. +// Idempotent unless `force` is true. Cancellable via `cancel` flag. +// Uses short timeouts to keep the UI responsive. +// Returns true on a successful sync (even if the list was empty). +bool password_cache_refresh(WiFiApp* app, bool force, volatile bool* cancel); + +// High-level helper: try cache, refresh from firmware if needed, try cache again. +// Writes the password to `out` and returns true if the SSID is known. +bool attack_resolve_password( + WiFiApp* app, + const char* ssid, + char* out, + size_t out_size, + volatile bool* cancel);