一行程式碼幫你檢測Android模擬器

lamster2018發表於2018-07-13
宣告:本文章獨家授權微信公眾號碼個蛋原創推文

目錄

  • 簡介
  • 初代常規手段
  • 進階手段
  • 改良手段和新思路
  • 最終方案
  • 測試結果
  • Demo地址

簡介

最近有業務上的要求,要求app在本地進行諸如軟體多開、hook框架、模擬器等安全檢測,防止作弊行為。

防作弊一直是老生常談的問題,而模擬器的檢測往往是防作弊中的重要一環,但在查詢資料的過程中發現,網上的模擬器檢測方案已經有些過時了,只能自己再跟進學習,本文對這次學習內容進行總結: 模擬器的檢測秉持一句話:抓取特徵值與真機比較。

初代常規手段

早期模擬器沒那麼多套路,特徵值非常明顯,某些值甚至是一長串的0,檢測起來很方便,常規的方案如

  • 檢查手機IMEI等一系列編號
TelephonyManager tm = (TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE);  
  String deviceid = tm.getDeviceId();//獲取IMEI號  
  String te1  = tm.getLine1Number();//獲取本機號碼  
  String imei = tm.getSimSerialNumber();//獲得SIM卡的序號  
  String imsi = tm.getSubscriberId();//得到使用者Id 
 
複製程式碼
  • 讀取手機品牌資訊
	android.os.Build.BRAND,
	android.os.Build.MANUFACTURER,
	android.os.Build.MODEL
	...
複製程式碼
  • 檢查cpu資訊
		String value = null;
        Object roSecureObj;
        try {
            roSecureObj = Class.forName("android.os.SystemProperties")
                    .getMethod("get", String.class)
                    .invoke(null, "ro.product.board");
            if (roSecureObj != null) value = (String) roSecureObj;
        } catch (Exception e) {
            value = null;
        } finally {
            return value;
        }

複製程式碼

優點:通過檢查真機上最直白的幾個特徵,就可以完成模擬器的檢測

缺點:

1.現在的模擬器基本可以做到模擬手機號碼,手機品牌,cpu資訊等,比如逍遙/夜神模擬器讀取ro.product.board進行了處理,能得到預先設定的cpu資訊;

2.真機的手機號碼也不一定就能拿到(比如電信卡);

3.拿手機號碼這個需要許可權,使用者不一定喜歡。

所以決定棄用以上方案。

進階手段

再思考真機上的特徵,進一步我們有通過檢查硬體資訊的思路,形如藍芽,語音輸入裝置,wlan,相機等

  • 檢查mac地址
		Enumeration networkInterfaces;
        String str = null;
        networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while (networkInterfaces.hasMoreElements()) {
            NetworkInterface networkInterface = (NetworkInterface) networkInterfaces.nextElement();
            if (networkInterface != null) {
                byte[] hardwareAddress;
                byte[] bArr = new byte[0];
                hardwareAddress = networkInterface.getHardwareAddress();
                if (!(hardwareAddress == null || hardwareAddress.length == 0)) {
                    String str2;
                    StringBuilder stringBuilder = new StringBuilder();
                 ...//方法太長
                }
            }
        }
}
複製程式碼
  • 檢查電池溫度,輪詢檢查電量,充電狀態
		IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent batteryStatus = context.registerReceiver(null, filter);
        if (batteryStatus == null) return false;
        int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
        return chargePlug == BatteryManager.BATTERY_PLUGGED_USB;//檢測usb充電
複製程式碼

優點:比初代方案有深入;

缺點: 1.mac地址現在可以被模擬,且獲取mac地址的程式碼有點長(M以下版本還要傳context)寫起來不不優雅;

2.通過電池資訊來準確檢測,需要一定的時間間隔,屬於非實時方案;

3.藍芽和相機需要新增相應許可權。

所以不推薦整合。

改進方案和新的研究

在研究各個模擬器的過程中,尤其是在研究build.prop檔案時,發現以下(但不限於)問題 1.基帶資訊幾乎沒有;

2.處理器資訊ro.product.board和ro.board.platform有衝突或者不一致;

3.部分模擬器在讀控制組資訊時讀取不到;

4.連上wifi但會出現 Link encap:UNSPEC未指定網路卡型別的情況。

藉著問題依次進行解析。

  • 基帶資訊

基帶是手機上的一塊電路板,刷基帶實際上就是刷這個電路的控制軟體。

我是這樣去理解模擬器沒有基帶資訊的情況"因為模擬器沒有真實的電路板(基帶電路),所以沒法刷基帶軟體進去,所以沒辦法得到基帶資訊",不知道這樣理解對不對,歡迎拍磚。

當然了,部分真機在刷機失敗的時候也會出現丟失基帶的情況,這部分機器我們不多討論。

		try {
            roSecureObj = Class.forName("android.os.SystemProperties")
                    .getMethod("get", String.class)
                    .invoke(null, "gsm.version.baseband");
            if (roSecureObj != null) value = (String) roSecureObj;
        } catch (Exception e) {
            value = null;
        }
複製程式碼
  • 處理器資訊

最簡單的方法就是直接拿android.os.Build.BOARD,實際上也是去讀取ro.product.board值,

這個值代表cpu型號,比如msm8998是驍龍835,hi3650是麒麟950。

這個值真機幾乎不為空,AS模擬器會有如gphone的特徵值,部分模擬器上是可以隨時變更的(因為拿模擬器來玩高幀率模式的手遊)。

可是還有一個ro.board.platform值,這個值代表主機板平臺,極少的模擬器會去更改這個值,甚至有的模擬器沒有這個值,一般來說真機的兩值相等。

當然真機也有例外,測試機一加5T兩者都是msm8998,而華為P9 board值EVA-AL10,platform值hi3650。

兩者不一致

根據處理器資訊做一個檢測指標。

String productBoard = CommandUtil.getSingleInstance().getProperty("ro.product.board");
if (productBoard == null | "".equals(productBoard)) ++suspectCount;

String boardPlatform = CommandUtil.getSingleInstance().getProperty("ro.board.platform");
if (boardPlatform == null | "".equals(boardPlatform)) ++suspectCount;

if (productBoard != null && boardPlatform != null && !productBoard.equals(boardPlatform))
    ++suspectCount;
複製程式碼
  • 渠道資訊

渠道資訊是ro.build.flavor值,在有限的真機和模擬機器的測試情況下,有以下推測 『真機基本上都有這個值,部分模擬器沒有這個值,基於vbox的模擬器上有特徵值:vbox』

vbox特徵值,平臺資訊空

根據渠道資訊做一個檢測指標

String buildFlavor = CommandUtil.getSingleInstance().getProperty("ro.build.flavor");
if (buildFlavor == null | "".equals(buildFlavor) | (buildFlavor != null && buildFlavor.contains("vbox")))
    ++suspectCount;
複製程式碼
  • 程式組資訊

利用讀取maps檔案檢測軟體多開的時候,在部分模擬器上卻遇到了runtimeException異常。 原因是讀取/proc/self/cgroup程式組資訊的時候,部分模擬器沒有這個值,因為個人水平有限,暫時不知道原因是什麼,不過卻剛好拿這個做檢測方案。

關鍵程式碼
process = Runtime.getRuntime().exec("sh");
            bufferedOutputStream = new BufferedOutputStream(process.getOutputStream());
            bufferedInputStream = new BufferedInputStream(process.getInputStream());
            bufferedOutputStream.write("cat /proc/self/cgroup");
            bufferedOutputStream.write('\n');
            bufferedOutputStream.flush();
複製程式碼
  • wlan驅動未指定異常

Android離不開unix,所以嘗試了adb shell 執行指令。執行ifconfig時,發現在連線wifi的情況下,AS模擬器顯示 『wlan0 Link encap:UNSPEC』 未指定網路卡型別,而真機情況下是『wlan0 Link encap:Ethernet』乙太網。

AS模擬器的wlan情況

不過接著測試非wifi情況下,該值都拿不到,所以不推薦使用。

最終方案

結合以上研究,得出一個嫌疑指數,綜合判斷是否執行在模擬器中。

*EasyProtectorLib.checkIsRunningInEmulator()*的程式碼實現如下

    public boolean readSysProperty() {
        int suspectCount = 0;
        //讀基帶資訊
        String basebandVersion = CommandUtil.getSingleInstance().getProperty("gsm.version.baseband");
        if (TextUtils.isEmpty(baseBandVersion))
           ++suspectCount;
        //讀渠道資訊,針對一些基於vbox的模擬器
        String buildFlavor = CommandUtil.getSingleInstance().getProperty("ro.build.flavor");
        if (TextUtils.isEmpty(buildFlavor) | (buildFlavor != null && buildFlavor.contains("vbox")))
            ++suspectCount;
        //讀處理器資訊,這裡經常會被處理
        String productBoard = CommandUtil.getSingleInstance().getProperty("ro.product.board");
        if (TextUtils.isEmpty(productBoard) | (productBoard != null && productBoard.contains("android")))
            ++suspectCount;
        //讀處理器平臺,這裡不常會處理
        String boardPlatform = CommandUtil.getSingleInstance().getProperty("ro.board.platform");
        if (TextUtils.isEmpty(boardPlatform) | (boardPlatform != null && boardPlatform.contains("android"))) 
            ++suspectCount;
        //高通的cpu兩者資訊一般是一致的
       if (!TextUtils.isEmpty(productBoard)
                && !TextUtils.isEmpty(boardPlatform)
                && !productBoard.equals(boardPlatform))
            ++suspectCount;
        //一些模擬器讀取不到程式租資訊
        String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");
        if (filter == null || filter.length() == 0) ++suspectCount;

        return suspectCount > 2;
    }
複製程式碼

以下是測試情況*

機器/測試方案 基帶資訊 渠道資訊 處理器資訊 程式組 檢測結果
AS自帶模擬器 O O O X 模擬器
Genymotion2.12.1 O O O X 模擬器
逍遙模擬器5.3.2 O X X O 模擬器
Appetize O X O X 模擬器
夜神模擬器6.1.1 O O O O 模擬器
騰訊手遊助手2.0.5 O O O X 模擬器
雷電模擬器3.27 O X X X 真機*
一加5T X X X X 真機
華為P9 X X O X 真機

*O代表該方案檢測為模擬器,X代表檢測正常

*Xamarin/Manymo因為網路原因暫未進行測試

*雷電模擬器後續修復

*因安卓機型太廣,真機覆蓋測試不完全,有空大家去git提issue

Demo地址

本文方案已經整合到EasyProtectorLib

github地址: github.com/lamster2018…

中文文件見:www.jianshu.com/p/c37b1bdb4…

Todo

1.檢測到多開應該提供回撥給開發者自行處理;

相關文章