Wi-Fi og margar aðrar skammstafanir. Hvernig á að fá gögn um Wi-Fi hnúta í Android forriti án þess að bólgna

Einn daginn þurfti ég að skanna Wi-Fi net úr Android forritum og fá nákvæmar upplýsingar um aðgangsstaði.

Hér þurftum við að takast á við ýmsa erfiðleika: off.Android skjöl margir af þeim flokkum sem lýst er urðu úreltir (API stig > 26), sem endurspeglaðist ekki í því; lýsingin á sumum hlutum í skjölunum er í lágmarki (til dæmis hæfileikasvið bekkjarins Skannaniðurstaða þegar þetta er skrifað er nánast engu lýst, þó að það innihaldi mikið af mikilvægum gögnum). Þriðji erfiðleikinn kann að liggja í þeirri staðreynd að þegar þú kemst fyrst nálægt Wi-Fi, annað en að lesa kenninguna og setja upp beininn í gegnum localhost, þarftu að takast á við fjölda skammstafana sem virðast skiljanlegar hver fyrir sig. En það er kannski ekki augljóst hvernig á að tengja þau og skipuleggja þau (mat er huglægt og fer eftir fyrri reynslu).

Þessi grein fjallar um hvernig á að fá alhliða gögn um Wi-Fi umhverfið frá Android kóða án NDK, járnsög, en aðeins með því að nota Android API og skilja hvernig á að túlka það.

Við skulum ekki tefja og byrja að skrifa kóða.

1. Búðu til verkefni

Þessi athugasemd er ætluð þeim sem hafa búið til Android verkefni oftar en einu sinni, svo við sleppum upplýsingum um þetta atriði. Kóðinn hér að neðan verður kynntur í Kotlin, minSdkVersion=23.

2. Aðgangsheimildir

Til að vinna með Wi-Fi frá forritinu þarftu að fá nokkrar heimildir frá notandanum. Í samræmi við skjöl, til að skanna netið á tækjum með stýrikerfisútgáfur eftir 8.0, auk aðgangs til að skoða stöðu netumhverfisins, þarftu annað hvort aðgang til að breyta stöðu Wi-Fi einingarinnar eða aðgang að hnitum (u.þ.b. eða nákvæmlega). Frá og með útgáfu 9.0 verður þú að biðja notandann um hvort tveggja og einnig beinlínis biðja notandann um að kveikja á staðsetningarþjónustu. Ekki gleyma að útskýra af kappi fyrir notandanum að þetta sé duttlunga Google og ekki löngun okkar til að njósna um hann :)

Svo, í AndroidManifest.xml munum við bæta við:

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

Og í kóðanum sem inniheldur tengil á núverandi starfsemi:

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. Búðu til Broadcast Receiver og gerðu áskrifandi að gagnauppfærsluviðburðum um að skanna Wi-Fi netumhverfið

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
}

WiFiManager.startScan aðferðin í skjölunum er merkt sem afskrifuð frá API útgáfu 28, en slökkt. leiðbeina leggur til að nota það.

Alls fengum við lista yfir hluti Skannaniðurstaða.

4. Horfðu á ScanResult og skildu skilmálana

Við skulum skoða nokkur svið þessa flokks og lýsa hvað þau þýða:

SSID — Þjónustusett auðkenni er nafn netsins

BSSID – Basic Service Set Identifier – MAC vistfang netmillistykkisins (Wi-Fi punktur)

stigi — Móttekin merkistyrksvísir [dBm (rússneskur dBm) — Desibel, viðmiðunarafl 1 mW.] — Vísir fyrir móttekinn merkistyrk. Tekur gildi frá 0 til -100, því lengra frá 0, því meira merki tapaðist á leiðinni frá Wi-Fi punktinum að tækinu þínu. Nánari upplýsingar má td finna á Wikipedia. Hér skal ég segja þér að með því að nota Android bekkinn WifiManager þú getur kvarðað merkjastigið á kvarða frá frábæru til hræðilegu í skrefinu sem þú velur:

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

tíðni — notkunartíðni Wi-Fi punktsins [Hz]. Auk tíðnarinnar sjálfrar gætirðu haft áhuga á svokölluðu rásinni. Hver punktur hefur sinn rekstrarhreinleika. Þegar þetta er skrifað er vinsælasta úrval Wi-Fi punkta 2.4 GHz. En til að vera nákvæmari sendir punkturinn upplýsingar í símann þinn á númeraðri tíðni nálægt þeirri sem nefnd er. Fjöldi rása og samsvarandi tíðni staðlað. Þetta er gert til að nálægir punktar starfi á mismunandi tíðni og trufli þar með ekki hver annan og dragi ekki gagnkvæmt úr hraða og gæðum sendingar. Í þessu tilviki starfa punktarnir ekki á einni tíðni, heldur á tíðnisviði (breytu rásBreidd), sem kallast rásbreidd. Það er, punktar sem starfa á aðliggjandi (og ekki aðeins aðliggjandi, heldur jafnvel 3 frá sjálfum sér) rásum trufla hver aðra. Þú gætir fundið þennan einfalda kóða gagnlegan, sem gerir þér kleift að reikna út rásarnúmerið út frá tíðnigildinu fyrir punkta með tíðnina 2.4 og 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
            }
        }

getu - áhugaverðasta greiningarsviðið, vinna með sem krafðist mikillar tíma. Hér er „geta“ punktsins skrifað í línuna. Í þessu tilviki þarftu ekki að leita að upplýsingum um strengjatúlkun í skjölunum. Hér eru nokkur dæmi um hvað gæti verið í þessari línu:

[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. Að skilja skammstafanir og þáttunargetu

Þess má geta að flokkar android.net.wifi.* pakkans eru notaðir undir hettunni af Linux tóli wpa_supplicant og úttaksniðurstaðan í hæfileikareitnum er afrit af fánareitnum við skönnun.

Við munum starfa stöðugt. Við skulum fyrst íhuga úttak sniðs þar sem þættir innan sviga eru aðskildir með „-“ tákni:

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

Fyrsta merkingin lýsir svokölluðu. auðkenningaraðferð. Það er, hvaða röð aðgerða þarf tækið og aðgangsstaðurinn að framkvæma til að aðgangsstaðurinn leyfi sér að nota og hvernig á að dulkóða farminn. Þegar þú skrifar þessa færslu eru algengustu valkostirnir WPA og WPA2, þar sem annað hvort hvert tengt tæki beint eða í gegnum svokallaða. RADIUS þjónninn (WPA-Enterprice) veitir lykilorðið yfir dulkóðaða rás. Líklegast veitir aðgangsstaðurinn á heimili þínu tengingu samkvæmt þessu kerfi. Munurinn á annarri útgáfunni og þeirri fyrstu er að hún hefur sterkari dulmál: AES á móti óörugga TKIP. WPA3, sem er flóknara og háþróaðra, er einnig smám saman að koma í notkun. Fræðilega séð getur verið möguleiki með enterprice lausnina CCKM (Cisco Centralized Key Management), en ég hef aldrei rekist á það.

Aðgangsstaðurinn gæti hafa verið stilltur til að auðkenna með MAC vistfangi. Eða ef aðgangsstaðurinn veitir gögn með því að nota úrelta WEP reikniritið, þá er í raun engin auðkenning (leynilykillinn hér er dulkóðunarlykillinn). Við flokkum slíka valkosti sem ANNAÐ.
Það er líka til aðferð sem er vinsæl í almennu Wi-Fi interneti með falinni Captive Portal Detection - auðkenningarbeiðni í gegnum vafra. Slíkir aðgangsstaðir birtast skannanum sem opnir (sem þeir eru frá sjónarhóli líkamlegrar tengingar). Þess vegna flokkum við þau sem OPIN.

Annað gildið má tákna sem lykilstjórnunaralgrím. Þetta er færibreyta auðkenningaraðferðarinnar sem lýst er hér að ofan. Talar um nákvæmlega hvernig skipt er um dulkóðunarlykla. Við skulum íhuga mögulega valkosti. EAP - notað í nefndu WPA-Enterprice, notar gagnagrunn til að sannreyna innslögð auðkenningargögn. SAE - notað í háþróaðri WPA3, ónæmari fyrir grimmdarkrafti. PSK - algengasti kosturinn, felur í sér að slá inn lykilorð og senda það á dulkóðuðu formi. IEEE8021X - samkvæmt alþjóðlegum staðli (öðruvísi en WPA fjölskyldan styður). OWE (Opportunistic Wireless Encryption) er framlenging á IEEE 802.11 staðlinum fyrir punkta sem við flokkuðum sem OPEN. OWE tryggir öryggi gagna sem send eru um ótryggt net með því að dulkóða þau. Valkostur er líka mögulegur þegar það eru engir aðgangslyklar, við skulum kalla þennan valkost ENGINN.

Þriðja færibreytan er svokölluð. dulkóðunarkerfi — hvernig nákvæmlega dulmálið er notað til að vernda send gögn. Við skulum lista valkostina. WEP - notar RC4 straum dulmál, leynilykillinn er dulkóðunarlykillinn, sem er talið óviðunandi í heimi nútíma dulritunar. TKIP - notað í WPA, CKIP - í WPA2. TKIP+CKIP - er hægt að tilgreina í punktum sem geta notað WPA og WPA2 fyrir afturábak samhæfni.

Í stað þriggja þátta geturðu fundið einmanalegt WEP-merki:

[WEP]

Eins og við ræddum hér að ofan er þetta nóg til að tilgreina ekki reikniritið fyrir notkun lykla, sem er ekki til, og dulkóðunaraðferðina, sem er sjálfgefið sú sama.

Íhugaðu nú þessa sviga:

[ESS]

Það Wi-Fi rekstrarhamur eða Staðfræði Wi-Fi netkerfis. Þú gætir lent í BSS-stillingu (Basic Service Set) - þegar það er einn aðgangsstaður sem tengd tæki eiga samskipti í gegnum. Hægt að finna á staðarnetum. Að jafnaði þarf aðgangsstaði til að tengja tæki frá mismunandi staðarnetum, þannig að þeir eru hluti af Extended Service Sets - ESS. Gerð IBSSs (Independent Basic Service Sets) gefur til kynna að tækið sé hluti af jafningjaneti.

Þú gætir líka séð WPS fánann:

[WPS]

WPS (Wi-Fi Protected Setup) er samskiptaregla fyrir hálfsjálfvirka frumstillingu á Wi-Fi neti. Til að frumstilla slær notandinn annaðhvort inn 8 stafa lykilorð eða ýtir á hnapp á beini. Ef aðgangsstaðurinn þinn er af fyrstu gerð og þessi gátreitur birtist við hliðina á nafni aðgangsstaðarins þíns, er eindregið mælt með því að fara á stjórnborðið og slökkva á WPS aðgangi. Staðreyndin er sú að oft er hægt að finna 8 stafa PIN-númerið með MAC-tölu, eða það er hægt að redda því á fyrirsjáanlegum tíma, sem einhver óheiðarlegur getur nýtt sér.

6. Búðu til líkan og þáttunaraðgerð

Byggt á því sem við komumst að hér að ofan munum við lýsa því sem gerðist með því að nota gagnaflokka:

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

Nú skulum við skrifa aðgerð sem mun flokka hæfileikareitinn:


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. Sjá útkomuna

Ég skal skanna netið og sýna þér hvað ég fann. Sýndar eru niðurstöður einfalds úttaks í gegnum Log.d:

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

Spurningin um tengingu við netið frá forritskóðanum var órannsökuð. Ég segi bara að til að lesa vistuð lykilorð úr stýrikerfi farsíma þarf rótarréttindi og vilja til að grúska í gegnum skráarkerfið til að lesa wpa_supplicant.conf. Ef forritsrökfræðin krefst þess að lykilorð sé slegið inn að utan er hægt að koma á tengingunni í gegnum bekkinn android.net.wifi.WifiManager.

Takk Egor Ponomarev fyrir verðmætar viðbætur.

Ef þú telur að eitthvað þurfi að bæta við eða leiðrétta skaltu skrifa í athugasemdir :)

Heimild: www.habr.com

Bæta við athugasemd