如何判断当前 app 是运行在android真机,还是运行在模拟器? 可能做 framework 的朋友思维会更开阔一些,不过现在也可以跟我这门外汉一起来稍微了解下
攻略过程
android 有非常多的模拟器,我已知的有官方自带的 genymotion
模拟器,三方平台的夜神模拟器、天天模拟器等,所以想要完全鉴别出设备的运行环境,其实应该是存在一定问题的,我们只能说尽可能保证一定的容错率(我有想过很多应用平台提供的云机,但好像大多提供的都是真机,所以此项不在考虑范围之内)
基础思考
以下的一些思考主要结合了 、、一行代码帮你检测android模拟器、 等多篇新旧文章
- imei 设备识别码:用于唯一标识移动设备(
放弃
)
模拟器的 imei
可以修改,早期平板可能没有imei
,但是随着时代发展很多平板设备已拥有了属于自己的imei
- mac地址:物理地址,硬件地址,也称局域网地址,由网络设备制造商生产时烧录在网卡(
放弃
)
模拟器的mac地址是固定的几种,但是这些固定的地址随着模拟器类型递增,没有找到合适的,同时mac地址现在可以被模拟…
- 通过调用公开或者隐藏的系统api判断(
放弃
)
因为调用结果可以轻易被修改,比如直接修改android的源代码或者借助 xposed framework
进行修改(这种场景我虽未参与,但是应该可以参考java的反射机制)
- 功能验证:初期模拟器功能并不完善,可以采用类似
打电话、发短信
等方式进行功能测试,但是后续随着模拟器升级已补全对应功能 (放弃
)
public boolean issimulator1() {
string url = "tel:" + "10086";
intent intent = new intent();
intent.setdata(uri.parse(url));
intent.setaction(intent.action_dial);
// 是否可以处理跳转到拨号的 intent
boolean canresolveintent = intent.resolveactivity(mcontext.getpackagemanager()) != null;
return !canresolveintent;
}
- 设备ids、特有文件验证(
放弃
)
涉及到敏感权限时需要申请权限,根据授权结果容易出现误判,同时影响用户体验
private static string[]known_numbers = {"15555215554","15555215556",
"15555215558","15555215560","15555215562","15555215564",
"15555215566","15555215568","15555215570","15555215572",
"15555215574","15555215576","15555215578","15555215580",
"15555215582","15555215584",};
public static boolean checkphonenumber(context context){
telephonymanager telephonymanager =(telephonymanager)context
.getsystemservice(context.telephony_service);
string phonenumber =telephonymanager.getline1number();
for(string number :known_numbers){
if(number.equalsignorecase(phonenumber)){
log.v("result:","find phonenumber!");
return true;
}
}
log.v("result:","not find phonenumber!");
return false;
}
特有文件检测 - 权限要求
设备ids检测 - 权限要求
- cpu检测方法:现在的模拟器基本可以做到模拟手机号码,手机品牌,cpu信息等,比如逍遥/夜神模拟器读取
ro.product.board
进行了处理,能得到预先设置的cpu信息(放弃
)
public static boolean checkisnotrealphone() {
string cpuinfo = readcpuinfo();
if ((cpuinfo.contains("intel") || cpuinfo.contains("amd"))) {
return true;
}
return false;
}
public static string readcpuinfo() {
string result = "";
try {
string[] args = {"/system/bin/cat", "/proc/cpuinfo"};
processbuilder cmd = new processbuilder(args);
process process = cmd.start();
stringbuffer sb = new stringbuffer();
string readline = "";
bufferedreader responsereader = new bufferedreader(new inputstreamreader(process.getinputstream(), "utf-8"));
while ((readline = responsereader.readline()) != null) {
sb.append(readline);
}
responsereader.close();
result = sb.tostring().tolowercase();
} catch (ioexception ex) {
}
return result;
}
进阶思考
在一篇blog内有看到这样一副图,值得借鉴,因为我最后使用的 easyprotector
框架就做了硬件信息检测
tip:有兴趣的可以参考以下方法扩展 easyprotector
框架内的模拟器检测部分
模拟器框架文件
unexport void antiemulator::check_file() {
char *(path[]) = {
"/system/bin/androvm-prop", //检测androidvm
"/system/bin/microvirt-prop", //检测逍遥模拟器--新版本找不到特征
"/system/lib/libdroid4x.so", //检测海马模拟器
"/system/bin/windroyed", //检测文卓爷模拟器
"/system/bin/nox-prop", //检测夜神模拟器--某些版本找不到特征
"/system/lib/libnoxspeedup.so",//检测夜神模拟器
"/system/bin/ttvm-prop", //检测天天模拟器
"/data/.bluestacks.prop", //检测bluestacks模拟器 51模拟器
"/system/bin/duosconfig", //检测amiduos模拟器
"/system/etc/xxzs_prop.sh", //检测星星模拟器
"/system/etc/mumu-configs/device-prop-configs/mumu.config", //网易mumu模拟器
"/system/priv-app/ldappstore", //雷电模拟器
"/system/bin/ldinit", //雷电模拟器
"/system/bin/ldmountsf", //雷电模拟器
"/system/app/antstore", //小蚁模拟器
"/system/app/antlauncher", //小蚁模拟器
"vmos.prop", //vmos虚拟机
"fstab.titan", //光速虚拟机
"init.titan.rc", //光速虚拟机
"x8.prop", //x8沙箱和51虚拟机
"/system/lib/libc_malloc_debug_qemu.so", //avd qemu
"/system/bin/microvirtd",
"/dev/socket/qemud",
"/dev/qemu_pipe"};
for (int i = 0; i < sizeof(path) / sizeof(char*); i++){
if (syscall::check_file_or_dir_exists(path[i])){
logi("check_file %s file existing", path[i]);
// todo 风险
}
}
}
easyprotector 框架解析
我之所以在 github
选这个框架,主要有几点原因
- 模拟器检测框架有限
- 该框架star高
- 检测方面考虑全面(检测渠道、设备型号、硬件制造商、主板名称、基带信息、第三方应用数量、传感器数量、是否支持蓝牙、是否支持相机、是否支持闪光灯等)
- 能力之内可以适当扩展检测项
- 为了避免误判设备类型,内部加入条件判断,只有满足3项模拟器特征才会判定为模拟器
直接通过框架源码,查看下模拟器检测的执行过程
- 调用了
easyprotectorlib.checkisrunninginemulator
方法
- 在
easyprotectorlib
中找到了checkisrunninginemulator
实际调用了emulatorcheckutil
- 查看
emulatorcheckutil
-readsysproperty
源码即可
之前有提到功能扩展和延伸,大家可自行在该类源码中进行扩展,不过最好另起方法,有自信的话改原方法也行
剥离 easyprotector框架 模拟器检测功能
因为 easyprotector框架
中涉及的功能比较多,我习惯性只抽出了我所需要的部分
commandutil (公共、基础)
简单来看主要是通过反射机制获取一些系统的公共资源信息
import java.io.bufferedinputstream;
import java.io.bufferedoutputstream;
import java.io.ioexception;
/**
* project name:easyprotector
* package name:com.lahm.library
* created by lahm on 2018/6/8 16:23 .
*/
public class commandutil {
private commandutil() {
}
private static class singletonholder {
private static final commandutil instance = new commandutil();
}
public static final commandutil getsingleinstance() {
return singletonholder.instance;
}
public string getproperty(string propname) {
string value = null;
object rosecureobj;
try {
rosecureobj = class.forname("android.os.systemproperties")
.getmethod("get", string.class)
.invoke(null, propname);
if (rosecureobj != null) value = (string) rosecureobj;
} catch (exception e) {
value = null;
} finally {
return value;
}
}
public string exec(string command) {
bufferedoutputstream bufferedoutputstream = null;
bufferedinputstream bufferedinputstream = null;
process process = null;
try {
process = runtime.getruntime().exec("sh");
bufferedoutputstream = new bufferedoutputstream(process.getoutputstream());
bufferedinputstream = new bufferedinputstream(process.getinputstream());
bufferedoutputstream.write(command.getbytes());
bufferedoutputstream.write('\n');
bufferedoutputstream.flush();
bufferedoutputstream.close();
process.waitfor();
string outputstr = getstrfrombufferinputsteam(bufferedinputstream);
return outputstr;
} catch (exception e) {
return null;
} finally {
if (bufferedoutputstream != null) {
try {
bufferedoutputstream.close();
} catch (ioexception e) {
e.printstacktrace();
}
}
if (bufferedinputstream != null) {
try {
bufferedinputstream.close();
} catch (ioexception e) {
e.printstacktrace();
}
}
if (process != null) {
process.destroy();
}
}
}
private static string getstrfrombufferinputsteam(bufferedinputstream bufferedinputstream) {
if (null == bufferedinputstream) {
return "";
}
int buffer_size = 512;
byte[] buffer = new byte[buffer_size];
stringbuilder result = new stringbuilder();
try {
while (true) {
int read = bufferedinputstream.read(buffer);
if (read > 0) {
result.append(new string(buffer, 0, read));
}
if (read < buffer_size) {
break;
}
}
} catch (exception e) {
e.printstacktrace();
}
return result.tostring();
}
}
emulatorcheckutil (核心)
import android.content.context;
import android.content.pm.packagemanager;
import android.hardware.sensor;
import android.hardware.sensormanager;
import android.text.textutils;
import static android.content.context.sensor_service;
import static cn.com.huaan.fund.acts.base.safe.checkresult.result_emulator;
import static cn.com.huaan.fund.acts.base.safe.checkresult.result_maybe_emulator;
import static cn.com.huaan.fund.acts.base.safe.checkresult.result_unknown;
/**
* project name:easyprotector
* package name:com.lahm.library
* created by lahm on 2018/6/8 15:01 .
*/
public class emulatorcheckutil {
private emulatorcheckutil() {
}
private static class singletonholder {
private static final emulatorcheckutil instance = new emulatorcheckutil();
}
public static final emulatorcheckutil getsingleinstance() {
return singletonholder.instance;
}
public boolean readsysproperty(context context, emulatorcheckcallback callback) {
if (context == null)
throw new illegalargumentexception("context must not be null");
int suspectcount = 0;
//检测硬件名称
checkresult hardwareresult = checkfeaturesbyhardware();
switch (hardwareresult.result) {
case result_maybe_emulator:
++suspectcount;
break;
case result_emulator:
if (callback != null) callback.findemulator("hardware = " + hardwareresult.value);
return true;
}
//检测渠道
checkresult flavorresult = checkfeaturesbyflavor();
switch (flavorresult.result) {
case result_maybe_emulator:
++suspectcount;
break;
case result_emulator:
if (callback != null) callback.findemulator("flavor = " + flavorresult.value);
return true;
}
//检测设备型号
checkresult modelresult = checkfeaturesbymodel();
switch (modelresult.result) {
case result_maybe_emulator:
++suspectcount;
break;
case result_emulator:
if (callback != null) callback.findemulator("model = " + modelresult.value);
return true;
}
//检测硬件制造商
checkresult manufacturerresult = checkfeaturesbymanufacturer();
switch (manufacturerresult.result) {
case result_maybe_emulator:
++suspectcount;
break;
case result_emulator:
if (callback != null)
callback.findemulator("manufacturer = " + manufacturerresult.value);
return true;
}
//检测主板名称
checkresult boardresult = checkfeaturesbyboard();
switch (boardresult.result) {
case result_maybe_emulator:
++suspectcount;
break;
case result_emulator:
if (callback != null) callback.findemulator("board = " + boardresult.value);
return true;
}
//检测主板平台
checkresult platformresult = checkfeaturesbyplatform();
switch (platformresult.result) {
case result_maybe_emulator:
++suspectcount;
break;
case result_emulator:
if (callback != null) callback.findemulator("platform = " + platformresult.value);
return true;
}
//检测基带信息
checkresult basebandresult = checkfeaturesbybaseband();
switch (basebandresult.result) {
case result_maybe_emulator:
suspectcount += 2;//模拟器基带信息为null的情况概率相当大
break;
case result_emulator:
if (callback != null) callback.findemulator("baseband = " + basebandresult.value);
return true;
}
//检测传感器数量
int sensornumber = getsensornumber(context);
if (sensornumber <= 7) ++suspectcount;
//检测已安装第三方应用数量
int userappnumber = getuserappnumber();
if (userappnumber <= 5) ++suspectcount;
//检测是否支持闪光灯
boolean supportcameraflash = supportcameraflash(context);
if (!supportcameraflash) ++suspectcount;
//检测是否支持相机
boolean supportcamera = supportcamera(context);
if (!supportcamera) ++suspectcount;
//检测是否支持蓝牙
boolean supportbluetooth = supportbluetooth(context);
if (!supportbluetooth) ++suspectcount;
//检测光线传感器
boolean haslightsensor = haslightsensor(context);
if (!haslightsensor) ++suspectcount;
//检测进程组信息
checkresult cgroupresult = checkfeaturesbycgroup();
if (cgroupresult.result == result_maybe_emulator) ++suspectcount;
if (callback != null) {
stringbuffer stringbuffer = new stringbuffer("test start")
.append("\r\n").append("hardware = ").append(hardwareresult.value)
.append("\r\n").append("flavor = ").append(flavorresult.value)
.append("\r\n").append("model = ").append(modelresult.value)
.append("\r\n").append("manufacturer = ").append(manufacturerresult.value)
.append("\r\n").append("board = ").append(boardresult.value)
.append("\r\n").append("platform = ").append(platformresult.value)
.append("\r\n").append("baseband = ").append(basebandresult.value)
.append("\r\n").append("sensornumber = ").append(sensornumber)
.append("\r\n").append("userappnumber = ").append(userappnumber)
.append("\r\n").append("supportcamera = ").append(supportcamera)
.append("\r\n").append("supportcameraflash = ").append(supportcameraflash)
.append("\r\n").append("supportbluetooth = ").append(supportbluetooth)
.append("\r\n").append("haslightsensor = ").append(haslightsensor)
.append("\r\n").append("cgroupresult = ").append(cgroupresult.value)
.append("\r\n").append("suspectcount = ").append(suspectcount);
callback.findemulator(stringbuffer.tostring());
}
//嫌疑值大于3,认为是模拟器
return suspectcount > 3;
}
private int getuserappnum(string userapps) {
if (textutils.isempty(userapps)) return 0;
string[] result = userapps.split("package:");
return result.length;
}
private string getproperty(string propname) {
string property = commandutil.getsingleinstance().getproperty(propname);
return textutils.isempty(property) ? null : property;
}
/**
* 特征参数-硬件名称
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbyhardware() {
string hardware = getproperty("ro.hardware");
if (null == hardware) return new checkresult(result_maybe_emulator, null);
int result;
string tempvalue = hardware.tolowercase();
switch (tempvalue) {
case "ttvm"://天天模拟器
case "nox"://夜神模拟器
case "cancro"://网易mumu模拟器
case "intel"://逍遥模拟器
case "vbox":
case "vbox86"://腾讯手游助手
case "android_x86"://雷电模拟器
result = result_emulator;
break;
default:
result = result_unknown;
break;
}
return new checkresult(result, hardware);
}
/**
* 特征参数-渠道
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbyflavor() {
string flavor = getproperty("ro.build.flavor");
if (null == flavor) return new checkresult(result_maybe_emulator, null);
int result;
string tempvalue = flavor.tolowercase();
if (tempvalue.contains("vbox")) result = result_emulator;
else if (tempvalue.contains("sdk_gphone")) result = result_emulator;
else result = result_unknown;
return new checkresult(result, flavor);
}
/**
* 特征参数-设备型号
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbymodel() {
string model = getproperty("ro.product.model");
if (null == model) return new checkresult(result_maybe_emulator, null);
int result;
string tempvalue = model.tolowercase();
if (tempvalue.contains("google_sdk")) result = result_emulator;
else if (tempvalue.contains("emulator")) result = result_emulator;
else if (tempvalue.contains("android sdk built for x86")) result = result_emulator;
else result = result_unknown;
return new checkresult(result, model);
}
/**
* 特征参数-硬件制造商
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbymanufacturer() {
string manufacturer = getproperty("ro.product.manufacturer");
if (null == manufacturer) return new checkresult(result_maybe_emulator, null);
int result;
string tempvalue = manufacturer.tolowercase();
if (tempvalue.contains("genymotion")) result = result_emulator;
else if (tempvalue.contains("netease")) result = result_emulator;//网易mumu模拟器
else result = result_unknown;
return new checkresult(result, manufacturer);
}
/**
* 特征参数-主板名称
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbyboard() {
string board = getproperty("ro.product.board");
if (null == board) return new checkresult(result_maybe_emulator, null);
int result;
string tempvalue = board.tolowercase();
if (tempvalue.contains("android")) result = result_emulator;
else if (tempvalue.contains("goldfish")) result = result_emulator;
else result = result_unknown;
return new checkresult(result, board);
}
/**
* 特征参数-主板平台
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbyplatform() {
string platform = getproperty("ro.board.platform");
if (null == platform) return new checkresult(result_maybe_emulator, null);
int result;
string tempvalue = platform.tolowercase();
if (tempvalue.contains("android")) result = result_emulator;
else result = result_unknown;
return new checkresult(result, platform);
}
/**
* 特征参数-基带信息
*
* @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机
*/
private checkresult checkfeaturesbybaseband() {
string basebandversion = getproperty("gsm.version.baseband");
if (null == basebandversion) return new checkresult(result_maybe_emulator, null);
int result;
if (basebandversion.contains("1.0.0.0")) result = result_emulator;
else result = result_unknown;
return new checkresult(result, basebandversion);
}
/**
* 获取传感器数量
*/
private int getsensornumber(context context) {
sensormanager sm = (sensormanager) context.getsystemservice(sensor_service);
return sm.getsensorlist(sensor.type_all).size();
}
/**
* 获取已安装第三方应用数量
*/
private int getuserappnumber() {
string userapps = commandutil.getsingleinstance().exec("pm list package -3");
return getuserappnum(userapps);
}
/**
* 是否支持相机
*/
private boolean supportcamera(context context) {
return context.getpackagemanager().hassystemfeature(packagemanager.feature_camera);
}
/**
* 是否支持闪光灯
*/
private boolean supportcameraflash(context context) {
return context.getpackagemanager().hassystemfeature(packagemanager.feature_camera_flash);
}
/**
* 是否支持蓝牙
*/
private boolean supportbluetooth(context context) {
return context.getpackagemanager().hassystemfeature(packagemanager.feature_bluetooth);
}
/**
* 判断是否存在光传感器来判断是否为模拟器
* 部分真机也不存在温度和压力传感器。其余传感器模拟器也存在。
*
* @return false为模拟器
*/
private boolean haslightsensor(context context) {
sensormanager sensormanager = (sensormanager) context.getsystemservice(sensor_service);
sensor sensor = sensormanager.getdefaultsensor(sensor.type_light); //光线传感器
if (null == sensor) return false;
else return true;
}
/**
* 特征参数-进程组信息
*/
private checkresult checkfeaturesbycgroup() {
string filter = commandutil.getsingleinstance().exec("cat /proc/self/cgroup");
if (null == filter) return new checkresult(result_maybe_emulator, null);
return new checkresult(result_unknown, filter);
}
}
emulatorcheckcallback (配置)
回调监听,可以获取到具体检测结果
public interface emulatorcheckcallback {
void findemulator(string emulatorinfo);
}
checkresult(配置)
对检测结果进行类别划分,方便管理
public class checkresult {
public static final int result_maybe_emulator = 0;//可能是模拟器
public static final int result_emulator = 1;//模拟器
public static final int result_unknown = 2;//可能是真机
public int result;
public string value;
public checkresult(int result, string value) {
this.result = result;
this.value = value;
}
}
调用方式
val readsysproperty = emulatorcheckutil.getsingleinstance().readsysproperty(context, null)
if (readsysproperty) {
//根据需要进行风险提示等相关业务
toastutils.showtoast("您当前可能运行在模拟器设备,请谨防安全风险!")
}
发表评论