Wi-Fi un daudzi citi saīsinājumi. Kā iegūt datus par Wi-Fi mezgliem Android lietojumprogrammā bez pietūkuma

Kādu dienu man vajadzēja skenēt Wi-Fi tīklus no Android lietojumprogrammām un iegūt detalizētus datus par piekļuves punktiem.

Šeit mums bija jāsaskaras ar vairākām grūtībām: izslēgta.Android dokumentācija daudzas no aprakstītajām klasēm kļuva novecojušas (API līmenis > 26), kas tajā netika atspoguļots; dažu lietu apraksts dokumentācijā ir minimāls (piemēram, klases iespēju lauks ScanResult rakstīšanas laikā gandrīz nekas nav aprakstīts, lai gan tajā ir daudz svarīgu datu). Trešā grūtība var būt saistīta ar faktu, ka, pirmo reizi nokļūstot Wi-Fi tuvumā, izņemot teorijas lasīšanu un maršrutētāja iestatīšanu, izmantojot localhost, jums ir jātiek galā ar vairākiem saīsinājumiem, kas šķiet saprotami atsevišķi. Taču var nebūt skaidrs, kā tās saistīt un strukturēt (spriedums ir subjektīvs un atkarīgs no iepriekšējās pieredzes).

Šajā rakstā ir apskatīts, kā iegūt visaptverošus datus par Wi-Fi vidi no Android koda bez NDK, uzlaušanas, bet tikai izmantojot Android API un saprast, kā to interpretēt.

Nekavēsimies un sāksim rakstīt kodu.

1. Izveidojiet projektu

Šī piezīme ir paredzēta tiem, kuri Android projektu ir izveidojuši vairāk nekā vienu reizi, tāpēc mēs izlaižam sīkāku informāciju par šo vienumu. Tālāk norādītais kods tiks parādīts valodā Kotlin, minSdkVersion=23.

2. Piekļuves atļaujas

Lai strādātu ar Wi-Fi no lietojumprogrammas, jums būs jāsaņem vairākas lietotāja atļaujas. Saskaņā ar dokumentācija, lai skenētu tīklu ierīcēs ar OS versijām pēc 8.0, papildus piekļuvei tīkla vides stāvokļa apskatei ir nepieciešama vai nu piekļuve, lai mainītu ierīces Wi-Fi moduļa stāvokli, vai piekļuve koordinātām (aptuvena vai precīzi). Sākot ar versiju 9.0, jums ir jāpieprasa lietotājam abi, kā arī skaidri jāpieprasa, lai lietotājs ieslēgtu atrašanās vietas pakalpojumus. Neaizmirstiet galanti paskaidrot lietotājam, ka tā ir Google kaprīze, nevis mūsu vēlme viņu izspiegot :)

Tātad failā AndroidManifest.xml mēs pievienosim:

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

Un kodā, kurā ir saite uz pašreizējo darbību:

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. Izveidojiet BroadcastReceiver un abonējiet datu atjaunināšanas pasākumus par Wi-Fi tīkla vides skenēšanu.

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
}

Metode WiFiManager.startScan dokumentācijā ir atzīmēta kā novecojusi kopš API 28. versijas, bet izslēgta. vadīt iesaka to izmantot.

Kopumā saņēmām objektu sarakstu ScanResult.

4. Apskatiet ScanResult un izprotiet noteikumus

Apskatīsim dažus šīs klases laukus un aprakstīsim, ko tie nozīmē:

SSID — Service Set Identifier ir tīkla nosaukums

BSSID – Pamatpakalpojumu kopas identifikators – tīkla adaptera MAC adrese (Wi-Fi punkts)

līmenis — Saņemtā signāla stipruma indikators [dBm (Krievijas dBm) — Decibels, atsauces jauda 1 mW.] — Saņemtā signāla stipruma indikators. Tiek izmantota vērtība no 0 līdz -100, jo tālāk no 0, jo vairāk signāla jaudas tika zaudēts ceļā no Wi-Fi punkta uz ierīci. Sīkāku informāciju var atrast, piemēram, vietnē Wikipedia. Šeit es jums to pastāstīšu, izmantojot Android klasi Wifi pārvaldnieks Jūs varat kalibrēt signāla līmeni skalā no teicama līdz briesmīgam izvēlētajā solī:

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

biežums — Wi-Fi punkta darbības frekvence [Hz]. Papildus pašai frekvencei jūs varētu interesēt tā sauktais kanāls. Katram punktam ir sava darbības tīrība. Rakstīšanas laikā vispopulārākais Wi-Fi punktu diapazons ir 2.4 GHz. Bet, precīzāk sakot, punkts pārsūta informāciju uz jūsu tālruni ar numurētu frekvenci, kas ir tuvu nosauktajai. Kanālu skaits un atbilstošās frekvences standartizēts. Tas tiek darīts, lai tuvumā esošie punkti darbotos dažādās frekvencēs, tādējādi netraucējot viens otram un savstarpēji nesamazinot pārraides ātrumu un kvalitāti. Šajā gadījumā punkti darbojas nevis vienā frekvencē, bet gan vairākās frekvenču diapazonā (parametrs kanāla platums), ko sauc par kanāla platumu. Tas ir, punkti, kas darbojas blakus (un ne tikai blakus, bet pat 3 no sevis) kanāliem, traucē viens otru. Iespējams, jums noderēs šis vienkāršais kods, kas ļauj aprēķināt kanāla numuru no frekvences vērtības punktiem ar frekvenci 2.4 un 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
            }
        }

iespējas - visinteresantākā analīzes joma, ar kuru darbs prasīja daudz laika. Šeit rindā ir ierakstītas punkta “iespējas”. Šajā gadījumā jums nav jāmeklē informācija par virkņu interpretāciju dokumentācijā. Šeit ir daži piemēri tam, kas varētu būt šajā rindā:

[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. Izpratne par saīsinājumiem un parsēšanas iespējām

Ir vērts pieminēt, ka android.net.wifi.* pakotnes klases zem pārsega izmanto Linux utilīta. wpa_supplicant un izvades rezultāts iespēju laukā ir karodziņu lauka kopija skenēšanas laikā.

Mēs rīkosimies konsekventi. Vispirms apskatīsim tāda formāta izvadi, kurā elementi iekavās ir atdalīti ar “-” zīmi:

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

Pirmā nozīme raksturo tā saukto. autentifikācijas metode. Tas ir, kāda darbību secība ir jāveic ierīcei un piekļuves punktam, lai piekļuves punkts ļautu sevi izmantot un kā šifrēt lietderīgo slodzi. Rakstot šo ierakstu, visizplatītākās iespējas ir WPA un WPA2, kurās vai nu katra pieslēgtā ierīce tieši vai caur t.s. RADIUS serveris (WPA-Enterprice) nodrošina paroli, izmantojot šifrētu kanālu. Visticamāk, piekļuves punkts jūsu mājās nodrošina savienojumu saskaņā ar šo shēmu. Atšķirība starp otro versiju un pirmo ir tā, ka tai ir spēcīgāks šifrs: AES pret nedrošo TKIP. Pamazām tiek ieviests arī WPA3, kas ir sarežģītāks un uzlabots. Teorētiski var būt iespēja ar uzņēmuma risinājumu CCKM (Cisco Centralized Key Management), bet es ar to nekad neesmu saskāries.

Piekļuves punkts, iespējams, ir konfigurēts autentifikācijai pēc MAC adreses. Vai arī, ja piekļuves punkts nodrošina datus, izmantojot novecojušu WEP algoritmu, autentifikācijas faktiski nav (šeit slepenā atslēga ir šifrēšanas atslēga). Mēs klasificējam šādas iespējas kā CITI.
Ir arī metode, kas ir populāra publiskajā wi-fi ar slēpto Captive Portal Detection - autentifikācijas pieprasījums, izmantojot pārlūkprogrammu. Šādi piekļuves punkti skenerim šķiet atvērti (kas tie ir no fiziskā savienojuma viedokļa). Tāpēc mēs tos klasificējam kā OPEN.

Otro vērtību var apzīmēt kā atslēgu pārvaldības algoritms. Šis ir iepriekš aprakstītās autentifikācijas metodes parametrs. Runā par to, kā tieši notiek šifrēšanas atslēgu apmaiņa. Apsvērsim iespējamās iespējas. EAP - izmanto minētajā WPA-Enterprice, izmanto datu bāzi, lai pārbaudītu ievadītos autentifikācijas datus. SAE - izmantots uzlabotajā WPA3, izturīgāks pret brutālu spēku. PSK - visizplatītākā iespēja, kas ietver paroles ievadīšanu un pārsūtīšanu šifrētā veidā. IEEE8021X - saskaņā ar starptautisku standartu (atšķiras no tā, ko atbalsta WPA saime). OWE (Opportūnistiskā bezvadu šifrēšana) ir IEEE 802.11 standarta paplašinājums punktiem, kurus klasificējām kā OPEN. OWE nodrošina nedrošā tīklā pārsūtīto datu drošību, tos šifrējot. Iespējama arī iespēja, kad nav piekļuves taustiņu, sauksim šo opciju NONE.

Trešais parametrs ir tā sauktais. šifrēšanas shēmas — kā tieši šifrs tiek izmantots pārsūtīto datu aizsardzībai. Uzskaitīsim iespējas. WEP - izmanto RC4 straumes šifru, slepenā atslēga ir šifrēšanas atslēga, kas mūsdienu kriptogrāfijas pasaulē tiek uzskatīta par nepieņemamu. TKIP - izmanto WPA, CKIP - WPA2. TKIP+CKIP - var norādīt punktos, kas spēj WPA un WPA2, lai nodrošinātu atpakaļejošu saderību.

Trīs elementu vietā varat atrast vientuļu WEP zīmi:

[WEP]

Kā mēs apspriedām iepriekš, tas ir pietiekami, lai nenorādītu atslēgu izmantošanas algoritmu, kas neeksistē, un šifrēšanas metodi, kas pēc noklusējuma ir tāda pati.

Tagad apsveriet šo iekavu:

[ESS]

Wi-Fi darbības režīms vai Wi-Fi tīkla topoloģija. Jūs varat saskarties ar BSS (Basic Service Set) režīmu — ja ir viens piekļuves punkts, caur kuru savienotās ierīces sazinās. To var atrast vietējos tīklos. Parasti piekļuves punkti ir nepieciešami, lai savienotu ierīces no dažādiem vietējiem tīkliem, tāpēc tie ir daļa no paplašinātajām pakalpojumu kopām — ESS. IBSS (Independent Basic Service Sets) tips norāda, ka ierīce ir vienādranga tīkla daļa.

Varat arī redzēt WPS karogu:

[WPS]

WPS (Wi-Fi Protected Setup) ir protokols pusautomātiskai Wi-Fi tīkla inicializācijai. Lai inicializētu, lietotājs vai nu ievada 8 rakstzīmju paroli, vai nospiež maršrutētāja pogu. Ja jūsu piekļuves punkts ir pirmā tipa un šī izvēles rūtiņa tiek parādīta blakus piekļuves punkta nosaukumam, ļoti ieteicams doties uz administratora paneli un atspējot WPS piekļuvi. Lieta tāda, ka nereti 8 ciparu PIN var uzzināt pēc MAC adreses vai arī to var sakārtot pārskatāmā laikā, ko kāds negodprātīgi var izmantot.

6. Izveidojiet modeli un parsēšanas funkciju

Pamatojoties uz to, ko noskaidrojām iepriekš, mēs aprakstīsim notikušo, izmantojot datu klases:

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

Tagad uzrakstīsim funkciju, kas parsēs iespēju lauku:


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. Skatiet rezultātu

Es pārmeklēšu tīklu un parādīšu, ko atradu. Tiek parādīti vienkāršas izvades rezultāti, izmantojot Log.d:

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

Jautājums par savienojuma izveidi ar tīklu no lietojumprogrammas koda palika neizskatīts. Teikšu tikai to, ka, lai nolasītu saglabātās paroles no mobilās ierīces OS, ir nepieciešamas root tiesības un vēlme rakņāties pa failu sistēmu, lai lasītu wpa_supplicant.conf. Ja lietojumprogrammas loģika prasa ievadīt paroli no ārpuses, savienojumu var izveidot caur klasi android.net.wifi.WifiManager.

Paldies Jegors Ponomarjovs par vērtīgiem papildinājumiem.

Ja uzskati, ka kaut kas jāpapildina vai jālabo, raksti komentāros :)

Avots: www.habr.com

Pievieno komentāru