Android掌控WiFi不完全指南

前言

如果想要對針對WiFi的攻擊進行監測,就需要定期獲取WiFi的運行狀態,例如WiFi的SSID,WiFi強度,是否開放,加密方式等資訊,在Android中通過WiFiManager來實現

WiFiManager簡介

WiFiManager這個類是Android暴露給開發者使用的一個系統服務管理類,其中包含對WiFi響應的操作函數;其隱藏掉的系統服務類為IWifiService,這個類是google私有的,屬於系統安全級別的API類
我們需要通過WifiManager進行函數操作完成UI,監聽對應的廣播消息,從而實現獲取WiFi資訊的功能

內置方法

方法 含義
addNetwork(WifiConfiguration config) 通過獲取到的網路的鏈接狀態資訊,來加入網路
calculateSignalLevel(int rssi , int numLevels) 計算訊號的等級
compareSignalLevel(int rssiA, int rssiB) 對照連接A 和連接B
createWifiLock(int lockType, String tag) 創建一個wifi 鎖,鎖定當前的wifi 連接
disableNetwork(int netId) 讓一個網路連接失效
disconnect() 斷開連接
enableNetwork(int netId, Boolean disableOthers) 連接一個連接
getConfiguredNetworks() 獲取網路連接的狀態
getConnectionInfo() 獲取當前連接的資訊
getDhcpInfo() 獲取DHCP 的資訊
getScanResulats() 獲取掃描測試的結果
getWifiState() 獲取一個wifi 接入點是否有效
isWifiEnabled() 推斷一個wifi 連接是否有效
pingSupplicant() ping 一個連接。推斷能否連通
ressociate() 即便連接沒有準備好,也要連通
reconnect() 假設連接準備好了,連通
removeNetwork() 移除某一個網路
saveConfiguration() 保留一個配置資訊
setWifiEnabled() 讓一個連接有效
startScan() 開始掃描
updateNetwork(WifiConfiguration config) 更新一個網路連接的資訊

其他常用基類

ScanResult

通過wifi 硬體的掃描來獲取一些周邊的wifi 熱點的資訊

欄位 含義
BSSID 接入點的地址,這裡主要是指小範圍幾個無線設備相連接所獲取的地址,比如說兩台筆記型電腦通過無線網卡進行連接,雙方的無線網卡分配的地址
SSID 網路的名字,當我們搜索一個網路時,就是靠這個來區分每個不同的網路接入點
Capabilities 網路接入的性能,這裡主要是來判斷網路的加密方式等
Frequency 頻率,每一個頻道交互的MHz 數
Level 等級,主要來判斷網路連接的優先數。

WifiInfo

WiFi連接成功後,可通過WifiInfo類獲取WiFi的一些具體資訊

方法 含義
getBSSID() 獲取BSSID
getDetailedStateOf() 獲取client的連通性
getHiddenSSID() 獲得SSID 是否被隱藏
getIpAddress() 獲取IP 地址
getLinkSpeed() 獲得連接的速度
getMacAddress() 獲得Mac 地址
getRssi() 獲得802.11n 網路的訊號
getSSID() 獲得SSID
getSupplicanState() 返回詳細client狀態的資訊

wifiConfiguration

WiFi的配置資訊

類名 含義
WifiConfiguration.AuthAlgorthm 用來判斷加密方法
WifiConfiguration.GroupCipher 獲取使用GroupCipher 的方法來進行加密
WifiConfiguration.KeyMgmt 獲取使用KeyMgmt 進行
WifiConfiguration.PairwiseCipher 獲取使用WPA 方式的加密
WifiConfiguration.Protocol 獲取使用哪一種協議進行加密
wifiConfiguration.Status 獲取當前網路的狀態

許可權

app AndroidManifest.xml 申請許可權

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

Android 6.0版本中如果未開啟GPS是無法獲取到掃描列表的,需要動態申請ACCESS_COARSE_LOCATION

// 檢測項目是否被賦予定位許可權
    public void checkPermissions(Context context){
        if(ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED){//未開啟定位許可權
            //開啟定位許可權,200是標識碼
            ActivityCompat.requestPermissions((Activity) context,new String[]{Manifest.permission.ACCESS_FINE_LOCATION},200);
        }
    }

在運行之前調用該函數進行申請即可

牛刀小試

WiFi狀態分類

  • 網卡正在關閉 WIFI_STATE_DISABLING WIFI ( 狀態碼:0 )
  • 網卡不可用 WIFI_STATE_DISABLED WIFI ( 狀態碼:1 )
  • 網卡正在打開 WIFI_STATE_ENABLING WIFI ( 狀態碼:2 )
  • 網卡可用 WIFI_STATE_ENABLED WIFI ( 狀態碼:3 )
  • 網卡狀態不可知 WIFI_STATE_UNKNOWN WIFI ( 狀態碼:4 )

程式碼中獲取WIFI的狀態

// 獲取 WIFI 的狀態.
public static int getWifiState(WifiManager manager) {
    return manager == null ? WifiManager.WIFI_STATE_UNKNOWN : manager.getWifiState();
}

獲取WiFiManager實例

// 獲取 WifiManager 實例. 
public static WifiManager getWifiManager(Context context) {
    return context == null ? null : (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
}

開啟、關閉WIFI

// 開啟/關閉 WIFI.
public static boolean setWifiEnabled(WifiManager manager, boolean enabled) {
    return manager != null && manager.setWifiEnabled(enabled);
}

掃描周圍的WiFi

// 開始掃描 WIFI. 
public static void startScanWifi(WifiManager manager) {
    if (manager != null) {
        manager.startScan();
    }
}

獲取掃描結果

// 獲取掃描 WIFI 的熱點: 
public static List<ScanResult> getScanResult(WifiManager manager) {
    return manager == null ? null : manager.getScanResult();
}

獲取歷史WiFi配置資訊

// 獲取已經保存過的/配置好的 WIFI 熱點. 
public static List<WifiConfiguration> getConfiguredNetworks(WifiManager manager) {
    return manager == null ? null : manager.WifiConfiguration();
}

獲取對應scanResult的配置資訊

    List<WifiConfiguration> configs = wifiManager.getMatchingWifiConfig(scanResult);

    // 可以列印一下看具體的情況:
    if (configs == null || configs.isEmpty()) return;
    for (WifiConfiguration config : configs) {
        Log.v(TAG, "config = " + config);
    }

獲取WIFI MAC地址

public String getWifiBSSID() {
    return mWifiInfo.getBSSID();
}

獲取本機MAC地址

Android M版本之後,通過wifiInfo.getMacAddress()獲取的MAC地址是一個固定的假地址,值為02:00:00:00:00:00,在這裡通過getMacAddress函數獲取真實MAC

// 獲取本機MAC地址
// Android M版本之後,通過wifiInfo.getMacAddress()獲取的MAC地址是一個固定的假地址,值為02:00:00:00:00:00
public String getSelfMac(){
    String mac=mWifiInfo==null?"null":mWifiInfo.getMacAddress();
    if(TextUtils.equals(mac, "02:00:00:00:00:00")) {
        String temp = getMacAddress();
        if (!TextUtils.isEmpty(temp)) {
            mac = temp;
        }
    }
    return mac;
}

private static String getMacAddress(){
    String macAddress = "";
    try {
        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface iF = interfaces.nextElement();

            byte[] addr = iF.getHardwareAddress();
            if (addr == null || addr.length == 0) {
                continue;
            }

            StringBuilder buf = new StringBuilder();
            for (byte b : addr) {
                buf.append(String.format("%02X:", b));
            }
            if (buf.length() > 0) {
                buf.deleteCharAt(buf.length() - 1);
            }
            String mac = buf.toString();
            //                WifiMonitorLogger.i("mac", "interfaceName="+iF.getName()+", mac="+mac);
            if(TextUtils.equals(iF.getName(), "wlan0")){
                return mac;
            }
        }
    } catch (SocketException e) {
        e.printStackTrace();
        return macAddress;
    }

    return macAddress;
}

獲取WIFI的網路速度和速度單位

// 獲取當前連接wifi的速度
public int getConnWifiSpeed(){
    return mWifiInfo.getLinkSpeed();
}

// 獲取當前連接wifi的速度單位
public String getConnWifiSpeedUnit(){
    return WifiInfo.LINK_SPEED_UNITS;
}

獲取當前連接WIFI的訊號強度

// 獲取當前連接wifi的訊號強度
public int getConnWifiLevel(){
    return mWifiManager.calculateSignalLevel(mWifiInfo.getRssi(),5);
}

獲取當前連接的WIFI的加密方式

本來我以為wifiinfo裡面應該會有解決方案,但是搜索了一下之後發現 如何在不掃描所有wifi網路的情況下獲取當前wifi連接的加密類型?
看來還是需要遍歷scanresults,但是很顯然SSID容易重複,所以用WIFI BSSID來唯一確定

// 獲取當前WIFI連接的加密方式
// capabilities的格式是 [認證標準+秘鑰管理+加密方案]
public String getConnCap(){
    String currentBSSID=mWifiInfo.getBSSID();
    for(ScanResult result:scanResultList){
        //            WifiMonitorLogger.i(currentBSSID+":"+result.BSSID);
        if(currentBSSID.equals(result.BSSID)){
            return result.capabilities;
        }
    }
    return "null";
}

另外返回的capabilities格式一般為[認證標準+秘鑰管理+加密方案],所以看到的時候不用太慌張
可以通過以下方式來判定加密

static final int SECURITY_NONE = 0;
static final int SECURITY_WEP = 1;
static final int SECURITY_PSK = 2;
static final int SECURITY_EAP = 3;

private int getType(ScanResult result) {
    if (result == null) {
        return SECURITY_NONE;
    }
    String capbility = result.capabilities;
    if (capbility == null || capbility.isEmpty()) {
        return SECURITY_NONE;
    }
    // 如果包含WAP-PSK的話,則為WAP加密方式
    if (capbility.contains("WPA-PSK") || capbility.contains("WPA2-PSK")) {
        return SECURITY_PSK;
    } else if (capbility.contains("WPA2-EAP")) {
        return SECURITY_EAP;
    } else if (capbility.contains("WEP")) {
        return SECURITY_WEP;
    } else if (capbility.contains("ESS")) {
        // 如果是ESS則沒有密碼
        return SECURITY_NONE;
    }
    return SECURITY_NONE;
}

JAVA程式碼連接WiFi

Android提供了兩種方式連接WiFi:

  • 通過配置連接
  • 通過networkId連接

封裝後的函數如下

// 使用 WifiConfiguration 連接. 
public static void connectByConfig(WifiManager manager, WifiConfiguration config) {
    if (manager == null) {
        return;
    }
    try {
        Method connect = manager.getClass().getDeclaredMethod("connect", WifiConfiguration.class, Class.forName("android.net.wifi.WifiManager$ActionListener"));
        if (connect != null) {
            connect.setAccessible(true);
            connect.invoke(manager, config, null);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
    
// 使用 networkId 連接. 
public static void connectByNetworkId(WifiManager manager, int networkId) {
    if (manager == null) {
        return;
    }
    try {
        Method connect = manager.getClass().getDeclaredMethod("connect", int.class, Class.forName("android.net.wifi.WifiManager$ActionListener"));
        if (connect != null) {
            connect.setAccessible(true);
            connect.invoke(manager, networkId, null);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

保存網路

// 保存網路. 
public static void saveNetworkByConfig(WifiManager manager, WifiConfiguration config) {
    if (manager == null) {
        return;
    }
    try {
        Method save = manager.getClass().getDeclaredMethod("save", WifiConfiguration.class, Class.forName("android.net.wifi.WifiManager$ActionListener"));
        if (save != null) {
            save.setAccessible(true);
            save.invoke(manager, config, null);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

添加網路

// 添加網路. 
public static int addNetwork(WifiManager manager, WifiConfiguration config) {
    if (manager != null) {
        manager.addNetwork(config);
    }
}

忘記網路

// 忘記網路.
public static void forgetNetwork(WifiManager manager, int networkId) {
    if (manager == null) {
        return;
    }
    try {
        Method forget = manager.getClass().getDeclaredMethod("forget", int.class, Class.forName("android.net.wifi.WifiManager$ActionListener"));
        if (forget != null) {
            forget.setAccessible(true);
            forget.invoke(manager, networkId, null);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

禁用網路

// 禁用網路. 
public static void disableNetwork(WifiManager manager, int netId) {
    if (manager == null) {
        return;
    }
    try {
        Method disable = manager.getClass().getDeclaredMethod("disable", int.class, Class.forName("android.net.wifi.WifiManager$ActionListener"));
        if (disable != null) {
            disable.setAccessible(true);
            disable.invoke(manager, networkId, null);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

斷開連接

// 斷開連接. 
public static boolean disconnectNetwork(WifiManager manager) {
    return manager != null && manager.disconnect();
}

短暫禁用網路

// 禁用短暫網路.
public static void disableEphemeralNetwork(WifiManager manager, String SSID) {
    if (manager == null || TextUtils.isEmpty(SSID)) 
        return;
        try {
        Method disableEphemeralNetwork = manager.getClass().getDeclaredMethod("disableEphemeralNetwork", String.class);
        if (disableEphemeralNetwork != null) {
            disableEphemeralNetwork.setAccessible(true);
            disableEphemeralNetwork.invoke(manager, SSID);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

監控WIFI變化

我們很有可能會有這樣的需求:在WIFI斷開或者連接的時候,將當前的WIFI數據保存下來
事實上Android中WIFI發生變化的時候,會發送廣播,我們只需要監聽系統中發送的WIFI變化的廣播就可以實現相關的功能了

開啟許可權

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

註冊監聽廣播

我們先使用動態註冊網路狀態的監聽廣播

PS:註冊監聽有兩種方式,無論使用哪種註冊方式均需要在AndroidMainest清單文件裡面進行註冊

  • 靜態註冊

也就是說在AndroidManifest文件中對BroadcastReceiver進行註冊,通常還會加上action用來過濾;此註冊方式即使退出應用後,仍然能夠收到相應的廣播

  • 動態註冊

調用Context中的registerReceiver對廣播進行動態註冊,使用unRegisterReceiver方法對廣播進行取消註冊的操作;故此註冊方式一般都是隨著所在的Activity或者應用銷毀以後,不會再收到該廣播

動態註冊的程式碼如下

@Override
protected void onStart() {
    super.onStart();
    IntentFilter filter = new IntentFilter();
    filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);

    registerReceiver(NetworkReceiver.getInstance(),filter);
}

然後寫具體的NetworkReceiver

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.widget.Toast;

import static android.net.wifi.WifiManager.WIFI_STATE_DISABLED;
import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
import static android.net.wifi.WifiManager.WIFI_STATE_UNKNOWN;

/**
 * @author panyi
 * @date 2022/8/23
 * 廣播接收器 用來監聽WIFI的變化
 */
public class NetworkReceiver extends BroadcastReceiver {

    private volatile static NetworkReceiver sInstance;


    public NetworkReceiver(){}

    public static NetworkReceiver getInstance(){
        if (sInstance == null) {
            synchronized (NetworkReceiver.class) {
                if (sInstance == null) {
                    sInstance = new NetworkReceiver();
                }
            }
        }
        return sInstance;
    }


    // WIFI連接狀態改變的監聽
    @Override
    public void onReceive(Context context, Intent intent) {
        String action=intent.getAction();
        if(action==WifiManager.WIFI_STATE_CHANGED_ACTION){
            switch(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WIFI_STATE_UNKNOWN)){
                case WIFI_STATE_ENABLED :// WIFI連接
                    Toast.makeText(context, "WiFi enabled", Toast.LENGTH_SHORT).show();
                    break;
                case WIFI_STATE_DISABLED:// WIFI斷開
                    Toast.makeText(context, "WiFi disabled", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    }
}

繼承BroadcastReceiver廣播監聽類之後重寫onReceive方法,根據監聽到的不同內容進行具體需求的修改即可

最後,隨著Android版本的不斷迭代,上述的方法也許在今後的某個時候就不適用了,如果到了這個時候,就去官方文檔裡面去尋找答案吧 😄
//developer.android.com/docs?hl=zh-cn

參考鏈接

END

建了一個微信的安全交流群,歡迎添加我微信備註進群,一起來聊天吹水哇,以及一個會發布安全相關內容的公眾號,歡迎關注 😃

GIF
GIF