Wi-Fi in številne druge okrajšave. Kako pridobiti podatke o vozliščih Wi-Fi v aplikaciji za Android, ne da bi se zatekel

Nekega dne sem moral pregledati omrežja Wi-Fi iz aplikacij za Android in pridobiti podrobne podatke o dostopnih točkah.

Tu smo se morali soočiti s številnimi težavami: off.Dokumentacija za Android veliko opisanih razredov je postalo zastarelih (raven API > 26), kar se v tem ni odrazilo; opis nekaterih stvari v dokumentaciji je minimalen (na primer polje zmogljivosti razreda ScanResult v času pisanja ni skoraj nič opisanega, čeprav vsebuje veliko pomembnih podatkov). Tretja težava je morda v dejstvu, da se morate, ko se prvič približate Wi-Fi-ju, poleg branja teorije in nastavitve usmerjevalnika prek lokalnega gostitelja, ukvarjati s številnimi okrajšavami, ki se zdijo razumljive posamezno. Vendar morda ni očitno, kako jih povezati in strukturirati (presoja je subjektivna in odvisna od prejšnjih izkušenj).

Ta članek razpravlja o tem, kako pridobiti izčrpne podatke o okolju Wi-Fi iz kode Android brez NDK, vdorov, ampak samo z uporabo Android API-ja, in razumeti, kako to interpretirati.

Ne odlašajmo in začnimo pisati kodo.

1. Ustvarite projekt

Ta opomba je namenjena tistim, ki so projekt Android ustvarili več kot enkrat, zato izpuščamo podrobnosti tega elementa. Spodnja koda bo predstavljena v Kotlinu, minSdkVersion=23.

2. Dovoljenja za dostop

Za delo z Wi-Fi iz aplikacije boste morali od uporabnika pridobiti več dovoljenj. V skladu z dokumentacijo, če želite skenirati omrežje na napravah z različico OS po 8.0, potrebujete poleg dostopa do ogleda stanja omrežnega okolja bodisi dostop za spreminjanje stanja Wi-Fi modula naprave bodisi dostop do koordinat (približno ali natančno). Od različice 9.0 naprej morate uporabnika pozvati k obojemu in od uporabnika tudi izrecno zahtevati, da vklopi lokacijske storitve. Ne pozabite galantno razložiti uporabniku, da je to Googlova muha in ne naša želja, da bi vohunili za njim :)

Torej bomo v AndroidManifest.xml dodali:

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

In v kodi, ki vsebuje povezavo do trenutne dejavnosti:

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. Ustvarite BroadcastReceiver in se naročite na dogodke posodabljanja podatkov o skeniranju omrežnega okolja 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 dokumentaciji je označena kot opuščena od različice 28 API-ja, vendar izklopljena. vodi predlaga uporabo.

Skupaj smo prejeli seznam predmetov ScanResult.

4. Poglejte ScanResult in razumejte pogoje

Oglejmo si nekaj polj tega razreda in opišite, kaj pomenijo:

SSID — Identifikator nabora storitev je ime omrežja

BSSID – Basic Service Set Identifier – MAC naslov omrežne kartice (točka Wi-Fi)

raven — Indikator moči prejetega signala [dBm (rusko dBm) — decibel, referenčna moč 1 mW.] — Indikator moči prejetega signala. Zavzame vrednost od 0 do -100, dlje kot je od 0, večja moč signala je bila izgubljena na poti od točke Wi-Fi do vaše naprave. Več podrobnosti najdete na primer na Wikipedia. Tukaj vam bom povedal, da uporabljam razred Android WifiManager raven signala lahko umerite na lestvici od odlične do grozne v koraku, ki ga izberete:

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

frekvenca — frekvenca delovanja točke Wi-Fi [Hz]. Poleg same frekvence vas bo morda zanimal tako imenovani kanal. Vsaka točka ima svojo čistost delovanja. V času pisanja je najbolj priljubljen obseg točk Wi-Fi 2.4 GHz. Toda, če smo natančnejši, točka oddaja informacije v vaš telefon na oštevilčeni frekvenci, ki je blizu imenovani. Število kanalov in pripadajočih frekvenc standardizirana. To se naredi tako, da bližnje točke delujejo na različnih frekvencah, s čimer se ne motijo ​​druga druge in medsebojno ne zmanjšujejo hitrosti in kakovosti prenosa. V tem primeru točke ne delujejo na eni frekvenci, temveč v razponu frekvenc (parameter ChannelWidth), ki se imenuje širina kanala. To pomeni, da točke, ki delujejo na sosednjih (in ne samo sosednjih, ampak celo 3 od sebe) kanalih, motijo ​​druga drugo. Morda se vam zdi uporabna ta preprosta koda, ki vam omogoča izračun številke kanala iz vrednosti frekvence za točke s frekvenco 2.4 in 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
            }
        }

Zmogljivosti - najbolj zanimivo področje za analizo, delo s katerim je zahtevalo veliko časa. Tukaj so v vrstici zapisane »zmogljivosti« točke. V tem primeru vam ni treba iskati podrobnosti interpretacije nizov v dokumentaciji. Tukaj je nekaj primerov, kaj bi lahko bilo v tej vrstici:

[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. Razumevanje okrajšav in zmožnosti razčlenjevanja

Omeniti velja, da razrede paketa android.net.wifi.* pod pokrovom uporablja pripomoček za Linux wpa_supplicant izhodni rezultat v polju zmogljivosti pa je kopija polja zastavic pri skeniranju.

Delovali bomo dosledno. Najprej razmislimo o izhodu oblike, v kateri so elementi znotraj oklepajev ločeni z znakom »-«:

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

Prvi pomen opisuje t.i. metoda avtentikacije. To pomeni, kakšno zaporedje dejanj morata izvesti naprava in dostopna točka, da dostopna točka dovoli uporabo, in kako šifrirati koristni tovor. V času pisanja tega prispevka sta najpogostejši možnosti WPA in WPA2, pri katerih bodisi vsaka povezana naprava neposredno bodisi preko t.i. Strežnik RADIUS (WPA-Enterprice) zagotavlja geslo prek šifriranega kanala. Najverjetneje dostopna točka v vašem domu zagotavlja povezavo po tej shemi. Razlika med drugo in prvo različico je v tem, da ima močnejšo šifro: AES v primerjavi z nevarnim TKIP. Postopoma se uvaja tudi WPA3, ki je bolj zapleten in napreden. Teoretično lahko obstaja opcija s podjetniško rešitvijo CCKM (Cisco Centralized Key Management), vendar je še nisem srečal.

Dostopna točka je morda konfigurirana za preverjanje pristnosti z naslovom MAC. Ali pa, če dostopna točka zagotavlja podatke z uporabo zastarelega algoritma WEP, potem dejansko ni avtentikacije (skrivni ključ je tukaj šifrirni ključ). Takšne možnosti uvrščamo med DRUGE.
Obstaja tudi metoda, ki je priljubljena v javnih wi-fi s skritim zaznavanjem prestreznega portala - zahteva za preverjanje pristnosti prek brskalnika. Takšne dostopne točke se skenerju zdijo odprte (kar z vidika fizične povezave tudi so). Zato jih uvrščamo med ODPRTE.

Drugo vrednost lahko označimo kot algoritem upravljanja ključev. To je parameter zgoraj opisane metode avtentikacije. Govori o tem, kako natančno se izmenjujejo šifrirni ključi. Razmislimo o možnih možnostih. EAP - uporablja se v omenjeni WPA-Enterprice, uporablja bazo podatkov za preverjanje vnesenih avtentikacijskih podatkov. SAE - uporablja se v naprednem WPA3, bolj odporen na grobo silo. PSK - najpogostejša možnost, vključuje vnos gesla in prenos v šifrirani obliki. IEEE8021X - po mednarodnem standardu (različen od tistega, ki ga podpira družina WPA). OWE (Opportunistic Wireless Encryption) je razširitev standarda IEEE 802.11 za točke, ki smo jih uvrstili med OPEN. OWE zagotavlja varnost podatkov, ki se prenašajo po nezavarovanem omrežju, tako da jih šifrira. Možna je tudi možnost, ko ni ključev za dostop, recimo tej možnosti NI.

Tretji parameter je t.i. sheme šifriranja — kako natančno se šifra uporablja za zaščito prenesenih podatkov. Naštejmo možnosti. WEP - uporablja pretočno šifro RC4, skrivni ključ je šifrirni ključ, kar v svetu sodobne kriptografije velja za nesprejemljivo. TKIP - uporablja se v WPA, CKIP - v WPA2. TKIP+CKIP - lahko se določi v točkah, ki podpirajo WPA in WPA2 za združljivost nazaj.

Namesto treh elementov lahko najdete osamljeno oznako WEP:

[WEP]

Kot smo razpravljali zgoraj, je to dovolj, da ne določite algoritma za uporabo ključev, ki ne obstaja, in metode šifriranja, ki je privzeto enaka.

Zdaj razmislite o tem oklepaju:

[ESS]

To Način delovanja Wi-Fi ali Topologija omrežja Wi-Fi. Morda boste naleteli na način BSS (Basic Service Set) - ko obstaja ena dostopna točka, prek katere povezane naprave komunicirajo. Najdete ga v lokalnih omrežjih. Dostopne točke so praviloma potrebne za povezovanje naprav iz različnih lokalnih omrežij, zato so del razširjenih servisnih naborov – ESS. Vrsta IBSSs (Independent Basic Service Sets) označuje, da je naprava del omrežja enakovrednih.

Morda boste videli tudi zastavico WPS:

[WPS]

WPS (Wi-Fi Protected Setup) je protokol za polavtomatsko inicializacijo omrežja Wi-Fi. Za inicializacijo uporabnik vnese 8-mestno geslo ali pritisne gumb na usmerjevalniku. Če je vaša dostopna točka prve vrste in se to potrditveno polje prikaže poleg imena vaše dostopne točke, vam toplo priporočamo, da greste na skrbniško ploščo in onemogočite dostop WPS. Dejstvo je, da je pogosto 8-mestni PIN mogoče ugotoviti po MAC naslovu ali pa ga urediti v doglednem času, kar lahko nekdo nepošteno izkoristi.

6. Ustvarite model in funkcijo za razčlenjevanje

Na podlagi tega, kar smo ugotovili zgoraj, bomo opisali, kaj se je zgodilo z uporabo podatkovnih razredov:

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

Zdaj pa napišimo funkcijo, ki bo razčlenila polje zmogljivosti:


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. Oglejte si rezultat

Pregledal bom omrežje in vam pokazal, kaj sem našel. Prikazani so rezultati preprostega izpisa prek Log.d:

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

Vprašanje povezovanja v omrežje iz kode aplikacije je ostalo nepreverjeno. Rekel bom le, da za branje shranjenih gesel iz operacijskega sistema mobilne naprave potrebujete korenske pravice in pripravljenost brskati po datotečnem sistemu za branje wpa_supplicant.conf. Če logika aplikacije zahteva vnos gesla od zunaj, se lahko povezava vzpostavi prek razreda android.net.wifi.WifiManager.

Hvala Egor Ponomarev za dragocene dodatke.

Če menite, da je treba kaj dodati ali popraviti, napišite v komentar :)

Vir: www.habr.com

Dodaj komentar