Flipper Zero/Rogue AP Detector/scanner.c
From charlesreid1
Main article: Flipper Zero/Rogue AP Detector
Requirements
Required Features:
- Monitor Mode: Initializes ESP32 in station mode for passive scanning
- Channel Scanning: Can scan specific channels or perform full spectrum sweep (1-13)
- Beacon Parsing: Extracts SSID, BSSID, RSSI, encryption type, and channel info
- Thread Safety: Uses mutex protection for concurrent access
- Memory Management: Handles AP storage with configurable limits
Core Functions:
- wifi_scanner_init() - Initialize the global scanner
- wifi_scanner_full_scan() - Scan all channels and collect APs
- wifi_scanner_get_results() - Retrieve discovered APs thread-safely
- wifi_scanner_encryption_str() - Convert encryption enum to readable string
Data Structure:
- The ap_info_t structure captures all necessary AP information including handling of hidden networks and timestamp tracking for freshness analysis.
Integration Points:
- Uses Flipper Zero's FuriOS threading primitives
- ESP32 WiFi APIs for hardware interface
- Memory-efficient circular buffer approach for resource constraints
Version 1
#include <furi.h> #include <furi_hal.h> #include <gui/gui.h> #include <notification/notification_messages.h> #include <esp_wifi.h> #include <esp_event.h> #include <string.h> // Maximum number of APs we can track #define MAX_APS 50 #define SSID_MAX_LEN 32 #define SCAN_TIMEOUT_MS 5000 #define CHANNEL_DWELL_TIME_MS 200 // WiFi encryption types typedef enum { WIFI_ENC_OPEN = 0, WIFI_ENC_WEP, WIFI_ENC_WPA, WIFI_ENC_WPA2, WIFI_ENC_WPA3, WIFI_ENC_UNKNOWN } wifi_encryption_t; // AP information structure typedef struct { char ssid[SSID_MAX_LEN + 1]; uint8_t bssid[6]; int8_t rssi; wifi_encryption_t encryption; uint8_t channel; uint32_t timestamp; bool is_hidden; } ap_info_t; // Scanner context typedef struct { ap_info_t* aps; uint16_t ap_count; uint16_t max_aps; uint8_t current_channel; bool scanning; FuriMutex* mutex; } wifi_scanner_t; // Global scanner instance static wifi_scanner_t* scanner = NULL; // Convert ESP WiFi auth mode to our encryption enum static wifi_encryption_t get_encryption_type(wifi_auth_mode_t auth_mode) { switch(auth_mode) { case WIFI_AUTH_OPEN: return WIFI_ENC_OPEN; case WIFI_AUTH_WEP: return WIFI_ENC_WEP; case WIFI_AUTH_WPA_PSK: case WIFI_AUTH_WPA_WPA2_PSK: return WIFI_ENC_WPA; case WIFI_AUTH_WPA2_PSK: return WIFI_ENC_WPA2; case WIFI_AUTH_WPA3_PSK: return WIFI_ENC_WPA3; default: return WIFI_ENC_UNKNOWN; } } // Beacon frame parsing callback static void wifi_scan_callback(void* arg, wifi_event_base_t event_base, int32_t event_id, void* event_data) { if(!scanner || !scanner->scanning) return; if(event_id == WIFI_EVENT_SCAN_DONE) { wifi_scan_config_t scan_config = {0}; uint16_t ap_count = 0; // Get scan results count esp_wifi_scan_get_ap_num(&ap_count); if(ap_count > 0) { wifi_ap_record_t* ap_records = malloc(sizeof(wifi_ap_record_t) * ap_count); if(ap_records) { // Get actual scan results esp_wifi_scan_get_ap_records(&ap_count, ap_records); furi_mutex_acquire(scanner->mutex, FuriWaitForever); // Process each discovered AP for(uint16_t i = 0; i < ap_count && scanner->ap_count < scanner->max_aps; i++) { wifi_ap_record_t* record = &ap_records[i]; // Check if we already have this AP (by BSSID) bool found = false; for(uint16_t j = 0; j < scanner->ap_count; j++) { if(memcmp(scanner->aps[j].bssid, record->bssid, 6) == 0) { // Update existing entry with latest info scanner->aps[j].rssi = record->rssi; scanner->aps[j].timestamp = furi_get_tick(); found = true; break; } } if(!found) { // Add new AP ap_info_t* ap = &scanner->aps[scanner->ap_count]; // Copy SSID (handle hidden networks) if(record->ssid[0] == '\0') { snprintf(ap->ssid, sizeof(ap->ssid), "<Hidden>"); ap->is_hidden = true; } else { strncpy(ap->ssid, (char*)record->ssid, SSID_MAX_LEN); ap->ssid[SSID_MAX_LEN] = '\0'; ap->is_hidden = false; } // Copy BSSID memcpy(ap->bssid, record->bssid, 6); // Set other fields ap->rssi = record->rssi; ap->encryption = get_encryption_type(record->authmode); ap->channel = record->primary; ap->timestamp = furi_get_tick(); scanner->ap_count++; } } furi_mutex_release(scanner->mutex); free(ap_records); } } } } // Initialize the WiFi scanner wifi_scanner_t* wifi_scanner_alloc(uint16_t max_aps) { wifi_scanner_t* scanner = malloc(sizeof(wifi_scanner_t)); if(!scanner) return NULL; scanner->aps = malloc(sizeof(ap_info_t) * max_aps); if(!scanner->aps) { free(scanner); return NULL; } scanner->max_aps = max_aps; scanner->ap_count = 0; scanner->current_channel = 1; scanner->scanning = false; scanner->mutex = furi_mutex_alloc(FuriMutexTypeNormal); if(!scanner->mutex) { free(scanner->aps); free(scanner); return NULL; } // Initialize ESP32 WiFi wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); esp_wifi_set_mode(WIFI_MODE_STA); // Register event handler esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_scan_callback, NULL); return scanner; } // Free the scanner resources void wifi_scanner_free(wifi_scanner_t* scanner_instance) { if(!scanner_instance) return; scanner_instance->scanning = false; esp_wifi_stop(); esp_wifi_deinit(); if(scanner_instance->mutex) { furi_mutex_free(scanner_instance->mutex); } if(scanner_instance->aps) { free(scanner_instance->aps); } free(scanner_instance); } // Start scanning on specific channel bool wifi_scanner_start_channel_scan(wifi_scanner_t* scanner_instance, uint8_t channel) { if(!scanner_instance || channel < 1 || channel > 13) return false; scanner_instance->current_channel = channel; scanner_instance->scanning = true; // Configure scan parameters wifi_scan_config_t scan_config = { .ssid = NULL, // Scan all SSIDs .bssid = NULL, // Scan all BSSIDs .channel = channel, // Specific channel .show_hidden = true, // Include hidden networks .scan_type = WIFI_SCAN_TYPE_ACTIVE, .scan_time = { .active = { .min = 100, // Minimum scan time per channel .max = 300 // Maximum scan time per channel } } }; esp_err_t ret = esp_wifi_start(); if(ret != ESP_OK) return false; ret = esp_wifi_scan_start(&scan_config, false); return (ret == ESP_OK); } // Perform full spectrum scan (all channels) bool wifi_scanner_full_scan(wifi_scanner_t* scanner_instance) { if(!scanner_instance) return false; // Clear existing results furi_mutex_acquire(scanner_instance->mutex, FuriWaitForever); scanner_instance->ap_count = 0; furi_mutex_release(scanner_instance->mutex); // Scan each channel sequentially for(uint8_t channel = 1; channel <= 13; channel++) { if(!wifi_scanner_start_channel_scan(scanner_instance, channel)) { return false; } // Wait for scan to complete furi_delay_ms(CHANNEL_DWELL_TIME_MS); } return true; } // Get current AP list (thread-safe) uint16_t wifi_scanner_get_results(wifi_scanner_t* scanner_instance, ap_info_t* results, uint16_t max_results) { if(!scanner_instance || !results) return 0; furi_mutex_acquire(scanner_instance->mutex, FuriWaitForever); uint16_t count = (scanner_instance->ap_count < max_results) ? scanner_instance->ap_count : max_results; if(count > 0) { memcpy(results, scanner_instance->aps, sizeof(ap_info_t) * count); } furi_mutex_release(scanner_instance->mutex); return count; } // Get encryption type as string const char* wifi_scanner_encryption_str(wifi_encryption_t enc) { switch(enc) { case WIFI_ENC_OPEN: return "Open"; case WIFI_ENC_WEP: return "WEP"; case WIFI_ENC_WPA: return "WPA"; case WIFI_ENC_WPA2: return "WPA2"; case WIFI_ENC_WPA3: return "WPA3"; default: return "Unknown"; } } // Check if scanner is currently active bool wifi_scanner_is_scanning(wifi_scanner_t* scanner_instance) { return scanner_instance ? scanner_instance->scanning : false; } // Stop current scan void wifi_scanner_stop(wifi_scanner_t* scanner_instance) { if(!scanner_instance) return; scanner_instance->scanning = false; esp_wifi_scan_stop(); } // Initialize global scanner instance bool wifi_scanner_init(void) { if(scanner) return true; // Already initialized scanner = wifi_scanner_alloc(MAX_APS); return (scanner != NULL); } // Cleanup global scanner void wifi_scanner_deinit(void) { if(scanner) { wifi_scanner_free(scanner); scanner = NULL; } }
Version 2
scanner.h
#ifndef SCANNER_H #define SCANNER_H #include <furi.h> #include <stdint.h> // Define constants for AP data #define MAX_SSID_LEN 32 #define MAX_APS_TO_STORE 50 /** * @brief Enum for WiFi encryption types. */ typedef enum { WifiEncryptionTypeOpen, WifiEncryptionTypeWep, WifiEncryptionTypeWpa, WifiEncryptionTypeWpa2, WifiEncryptionTypeWpa3, WifiEncryptionTypeUnknown, } WifiEncryptionType; /** * @brief Structure to hold all relevant information for a single Access Point. */ typedef struct { char ssid[MAX_SSID_LEN + 1]; // Network name uint8_t bssid[6]; // MAC address int8_t rssi; // Signal strength WifiEncryptionType encryption; // Security protocol uint8_t channel; // WiFi channel uint32_t timestamp; // Furi tick at time of last sighting } ap_info_t; /** * @brief Main scanner state structure. */ typedef struct { ap_info_t* ap_list; // Dynamically allocated array of found APs uint16_t count; // Current number of unique APs found uint16_t capacity; // Maximum number of APs to store FuriMutex* mutex; // Mutex for thread-safe access to the ap_list } WifiScanner; /** * @brief Allocates and initializes a new WifiScanner instance. * @param capacity The maximum number of APs to store. * @return Pointer to the newly created WifiScanner. */ WifiScanner* wifi_scanner_alloc(uint16_t capacity); /** * @brief Frees all resources associated with a WifiScanner instance. * @param scanner Pointer to the WifiScanner instance to free. */ void wifi_scanner_free(WifiScanner* scanner); /** * @brief Initializes the ESP32 WiFi hardware. Must be called once at app startup. */ void wifi_scanner_init_hardware(); /** * @brief De-initializes the ESP32 WiFi hardware. Call when the app is closing. */ void wifi_scanner_deinit_hardware(); /** * @brief Performs a blocking scan across all 2.4GHz WiFi channels (1-13). * This function enables promiscuous mode, hops through channels, listens * for beacons, and then disables promiscuous mode. * @param scanner Pointer to the WifiScanner instance. */ void wifi_scanner_full_scan(WifiScanner* scanner); /** * @brief Thread-safely retrieves the list of discovered APs. * @param scanner Pointer to the WifiScanner instance. * @param results_buffer A user-provided buffer to copy the results into. * @param buffer_size The size of the results_buffer. * @return The number of APs copied into the buffer. */ uint16_t wifi_scanner_get_results(WifiScanner* scanner, ap_info_t* results_buffer, uint16_t buffer_size); /** * @brief Converts a WifiEncryptionType enum to a human-readable string. * @param encryption_type The enum value to convert. * @return A constant string representation (e.g., "WPA2"). */ const char* wifi_scanner_encryption_str(WifiEncryptionType encryption_type); #endif // SCANNER_H
scanner.c
#include "scanner.h" #include <furi_hal.h> #include <string.h> // A global pointer to the scanner instance is needed for the static callback function. static WifiScanner* g_scanner_instance = NULL; // --- Forward Declarations for internal functions --- static void wifi_promiscuous_rx_cb(void* buf, wifi_promiscuous_pkt_type_t type); static void parse_beacon_frame(const wifi_promiscuous_pkt_t* pkt); static WifiEncryptionType get_encryption_from_beacon(const uint8_t* frame_body, uint16_t body_len); static void scanner_add_or_update_ap(WifiScanner* scanner, const ap_info_t* new_ap); // --- Public Function Implementations --- void wifi_scanner_init_hardware() { // furi_hal_wifi_init() is deprecated in recent firmwares. // The wifi stack is now managed automatically. // We ensure the wifi module is powered on for our operations. furi_hal_wifi_acquire(); } void wifi_scanner_deinit_hardware() { furi_hal_wifi_release(); } WifiScanner* wifi_scanner_alloc(uint16_t capacity) { WifiScanner* scanner = malloc(sizeof(WifiScanner)); furi_check(scanner != NULL); scanner->ap_list = malloc(sizeof(ap_info_t) * capacity); furi_check(scanner->ap_list != NULL); scanner->capacity = capacity; scanner->count = 0; scanner->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); return scanner; } void wifi_scanner_free(WifiScanner* scanner) { if (!scanner) return; furi_mutex_free(scanner->mutex); free(scanner->ap_list); free(scanner); if(g_scanner_instance == scanner) { g_scanner_instance = NULL; } } void wifi_scanner_full_scan(WifiScanner* scanner) { furi_check(scanner != NULL); // Set global instance so the callback can access our scanner's state g_scanner_instance = scanner; // Clear previous results before starting a new scan furi_mutex_acquire(scanner->mutex, FuriWaitForever); scanner->count = 0; furi_mutex_release(scanner->mutex); // Set the callback and start promiscuous mode furi_hal_wifi_set_promiscuous_rx_callback(wifi_promiscuous_rx_cb); furi_hal_wifi_start_promiscuous_rx(); // Sequentially hop through all 13 channels for (uint8_t ch = 1; ch <= 13; ch++) { furi_hal_wifi_set_channel(ch); // Wait on each channel to capture broadcasted beacon frames furi_delay_ms(250); } // Stop listening and unregister the callback furi_hal_wifi_stop_promiscuous_rx(); furi_hal_wifi_set_promiscuous_rx_callback(NULL); g_scanner_instance = NULL; } uint16_t wifi_scanner_get_results(WifiScanner* scanner, ap_info_t* results_buffer, uint16_t buffer_size) { furi_check(scanner != NULL && results_buffer != NULL); if(furi_mutex_acquire(scanner->mutex, FuriWaitForever) != FuriStatusOk) { return 0; } uint16_t count_to_copy = (scanner->count < buffer_size) ? scanner->count : buffer_size; if (count_to_copy > 0) { memcpy(results_buffer, scanner->ap_list, count_to_copy * sizeof(ap_info_t)); } furi_mutex_release(scanner->mutex); return count_to_copy; } const char* wifi_scanner_encryption_str(WifiEncryptionType encryption_type) { switch(encryption_type) { case WifiEncryptionTypeOpen: return "Open"; case WifiEncryptionTypeWep: return "WEP"; case WifiEncryptionTypeWpa: return "WPA"; case WifiEncryptionTypeWpa2: return "WPA2"; case WifiEncryptionTypeWpa3: return "WPA3"; default: return "Unknown"; } } // --- Internal (Static) Function Implementations --- /** * @brief The core callback function that processes incoming WiFi packets. */ static void wifi_promiscuous_rx_cb(void* buf, wifi_promiscuous_pkt_type_t type) { // Ensure we have a scanner instance to work with and we are interested in this packet type if (!g_scanner_instance || type != WIFI_PKT_MGMT) { return; } wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf; const uint8_t* payload = pkt->payload; // Check if it's a beacon frame (Type 0, Subtype 8) // Frame Control field is the first byte. Subtype is bits 4-7. Type is bits 2-3. // 0b10000000 means Type: Mgmt (00), Subtype: Beacon (1000) if ((payload[0] & 0b11111100) == 0b10000000) { parse_beacon_frame(pkt); } } /** * @brief Parses the raw beacon frame to extract AP details. * @note This is a simplified parser. 802.11 frames can be very complex. */ static void parse_beacon_frame(const wifi_promiscuous_pkt_t* pkt) { ap_info_t ap = {0}; const uint8_t* payload = pkt->payload; ap.rssi = pkt->rx_ctrl.rssi; ap.channel = pkt->rx_ctrl.channel; ap.timestamp = furi_get_tick(); // BSSID is the "transmitter address" in a beacon frame (offset 10) memcpy(ap.bssid, payload + 10, 6); // The beacon body (tagged parameters) starts after the fixed header (24 bytes) and fixed parameters (12 bytes) const uint8_t* body = payload + 36; const uint8_t* end = payload + pkt->rx_ctrl.sig_len; // Iterate through tagged parameters (Element ID, Length, Value) const uint8_t* pos = body; while (pos < end - 1) { // -1 to ensure we can read EID and Length uint8_t eid = pos[0]; uint8_t len = pos[1]; // Ensure the tag length doesn't exceed packet bounds if (pos + 2 + len > end) break; if (eid == 0) { // Tag 0: SSID uint8_t ssid_len = (len > MAX_SSID_LEN) ? MAX_SSID_LEN : len; memcpy(ap.ssid, pos + 2, ssid_len); ap.ssid[ssid_len] = '\0'; // Handle hidden SSIDs where length is 0 or contains null bytes if(ssid_len == 0 || ap.ssid[0] == '\0') { strcpy(ap.ssid, "[Hidden SSID]"); } } pos += 2 + len; } // Determine encryption by parsing the entire beacon body for security tags ap.encryption = get_encryption_from_beacon(body, end - body); scanner_add_or_update_ap(g_scanner_instance, &ap); } /** * @brief Adds a new AP to the list or updates an existing one. */ static void scanner_add_or_update_ap(WifiScanner* scanner, const ap_info_t* new_ap) { if(furi_mutex_acquire(scanner->mutex, FuriWaitForever) != FuriStatusOk) return; // Check if this BSSID has been seen before bool found = false; for (uint16_t i = 0; i < scanner->count; i++) { if (memcmp(scanner->ap_list[i].bssid, new_ap->bssid, 6) == 0) { // Found: update RSSI and timestamp scanner->ap_list[i].rssi = new_ap->rssi; scanner->ap_list[i].timestamp = new_ap->timestamp; found = true; break; } } // Not found: add it to the list if there is space if (!found && scanner->count < scanner->capacity) { memcpy(&scanner->ap_list[scanner->count], new_ap, sizeof(ap_info_t)); scanner->count++; } furi_mutex_release(scanner->mutex); } /** * @brief Scans the tagged parameters of a beacon for security information. * @return The highest level of encryption found. */ static WifiEncryptionType get_encryption_from_beacon(const uint8_t* body_start, uint16_t body_len) { bool wpa = false, wpa2 = false, wpa3 = false; // Check capabilities field (2 bytes) for the privacy bit (indicates WEP) const uint8_t* header_start = body_start - 12; // Go back to start of fixed params uint16_t capabilities = (header_start[1] << 8) | header_start[0]; bool privacy_bit = (capabilities & 0x0010) != 0; const uint8_t* pos = body_start; const uint8_t* end = body_start + body_len; while (pos + 2 <= end) { uint8_t eid = pos[0]; uint8_t len = pos[1]; if (pos + 2 + len > end) break; // Avoid buffer overflow if (eid == 48) { // Tag 48: RSN (WPA2/WPA3) wpa2 = true; // Simple check for WPA3: look for AKM suite for SAE (00-0F-AC:8) if (len > 8 && pos[8] == 0x00 && pos[9] == 0x0F && pos[10] == 0xAC && pos[11] == 0x08) { wpa3 = true; } } else if (eid == 221) { // Tag 221: Vendor Specific (WPA1) // Check for Microsoft OUI for WPA1 (00-50-F2:1) if (len > 4 && pos[2] == 0x00 && pos[3] == 0x50 && pos[4] == 0xF2 && pos[5] == 0x01) { wpa = true; } } pos += 2 + len; } if (wpa3) return WifiEncryptionTypeWpa3; if (wpa2) return WifiEncryptionTypeWpa2; if (wpa) return WifiEncryptionTypeWpa; if (privacy_bit) return WifiEncryptionTypeWep; return WifiEncryptionTypeOpen; }