检测App是否在Android模拟器中运行

引言

在Android开发中,经常会使用到Android模拟器,普通用户也可能由于游戏等其他需求而使用Android模拟器。

但是,由于模拟器往往与实际真机有差异,会存在使用模拟器刷单、频繁大量请求等恶意行为,于是就产生了区分模拟器与真机的需求。

在此,提供了区分模拟器与真机的方法,并提供了Java工具类 EmulatorHelper,方便各位开发同事在业务需求中调用以区分当前程序是否运行在模拟器当中。

以下是该工具类实现思路的说明,如有不妥当之处,欢迎指出并与我交流。

实现思路

区分模拟器的基本思路就是根据Android的Build类中一些与硬件相关联的常量及系统参数,在真机与模拟器中值的不同,从而区分模拟器。

但是实际使用中,现在的模拟器往往都会支持修改这当中的一些值,导致只是用静态变量及系统参数的方法准确率不高,因此还需要辅助其他方法(例如:用户的行为、检查传感器、基带信息、进程信息等),再综合判断:

硬件名称检查

硬件名称(ro.hardware),是安卓系统变量文件build.prop中的一个参数,用于描述该硬件的名称,而部分模拟器的某些版本(早期版本),该值的内容是有特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值是可以在模拟器中进行修改的,因此该因素判断的准确率一般。

/**
 * 特征参数-硬件名称
 */
private EmulatorCheckResult checkFeaturesByHardware() {
    String hardware = getProperty("ro.hardware");
 if (null == hardware) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result 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.RESULT_CONFIRM_EMULATOR;
 break;
 default:
            result = Result.RESULT_UNKNOWN;
 break;
 }
    return new EmulatorCheckResult(result, hardware);
}

发布渠道检查

设备发布渠道信息(ro.build.flavor),是安卓系统变量文件build.prop中的一个参数,用于描述该设备ISO发布时的渠道,而在部分模拟器中,该值的内容是有特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。

/**
 * 特征参数-渠道
 */
private EmulatorCheckResult checkFeaturesByFlavor() {
    String flavor = getProperty("ro.build.flavor");
 if (null == flavor) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = flavor.toLowerCase();
 if (tempValue.contains("vbox")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("sdk_gphone")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, flavor);
}

设备型号检查

设备型号(ro.product.model),是安卓系统变量文件build.prop中的一个参数,用于描述该设备型号,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。

/**
 * 特征参数-设备型号
 */
private EmulatorCheckResult checkFeaturesByModel() {
    String model = getProperty("ro.product.model");
 if (null == model) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = model.toLowerCase();
 if (tempValue.contains("google_sdk")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("emulator")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("android sdk built for x86")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, model);
}

硬件制造商检查

硬件制造商(ro.product.manufacturer),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的制造商,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。

/**
 * 特征参数-硬件制造商
 */
private EmulatorCheckResult checkFeaturesByManufacturer() {
    String manufacturer = getProperty("ro.product.manufacturer");
 if (null == manufacturer) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = manufacturer.toLowerCase();
 if (tempValue.contains("genymotion")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("netease")) {
        //网易MUMU模拟器
 result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, manufacturer);
}

主板名称检查

主板名称(ro.product.board),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的主板名称信息,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。

/**
 * 特征参数-主板名称
 */
private EmulatorCheckResult checkFeaturesByBoard() {
    String board = getProperty("ro.product.board");
 if (null == board) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = board.toLowerCase();
 if (tempValue.contains("android")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("goldfish")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, board);
}

主板平台检查

主板平台(ro.product.platform),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的主板平台信息,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。

/**
 * 特征参数-主板平台
 */
private EmulatorCheckResult checkFeaturesByPlatform() {
    String platform = getProperty("ro.board.platform");
 if (null == platform) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = platform.toLowerCase();
 if (tempValue.contains("android")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, platform);
}

基带信息检查

基带信息(gsm.version.baseband),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的基带信息,部分模拟器中该值是存在特定值的(AS自带模拟器),由于该值是在基带芯片中写入的,因而大部门市面主流的模拟器,该值都是无法获取的。

因此该因素可以作为一个判断模拟器的因素,且该值无法获取时,大概率是模拟器,具体值在下方代码中已经列出,该值获取失败时,是模拟器的可能性非常大。

/**
 * 特征参数-基带信息
 */
private EmulatorCheckResult checkFeaturesByBaseBand() {
    String baseBandVersion = getProperty("gsm.version.baseband");
 if (null == baseBandVersion) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 if (baseBandVersion.contains("1.0.0.0")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, baseBandVersion);
}

传感器数量检查

当前(2020年初),大部分市面上销售的手机都具有很多传感器(例如陀螺仪、温度传感器、光线传感器等),我个人的小米9 Pro 5G手机,传感器检测出的数量多达42个,而模拟器中,该数量仅为个位数。因此该因素可以作为一个判断模拟器的因素,且该值数量较少(<7)时,大概率是模拟器,判断逻辑在下方已经列出。

/**
 * 获取传感器数量
 */
private int getSensorNumber(Context context) {
    SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);
 return sm.getSensorList(Sensor.TYPE_ALL).size();
}

第三方应用数量检查

根据我们日常的手机使用经验可以得知,正常的用户一般都会下载安装多个应用软件(微信、支付宝、微博、抖音、视频软件、游戏软件等),而模拟器中的软件数量一般较少。因此该因素可以作为一个判断模拟器的因素,且该值数量较少(<5)时,大概率是模拟器,但该因素不绝对正确,可能一个用户手机上一个软件都没有,也可能一个模拟器上应用软件非常多,因此该因素无法绝对确定,判断逻辑在下方已经列出。

/**
 * 获取已安装第三方应用数量
 */
private int getUserAppNumber() {
    String userApps = exec("pm list package -3");
 return getUserAppNum(userApps);
}

相机支持检查

部分模拟器的某些版本,是不支持相机的,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持相机的,因此该因素也无法绝对确定,获取是否支持相机的代码逻辑在下方已经列出。

/**
 * 是否支持相机
 */
private boolean supportCamera(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}

闪光灯支持检查

与相机不同,我调研的目前市面上的主流模拟器(包含支持相机的模拟器)基本都不支持闪光灯,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持闪光灯的,因此该因素也无法绝对确定,获取是否支持闪光灯的代码逻辑在下方已经列出。

/**
 * 是否支持闪光灯
 */
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);
}

光传感器支持检查

与闪光灯类似,我调研的目前市面上的主流模拟器(包含支持相机的模拟器)基本都不支持光传感器,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持光传感器的,因此该因素也无法绝对确定,获取是否支持光传感器的代码逻辑在下方已经列出。

/**
 * 判断是否存在光传感器来判断是否为模拟器
 * 部分真机也不存在温度和压力传感器。其余传感器模拟器也存在。
 */
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;
 }
}

进程组信息检查

在真机中,进程组信息都会存储在/proc/self/cgroup目录中,使用cat命令可以查看到该信息,但某些模拟器是不会写入该信息的,因此该检查也可以作为一个模拟器检查的因素,但是因为不绝对,因此无法100%确定是模拟器。

/**
 * 特征参数-进程组信息
 */
private EmulatorCheckResult checkFeaturesByCgroup() {
    String filter = exec("cat /proc/self/cgroup");
 if (null == filter) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    return new EmulatorCheckResult(Result.RESULT_UNKNOWN, filter);
}

核心判断逻辑

到这里,可以发现,上述的各个因素,都无法确定程序的运行环境一定是模拟器,而是只存在一定可能性。

因此,该工具类综合使用了上述所有的因素来综合确定当前是否是模拟器

(例如,当前有一个设备同时满足了好几个条件:用户安装的第三方软件不到5个、检查不到基带信息、没有相机、也没有光线传感器,我们就可以认为这个设备是模拟器了)。

代码中引入了一个变量

currentDoubt
怀疑值,来代表对当前设备是模拟器的怀疑程度,满足一个条件,该值就+1,这样就可以根据这个值来确定当前是不是模拟器。

设置这个值的阈值,就可以调整这个工具类对于模拟器探测的敏感程度(比如:设置为1,表示非常敏感,只要有一个因素满足了就认为是模拟器;

设置为10,表示比较不敏感,必须有10个因素都不满足,才认为是模拟器)。

当前代码中,这个值设置为3,即三个因素同时达到了,就认为当前设备是模拟器。

public boolean check(Context context) {
    if (context == null) {
        Log.e(TAG, "check(), context is null!");
 return false;
 }
    currentDoubt = 0;

 //检测硬件名称
 EmulatorCheckResult hardwareResult = checkFeaturesByHardware();
 if (handleCheckResult(hardwareResult, CheckType.HARDWARE_NAME)) {
        return true;
 }

    //检测渠道
 EmulatorCheckResult flavorResult = checkFeaturesByFlavor();
 if (handleCheckResult(flavorResult, CheckType.FLAVOR)) {
        return true;
 }

    //检测设备型号
 EmulatorCheckResult modelResult = checkFeaturesByModel();
 if (handleCheckResult(modelResult, CheckType.DEVICE_MODULE)) {
        return true;
 }

    //检测硬件制造商
 EmulatorCheckResult manufacturerResult = checkFeaturesByManufacturer();
 if (handleCheckResult(manufacturerResult, CheckType.MANUFACTURER)) {
        return true;
 }

    //检测主板名称
 EmulatorCheckResult boardResult = checkFeaturesByBoard();
 if (handleCheckResult(boardResult, CheckType.BOARD)) {
        return true;
 }

    //检测主板平台
 EmulatorCheckResult platformResult = checkFeaturesByPlatform();
 if (handleCheckResult(platformResult, CheckType.PLATFORM)) {
        return true;
 }

    //检测基带信息
 EmulatorCheckResult baseBandResult = checkFeaturesByBaseBand();
 if (handleCheckResult(baseBandResult, CheckType.BASE_BAND)) {
        return true;
 }

    //检测传感器数量
 int sensorNumber = getSensorNumber(context);
 if (sensorNumber <= 7) {
        ++currentDoubt;
 }

    //检测已安装第三方应用数量
 int userAppNumber = getUserAppNumber();
 if (userAppNumber <= 5) {
        ++currentDoubt;
 }

    //检测是否支持闪光灯
 boolean supportCameraFlash = supportCameraFlash(context);
 if (!supportCameraFlash) {
        ++currentDoubt;
 }
    //检测是否支持相机
 boolean supportCamera = supportCamera(context);
 if (!supportCamera) {
        ++currentDoubt;
 }
    //检测是否支持蓝牙
 boolean supportBluetooth = supportBluetooth(context);
 if (!supportBluetooth) {
        ++currentDoubt;
 }

    //检测光线传感器
 boolean hasLightSensor = hasLightSensor(context);
 if (!hasLightSensor) {
        ++currentDoubt;
 }

    //检测进程组信息
 EmulatorCheckResult cGroupResult = checkFeaturesByCgroup();
 if (cGroupResult.result == Result.RESULT_CONFIRM_EMULATOR) {
        ++currentDoubt;
 }

    //可疑值>3,就认为是模拟器,可调整这个值,调节认为是模拟器的灵敏度
 return currentDoubt > 3;
}

测试结果

经过本人、本人所在团队同事的测试,该工具类在如下环境中验证通过:

Android Studio 自带模拟器、网易MUMU模拟器、夜神模拟器、Xiaomi 9 Pro 5G、Xiaomi 6X、Google Pixel 2、Huawei Mate20 Pro、Huawei 畅享9、Huawei Mate30 Pro等机型上验证通过。

由于条件有限,无法验证全部机型与模拟器,如果在使用中遇到了检测错误的情况,请在此处留言,我们可以一起来改进该工具类。

真正在生产环境使用该工具类时,还需要在灰度升级过程中,根据用户规模、日志回传情况,来调节怀疑值的阈值,以达到一个检测效果与设备规模的平衡。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容