在很多安全机构的检测中,关于模拟器的运行环境一般也会做监听处理,有的可能允许执行但是会提示用户,有的可能直接禁止在模拟器上运行我方APP
如何判断当前 app 是运行在Android真机,还是运行在模拟器? 可能做 Framework 的朋友思维会更开阔一些,不过现在也可以跟我这门外汉一起来稍微了解下
攻略过程
其实我已经很久没有用过模拟器了,不过可以肯定的是模拟器与真机的本质区别大概率在于运行载体
Android 有非常多的模拟器,我已知的有官方自带的
Genymotion
模拟器,三方平台的夜神模拟器、天天模拟器等,所以想要完全鉴别出设备的运行环境,其实应该是存在一定问题的,我们只能说尽可能保证一定的容错率(我有想过很多应用平台提供的云机,但好像大多提供的都是真机,所以此项不在考虑范围之内)
基础思考
以下的一些思考主要结合了 如何判断是否是模拟器还是真机、全面检测设备是否模拟器、一行代码帮你检测Android模拟器、安卓逆向环境检测–模拟器 等多篇新旧文章
- IMEI 设备识别码:用于唯一标识移动设备(**
放弃
**)
模拟器的
IMEI
可以修改,早期平板可能没有
IMEI
,但是随着时代发展很多平板设备已拥有了属于自己的
IMEI
- MAC地址:物理地址,硬件地址,也称局域网地址,由网络设备制造商生产时烧录在网卡(**
放弃
**)
模拟器的MAC地址是固定的几种,但是这些固定的地址随着模拟器类型递增,没有找到合适的,同时mac地址现在可以被模拟…
- 通过调用公开或者隐藏的系统API判断(**
放弃
**)
因为调用结果可以轻易被修改,比如直接修改Android的源代码或者借助 Xposed
Framework
进行修改(这种场景我虽未参与,但是应该可以参考Java的反射机制)
- 功能验证:初期模拟器功能并不完善,可以采用类似
打电话、发短信
等方式进行功能测试,但是后续随着模拟器升级已补全对应功能 (放弃
)
publicbooleanisSimulator1(){String url ="tel:"+"10086";Intent intent =newIntent();
intent.setData(Uri.parse(url));
intent.setAction(Intent.ACTION_DIAL);// 是否可以处理跳转到拨号的 Intentboolean 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!");returntrue;}}
Log.v("Result:","Not Find PhoneNumber!");returnfalse;}
特有文件检测 - 权限要求
设备IDS检测 - 权限要求
- CPU检测方法:现在的模拟器基本可以做到模拟手机号码,手机品牌,cpu信息等,比如逍遥/夜神模拟器读取
ro.product.board
进行了处理,能得到预先设置的cpu信息(**放弃
**)
publicstaticbooleancheckIsNotRealPhone(){String cpuInfo =readCpuInfo();if((cpuInfo.contains("intel")|| cpuInfo.contains("amd"))){returntrue;}returnfalse;}publicstaticStringreadCpuInfo(){String result ="";try{String[] args ={"/system/bin/cat","/proc/cpuinfo"};ProcessBuilder cmd =newProcessBuilder(args);Process process = cmd.start();StringBuffer sb =newStringBuffer();String readLine ="";BufferedReader responseReader =newBufferedReader(newInputStreamReader(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 框架解析
关于
easyprotector框架
文档可以参考 一行代码帮你检测Android模拟器(更新至1.1.0) 会更详细一些
我之所以在
github
选这个框架,主要有几点原因
- 模拟器检测框架有限
- 该框架star高
- 检测方面考虑全面(检测渠道、设备型号、硬件制造商、主板名称、基带信息、第三方应用数量、传感器数量、是否支持蓝牙、是否支持相机、是否支持闪光灯等)
- 能力之内可以适当扩展检测项
- 为了避免误判设备类型,内部加入条件判断,只有满足3项模拟器特征才会判定为模拟器
直接通过框架源码,查看下模拟器检测的执行过程
- 调用了
EasyProtectorLib.checkIsRunningInEmulator
方法
- 在
EasyProtectorLib
中找到了checkIsRunningInEmulator
实际调用了EmulatorCheckUtil
- 查看
EmulatorCheckUtil
-readSysProperty
源码即可
源码中
ro.build
、
ro.product
、
gsm.version
含义,做Framework朋友可能比较了解,主要用于检测一些系统级信息
之前有提到功能扩展和延伸,大家可自行在该类源码中进行扩展,不过最好另起方法,有自信的话改原方法也行
剥离 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 .
*/publicclass CommandUtil {privateCommandUtil(){}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){returnnull;}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 .
*/publicclass EmulatorCheckUtil {privateEmulatorCheckUtil(){}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);returntrue;}//检测渠道
CheckResult flavorResult =checkFeaturesByFlavor();switch(flavorResult.result){
case RESULT_MAYBE_EMULATOR:++suspectCount;break;
case RESULT_EMULATOR:if(callback !=null) callback.findEmulator("flavor = "+ flavorResult.value);returntrue;}//检测设备型号
CheckResult modelResult =checkFeaturesByModel();switch(modelResult.result){
case RESULT_MAYBE_EMULATOR:++suspectCount;break;
case RESULT_EMULATOR:if(callback !=null) callback.findEmulator("model = "+ modelResult.value);returntrue;}//检测硬件制造商
CheckResult manufacturerResult =checkFeaturesByManufacturer();switch(manufacturerResult.result){
case RESULT_MAYBE_EMULATOR:++suspectCount;break;
case RESULT_EMULATOR:if(callback !=null)
callback.findEmulator("manufacturer = "+ manufacturerResult.value);returntrue;}//检测主板名称
CheckResult boardResult =checkFeaturesByBoard();switch(boardResult.result){
case RESULT_MAYBE_EMULATOR:++suspectCount;break;
case RESULT_EMULATOR:if(callback !=null) callback.findEmulator("board = "+ boardResult.value);returntrue;}//检测主板平台
CheckResult platformResult =checkFeaturesByPlatform();switch(platformResult.result){
case RESULT_MAYBE_EMULATOR:++suspectCount;break;
case RESULT_EMULATOR:if(callback !=null) callback.findEmulator("platform = "+ platformResult.value);returntrue;}//检测基带信息
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);returntrue;}//检测传感器数量
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))return0;
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;elseif(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;elseif(tempValue.contains("emulator")) result = RESULT_EMULATOR;elseif(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;elseif(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;elseif(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");returngetUserAppNum(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)returnfalse;elsereturntrue;}/**
* 特征参数-进程组信息
*/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 (配置)
回调监听,可以获取到具体检测结果
publicinterface EmulatorCheckCallback {
void findEmulator(String emulatorInfo);}
CheckResult(配置)
对检测结果进行类别划分,方便管理
publicclass 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;publicCheckResult(int result, String value){this.result = result;this.value = value;}}
调用方式
val readSysProperty = EmulatorCheckUtil.getSingleInstance().readSysProperty(context,null)if(readSysProperty){//根据需要进行风险提示等相关业务
ToastUtils.showToast("您当前可能运行在模拟器设备,请谨防安全风险!")}
版权归原作者 远方那座山 所有, 如有侵权,请联系我们删除。