Wi-Fi やその他多くの略語。 Android アプリケーションで Wi-Fi ノードに関するデータを膨張せずに取得する方法

ある日、Android アプリケーションから Wi-Fi ネットワークをスキャンし、アクセス ポイントに関する詳細なデータを取得する必要がありました。

ここで私たちはいくつかの困難に直面する必要がありました。 オフ。Android ドキュメント 説明されているクラスの多くは非推奨になりました (API レベル > 26) が、これには反映されていませんでした。 ドキュメント内のいくつかの事項の説明は最小限です (たとえば、クラスの機能フィールドなど) スキャン結果 多くの重要なデータが含まれていますが、執筆時点ではほとんど何も説明されていません)。 XNUMX 番目の難しさは、初めて Wi-Fi に近づくとき、理論を読んで localhost 経由でルーターを設定する以外に、個別に理解できると思われる多数の略語に対処しなければならないという事実にあるかもしれません。 しかし、それらをどのように関連付けて構造化するかは明らかではないかもしれません (判断は主観的であり、これまでの経験に依存します)。

この記事では、NDK やハックを使用せず、Android API のみを使用して Android コードから Wi-Fi 環境に関する包括的なデータを取得し、その解釈方法を理解する方法について説明します。

先延ばしにせずにコードを書き始めましょう。

1. プロジェクトを作成する

このnoteはAndroidプロジェクトを複数回作成したことがある方を対象としているため、本項目の詳細は割愛させていただきます。 以下のコードは、Kotlin (minSdkVersion=23) で表示されます。

2. アクセス権限

アプリケーションから Wi-Fi を使用するには、ユーザーからいくつかの許可を取得する必要があります。 に従って ドキュメンテーション、OS バージョン 8.0 以降のデバイスでネットワークをスキャンするには、ネットワーク環境の状態を表示するためのアクセスに加えて、デバイスの Wi-Fi モジュールの状態を変更するためのアクセス、または座標 (おおよその) へのアクセスが必要です。または正確)。 バージョン 9.0 以降では、両方の要求をユーザーに要求し、位置情報サービスをオンにするようユーザーに明示的に要求する必要があります。 これは Google の気まぐれであり、ユーザーをスパイしたいという私たちの願望ではないことをユーザーに勇敢に説明することを忘れないでください :)

そこで、AndroidManifest.xml に以下を追加します。

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

現在のアクティビティへのリンクを含むコードには次のようになります。

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. BroadcastReceiver を作成し、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
}

ドキュメントの WiFiManager.startScan メソッドは API バージョン 28 以降非推奨としてマークされていますが、オフになっています。 ガイド それを使用することを提案します。

合計で、オブジェクトのリストを受け取りました スキャン結果.

4. ScanResult を見て用語を理解する

このクラスのいくつかのフィールドを見て、その意味を説明しましょう。

SSID — サービスセット識別子はネットワークの名前です

BSSID – 基本サービス セット識別子 – ネットワーク アダプター (Wi-Fi ポイント) の MAC アドレス

レベル — 受信信号強度インジケーター [dBm (ロシア dBm) — デシベル、基準電力 1 mW。] — 受信信号強度のインジケーター。 0 から -100 までの値をとり、0 から離れるほど、Wi-Fi ポイントからデバイスまでの途中で失われる信号電力が増加します。 詳細については、たとえば、次のサイトを参照してください。 ウィキペディア。 ここでは Android クラスを使用して説明します Wifiマネージャー 選択したステップで、信号レベルを優れたレベルからひどいレベルまで調整できます。

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

周波数 — Wi-Fi ポイントの動作周波数 [Hz]。 周波数自体に加えて、いわゆるチャネルにも興味があるかもしれません。 各ポイントには独自の動作純度があります。 この記事の執筆時点では、Wi-Fi ポイントの最も一般的な範囲は 2.4 GHz です。 ただし、より正確に言うと、ポイントは、名前に近い番号の周波数で携帯電話に情報を送信します。 チャンネル数と対応周波数 標準化された。 これは、近くのポイントが異なる周波数で動作し、それによって相互に干渉せず、相互に伝送速度と品質を低下させないようにするために行われます。 この場合、ポイントは XNUMX つの周波数ではなく、周波数範囲 (パラメータ) で動作します。 チャネル幅)、チャネル幅と呼ばれます。 つまり、隣接する (隣接するだけでなく、さらに 3 つ離れた) チャネル上で動作するポイントが相互に干渉します。 次の単純なコードが役に立つかもしれません。これにより、周波数 2.4 および 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
            }
        }

機能 - 分析にとって最も興味深い分野であり、多くの時間を必要とする作業です。 ここではポイントの「能力」が線に書かれています。 この場合、文字列解釈の詳細をドキュメントで探す必要はありません。 この行に含まれる可能性のある例をいくつか示します。

[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. 略語と解析機能を理解する

android.net.wifi.* パッケージのクラスは、Linux ユーティリティによって内部で使用されることに言及する価値があります。 wpa_supplicantの また、capability フィールドの出力結果は、スキャン時の flags フィールドのコピーです。

私たちは一貫して行動します。 まず、括弧内の要素が「-」記号で区切られた形式の出力を考えてみましょう。

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

最初の意味は、いわゆるものを説明します。 認証方法。 つまり、アクセス ポイント自体の使用を許可するためにデバイスとアクセス ポイントがどのような一連のアクションを実行する必要があるか、またペイロードをどのように暗号化するかということです。 この記事の執筆時点では、最も一般的なオプションは WPA と WPA2 で、各デバイスが直接接続されるか、いわゆる WPA3 経由で接続されます。 RADIUS サーバー (WPA-Enterprice) は、暗号化されたチャネル経由でパスワードを提供します。 ほとんどの場合、自宅のアクセス ポイントはこの方式に従って接続を提供します。 XNUMX 番目のバージョンと最初のバージョンの違いは、安全でない TKIP に対して AES という、より強力な暗号が採用されていることです。 より複雑で高度な WPAXNUMX も徐々に導入されています。 理論的には、enterprice ソリューション CCKM (Cisco Centralized Key Management) を使用するオプションがあるかもしれませんが、私はこれに出会ったことがありません。

アクセス ポイントは MAC アドレスによって認証するように設定されている可能性があります。 または、アクセス ポイントが古い WEP アルゴリズムを使用してデータを提供する場合、実際には認証は行われません (ここでの秘密キーは暗号化キーです)。 このようなオプションは「その他」として分類されます。
隠しキャプティブ ポータル検出を使用した公共 Wi-Fi で一般的な方法、つまりブラウザを介した認証リクエストもあります。 このようなアクセス ポイントは、スキャナーにはオープンしているように見えます (物理接続の観点からはオープンです)。 したがって、それらをオープンとして分類します。

XNUMX 番目の値は次のように表すことができます。 鍵管理アルゴリズム。 これは、上記の認証方法のパラメータです。 暗号化キーがどのように交換されるかについて説明します。 考えられるオプションを検討してみましょう。 EAP - 前述の WPA-Enterprice で使用され、データベースを使用して入力された認証データを検証します。 SAE - 高度な WPA3 で使用され、ブルート フォースに対する耐性が強化されています。 PSK - 最も一般的なオプションでは、パスワードを入力し、暗号化された形式で送信します。 IEEE8021X - 国際標準に準拠しています (WPA ファミリでサポートされているものとは異なります)。 OWE (Opportunistic Wireless Encryption) は、OPEN として分類されたポイントに対する IEEE 802.11 標準の拡張です。 OWE は、セキュリティで保護されていないネットワーク上で送信されるデータを暗号化することで、そのセキュリティを確保します。 アクセス キーがない場合にもオプションを使用できます。このオプションを NONE と呼びます。

XNUMX 番目のパラメータはいわゆる。 暗号化スキーム — 送信データを保護するために暗号がどの程度正確に使用されるか。 オプションを列挙してみましょう。 WEP - RC4 ストリーム暗号を使用し、秘密キーは暗号化キーですが、これは現代の暗号化の世界では受け入れられないと考えられています。 TKIP - WPA で使用され、CKIP - WPA2 で使用されます。 TKIP+CKIP - 下位互換性のために、WPA および WPA2 が可能なポイントで指定できます。

XNUMX つの要素の代わりに、孤立した WEP マークが表示されます。

[WEP]

上で説明したように、存在しないキーを使用するためのアルゴリズムや、デフォルトで同じである暗号化方式を指定しなくても十分です。

次に、この括弧について考えてみましょう。

[ESS]

それ Wi-Fi動作モード または Wi-Fi ネットワーク トポロジ。 接続されたデバイスが通信するアクセス ポイントが XNUMX つある場合、BSS (Basic Service Set) モードが発生することがあります。 ローカルネットワーク上で見つけることができます。 通常、アクセス ポイントはさまざまなローカル ネットワークからデバイスを接続するために必要なため、拡張サービス セット (ESS) の一部です。 IBSS (Independent Basic Service Sets) タイプは、デバイスがピアツーピア ネットワークの一部であることを示します。

WPS フラグが表示される場合もあります。

[WPS]

WPS (Wi-Fi Protected Setup) は、Wi-Fi ネットワークを半自動で初期化するためのプロトコルです。 初期化するには、ユーザーは 8 文字のパスワードを入力するか、ルーターのボタンを押します。 アクセス ポイントが最初のタイプで、アクセス ポイント名の横にこのチェックボックスが表示されている場合は、管理パネルに移動して WPS アクセスを無効にすることを強くお勧めします。 実際のところ、8 桁の PIN は MAC アドレスによって判明することが多く、あるいは予見可能な時間内に特定できるため、誰かがそれを悪用する可能性があります。

6. モデルと解析関数を作成する

上記でわかったことに基づいて、データ クラスを使用して何が起こったかを説明します。

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

次に、機能フィールドを解析する関数を作成しましょう。


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. 結果を見る

ネットワークをスキャンして、見つけたものを紹介します。 Log.d による単純な出力の結果を示します。

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

アプリケーション コードからネットワークに接続する問題は未調査のままでした。 モバイル デバイスの OS から保存されたパスワードを読み取るには、root 権限と、ファイル システムを調べて wpa_supplicant.conf を読み取る意欲が必要であることだけを述べておきます。 アプリケーション ロジックで外部からのパスワードの入力が必要な場合は、クラス経由で接続できます。 android.net.wifi.WifiManager.

感謝 エゴール・ポノマレフ 貴重な追加のために。

何かを追加または修正する必要があると思われる場合は、コメントに書いてください:)

出所: habr.com

コメントを追加します