Wi-Fi e moitas outras abreviaturas. Como obter datos sobre nodos wifi nunha aplicación de Android sen incharse

Un día necesitaba escanear redes wifi desde aplicacións de Android e obter datos detallados sobre puntos de acceso.

Aquí tivemos que afrontar varias dificultades: off.Documentación de Android moitas das clases descritas quedaron obsoletas (nivel de API > 26), o que non se reflectiu nela; a descrición dalgunhas cousas da documentación é mínima (por exemplo, o campo de capacidades da clase ScanResult no momento de escribir este artigo non se describe case nada, aínda que contén moitos datos importantes). A terceira dificultade pode estar no feito de que cando te achegas por primeira vez á wifi, ademais de ler a teoría e configurar o enrutador a través de localhost, tes que xestionar unha serie de abreviaturas que parecen comprensibles individualmente. Pero pode que non sexa obvio como relacionalos e estruturalos (o xuízo é subxectivo e depende da experiencia previa).

Este artigo discute como obter datos completos sobre o ambiente Wi-Fi a partir de código de Android sen NDK, hacks, pero só usando a API de Android e entender como interpretalo.

Non demoremos e empecemos a escribir código.

1. Crea un proxecto

Esta nota está destinada a aqueles que crearon un proxecto de Android máis dunha vez, polo que omitimos os detalles deste elemento. O código a continuación presentarase en Kotlin, minSdkVersion=23.

2. Permisos de acceso

Para traballar con Wi-Fi desde a aplicación, terás que obter varios permisos do usuario. Dacordo con documentación, para escanear a rede en dispositivos con versións do SO posteriores á 8.0, ademais de acceder a ver o estado do contorno de rede, necesitas acceso para cambiar o estado do módulo Wi-Fi do dispositivo ou acceso ás coordenadas (aproximado ou exacta). A partir da versión 9.0, debe solicitarlle ao usuario ambas as dúas e tamén solicitar explícitamente ao usuario que active os servizos de localización. Non esquezas explicarlle galantemente ao usuario que este é o capricho de Google, e non o noso desexo de espiarlle :)

Entón, en AndroidManifest.xml engadiremos:

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

E no código que contén unha ligazón á actividade actual:

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. Crea un BroadcastReceiver e subscríbete aos eventos de actualización de datos sobre a exploración do contorno de rede wifi

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
}

O método WiFiManager.startScan da documentación está marcado como desfavorecido desde a versión 28 da API, pero está desactivado. orientar suxire usalo.

En total, recibimos unha lista de obxectos ScanResult.

4. Mira ScanResult e comprende os termos

Vexamos algúns campos desta clase e describimos o que significan:

SSID — O identificador do conxunto de servizos é o nome da rede

BSSID – Identificador de conxunto de servizos básicos – Enderezo MAC do adaptador de rede (punto Wi-Fi)

nivel — Indicador de intensidade do sinal recibido [dBm (dBm ruso) — Decibelios, potencia de referencia 1 mW.] — Un indicador da intensidade do sinal recibido. Toma un valor de 0 a -100, canto máis lonxe de 0, máis potencia de sinal se perdeu no camiño desde o punto wifi ata o dispositivo. Pódense atopar máis detalles, por exemplo, en Wikipedia. Aquí vos contarei que usando a clase de Android WifiManager pode calibrar o nivel de sinal nunha escala de excelente a terrible no paso que elixa:

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

frecuencia — frecuencia de funcionamento do punto Wi-Fi [Hz]. Ademais da propia frecuencia, pode estar interesado na chamada canle. Cada punto ten a súa propia pureza de funcionamento. No momento de escribir este artigo, o rango máis popular de puntos Wi-Fi é de 2.4 GHz. Pero, para ser máis precisos, o punto transmite información ao teu teléfono nunha frecuencia numerada próxima á nomeada. Número de canles e frecuencias correspondentes estandarizados. Isto faise para que os puntos próximos operen a diferentes frecuencias, polo que non interfiran entre si e non se reducen mutuamente a velocidade e a calidade da transmisión. Neste caso, os puntos non operan nunha frecuencia, senón nun rango de frecuencias (parámetro ancho de canle), chamado ancho da canle. É dicir, os puntos que operan en canles adxacentes (e non só adxacentes, senón incluso 3 de si mesmos) interfiren entre si. Podes considerar útil este código sinxelo, que che permite calcular o número de canle a partir do valor de frecuencia para puntos cunha frecuencia de 2.4 e 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
            }
        }

capacidades - o campo máis interesante para a análise, traballo co que requiriu moito tempo. Aquí as "capacidades" do punto están escritas na liña. Neste caso, non tes que buscar detalles sobre a interpretación de cadeas na documentación. Aquí tes algúns exemplos do que pode haber nesta liña:

[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. Comprensión de abreviaturas e capacidades de análise

Cómpre mencionar que as clases do paquete android.net.wifi.* son utilizadas baixo o capó por unha utilidade de Linux wpa_suplicante e o resultado de saída no campo de capacidades é unha copia do campo de bandeiras ao dixitalizar.

Actuaremos con coherencia. Consideremos primeiro a saída dun formato no que os elementos entre parénteses están separados por un signo “-“:

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

O primeiro significado describe o chamado. método de autenticación. É dicir, que secuencia de accións deben realizar o dispositivo e o punto de acceso para que o punto de acceso se permita usar e como cifrar a carga útil. No momento de escribir este post, as opcións máis habituais son WPA e WPA2, nas que se conectan ou ben cada dispositivo directamente ou ben a través do chamado. O servidor RADIUS (WPA-Enterprice) proporciona o contrasinal a través dunha canle cifrada. O máis probable é que o punto de acceso da túa casa proporcione unha conexión segundo este esquema. A diferenza entre a segunda versión e a primeira é que ten un cifrado máis forte: AES fronte ao TKIP inseguro. WPA3, que é máis complexo e avanzado, tamén se está introducindo aos poucos. Teoricamente, pode haber unha opción coa solución enterprice CCKM (Cisco Centralized Key Management), pero nunca a atopei.

O punto de acceso puido estar configurado para autenticarse mediante o enderezo MAC. Ou, se o punto de acceso proporciona datos usando o algoritmo WEP desactualizado, entón non hai autenticación (a clave secreta aquí é a clave de cifrado). Clasificamos este tipo de opcións como OUTRAS.
Tamén hai un método popular na wifi pública con detección de portal cativo oculto: unha solicitude de autenticación a través dun navegador. Tales puntos de acceso aparecen para o escáner como abertos (que son desde o punto de vista da conexión física). Polo tanto, clasificámolos como ABERTOS.

O segundo valor pódese indicar como algoritmo de xestión de claves. Este é un parámetro do método de autenticación descrito anteriormente. Fala sobre como se intercambian exactamente as claves de cifrado. Consideremos as opcións posibles. EAP: usado no mencionado WPA-Enterprice, usa unha base de datos para verificar os datos de autenticación introducidos. SAE: usado en WPA3 avanzado, máis resistente á forza bruta. PSK - a opción máis común, implica introducir un contrasinal e transmitilo en forma cifrada. IEEE8021X - segundo un estándar internacional (diferente do admitido pola familia WPA). OWE (Opportunistic Wireless Encryption) é unha extensión do estándar IEEE 802.11 para puntos que clasificamos como OPEN. OWE garante a seguridade dos datos transmitidos a través dunha rede non segura mediante o seu cifrado. Tamén é posible unha opción cando non hai claves de acceso, chamemos a esta opción NINGUNHA.

O terceiro parámetro é o chamado. esquemas de cifrado — como se usa exactamente o cifrado para protexer os datos transmitidos. Imos enumerar as opcións. WEP - usa un cifrado de fluxo RC4, a clave secreta é a clave de cifrado, que se considera inaceptable no mundo da criptografía moderna. TKIP - usado en WPA, CKIP - en WPA2. TKIP+CKIP: pódese especificar en puntos capaces de WPA e WPA2 para compatibilidade con versións anteriores.

En lugar de tres elementos, podes atopar unha marca WEP solitaria:

[WEP]

Como comentamos anteriormente, isto é suficiente para non especificar o algoritmo de uso de claves, que non existe, e o método de cifrado, que é o mesmo por defecto.

Agora considere este paréntese:

[ESS]

El Modo de funcionamento Wi-Fi ou Topoloxía de rede wifi. Podes atopar o modo BSS (Conxunto de servizos básicos), cando hai un punto de acceso a través do cal se comunican os dispositivos conectados. Pódese atopar nas redes locais. Como regra xeral, os puntos de acceso son necesarios para conectar dispositivos de diferentes redes locais, polo que forman parte de Extended Service Sets - ESS. O tipo IBSSs (Independent Basic Service Sets) indica que o dispositivo forma parte dunha rede Peer-to-Peer.

Tamén podes ver a bandeira WPS:

[WPS]

WPS (Wi-Fi Protected Setup) é un protocolo para a inicialización semiautomática dunha rede Wi-Fi. Para inicializar, o usuario introduce un contrasinal de 8 caracteres ou preme un botón do router. Se o seu punto de acceso é do primeiro tipo e esta caixa de verificación aparece xunto ao nome do seu punto de acceso, recoméndase encarecidamente que vaia ao panel de administración e desactive o acceso WPS. O caso é que moitas veces o PIN de 8 díxitos pódese descubrir polo enderezo MAC, ou pode resolverse nun tempo previsible, que alguén pode aproveitar de forma deshonesta.

6. Crea un modelo e unha función de análise

En base ao que descubrimos anteriormente, describiremos o que pasou usando clases de datos:

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

Agora imos escribir unha función que analizará o campo de capacidades:


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. Consulta o resultado

Analizarei a rede e mostrarei o que atopei. Amósanse os resultados da saída simple a través de Log.d:

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

O problema de conectarse á rede desde o código da aplicación permaneceu sen examinar. Só direi que para ler contrasinais gardados desde o sistema operativo dun dispositivo móbil, necesitas dereitos de root e vontade de rebuscar no sistema de ficheiros para ler wpa_supplicant.conf. Se a lóxica da aplicación require introducir un contrasinal desde fóra, a conexión pódese facer a través da clase android.net.wifi.WifiManager.

Grazas Egor Ponomarev para adicións valiosas.

Se cres que hai que engadir ou corrixir algo, escribe nos comentarios :)

Fonte: www.habr.com

Engadir un comentario