一行代码帮你检测Android模拟器(更新至1.1.0)

声明:本文章独家授权微信公众号码个蛋原创推文

目录

  • 简介
  • 初代常规手段
  • 进阶手段
  • 改良手段和新思路
  • 最终方案
  • 测试结果
  • 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情况下,该值都拿不到,所以不推荐使用。

新增思路

增加特定值的检测,比如天天模拟器的hardware是ttVM
传感器的检测,部分模拟器的传感器个数只有1个
安装包的检测,模拟器的用户预装app很少

最终方案

结合以上研究,得出一个嫌疑指数,综合判断是否运行在模拟器中。

EasyProtectorLib.checkIsRunningInEmulator()的代码实现如下

    @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;//传感器个数
            sensorNum = sensorNum + sensorSize;
        }

        String userApps = CommandUtil.getSingleInstance().exec("pm list package -3");
        String userAppNum = "userAppNum";
        int userAppSize = getUserAppNums(userApps);
        if (userAppSize < 5) ++suspectCount;//用户安装的app个数
        userAppNum = userAppNum + userAppSize;

        String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");
        if (null == filter) ++suspectCount;//进程租

        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;
    }

以下是测试情况*

机器/测试方案 检测结果
AS自带模拟器 9.0 模拟器
Genymotion2.12.1 模拟器
逍遥模拟器6.0.0 模拟器
Appetize 模拟器
夜神模拟器6.2.5.3010 模拟器
腾讯手游助手2.0.6.8 模拟器
雷电模拟器3.41 模拟器
木木模拟器2.0.25 模拟器
一加5T 真机
华为P9 真机

*因安卓机型太广,真机覆盖测试不完全,有空大家去git提issue

Demo地址

本文方案已经集成到EasyProtectorLib

github地址: https://github.com/lamster2018/EasyProtector

中文文档见:https://www.jianshu.com/p/c37b1bdb4757

Todo

1.检测到多开应该提供回调给开发者自行处理;---v1.0.4 support

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

推荐阅读更多精彩内容