Wi-Fi et bien d’autres abréviations. Comment obtenir des données sur les nœuds Wi-Fi dans une application Android sans enfler

Un jour, j'avais besoin d'analyser les réseaux Wi-Fi à partir d'applications Android et d'obtenir des données détaillées sur les points d'accès.

Ici, nous avons dû faire face à plusieurs difficultés : documentation off.Android de nombreuses classes décrites sont devenues obsolètes (niveau API > 26), ce qui n'y est pas reflété ; la description de certaines choses dans la documentation est minime (par exemple, le champ capacités de la classe Résultat de scanner au moment de la rédaction de cet article, presque rien n'est décrit, bien qu'il contienne beaucoup de données importantes). La troisième difficulté réside peut-être dans le fait que lorsqu'on s'approche pour la première fois du Wi-Fi, outre la lecture de la théorie et la configuration du routeur via localhost, il faut composer avec un certain nombre d'abréviations qui semblent compréhensibles individuellement. Mais il n’est peut-être pas évident de savoir comment les relier et les structurer (le jugement est subjectif et dépend de l’expérience antérieure).

Cet article explique comment obtenir des données complètes sur l'environnement Wi-Fi à partir du code Android sans NDK, ni hacks, mais uniquement en utilisant l'API Android et comprend comment l'interpréter.

Ne tardons pas et commençons à écrire du code.

1. Créez un projet

Cette note est destinée à ceux qui ont créé un projet Android plus d'une fois, nous omettons donc les détails de cet élément. Le code ci-dessous sera présenté en Kotlin, minSdkVersion=23.

2. Autorisations d'accès

Pour travailler avec le Wi-Fi depuis l'application, vous devrez obtenir plusieurs autorisations de la part de l'utilisateur. Conformément à Documentation, afin d'analyser le réseau sur les appareils dotés de versions d'OS postérieures à 8.0, en plus de l'accès à la visualisation de l'état de l'environnement réseau, vous avez besoin soit d'un accès pour modifier l'état du module Wi-Fi de l'appareil, soit d'un accès aux coordonnées (approximatives ou exacte). À partir de la version 9.0, vous devez demander à l'utilisateur les deux et lui demander explicitement d'activer les services de localisation. N'oubliez pas d'expliquer galamment à l'utilisateur qu'il s'agit d'un caprice de Google, et non de notre envie de l'espionner :)

Ainsi, dans AndroidManifest.xml nous ajouterons :

    <uses-permission android_name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android_name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android_name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android_name="android.permission.ACCESS_FINE_LOCATION"/>

Et dans le code qui contient un lien vers l'activité en cours :

import android.app.Activity
import android.content.Context
import android.location.LocationManager
import androidx.core.app.ActivityCompat

....

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CHANGE_WIFI_STATE),
                1
            )
            makeEnableLocationServices(activity.applicationContext)
        } else {
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(Manifest.permission.CHANGE_WIFI_STATE),
                1
            )
        }

    /* включает экран включения службы по определению местоположения */
    fun makeEnableLocationServices(context: Context) {
        // TODO: перед вызовом этой функции надо рассказать пользователю, зачем Вам доступ к местоположению
        val lm: LocationManager =
            context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager

        val gpsEnabled: Boolean = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
        val networkEnabled: Boolean = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

        if (!gpsEnabled && !networkEnabled) {
            context.startActivity(Intent(ACTION_LOCATION_SOURCE_SETTINGS));
        }
    }

3. Créez un BroadcastReceiver et abonnez-vous aux événements de mise à jour des données sur l'analyse de l'environnement du réseau Wi-Fi.

val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager

val wifiScanReceiver = object : BroadcastReceiver() {

  override fun onReceive(context: Context, intent: Intent) {
    val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
    if (success) {
      scanSuccess()
    } 
  }
}

val intentFilter = IntentFilter()
/* подписываемся на сообщения о получении новых результатов сканирования */
intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(wifiScanReceiver, intentFilter)

val success = wifiManager.startScan()
if (!success) {
  /* что-то не получилось при запуске сканирования, проверьте выданые разрешения */
}

....

private fun scanSuccess() {
 /* вот они, результаты сканирования */
  val results: List<ScanResult> = wifiManager.scanResults
}

La méthode WiFiManager.startScan dans la documentation est marquée comme obsolète depuis la version 28 de l'API, mais désactivée. guide suggère de l'utiliser.

Au total, nous avons reçu une liste d'objets Résultat de scanner.

4. Regardez ScanResult et comprenez les termes

Examinons quelques champs de cette classe et décrivons ce qu'ils signifient :

SSID — Service Set Identifier est le nom du réseau

BSSID – Basic Service Set Identifier – Adresse MAC de l'adaptateur réseau (point Wi-Fi)

niveau — Indicateur de la force du signal reçu [dBm (dBm russe) — Décibel, puissance de référence 1 mW.] — Un indicateur de la force du signal reçu. Prend une valeur de 0 à -100, plus on s'éloigne de 0, plus la puissance du signal est perdue entre le point Wi-Fi et votre appareil. Plus de détails peuvent être trouvés, par exemple, sur Wikipedia. Ici, je vais vous dire qu'en utilisant la classe Android Gestionnaire Wifi vous pouvez calibrer le niveau du signal sur une échelle allant d'excellent à terrible selon l'étape que vous choisissez :

        val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
        val numberOfLevels = 5
        val level = WifiManager.calculateSignalLevel(level, numberOfLevels)

fréquence — fréquence de fonctionnement du point Wi-Fi [Hz]. En plus de la fréquence elle-même, vous pourriez être intéressé par ce qu'on appelle la chaîne. Chaque point a sa propre pureté de fonctionnement. Au moment de la rédaction de cet article, la gamme de points Wi-Fi la plus populaire est de 2.4 GHz. Mais, pour être plus précis, le point transmet des informations à votre téléphone sur une fréquence numérotée proche de celle nommée. Nombre de canaux et fréquences correspondantes standardisé. Ceci est fait pour que les points proches fonctionnent à des fréquences différentes, n'interférant ainsi pas les uns avec les autres et ne réduisant pas mutuellement la vitesse et la qualité de la transmission. Dans ce cas, les points fonctionnent non pas à une fréquence, mais sur une plage de fréquences (paramètre Largeur de canal), appelée largeur du canal. C'est-à-dire que les points fonctionnant sur des canaux adjacents (et pas seulement adjacents, mais même 3 d'eux-mêmes) interfèrent les uns avec les autres. Vous trouverez peut-être utile ce code simple, qui vous permet de calculer le numéro de canal à partir de la valeur de fréquence pour les points avec une fréquence de 2.4 et 5 Ghz :


    /* по частоте определяем номер канала */
    val channel: Int
        get() {
            return if (frequency in 2412..2484) {
                (frequency - 2412) / 5 + 1
            } else if (frequency in 5170..5825) {
                (frequency - 5170) / 5 + 34
            } else {
                -1
            }
        }

capacités - le domaine d'analyse le plus intéressant, dont le travail a demandé beaucoup de temps. Ici, les « capacités » du point sont écrites dans la ligne. Dans ce cas, vous n’avez pas besoin de rechercher des détails sur l’interprétation des chaînes dans la documentation. Voici quelques exemples de ce que pourrait contenir cette ligne :

[WPA-PSK-TKIP+CCMP][WPA2-PSK-TKIP+CCMP][WPS][ESS]
[WPA2-PSK-CCMP][ESS]
[WPA2-PSK-CCMP+TKIP][ESS]
[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS]
[ESS][WPS]

5. Comprendre les abréviations et les capacités d'analyse

Il est à noter que les classes du package android.net.wifi.* sont utilisées sous le capot par un utilitaire Linux wpa_supplicant et le résultat de sortie dans le champ capacités est une copie du champ drapeaux lors de l'analyse.

Nous agirons de manière cohérente. Considérons d'abord le résultat d'un format dans lequel les éléments entre parenthèses sont séparés par un signe « - » :

[WPA-PSK-TKIP+CCMP]
[WPA2-PSK-CCMP]

Le premier sens décrit ce qu'on appelle. Méthode d'authentification. Autrement dit, quelle séquence d'actions l'appareil et le point d'accès doivent effectuer pour que le point d'accès puisse être utilisé et comment chiffrer la charge utile. Au moment de la rédaction de cet article, les options les plus courantes sont WPA et WPA2, dans lesquelles chaque appareil est connecté directement ou via ce qu'on appelle. Le serveur RADIUS (WPA-Enterprice) fournit le mot de passe via un canal crypté. Très probablement, le point d'accès de votre maison fournit une connexion selon ce schéma. La différence entre la deuxième version et la première est qu'elle a un chiffrement plus fort : AES contre TKIP non sécurisé. WPA3, plus complexe et avancé, est également progressivement introduit. Théoriquement, il peut y avoir une option avec la solution enterprice CCKM (Cisco Centralized Key Management), mais je ne l'ai jamais rencontrée.

Le point d'accès peut avoir été configuré pour s'authentifier par adresse MAC. Ou, si le point d'accès fournit des données à l'aide d'un algorithme WEP obsolète, il n'y a en réalité aucune authentification (la clé secrète ici est la clé de cryptage). Nous classons ces options dans la catégorie AUTRES.
Il existe également une méthode populaire dans le Wi-Fi public avec la détection de portail captif caché : une demande d'authentification via un navigateur. De tels points d'accès apparaissent au scanner comme ouverts (ce qu'ils sont du point de vue de la connexion physique). Par conséquent, nous les classons comme OUVERTS.

La deuxième valeur peut être notée algorithme de gestion des clés. Il s'agit d'un paramètre de la méthode d'authentification décrite ci-dessus. Explique exactement comment les clés de chiffrement sont échangées. Considérons les options possibles. EAP - utilisé dans WPA-Enterprice mentionné, utilise une base de données pour vérifier les données d'authentification saisies. SAE - utilisé dans le WPA3 avancé, plus résistant à la force brute. PSK - l'option la plus courante consiste à saisir un mot de passe et à le transmettre sous forme cryptée. IEEE8021X - selon une norme internationale (différente de celle prise en charge par la famille WPA). OWE (Opportunistic Wireless Encryption) est une extension de la norme IEEE 802.11 pour les points que nous avons classés comme OPEN. OWE assure la sécurité des données transmises sur un réseau non sécurisé en les chiffrant. Une option est également possible lorsqu'il n'y a pas de clés d'accès, appelons cette option NONE.

Le troisième paramètre est ce qu'on appelle. schémas de cryptage — comment exactement le chiffre est utilisé pour protéger les données transmises. Listons les options. WEP - utilise un chiffrement de flux RC4, la clé secrète est la clé de cryptage, considérée comme inacceptable dans le monde de la cryptographie moderne. TKIP - utilisé dans WPA, CKIP - dans WPA2. TKIP+CKIP - peut être spécifié en points compatibles WPA et WPA2 pour une compatibilité ascendante.

Au lieu de trois éléments, vous pouvez trouver une seule marque WEP :

[WEP]

Comme nous l'avons évoqué plus haut, cela suffit pour ne pas préciser l'algorithme d'utilisation des clés, qui n'existe pas, et la méthode de cryptage, qui est la même par défaut.

Considérons maintenant cette tranche :

[ESS]

Il Mode de fonctionnement Wi-Fi ou Topologie du réseau Wi-Fi. Vous pouvez rencontrer le mode BSS (Basic Service Set) - lorsqu'il existe un point d'accès par lequel les appareils connectés communiquent. A retrouver sur les réseaux locaux. En règle générale, les points d'accès sont nécessaires pour connecter des appareils de différents réseaux locaux. Ils font donc partie des ensembles de services étendus - ESS. Le type IBSS (Independent Basic Service Sets) indique que l'appareil fait partie d'un réseau Peer-to-Peer.

Vous pouvez également voir le drapeau WPS :

[WPS]

WPS (Wi-Fi Protected Setup) est un protocole d'initialisation semi-automatique d'un réseau Wi-Fi. Pour initialiser, l'utilisateur saisit un mot de passe à 8 caractères ou appuie sur un bouton du routeur. Si votre point d'accès est du premier type et que cette case à cocher apparaît à côté du nom de votre point d'accès, il est fortement recommandé d'aller dans le panneau d'administration et de désactiver l'accès WPS. Le fait est que souvent le code PIN à 8 chiffres peut être découvert par l'adresse MAC, ou il peut être trié dans un délai prévisible, dont quelqu'un de manière malhonnête peut profiter.

6. Créer un modèle et une fonction d'analyse

Sur la base de ce que nous avons découvert ci-dessus, nous décrirons ce qui s'est passé en utilisant les classes de données :

/* схема аутентификации */
enum class AuthMethod {
    WPA3,
    WPA2,
    WPA, // Wi-Fi Protected Access
    OTHER, // включает в себя Shared Key Authentication и др. использующие mac-address-based и WEP
    CCKM, // Cisco
    OPEN // Open Authentication. Может быть со скрытым Captive Portal Detection - запрос аутентификации через браузер
}

/* алгоритм ввода ключей */
enum class KeyManagementAlgorithm {
    IEEE8021X, // по стандарту
    EAP, // Extensible Authentication Protocol, расширяемый протокол аутентификации
    PSK, // Pre-Shared Key — каждый узел вводит пароль для доступа к сети
    WEP, // в WEP пароль является ключом шифрования (No auth key)
    SAE, // Simultaneous Authentication of Equals - может быть в WPA3
    OWE, // Opportunistic Wireless Encryption - в роутерах новых поколений, публичных сетях типа OPEN
    NONE // может быть без шифрования в OPEN, OTHER
}

/* метод шифрования */
enum class CipherMethod {
    WEP, // Wired Equivalent Privacy, Аналог шифрования трафика в проводных сетях
    TKIP, // Temporal Key Integrity Protocol
    CCMP, // Counter Mode with Cipher Block Chaining Message Authentication Code Protocol,
    // протокол блочного шифрования с кодом аутентичности сообщения и режимом сцепления блоков и счетчика
    // на основе AES
    NONE // может быть без шифрования в OPEN, OTHER
}

/* набор методов шифрования и протоколов, по которым может работать точка */
data class Capability(
    var authScheme: AuthMethod? = null,
    var keyManagementAlgorithm: KeyManagementAlgorithm? = null,
    var cipherMethod: CipherMethod? = null
)

/* Режим работы WiFi (или топология сетей WiFi) */
enum class TopologyMode {
    IBSS, // Эпизодическая сеть (Ad-Hoc или IBSS – Independent Basic Service Set).
    BSS, // Основная зона обслуживания Basic Service Set (BSS) или Infrastructure Mode.
    ESS // Расширенная зона обслуживания ESS – Extended Service Set.
}

Écrivons maintenant une fonction qui analysera le champ capacités :


private fun parseCapabilities(capabilitiesString: String): List < Capability > {
    val capabilities: List < Capability > = capabilitiesString
        .splitByBrackets()
        .filter {
            !it.isTopology() && !it.isWps()
        }
        .flatMap {
            parseCapability(it)
        }
    return
        if (!capabilities.isEmpty()) {
            capabilities
        } else {
            listOf(Capability(AuthMethod.OPEN, KeyManagementAlgorithm.NONE, CipherMethod.NONE))
        }
}

private fun parseCapability(part: String): List < Capability > {
    if (part.contains("WEP")) {
        return listOf(Capability(
            AuthMethod.OTHER,
            KeyManagementAlgorithm.WEP,
            CipherMethod.WEP
        ))
    }

    val authScheme = when {
        part.contains("WPA3") - > AuthMethod.WPA3
        part.contains("WPA2") - > AuthMethod.WPA2
        part.contains("WPA") - > AuthMethod.WPA
        else - > null
    }

    val keyManagementAlgorithm = when {
        part.contains("OWE") - > KeyManagementAlgorithm.OWE
        part.contains("SAE") - > KeyManagementAlgorithm.SAE
        part.contains("IEEE802.1X") - > KeyManagementAlgorithm.IEEE8021X
        part.contains("EAP") - > KeyManagementAlgorithm.EAP
        part.contains("PSK") - > KeyManagementAlgorithm.PSK
        else - > null
    }

    val capabilities = ArrayList < Capability > ()
    if (part.contains("TKIP") || part.contains("CCMP")) {
        if (part.contains("TKIP")) {
            capabilities.add(Capability(
                authScheme ? : AuthMethod.OPEN,
                keyManagementAlgorithm ? : KeyManagementAlgorithm.NONE,
                CipherMethod.TKIP
            ))
        }
        if (part.contains("CCMP")) {
            capabilities.add(Capability(
                authScheme ? : AuthMethod.OPEN,
                keyManagementAlgorithm ? : KeyManagementAlgorithm.NONE,
                CipherMethod.CCMP
            ))
        }
    } else if (authScheme != null || keyManagementAlgorithm != null) {
        capabilities.add(Capability(
            authScheme ? : AuthMethod.OPEN,
            keyManagementAlgorithm ? : KeyManagementAlgorithm.NONE,
            CipherMethod.NONE
        ))
    }

    return capabilities
}

private fun parseTopologyMode(capabilitiesString: String): TopologyMode ? {
    return capabilitiesString
        .splitByBrackets()
        .mapNotNull {
            when {
                it.contains("ESS") - > TopologyMode.ESS
                it.contains("BSS") - > TopologyMode.BSS
                it.contains("IBSS") - > TopologyMode.IBSS
                else - > null
            }
        }
        .firstOrNull()
}

private fun parseWPSAvailable(capabilitiesString: String): Boolean {
    return capabilitiesString
        .splitByBrackets()
        .any {
            it.isWps()
        }
}

private fun String.splitByBrackets(): List < String > {
    val m = Pattern.compile("[(.*?)]").matcher(this)
    val parts = ArrayList < String > ()
    while (m.find()) {
        parts.add(m.group().replace("[", "").replace("]", ""))
    }
    return parts
}

private fun String.isTopology(): Boolean {
    return TopologyMode.values().any {
        this == it.name
    }
}

private fun String.isWps(): Boolean {
    return this == "WPS"
}

8. Voir le résultat

Je vais scanner le réseau et vous montrer ce que j'ai trouvé. Voici les résultats d'une sortie simple via Log.d :

Capability of Home-Home [WPA2-PSK-CCMP][ESS][WPS]
...
capabilities=[Capability(authScheme=WPA2, keyManagementAlgorithm=PSK, cipherMethod=CCMP)], topologyMode=ESS, availableWps=true

La question de la connexion au réseau à partir du code de l'application n'a pas été examinée. Je dirai seulement que pour lire les mots de passe enregistrés à partir du système d'exploitation d'un appareil mobile, vous avez besoin des droits root et de la volonté de fouiller dans le système de fichiers pour lire wpa_supplicant.conf. Si la logique de l'application nécessite la saisie d'un mot de passe de l'extérieur, la connexion peut être établie via la classe android.net.wifi.WifiManager.

merci Egor Ponomarev pour des ajouts précieux.

Si vous pensez que quelque chose doit être ajouté ou corrigé, écrivez dans les commentaires :)

Source: habr.com

Ajouter un commentaire