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可能改用MEID或Serial Number来检测。另外,有些App会检查WIFI MAC地址,模拟器的MAC地址通常是02:00:00:00:00:00。
27.3 绕过方案:兵来将挡,水来土掩
检测手段五花八门,绕过方案也各有千秋。我按难度从低到高给你捋一遍。
27.3.1 方案一:修改模拟器配置文件
这是最基础的方法。很多模拟器允许你修改build.prop文件,把里面的属性改成真机的值。
操作步骤:
- 用adb连接模拟器:
adb shell - 获取root权限:
su - 挂载系统分区为可读写:
mount -o rw,remount /system - 修改
/system/build.prop文件,把ro.product.brand、ro.product.model等改成真机值 - 重启模拟器
但说实话,这招现在不太管用了。因为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 知识体系总览
下面这张图,帮你把模拟器检测与绕过的核心逻辑串起来:
27.5 实战中的坑与建议
最后,分享几个我在实战中踩过的坑:
- 别只改一个属性:有些App会交叉验证。比如你改了Build.BRAND,但Build.FINGERPRINT还是模拟器的,一对比就露馅。
- 注意时序问题:有些App在启动时检测一次,运行中再检测一次。你Hook早了或晚了都不行。
- 模拟器也有“真机模式”:比如Android Studio自带的模拟器,可以配置“真机皮肤”和“传感器模拟”。但说实话,这些模拟数据太干净了,反而容易被识破。
我曾经遇到过一个App,它检测/sys/class/thermal/thermal_zone0/temp这个文件。真机这个文件会返回CPU温度,模拟器里要么没有这个文件,要么返回固定值。我花了整整一天才找到这个检测点。所以,逆向分析时一定要有耐心,多翻翻系统文件。
好了,模拟器检测与绕过这块,核心内容就这些。记住一句话:没有完美的检测,也没有完美的绕过。关键看谁更细心,谁更了解系统底层。
公众号:蓝海资料掘金营,微信deep3321