有一天,我需要从 Android 应用程序扫描 Wi-Fi 网络并获取有关接入点的详细数据。
在这里我们不得不面对几个困难:
本文讨论如何从 Android 代码中获取有关 Wi-Fi 环境的全面数据,无需 NDK、hacks,而仅使用 Android API 并了解如何解释它。
让我们事不宜迟,开始编写代码吧。
1. 创建项目
本说明适用于那些多次创建 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,其中每个连接的设备直接或通过所谓的。 RADIUS 服务器 (WPA-Enterprice) 通过加密通道提供密码。 您家中的接入点很可能会根据此方案提供连接。 第二个版本与第一个版本的区别在于它具有更强的密码:AES 与不安全的 TKIP。 更复杂、更先进的WPA3也正在逐步推出。 理论上,企业解决方案CCKM(思科集中密钥管理)可能有一个选项,但我从未遇到过。
接入点可能已配置为通过 MAC 地址进行身份验证。 或者,如果接入点使用过时的WEP算法提供数据,那么实际上没有身份验证(这里的密钥是加密密钥)。 我们将此类选项归类为“其他”。
还有一种在公共 Wi-Fi 中流行的隐藏强制门户检测的方法 - 通过浏览器发出身份验证请求。 此类接入点在扫描仪看来是开放的(从物理连接的角度来看)。 因此,我们将它们归类为 OPEN。
第二个值可以表示为 密钥管理算法。 这是上述认证方法的一个参数。 讨论加密密钥的具体交换方式。 让我们考虑一下可能的选择。 EAP - 在提到的 WPA-Enterprice 中使用,使用数据库来验证输入的身份验证数据。 SAE——采用先进的WPA3,更耐暴力破解。 PSK - 最常见的选项,涉及输入密码并以加密形式传输。 IEEE8021X - 根据国际标准(与 WPA 系列支持的标准不同)。 OWE(机会无线加密)是 IEEE 802.11 标准的扩展,针对我们归类为开放的点。 OWE 通过加密来确保通过不安全网络传输的数据的安全性。 当没有访问键时,也可以选择一个选项,我们将此选项称为“NONE”。
第三个参数就是所谓的。 加密方案 — 密码究竟如何用于保护传输的数据。 让我们列出选项。 WEP - 使用 RC4 流密码,秘密密钥是加密密钥,这在现代密码学领域被认为是不可接受的。 TKIP - 用于 WPA,CKIP - 用于 WPA2。 TKIP+CKIP - 可以在支持 WPA 和 WPA2 的点中指定,以实现向后兼容性。
您可以找到一个单独的 WEP 标记,而不是三个元素:
[WEP]
正如我们上面所讨论的,这足以不指定使用密钥的算法(该算法不存在)和加密方法(默认情况下相同)。
现在考虑这个括号:
[ESS]
它 Wi-Fi工作模式 или Wi-Fi 网络拓扑。 您可能会遇到 BSS(基本服务集)模式 - 当连接的设备通过一个接入点进行通信时。 可以在本地网络上找到。 通常,需要接入点来连接来自不同本地网络的设备,因此它们是扩展服务集 - ESS 的一部分。 IBSS(独立基本服务集)类型指示设备是对等网络的一部分。
您还可能会看到 WPS 标志:
[WPS]
WPS(Wi-Fi 保护设置)是一种用于半自动初始化 Wi-Fi 网络的协议。 要进行初始化,用户可以输入 8 个字符的密码或按下路由器上的按钮。 如果您的接入点是第一种类型,并且此复选框出现在您的接入点名称旁边,强烈建议您转到管理面板并禁用 WPS 访问。 事实是,通常可以通过 MAC 地址找到 8 位 PIN,或者可以在可预见的时间内整理出来,从而被不诚实的人利用。
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
从应用程序代码连接到网络的问题仍未得到检查。 我只想说,为了从移动设备的操作系统中读取保存的密码,您需要 root 权限并且愿意翻阅文件系统以读取 wpa_supplicant.conf。 如果应用逻辑需要从外部输入密码,可以通过类进行连接
谢谢
如果您认为需要添加或更正某些内容,请在评论中写下:)
来源: habr.com