ある日、Android アプリケーションから Wi-Fi ネットワークをスキャンし、アクセス ポイントに関する詳細なデータを取得する必要がありました。
ここで私たちはいくつかの困難に直面する必要がありました。
この記事では、NDK やハックを使用せず、Android API のみを使用して Android コードから Wi-Fi 環境に関する包括的なデータを取得し、その解釈方法を理解する方法について説明します。
先延ばしにせずにコードを書き始めましょう。
1. プロジェクトを作成する
このnoteはAndroidプロジェクトを複数回作成したことがある方を対象としているため、本項目の詳細は割愛させていただきます。 以下のコードは、Kotlin (minSdkVersion=23) で表示されます。
2. アクセス権限
アプリケーションから Wi-Fi を使用するには、ユーザーからいくつかの許可を取得する必要があります。 に従って
そこで、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 ポイントからデバイスまでの途中で失われる信号電力が増加します。 詳細については、たとえば、次のサイトを参照してください。
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 です。 ただし、より正確に言うと、ポイントは、名前に近い番号の周波数で携帯電話に情報を送信します。 チャンネル数と対応周波数
/* по частоте определяем номер канала */
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-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 を読み取る意欲が必要であることだけを述べておきます。 アプリケーション ロジックで外部からのパスワードの入力が必要な場合は、クラス経由で接続できます。
感謝
何かを追加または修正する必要があると思われる場合は、コメントに書いてください:)
出所: habr.com