聲明:本文章獨(dú)家授權(quán)微信公眾號碼個蛋原創(chuàng)推文
目錄
- 簡介
- 初代常規(guī)手段
- 進(jìn)階手段
- 改良手段和新思路
- 最終方案
- 測試結(jié)果
- Demo地址
簡介
最近有業(yè)務(wù)上的要求圾结,要求app在本地進(jìn)行諸如軟件多開渣触、hook框架、模擬器等安全檢測届腐,防止作弊行為。
防作弊一直是老生常談的問題蜂奸,而模擬器的檢測往往是防作弊中的重要一環(huán)犁苏,但在查找資料的過程中發(fā)現(xiàn),網(wǎng)上的模擬器檢測方案已經(jīng)有些過時了扩所,只能自己再跟進(jìn)學(xué)習(xí)围详,本文對這次學(xué)習(xí)內(nèi)容進(jìn)行總結(jié):
模擬器的檢測秉持一句話:抓取特征值與真機(jī)比較。
初代常規(guī)手段
早期模擬器沒那么多套路,特征值非常明顯助赞,某些值甚至是一長串的0买羞,檢測起來很方便,常規(guī)的方案如
- 檢查手機(jī)IMEI等一系列編號
TelephonyManager tm = (TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE);
String deviceid = tm.getDeviceId();//獲取IMEI號
String te1 = tm.getLine1Number();//獲取本機(jī)號碼
String imei = tm.getSimSerialNumber();//獲得SIM卡的序號
String imsi = tm.getSubscriberId();//得到用戶Id
- 讀取手機(jī)品牌信息
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;
}
優(yōu)點(diǎn):通過檢查真機(jī)上最直白的幾個特征雹食,就可以完成模擬器的檢測
缺點(diǎn):
1.現(xiàn)在的模擬器基本可以做到模擬手機(jī)號碼畜普,手機(jī)品牌,cpu信息等群叶,比如逍遙/夜神模擬器讀取ro.product.board進(jìn)行了處理吃挑,能得到預(yù)先設(shè)置的cpu信息;
2.真機(jī)的手機(jī)號碼也不一定就能拿到(比如電信卡);
3.拿手機(jī)號碼這個需要權(quán)限街立,用戶不一定喜歡舶衬。
所以決定棄用以上方案。
進(jìn)階手段
再思考真機(jī)上的特征赎离,進(jìn)一步我們有通過檢查硬件信息的思路逛犹,形如藍(lán)牙,語音輸入設(shè)備梁剔,wlan虽画,相機(jī)等
- 檢查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();
...//方法太長
}
}
}
}
- 檢查電池溫度,輪詢檢查電量荣病,充電狀態(tài)
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充電
優(yōu)點(diǎn):比初代方案有深入狸捕;
缺點(diǎn):
1.mac地址現(xiàn)在可以被模擬,且獲取mac地址的代碼有點(diǎn)長(M以下版本還要傳context)寫起來不不優(yōu)雅众雷;
2.通過電池信息來準(zhǔn)確檢測灸拍,需要一定的時間間隔,屬于非實(shí)時方案砾省;
3.藍(lán)牙和相機(jī)需要添加相應(yīng)權(quán)限鸡岗。
所以不推薦集成。
改進(jìn)方案和新的研究
在研究各個模擬器的過程中编兄,尤其是在研究build.prop文件時轩性,發(fā)現(xiàn)以下(但不限于)問題
1.基帶信息幾乎沒有;
2.處理器信息ro.product.board和ro.board.platform有沖突或者不一致狠鸳;
3.部分模擬器在讀控制組信息時讀取不到揣苏;
4.連上wifi但會出現(xiàn) Link encap:UNSPEC未指定網(wǎng)卡類型的情況。
借著問題依次進(jìn)行解析件舵。
- 基帶信息
基帶是手機(jī)上的一塊電路板卸察,刷基帶實(shí)際上就是刷這個電路的控制軟件。
我是這樣去理解模擬器沒有基帶信息的情況"因?yàn)槟M器沒有真實(shí)的電路板(基帶電路)铅祸,所以沒法刷基帶軟件進(jìn)去坑质,所以沒辦法得到基帶信息"合武,不知道這樣理解對不對,歡迎拍磚涡扼。
當(dāng)然了稼跳,部分真機(jī)在刷機(jī)失敗的時候也會出現(xiàn)丟失基帶的情況,這部分機(jī)器我們不多討論吃沪。
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汤善,實(shí)際上也是去讀取ro.product.board值,
這個值代表cpu型號票彪,比如msm8998是驍龍835萎津,hi3650是麒麟950。
這個值真機(jī)幾乎不為空抹镊,AS模擬器會有如gphone的特征值,部分模擬器上是可以隨時變更的(因?yàn)槟媚M器來玩高幀率模式的手游)荤傲。
可是還有一個ro.board.platform值垮耳,這個值代表主板平臺,極少的模擬器會去更改這個值遂黍,甚至有的模擬器沒有這個值终佛,一般來說真機(jī)的兩值相等。
當(dāng)然真機(jī)也有例外雾家,測試機(jī)一加5T兩者都是msm8998铃彰,而華為P9 board值EVA-AL10,platform值hi3650。
根據(jù)處理器信息做一個檢測指標(biāo)芯咧。
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值牙捉,在有限的真機(jī)和模擬機(jī)器的測試情況下,有以下推測
『真機(jī)基本上都有這個值敬飒,部分模擬器沒有這個值邪铲,基于vbox的模擬器上有特征值:vbox』
根據(jù)渠道信息做一個檢測指標(biāo)
String buildFlavor = CommandUtil.getSingleInstance().getProperty("ro.build.flavor");
if (buildFlavor == null | "".equals(buildFlavor) | (buildFlavor != null && buildFlavor.contains("vbox")))
++suspectCount;
- 進(jìn)程組信息
利用讀取maps文件檢測軟件多開的時候无拗,在部分模擬器上卻遇到了runtimeException異常带到。
原因是讀取/proc/self/cgroup進(jìn)程組信息的時候,部分模擬器沒有這個值英染,因?yàn)閭€人水平有限揽惹,暫時不知道原因是什么,不過卻剛好拿這個做檢測方案四康。
關(guān)鍵代碼
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驅(qū)動未指定異常
Android離不開unix搪搏,所以嘗試了adb shell 運(yùn)行指令。運(yùn)行ifconfig時闪金,發(fā)現(xiàn)在連接wifi的情況下慕嚷,AS模擬器顯示 『wlan0 Link encap:UNSPEC』 未指定網(wǎng)卡類型,而真機(jī)情況下是『wlan0 Link encap:Ethernet』以太網(wǎng)。
不過接著測試非wifi情況下喝检,該值都拿不到嗅辣,所以不推薦使用。
新增思路
增加特定值的檢測挠说,比如天天模擬器的hardware是ttVM
傳感器的檢測澡谭,部分模擬器的傳感器個數(shù)只有1個
安裝包的檢測,模擬器的用戶預(yù)裝app很少
最終方案
結(jié)合以上研究损俭,得出一個嫌疑指數(shù)蛙奖,綜合判斷是否運(yùn)行在模擬器中。
EasyProtectorLib.checkIsRunningInEmulator()的代碼實(shí)現(xiàn)如下
@Deprecated
public boolean readSysProperty() {
return readSysProperty(null, null);
}
public boolean readSysProperty(Context context, EmulatorCheckCallback callback) {
this.emulatorCheckCallback = callback;
int suspectCount = 0;
String baseBandVersion = getProperty("gsm.version.baseband");
if (null == baseBandVersion || baseBandVersion.contains("1.0.0.0"))
++suspectCount;//基帶信息
String buildFlavor = getProperty("ro.build.flavor");
if (null == buildFlavor || buildFlavor.contains("vbox") || buildFlavor.contains("sdk_gphone"))
++suspectCount;//渠道
String productBoard = getProperty("ro.product.board");
if (null == productBoard || productBoard.contains("android") | productBoard.contains("goldfish"))
++suspectCount;//芯片
String boardPlatform = getProperty("ro.board.platform");
if (null == boardPlatform || boardPlatform.contains("android"))
++suspectCount;//芯片平臺
String hardWare = getProperty("ro.hardware");
if (null == hardWare) ++suspectCount;
else if (hardWare.toLowerCase().contains("ttvm")) suspectCount += 10;//天天
else if (hardWare.toLowerCase().contains("nox")) suspectCount += 10;//夜神
String cameraFlash = "";
String sensorNum = "sensorNum";
if (context != null) {
boolean isSupportCameraFlash = context.getPackageManager().hasSystemFeature("android.hardware.camera.flash");//是否支持閃光燈
if (!isSupportCameraFlash) ++suspectCount;
cameraFlash = isSupportCameraFlash ? "support CameraFlash" : "unsupport CameraFlash";
SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
int sensorSize = sm.getSensorList(Sensor.TYPE_ALL).size();
if (sensorSize < 7) ++suspectCount;//傳感器個數(shù)
sensorNum = sensorNum + sensorSize;
}
String userApps = CommandUtil.getSingleInstance().exec("pm list package -3");
String userAppNum = "userAppNum";
int userAppSize = getUserAppNums(userApps);
if (userAppSize < 5) ++suspectCount;//用戶安裝的app個數(shù)
userAppNum = userAppNum + userAppSize;
String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");
if (null == filter) ++suspectCount;//進(jìn)程租
if (callback != null) {
StringBuffer stringBuffer = new StringBuffer("ceshi start|")
.append(baseBandVersion).append("|")
.append(buildFlavor).append("|")
.append(productBoard).append("|")
.append(boardPlatform).append("|")
.append(hardWare).append("|")
.append(cameraFlash).append("|")
.append(sensorNum).append("|")
.append(userAppNum).append("|")
.append(filter).append("|end");
callback.findEmulator(stringBuffer.toString());
}
return suspectCount > 3;
}
以下是測試情況*
機(jī)器/測試方案 | 檢測結(jié)果 |
---|---|
AS自帶模擬器 9.0 | 模擬器 |
Genymotion2.12.1 | 模擬器 |
逍遙模擬器6.0.0 | 模擬器 |
Appetize | 模擬器 |
夜神模擬器6.2.5.3010 | 模擬器 |
騰訊手游助手2.0.6.8 | 模擬器 |
雷電模擬器3.41 | 模擬器 |
木木模擬器2.0.25 | 模擬器 |
一加5T | 真機(jī) |
華為P9 | 真機(jī) |
*因安卓機(jī)型太廣杆兵,真機(jī)覆蓋測試不完全雁仲,有空大家去git提issue
Demo地址
本文方案已經(jīng)集成到EasyProtectorLib
github地址: https://github.com/lamster2018/EasyProtector
中文文檔見:http://www.reibang.com/p/c37b1bdb4757
Todo
1.檢測到多開應(yīng)該提供回調(diào)給開發(fā)者自行處理;---v1.0.4 support