27、模拟器检测与绕过:模拟器特征检测(Build属性、传感器、IMEI)、绕过方案

模拟器检测,说白了就是App在问一个问题:“你是在真机上跑,还是在虚拟机里跑?”

我个人习惯把这类检测比作“查户口”。App会翻你的家底——Build属性、传感器列表、IMEI号,甚至是你屏幕的刷新率。一旦发现你是个“假人”,立马翻脸不认账。

嗯,咱们今天就把这些检测手段和绕过方案,掰开了揉碎了讲清楚。

27.1 为什么App要检测模拟器?

你想想看,模拟器对逆向工程师来说简直是“作弊器”。

  • 调试方便:可以随时断点、修改内存、hook函数。
  • 环境可控:GPS位置、网络状态、传感器数据都能伪造。
  • 成本极低:一台电脑开十个模拟器,批量操作毫无压力。

所以,金融类App、游戏、社交软件,都会想方设法拦住模拟器。我在项目中遇到过某银行App,检测到模拟器直接闪退,连登录界面都不给你看。

27.2 模拟器特征检测:三大核心维度

模拟器检测通常从三个方向入手:硬件特征系统属性行为特征。咱们一个一个说。

27.2.1 Build属性检测

Android系统里有个android.os.Build类,里面存了一堆设备信息。模拟器和真机在这些属性上差异很大。

常见的检测点包括:

属性 真机典型值 模拟器典型值
Build.BRAND samsung、xiaomi、huawei google、generic
Build.MODEL SM-G998B、Mi 11 sdk_gphone_x86、Android SDK built for x86
Build.MANUFACTURER Samsung、Xiaomi Google、unknown
Build.FINGERPRINT samsung/r8q/...:12/SP1A.210812.016/... generic/sdk_gphone_x86/...:12/SE.1/...
Build.HARDWARE qcom、exynos ranchu、goldfish

代码实现大概长这样:

public static boolean isEmulatorByBuild() {
    String brand = Build.BRAND;
    String model = Build.MODEL;
    String manufacturer = Build.MANUFACTURER;
    String hardware = Build.HARDWARE;

    if (brand.contains("generic") || brand.equals("google")) {
        return true;
    }
    if (model.contains("sdk") || model.contains("emulator")) {
        return true;
    }
    if (manufacturer.contains("Genymotion") || manufacturer.equals("unknown")) {
        return true;
    }
    if (hardware.equals("ranchu") || hardware.equals("goldfish")) {
        return true;
    }
    return false;
}

关键点:很多App不会只查一个属性,而是把多个属性组合起来打分。分数超过阈值,就判定为模拟器。

27.2.2 传感器检测

真机上有各种传感器:加速度计、陀螺仪、磁力计、光线传感器、接近传感器……模拟器呢?要么没有,要么全是假的。

我曾经遇到过一个App,它检测接近传感器。真机在通话时贴近耳朵,接近传感器会触发。模拟器里这个传感器要么不存在,要么返回值永远是0。

检测代码示例:

public static boolean isEmulatorBySensors() {
    SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
    
    // 检查接近传感器
    Sensor proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
    if (proximitySensor == null) {
        return true; // 没有接近传感器,大概率是模拟器
    }
    
    // 检查陀螺仪
    Sensor gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
    if (gyroscope == null) {
        return true;
    }
    
    // 检查光线传感器
    Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
    if (lightSensor == null) {
        return true;
    }
    
    return false;
}

避坑指南:有些模拟器会伪造传感器数据,但数据精度和真机有差异。比如真机的陀螺仪噪声是0.01级别,模拟器可能直接返回0。你可以通过分析传感器数据的方差来判断。

27.2.3 IMEI与设备ID检测

IMEI(国际移动设备识别码)是每台手机的唯一标识。模拟器里呢?要么是000000000000000,要么是随机的假号。

检测逻辑:

public static boolean isEmulatorByIMEI() {
    TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    if (tm == null) return true;
    
    String imei = tm.getDeviceId();
    if (imei == null || imei.isEmpty()) {
        return true;
    }
    
    // 模拟器常见IMEI模式
    if (imei.equals("000000000000000") || imei.matches("\\d{15}")) {
        // 真机IMEI是15位数字,但模拟器可能也是15个0
        if (imei.startsWith("00000")) {
            return true;
        }
    }
    
    // 检查Android ID
    String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
    if (androidId != null && androidId.equals("9774d56d682e549c")) {
        // 这是模拟器的经典Android ID
        return true;
    }
    
    return false;
}

注意:Android 10及以上版本,IMEI的获取受到严格限制。App可能改用MEIDSerial Number来检测。另外,有些App会检查WIFI MAC地址,模拟器的MAC地址通常是02:00:00:00:00:00

27.3 绕过方案:兵来将挡,水来土掩

检测手段五花八门,绕过方案也各有千秋。我按难度从低到高给你捋一遍。

27.3.1 方案一:修改模拟器配置文件

这是最基础的方法。很多模拟器允许你修改build.prop文件,把里面的属性改成真机的值。

操作步骤:

  1. 用adb连接模拟器:adb shell
  2. 获取root权限:su
  3. 挂载系统分区为可读写:mount -o rw,remount /system
  4. 修改/system/build.prop文件,把ro.product.brandro.product.model等改成真机值
  5. 重启模拟器

但说实话,这招现在不太管用了。因为App会检测ro.kernel.qemu这个属性,模拟器里这个值通常是1,真机是0。你改build.prop也改不掉这个。

27.3.2 方案二:使用Xposed或Frida Hook

这是逆向工程师的常规操作。用Frida Hook掉检测函数,让它永远返回“我是真机”。

Frida脚本示例:

Java.perform(function() {
    var Build = Java.use('android.os.Build');
    
    // Hook BRAND
    Build.BRAND.value = 'samsung';
    
    // Hook MODEL
    Build.MODEL.value = 'SM-G998B';
    
    // Hook MANUFACTURER
    Build.MANUFACTURER.value = 'Samsung';
    
    // Hook HARDWARE
    Build.HARDWARE.value = 'qcom';
    
    // Hook FINGERPRINT
    Build.FINGERPRINT.value = 'samsung/r8q/r8q:12/SP1A.210812.016/G998BXXU5BUK7:user/release-keys';
    
    console.log('[+] Build properties hooked successfully');
});

对于传感器检测,可以Hook SensorManager.getDefaultSensor(),让它永远返回非空:

Java.perform(function() {
    var SensorManager = Java.use('android.hardware.SensorManager');
    
    SensorManager.getDefaultSensor.overload('int').implementation = function(type) {
        var result = this.getDefaultSensor(type);
        if (result == null) {
            // 如果返回null,伪造一个传感器对象
            console.log('[+] Faking sensor for type: ' + type);
            // 这里需要构造一个Sensor对象,比较复杂
            // 实际项目中可以用Xposed模块
        }
        return result;
    };
});

个人经验:Hook传感器检测时,别只Hook一个函数。有些App会通过SensorManager.getSensorList()来获取传感器列表,你得把两个函数都Hook掉。

27.3.3 方案三:定制ROM或使用内核模块

这是最彻底的方案,但也是最麻烦的。你可以自己编译一个Android ROM,把模拟器相关的特征全部去掉。或者写一个内核模块,在系统层拦截所有检测请求。

我记得有个团队做过一个叫“Emulator Shield”的项目,它直接修改了内核的/proc/cpuinfo/sys/class/下的设备节点,让App完全无法区分模拟器和真机。

不过说实话,这方案对大多数人来说门槛太高。我建议你优先用Frida Hook,实在不行再考虑定制ROM。

27.4 知识体系总览

下面这张图,帮你把模拟器检测与绕过的核心逻辑串起来:

模拟器检测与绕过知识体系 检测手段 绕过方案 Build属性检测 传感器检测 IMEI/设备ID检测 其他特征检测 修改build.prop Frida/Xposed Hook 定制ROM/内核模块 模拟器插件/补丁 核心思路:让App认为自己在真机上运行,而非虚拟机环境

27.5 实战中的坑与建议

最后,分享几个我在实战中踩过的坑:

  • 别只改一个属性:有些App会交叉验证。比如你改了Build.BRAND,但Build.FINGERPRINT还是模拟器的,一对比就露馅。
  • 注意时序问题:有些App在启动时检测一次,运行中再检测一次。你Hook早了或晚了都不行。
  • 模拟器也有“真机模式”:比如Android Studio自带的模拟器,可以配置“真机皮肤”和“传感器模拟”。但说实话,这些模拟数据太干净了,反而容易被识破。

我曾经遇到过一个App,它检测/sys/class/thermal/thermal_zone0/temp这个文件。真机这个文件会返回CPU温度,模拟器里要么没有这个文件,要么返回固定值。我花了整整一天才找到这个检测点。所以,逆向分析时一定要有耐心,多翻翻系统文件。

好了,模拟器检测与绕过这块,核心内容就这些。记住一句话:没有完美的检测,也没有完美的绕过。关键看谁更细心,谁更了解系统底层。


公众号:蓝海资料掘金营,微信deep3321