Wi-Fi a mnoho dalších zkratek. Jak získat data o uzlech Wi-Fi v aplikaci pro Android, aniž by došlo k otoku

Jednoho dne jsem potřeboval skenovat Wi-Fi sítě z aplikací pro Android a získat podrobné údaje o přístupových bodech.

Zde jsme museli čelit několika potížím: off.Android dokumentace mnohé z popsaných tříd se staly zastaralými (úroveň API > 26), což se v tom neprojevilo; popis některých věcí v dokumentaci je minimální (například pole schopností třídy ScanResult v době psaní článku není popsáno téměř nic, ačkoliv obsahuje mnoho důležitých údajů). Třetí úskalí může spočívat v tom, že když se poprvé přiblížíte k Wi-Fi, musíte se kromě čtení teorie a nastavení routeru přes localhost vypořádat s řadou zkratek, které se jednotlivě zdají srozumitelné. Ale nemusí být zřejmé, jak je spojit a strukturovat (úsudek je subjektivní a závisí na předchozích zkušenostech).

Tento článek pojednává o tom, jak získat komplexní data o prostředí Wi-Fi z kódu Android bez NDK, hacků, ale pouze pomocí Android API a pochopit, jak je interpretovat.

Nezdržujme a začněme psát kód.

1. Vytvořte projekt

Tato poznámka je určena pro ty, kteří vytvořili projekt Android více než jednou, takže podrobnosti o této položce vynecháváme. Níže uvedený kód bude uveden v Kotlin, minSdkVersion=23.

2. Přístupová oprávnění

Pro práci s Wi-Fi z aplikace budete muset získat několik oprávnění od uživatele. V souladu s dokumentace, pro skenování sítě na zařízeních s verzemi OS po 8.0 potřebujete kromě přístupu k zobrazení stavu síťového prostředí buď přístup ke změně stavu Wi-Fi modulu zařízení, nebo přístup k souřadnicím (přibližné nebo přesné). Počínaje verzí 9.0 musíte uživatele požádat o obojí a také výslovně požádat uživatele, aby zapnul služby určování polohy. Nezapomeňte uživateli galantně vysvětlit, že jde o rozmar Googlu a ne o naši touhu ho špehovat :)

Takže do AndroidManifest.xml přidáme:

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

A v kódu, který obsahuje odkaz na aktuální aktivitu:

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. Vytvořte BroadcastReceiver a přihlaste se k odběru událostí aktualizace dat o skenování prostředí sítě 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
}

Metoda WiFiManager.startScan v dokumentaci je od verze API 28 označena jako zastaralá, ale vypnutá. průvodce doporučuje používat.

Celkem jsme dostali seznam objektů ScanResult.

4. Podívejte se na ScanResult a pochopte podmínky

Podívejme se na některá pole této třídy a popišme, co znamenají:

SSID — Service Set Identifier je název sítě

BSSID – Basic Service Set Identifier – MAC adresa síťového adaptéru (Wi-Fi bodu)

úroveň — Indikátor síly přijímaného signálu [dBm (Russian dBm) — Decibel, referenční výkon 1 mW.] — Indikátor síly přijímaného signálu. Nabývá hodnoty od 0 do -100, čím dále od 0, tím více energie signálu bylo ztraceno na cestě z bodu Wi-Fi do vašeho zařízení. Více podrobností najdete například na Wikipedia. Zde vám řeknu, že pomocí třídy Android WifiManager úroveň signálu můžete kalibrovat na stupnici od vynikající po hroznou ve zvoleném kroku:

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

frekvence — provozní frekvence bodu Wi-Fi [Hz]. Kromě samotné frekvence vás může zajímat tzv. channel. Každý bod má svou vlastní provozní čistotu. V době psaní tohoto článku je nejoblíbenější rozsah Wi-Fi bodů 2.4 GHz. Přesněji řečeno, bod přenáší informace do vašeho telefonu na očíslované frekvenci blízké jmenovanému. Počet kanálů a odpovídající frekvence standardizované. To se děje tak, aby blízké body fungovaly na různých frekvencích, čímž se vzájemně nerušily a vzájemně nesnižovaly rychlost a kvalitu přenosu. V tomto případě body nepracují na jedné frekvenci, ale v rozsahu frekvencí (parametr šířka kanálu), nazývá se šířka kanálu. To znamená, že body fungující na sousedních (a nejen sousedních, ale dokonce 3 od sebe) kanálech se navzájem ruší. Může se vám hodit tento jednoduchý kód, který vám umožní vypočítat číslo kanálu z hodnoty frekvence pro body s frekvencí 2.4 a 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
            }
        }

schopnosti - nejzajímavější pole pro analýzu, práce s nímž vyžadovala mnoho času. Zde jsou v řádku zapsány „schopnosti“ bodu. V tomto případě nemusíte hledat podrobnosti o interpretaci řetězců v dokumentaci. Zde je několik příkladů toho, co může být v tomto řádku:

[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. Pochopení zkratek a schopností analýzy

Za zmínku stojí, že třídy balíčku android.net.wifi.* používá pod kapotou linuxová utilita wpa_supplicant a výstupním výsledkem v poli schopností je kopie pole příznaků při skenování.

Budeme jednat důsledně. Podívejme se nejprve na výstup formátu, ve kterém jsou prvky uvnitř závorek odděleny znaménkem „-“:

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

První význam popisuje tzv. autentizační metoda. To znamená, jakou sekvenci akcí musí zařízení a přístupový bod provést, aby se přístupový bod mohl používat, a jak zašifrovat datovou část. V době psaní tohoto příspěvku jsou nejběžnější možnosti WPA a WPA2, ve kterých buď každé připojené zařízení přímo, nebo prostřednictvím tzv. Server RADIUS (WPA-Enterprice) poskytuje heslo přes šifrovaný kanál. S největší pravděpodobností přístupový bod ve vaší domácnosti poskytuje připojení podle tohoto schématu. Rozdíl mezi druhou verzí a první je v tom, že má silnější šifru: AES versus nezabezpečený TKIP. Postupně se také zavádí WPA3, který je složitější a pokročilejší. Teoreticky může existovat možnost s podnikovým řešením CCKM (Cisco Centralized Key Management), ale nikdy jsem se s tím nesetkal.

Přístupový bod mohl být nakonfigurován pro ověřování pomocí MAC adresy. Nebo pokud přístupový bod poskytuje data pomocí zastaralého algoritmu WEP, pak ve skutečnosti neexistuje žádná autentizace (tajným klíčem je zde šifrovací klíč). Takové možnosti klasifikujeme jako JINÉ.
Existuje také metoda, která je oblíbená ve veřejné wi-fi se skrytou detekcí Captive Portal Detection – žádostí o ověření přes prohlížeč. Takové přístupové body se skeneru jeví jako otevřené (což jsou z pohledu fyzického připojení). Proto je klasifikujeme jako OTEVŘENÉ.

Druhou hodnotu lze označit jako algoritmus správy klíčů. Toto je parametr výše popsané metody ověřování. Mluví o tom, jak přesně se vyměňují šifrovací klíče. Zvažme možné možnosti. EAP - používá se ve zmíněném WPA-Enterprice, využívá databázi k ověření zadaných autentizačních údajů. SAE - používá se v pokročilém WPA3, odolnější vůči hrubé síle. PSK - nejběžnější možnost, zahrnuje zadání hesla a jeho přenos v zašifrované podobě. IEEE8021X - podle mezinárodního standardu (odlišného od standardu podporovaného rodinou WPA). OWE (Opportunistic Wireless Encryption) je rozšířením standardu IEEE 802.11 pro body, které jsme klasifikovali jako OPEN. OWE zajišťuje bezpečnost dat přenášených přes nezabezpečenou síť jejich šifrováním. Možnost je také možná, když neexistují žádné přístupové klíče, nazvěme tuto možnost ŽÁDNÁ.

Třetím parametrem je tzv. šifrovací schémata — jak přesně je šifra použita k ochraně přenášených dat. Pojďme si vyjmenovat možnosti. WEP - používá proudovou šifru RC4, tajným klíčem je šifrovací klíč, který je ve světě moderní kryptografie považován za nepřijatelný. TKIP - používá se ve WPA, CKIP - ve WPA2. TKIP+CKIP - lze specifikovat v bodech schopných WPA a WPA2 pro zpětnou kompatibilitu.

Místo tří prvků můžete najít osamělou značku WEP:

[WEP]

Jak jsme diskutovali výše, to stačí k tomu, abychom nespecifikovali algoritmus pro použití klíčů, který neexistuje, a metodu šifrování, která je ve výchozím nastavení stejná.

Nyní zvažte tento držák:

[ESS]

To Provozní režim Wi-Fi nebo Topologie sítě Wi-Fi. Můžete se setkat s režimem BSS (Basic Service Set) – když existuje jeden přístupový bod, přes který komunikují připojená zařízení. Lze jej nalézt v místních sítích. Přístupové body jsou zpravidla potřeba pro připojení zařízení z různých lokálních sítí, proto jsou součástí Extended Service Sets - ESS. Typ IBSSs (Independent Basic Service Sets) označuje, že zařízení je součástí sítě Peer-to-Peer.

Můžete také vidět příznak WPS:

[WPS]

WPS (Wi-Fi Protected Setup) je protokol pro poloautomatickou inicializaci sítě Wi-Fi. Pro inicializaci uživatel buď zadá 8místné heslo, nebo stiskne tlačítko na routeru. Pokud je váš přístupový bod prvního typu a toto zaškrtávací políčko se zobrazí vedle názvu vašeho přístupového bodu, důrazně doporučujeme přejít na panel správce a zakázat přístup WPS. Faktem je, že často lze 8místný PIN zjistit podle MAC adresy nebo jej lze v dohledné době vyřešit, čehož může někdo nepoctivě využít.

6. Vytvořte model a funkci analýzy

Na základě toho, co jsme zjistili výše, popíšeme, co se stalo pomocí datových tříd:

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

Nyní napíšeme funkci, která bude analyzovat pole schopností:


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. Podívejte se na výsledek

Prohledám síť a ukážu vám, co jsem našel. Jsou zobrazeny výsledky jednoduchého výstupu přes Log.d:

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

Otázka připojení k síti z kódu aplikace zůstala neprozkoumaná. Řeknu pouze, že pro čtení uložených hesel z OS mobilního zařízení potřebujete práva root a ochotu prohrabovat se souborovým systémem, abyste mohli číst wpa_supplicant.conf. Pokud logika aplikace vyžaduje zadání hesla zvenčí, lze připojení provést prostřednictvím třídy android.net.wifi.WifiManager.

Díky Egor Ponomarev za cenné přírůstky.

Pokud si myslíte, že je potřeba něco doplnit nebo opravit, napište do komentářů :)

Zdroj: www.habr.com

Přidat komentář