Wi-Fi en vele andere afkortingen. Hoe u gegevens over Wi-Fi-knooppunten in een Android-applicatie kunt krijgen zonder op te zwellen

Op een dag moest ik Wi-Fi-netwerken scannen vanuit Android-applicaties en gedetailleerde gegevens over toegangspunten verkrijgen.

Hier werden we met verschillende moeilijkheden geconfronteerd: off.Android-documentatie veel van de beschreven klassen raakten verouderd (API-niveau> 26), wat er niet in tot uiting kwam; de beschrijving van sommige dingen in de documentatie is minimaal (bijvoorbeeld het mogelijkhedenveld van de klasse Scan resultaat op het moment van schrijven is er bijna niets beschreven, hoewel het veel belangrijke gegevens bevat). De derde moeilijkheid kan liggen in het feit dat wanneer je voor het eerst in de buurt van Wi-Fi komt, je, afgezien van het lezen van de theorie en het instellen van de router via localhost, te maken krijgt met een aantal afkortingen die individueel begrijpelijk lijken. Maar het is misschien niet duidelijk hoe ze met elkaar in verband moeten worden gebracht en hoe ze moeten worden gestructureerd (het oordeel is subjectief en hangt af van eerdere ervaringen).

Dit artikel bespreekt hoe u uitgebreide gegevens over de Wi-Fi-omgeving kunt verkrijgen uit Android-code zonder NDK, hacks, maar alleen met behulp van de Android API, en hoe u deze moet interpreteren.

Laten we niet uitstellen en beginnen met het schrijven van code.

1. Maak een project

Deze opmerking is bedoeld voor degenen die meer dan eens een Android-project hebben gemaakt, dus we laten de details van dit item achterwege. De onderstaande code wordt gepresenteerd in Kotlin, minSdkVersion=23.

2. Toegangsrechten

Om vanuit de applicatie met Wi-Fi te kunnen werken, moet u verschillende machtigingen van de gebruiker verkrijgen. In overeenstemming met documentatie, om het netwerk te scannen op apparaten met OS-versies na 8.0, heeft u naast toegang tot het bekijken van de status van de netwerkomgeving ook toegang nodig om de status van de Wi-Fi-module van het apparaat te wijzigen, of toegang tot coördinaten (geschatte of exact). Vanaf versie 9.0 moet u de gebruiker om beide vragen en de gebruiker ook expliciet vragen locatieservices in te schakelen. Vergeet niet om de gebruiker dapper uit te leggen dat dit de gril van Google is, en niet onze wens om hem te bespioneren :)

Dus in AndroidManifest.xml zullen we toevoegen:

    <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"/>

En in de code die een link naar de huidige activiteit bevat:

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. Maak een BroadcastReceiver en abonneer u op gegevensupdategebeurtenissen over het scannen van de Wi-Fi-netwerkomgeving

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
}

De WiFiManager.startScan-methode in de documentatie is sinds API-versie 28 gemarkeerd als verouderd, maar uitgeschakeld. gids stelt voor om het te gebruiken.

In totaal hebben we een lijst met objecten ontvangen Scan resultaat.

4. Kijk naar ScanResult en begrijp de voorwaarden

Laten we een aantal velden van deze klasse bekijken en beschrijven wat ze betekenen:

SSID — Service Set Identifier is de naam van het netwerk

BSSID – Basic Service Set Identifier – MAC-adres van de netwerkadapter (Wi-Fi-punt)

niveau — Indicator ontvangen signaalsterkte [dBm (Russische dBm) — Decibel, referentievermogen 1 mW.] — Een indicator van de ontvangen signaalsterkte. Neemt een waarde aan tussen 0 en -100. Hoe verder van 0, hoe meer signaalvermogen verloren gaat onderweg van het Wi-Fi-punt naar uw apparaat. Meer details vindt u bijvoorbeeld op Wikipedia. Hier zal ik je dat vertellen met behulp van de Android-klasse WifiManager u kunt het signaalniveau kalibreren op een schaal van uitstekend tot slecht in de stap die u kiest:

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

frequentie — werkfrequentie van het Wi-Fi-punt [Hz]. Naast de frequentie zelf ben je wellicht geïnteresseerd in het zogenaamde kanaal. Elk punt heeft zijn eigen operationele zuiverheid. Op het moment van schrijven is het populairste bereik van wifipunten 2.4 GHz. Maar om preciezer te zijn: het punt verzendt informatie naar uw telefoon op een genummerde frequentie die dicht bij de genoemde frequentie ligt. Aantal kanalen en bijbehorende frequenties gestandaardiseerd. Dit wordt gedaan zodat nabijgelegen punten op verschillende frequenties werken, waardoor ze elkaar niet hinderen en de snelheid en kwaliteit van de transmissie niet wederzijds worden verminderd. In dit geval werken de punten niet op één frequentie, maar over een reeks frequenties (parameter kanaalBreedte), de kanaalbreedte genoemd. Dat wil zeggen, punten die op aangrenzende (en niet alleen aangrenzende, maar zelfs op drie van elkaar gelegen) kanalen werken, interfereren met elkaar. Mogelijk vindt u deze eenvoudige code handig, waarmee u het kanaalnummer kunt berekenen op basis van de frequentiewaarde voor punten met een frequentie van 3 en 2.4 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
            }
        }

mogelijkheden - het meest interessante analysegebied, waarbij het werk veel tijd vergde. Hier worden de “mogelijkheden” van het punt in de lijn geschreven. In dit geval hoeft u niet in de documentatie naar details over de tekenreeksinterpretatie te zoeken. Hier zijn enkele voorbeelden van wat er in deze regel zou kunnen voorkomen:

[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. Afkortingen en parseermogelijkheden begrijpen

Het is vermeldenswaard dat de klassen van het pakket android.net.wifi.* onder de motorkap worden gebruikt door een Linux-hulpprogramma wpa_supplicant en het uitvoerresultaat in het mogelijkhedenveld is een kopie van het vlaggenveld tijdens het scannen.

Wij zullen consequent optreden. Laten we eerst eens kijken naar de uitvoer van een formaat waarin elementen tussen haakjes worden gescheiden door een “-“ teken:

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

De eerste betekenis beschrijft de zogenaamde. authenticatiemethode. Dat wil zeggen: welke reeks acties moeten het apparaat en het toegangspunt uitvoeren zodat het toegangspunt zichzelf kan gebruiken en hoe de payload moet worden gecodeerd. Op het moment dat dit bericht wordt geschreven, zijn de meest voorkomende opties WPA en WPA2, waarbij elk apparaat rechtstreeks of via de zogenaamde. De RADIUS-server (WPA-Enterprice) verstrekt het wachtwoord via een gecodeerd kanaal. Hoogstwaarschijnlijk zorgt het toegangspunt in uw huis voor een verbinding volgens dit schema. Het verschil tussen de tweede versie en de eerste is dat deze een sterker cijfer heeft: AES versus het onveilige TKIP. WPA3, dat complexer en geavanceerder is, wordt ook geleidelijk geïntroduceerd. Theoretisch zou er misschien een optie kunnen zijn met de zakelijke oplossing CCKM (Cisco Centralized Key Management), maar ik ben deze nog nooit tegengekomen.

Het toegangspunt is mogelijk geconfigureerd om te verifiëren via een MAC-adres. Of als het toegangspunt gegevens levert met behulp van het verouderde WEP-algoritme, dan is er feitelijk geen sprake van authenticatie (de geheime sleutel is hier de encryptiesleutel). Dergelijke opties classificeren we als ANDERE.
Er is ook een methode die populair is in openbare wifi met verborgen Captive Portal Detection: een authenticatieverzoek via een browser. Dergelijke toegangspunten lijken voor de scanner open (wat ze ook zijn vanuit het oogpunt van de fysieke verbinding). Daarom classificeren we ze als OPEN.

De tweede waarde kan worden aangeduid als algoritme voor sleutelbeheer. Dit is een parameter van de hierboven beschreven authenticatiemethode. Vertelt over hoe encryptiesleutels precies worden uitgewisseld. Laten we de mogelijke opties bekijken. EAP - gebruikt in de genoemde WPA-Enterprice, gebruikt een database om de ingevoerde authenticatiegegevens te verifiëren. SAE - gebruikt in geavanceerde WPA3, beter bestand tegen brute kracht. PSK - de meest gebruikelijke optie, waarbij een wachtwoord wordt ingevoerd en in gecodeerde vorm wordt verzonden. IEEE8021X - volgens een internationale standaard (anders dan die ondersteund door de WPA-familie). OWE (Opportunistic Wireless Encryption) is een uitbreiding van de IEEE 802.11-standaard voor punten die we als OPEN hebben geclassificeerd. OWE zorgt voor de veiligheid van gegevens die via een onbeveiligd netwerk worden verzonden door deze te coderen. Een optie is ook mogelijk als er geen toegangssleutels zijn, laten we deze optie GEEN noemen.

De derde parameter is de zogenaamde. encryptieschema's — hoe het cijfer precies wordt gebruikt om de verzonden gegevens te beschermen. Laten we de opties op een rij zetten. WEP - gebruikt een RC4-stroomcodering, de geheime sleutel is de coderingssleutel, die in de wereld van de moderne cryptografie als onaanvaardbaar wordt beschouwd. TKIP - gebruikt in WPA, CKIP - in WPA2. TKIP+CKIP - kan worden gespecificeerd in punten die geschikt zijn voor WPA en WPA2 voor achterwaartse compatibiliteit.

In plaats van drie elementen kun je een eenzaam WEP-teken vinden:

[WEP]

Zoals we hierboven hebben besproken, volstaat het om niet het algoritme te specificeren voor het gebruik van sleutels, dat niet bestaat, en de coderingsmethode, die standaard hetzelfde is.

Beschouw nu deze beugel:

[ESS]

Het Wi-Fi-bedrijfsmodus of Wi-Fi-netwerktopologie. U kunt de BSS-modus (Basic Service Set) tegenkomen - wanneer er één toegangspunt is waarmee aangesloten apparaten kunnen communiceren. Te vinden op lokale netwerken. In de regel zijn toegangspunten nodig om apparaten van verschillende lokale netwerken met elkaar te verbinden, daarom maken ze deel uit van Extended Service Sets - ESS. Het type IBSSs (Independent Basic Service Sets) geeft aan dat het apparaat deel uitmaakt van een peer-to-peer-netwerk.

Mogelijk ziet u ook de WPS-vlag:

[WPS]

WPS (Wi-Fi Protected Setup) is een protocol voor semi-automatische initialisatie van een Wi-Fi-netwerk. Om te initialiseren voert de gebruiker een wachtwoord van 8 tekens in of drukt op een knop op de router. Als uw toegangspunt van het eerste type is en dit selectievakje naast de naam van uw toegangspunt verschijnt, wordt u ten zeerste aanbevolen om naar het beheerderspaneel te gaan en WPS-toegang uit te schakelen. Feit is dat de 8-cijferige pincode vaak kan worden achterhaald aan de hand van het MAC-adres, of binnen afzienbare tijd kan worden opgelost, waar iemand op oneerlijke wijze misbruik van kan maken.

6. Maak een model en parseerfunctie

Op basis van wat we hierboven hebben ontdekt, zullen we beschrijven wat er is gebeurd met behulp van dataklassen:

/* схема аутентификации */
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.
}

Laten we nu een functie schrijven die het mogelijkhedenveld parseert:


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. Bekijk het resultaat

Ik scan het netwerk en laat je zien wat ik heb gevonden. Getoond worden de resultaten van eenvoudige uitvoer via Log.d:

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

De kwestie van verbinding maken met het netwerk vanuit de applicatiecode bleef ononderzocht. Ik wil alleen maar zeggen dat om opgeslagen wachtwoorden van het besturingssysteem van een mobiel apparaat te kunnen lezen, je rootrechten nodig hebt en de bereidheid om door het bestandssysteem te snuffelen om wpa_supplicant.conf te lezen. Als de applicatielogica het invoeren van een wachtwoord van buitenaf vereist, kan de verbinding via de klas tot stand worden gebracht android.net.wifi.WifiManager.

Dank Egor Ponomarev voor waardevolle aanvullingen.

Als je denkt dat er iets moet worden toegevoegd of gecorrigeerd, schrijf dan in de reacties :)

Bron: www.habr.com

Voeg een reactie