From charlesreid1

Requirements

  • Create analysis results that are quantitative (score 0-100) but also human-readable
  • A Levenshtein distance algorithm to detect typosquatting in SSIDs (but not run frequently, as it is computationally intensive)
  • An embedded OUI database to identify the manufacturer of an AP's network card

analyzer.h

#ifndef ANALYZER_H
#define ANALYZER_H

#include "scanner.h"
#include "baseline.h"

// Threshold for SSID similarity. A distance of 1 or 2 is highly suspicious.
#define LEVENSHTEIN_EVIL_TWIN_THRESHOLD 2

// How far outside the baseline RSSI an AP can be before it's considered anomalous.
#define RSSI_ANOMALY_THRESHOLD_DBM 10

/**
 * @brief Enumeration of potential threat types identified by the analyzer.
 */
typedef enum {
    THREAT_NONE,                // AP is known and operating within normal parameters.
    THREAT_EVIL_TWIN,           // An unknown AP with a very similar SSID to a known one.
    THREAT_SIGNAL_ANOMALY,      // A known AP is suddenly much stronger or weaker.
    THREAT_UNEXPECTED_CONFIG,   // A known AP has changed its SSID or encryption.
    THREAT_UNKNOWN_DEVICE,      // An unknown AP that doesn't match any evil twin patterns.
} ThreatType;

/**
 * @brief Structure to hold the complete result of an AP analysis.
 */
typedef struct {
    ap_info_t scanned_ap;       // A copy of the AP that was analyzed.
    int suspicion_score;        // Final score from 0 (safe) to 100 (highly suspicious).
    ThreatType threat_type;     // The classification of the threat.
    const char* reason;         // A human-readable explanation for the score.
    const char* manufacturer;   // The manufacturer of the AP, based on its OUI.
} AnalysisResult;

/**
 * @brief The core analysis function.
 * It analyzes a single scanned AP against the entire baseline of legitimate APs.
 * * @param manager Pointer to the BaselineManager containing known APs.
 * @param scanned_ap Pointer to the AP information from a recent scan.
 * @return An AnalysisResult struct containing the verdict.
 */
AnalysisResult analyze_ap(BaselineManager* manager, const ap_info_t* scanned_ap);

/**
 * @brief A helper function to get a string representation of a threat type.
 * @param threat The ThreatType enum.
 * @return A constant string describing the threat.
 */
const char* threat_type_to_string(ThreatType threat);

#endif // ANALYZER_H

analyzer.c

#include "analyzer.h"
#include <string.h>
#include <stdlib.h> // For MIN/MAX macros

#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif

// --- OUI (Organizationally Unique Identifier) Database ---
// This small, embedded database helps identify the manufacturer of an AP.
typedef struct {
    uint8_t oui[3];
    const char* manufacturer;
} OuiEntry;

// A selection of common WiFi hardware vendors. This can be expanded.
static const OuiEntry OUI_DATABASE[] = {
    {{0x00, 0x0F, 0xAC}, "TP-Link"},
    {{0x00, 0x1A, 0x70}, "Cisco"},
    {{0x00, 0x0B, 0x86}, "Aruba Networks"},
    {{0x00, 0x90, 0x4C}, "NETGEAR"},
    {{0x08, 0x00, 0x28}, "Dell"},
    {{0x18, 0x64, 0x72}, "ASUS"},
    {{0x20, 0xCF, 0x30}, "Belkin"},
    {{0x44, 0xD9, 0xE7}, "Ubiquiti Networks"},
    {{0x68, 0x72, 0x51}, "Linksys"},
    {{0x88, 0xDC, 0x96}, "D-Link"},
    {{0xB0, 0x39, 0x56}, "Google"},
    {{0xE0, 0x4F, 0x43}, "Hewlett Packard"},
    {{0xF8, 0xE4, 0xFB}, "Amazon"},
};
static const size_t OUI_DATABASE_SIZE = sizeof(OUI_DATABASE) / sizeof(OuiEntry);

// --- Forward Declarations for internal functions ---
static int levenshtein_distance(const char* s1, const char* s2);
static const char* find_manufacturer_by_oui(const uint8_t bssid[6]);

// --- Public Function Implementations ---

const char* threat_type_to_string(ThreatType threat) {
    switch(threat) {
        case THREAT_NONE: return "Known AP";
        case THREAT_EVIL_TWIN: return "Evil Twin";
        case THREAT_SIGNAL_ANOMALY: return "Signal Anomaly";
        case THREAT_UNEXPECTED_CONFIG: return "Config Change";
        case THREAT_UNKNOWN_DEVICE: return "Unknown Device";
        default: return "N/A";
    }
}

AnalysisResult analyze_ap(BaselineManager* manager, const ap_info_t* scanned_ap) {
    AnalysisResult result = {0};
    memcpy(&result.scanned_ap, scanned_ap, sizeof(ap_info_t));
    result.threat_type = THREAT_NONE;
    result.suspicion_score = 0;
    result.reason = "Known AP, operating normally.";
    result.manufacturer = find_manufacturer_by_oui(scanned_ap->bssid);

    if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) {
        result.reason = "Error: Could not access baseline.";
        return result;
    }

    baseline_ap_t* matched_ap = NULL;
    for(uint16_t i = 0; i < manager->count; i++) {
        if(memcmp(manager->baseline_aps[i].bssid, scanned_ap->bssid, 6) == 0) {
            matched_ap = &manager->baseline_aps[i];
            break;
        }
    }

    // CASE 1: The AP's BSSID is in our baseline (it's a known device)
    if(matched_ap) {
        // Check for signal strength anomaly
        if(scanned_ap->rssi < (matched_ap->rssi_min - RSSI_ANOMALY_THRESHOLD_DBM) ||
           scanned_ap->rssi > (matched_ap->rssi_max + RSSI_ANOMALY_THRESHOLD_DBM)) {
            result.threat_type = THREAT_SIGNAL_ANOMALY;
            result.suspicion_score = 40;
            result.reason = "Known AP has unusual signal strength.";
        }
        // Check for unexpected changes to SSID or encryption
        else if(strcmp(matched_ap->ssid, scanned_ap->ssid) != 0 || 
                matched_ap->encryption != scanned_ap->encryption) {
            result.threat_type = THREAT_UNEXPECTED_CONFIG;
            result.suspicion_score = 75;
            result.reason = "Known AP has changed its configuration.";
        }
    }
    // CASE 2: The AP's BSSID is NOT in our baseline (it's an unknown device)
    else {
        result.threat_type = THREAT_UNKNOWN_DEVICE;
        result.suspicion_score = 25; // Base score for any unknown device
        result.reason = "Unknown AP detected in the area.";

        // Check for SSID similarity (potential Evil Twin)
        for(uint16_t i = 0; i < manager->count; i++) {
            int distance = levenshtein_distance(scanned_ap->ssid, manager->baseline_aps[i].ssid);
            if(distance > 0 && distance <= LEVENSHTEIN_EVIL_TWIN_THRESHOLD) {
                result.threat_type = THREAT_EVIL_TWIN;
                result.suspicion_score = 95;
                result.reason = "SSID is deceptively similar to a known AP.";
                break; // Found a likely evil twin, no need to check further
            }
        }
    }

    furi_mutex_release(manager->mutex);
    return result;
}

// --- Internal (Static) Function Implementations ---

/**
 * @brief Finds the manufacturer name for a given BSSID.
 * @return Manufacturer name or "Unknown".
 */
static const char* find_manufacturer_by_oui(const uint8_t bssid[6]) {
    for(size_t i = 0; i < OUI_DATABASE_SIZE; i++) {
        if(memcmp(OUI_DATABASE[i].oui, bssid, 3) == 0) {
            return OUI_DATABASE[i].manufacturer;
        }
    }
    return "Unknown";
}

/**
 * @brief Calculates the Levenshtein distance between two strings.
 * This is a measure of how different two strings are.
 * @return The number of edits (insertions, deletions, substitutions) to change s1 to s2.
 */
static int levenshtein_distance(const char* s1, const char* s2) {
    int s1_len = strlen(s1);
    int s2_len = strlen(s2);
    // Use heap for the matrix to avoid stack overflow with long SSIDs
    int* d = malloc((s2_len + 1) * sizeof(int));
    if(!d) return 1000; // Return a large number on allocation failure

    for(int i = 0; i <= s2_len; i++) {
        d[i] = i;
    }

    for(int i = 0; i < s1_len; i++) {
        int old = d[0];
        d[0] = i + 1;
        for(int j = 0; j < s2_len; j++) {
            int temp = d[j+1];
            int cost = (s1[i] == s2[j]) ? 0 : 1;
            d[j+1] = MIN(MIN(d[j] + 1, d[j+1] + 1), old + cost);
            old = temp;
        }
    }
    
    int result = d[s2_len];
    free(d);
    return result;
}