ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MuMu模拟器过检测原理与硬件伪装实战:从Build属性到环境自洽

MuMu模拟器过检测原理与硬件伪装实战:从Build属性到环境自洽 模拟器这个东西圈内人用它来做App兼容性调试、自动化脚本跑批、灰度环境联调圈外人可能只是拿它来多开几个应用账号。但不管哪种用法你迟早会撞上一个共同的问题某个App一旦发现你跑在模拟器里要么闪退要么弹窗警告要么干脆拒绝登录。我自己这几年在MuMu模拟器上踩过不少类似的坑也花过不少时间梳理其中的原理。过检测不是一个玄学问题它背后对应的是模拟器和真机之间那一层看起来很像但实际上差挺远的差异把这一层差异搞明白很多问题都能迎刃而解。这篇文章不会教你去做任何踩线的事情核心是站在开发者的角度把MuMu模拟器上常见的检测机制、硬件伪装原理、API接口适配思路以及实操中的典型坑完整过一遍。无论你是做Android测试、自动化脚本还是遇到真机才能跑的App想弄清楚原因这篇文章都能给你一个相对完整的参考。1. 项目概述你说的过检测到底是在解决什么问题1.1 一个典型的场景先描述一个场景某天你在MuMu模拟器装好了一个银行类或业务类App启动之后刚刷到首页App直接弹了一个对话框说检测到当前运行环境为模拟器请使用真机登录然后退出。你换一个App又遇到当前设备不受信任。如果你是普通用户大概率只会觉得这App太严格了如果你是开发者你会意识到这是App在主动识别运行环境。这个识别运行环境的技术就叫模拟器检测Emulator Detection。它本身是App安全策略的一部分主要用于防止恶意自动化、批量注册、或者线下场景被模拟器替代。但从开发角度看模拟器又确实是日常调试和CI自动化中绕不开的工具。两边一冲突就有了过检测这个话题。1.2 过检测不等于破解这里我特别想说清楚一个边界。所谓过检测在开发测试语境下指的是让应用在模拟器环境里能像真机一样正常运行。它不是去破解App的付费功能也不是绕过账号风控去做坏事。你改了模拟器的Build信息、补上缺失的系统服务目的是让App的检测逻辑认为这是一台正常设备而不是去篡改App自身的数据逻辑。很多人一开始就把方向搞错了上来就问怎么Hook掉App的检测函数这种思路就容易走到灰色地带去。正规的开发调试场景里绝大多数时候你只需要把模拟器环境配置得足够像真机根本不需要去动App的代码。1.3 什么人需要看这篇文章如果你是Android开发尤其是做SDK调试、跨版本兼容性测试的这篇文章能帮你省很多不必要的沟通成本——很多测试问题并不是你代码的Bug而是模拟器环境被检测后导致的行为差异。如果你在做自动化测试也会经常碰到因为环境识别问题导致脚本跑到一半被中断的情况这篇文章对自动化框架的选择也有参考意义。哪怕是普通用户了解一下检测原理也能在遇到App在模拟器里不能用时快速判断是模拟器的问题还是App的问题而不是干着急。2. 模拟器为什么会被识别主流检测机制拆解模拟器能被人认出来靠的是特征比对。放在技术上也是一样App的检测逻辑通常分几层有些是静态扫描有些是运行时探查。2.1 第一层Build属性静态检测这是最常见也是最低成本的一层。Android系统里有一堆Build相关属性ro.product.model、ro.product.brand、ro.product.device、ro.hardware、ro.product.manufacturer等等每个真机都有自己的一套组合。模拟器由于硬件虚拟化的关系这些属性值往往带有明显的虚拟痕迹。比如很多模拟器的ro.product.model会是sdk_gphone_x86、google_sdk、vbox86p这类值真机基本不会出现。App拿到这些属性后只需要做一个字符串匹配就能很直接地把模拟器筛出来。这一层的检测门槛低到什么程度呢哪怕是一个刚入门的安全开发也大概率是先从这几个值开始写检测逻辑。2.2 第二层硬件特征检测Build属性可以改但硬件特征没那么好改。模拟器运行在虚拟机或容器层底层的CPU型号、GPU渲染器、内存、磁盘信息都和真机不同。比如跑在x86架构模拟器上的CPU通常会有goldfish、ranchu或与VirtualBox相关的设备标识而真机多数是ARM架构或对应的SoC型号。GPU方面模拟器的渲染器常见的是Android Emulator OpenGL ES Translator或者类似SwiftShader的软件渲染真机则是Adreno、Mali、PowerVR这些。这类硬件信息杜撰起来比Build属性麻烦但也不是不能改后面我会聊到一些思路。更隐蔽的是传感器。真机一般都有一套完整的传感器列表包括加速度计、陀螺仪、磁场计、光线传感器、距离传感器、步数计等。模拟器默认情况下没有或者只有很少的虚拟传感器。App只需要调用SensorManager去枚举一下传感器列表看数量和质量就能判断是不是真机。还有一些更细的检测比如检查是否存在环境温度传感器、气压计——这些是很多模拟器容易忽略的点。2.3 第三层运行时环境与系统服务检测除了静态信息和硬件信息App在运行时还会检查一些系统服务的返回值。最典型的比如TelephonyManager里面的手机号码、IMEI、运营商信息再比如PackageManager里安装的软件包列表。模拟器里通常没有完整的SIM卡信息所以TelephonyManager返回的PhoneType、SimState、NetworkOperator等字段要么是空值要么就是Android这种默认值。App可以根据没有SIM卡但其他环境又很完整这种异常组合来判定模拟器。另外有些检测会检查系统里是否存在Hook框架比如Xposed、Frida的痕迹或者检查开发者选项是否开启、USB调试是否开启。这类检测不是针对模拟器本身而是针对非正常调试环境但模拟器在默认配置下很容易中招——因为模拟器本身就是一台带root、带debug的高危设备。2.4 第四层行为与特征比对更高阶的检测会去观察设备的行为。比如真机的开机时间通常是比较近的某个时间点模拟器因为快照恢复的原因开机时间可能异常早真机的IP地址通常来自运营商网络模拟器则可能来自数据中心真机上的账号体系、通讯录、通话记录等数据往往有时间维度上的连续性模拟器里则是干净得异常。这一层已经偏向风控和大数据分析普通开发者一般接触不到但在银行、支付、社交类App的安全体系里很常见。最聪明的过检测方案也不会去伪造行为数据因为成本太高且需要持续维护。这也是为什么我一直强调过检测的前提是合法场景如果App的风控等级已经上升到行为分析层面那基本就不适合用模拟器来做正经开发调试了。3. 硬件伪装原理与MuMu模拟器配置从改Build到补全传感器现在进入大家最关心的部分。前面我把检测原理梳理了一遍接下来聊聊在MuMu模拟器里怎么通过硬件伪装让应用的检测逻辑尽量舒服地通过。3.1 先认清MuMu模拟器的技术底座MuMu模拟器本质上是一套Android系统在PC上的虚拟化方案。不同版本背后的技术栈不一样有的基于VirtualBox有的基于自研的容器方案。不管底层怎么变从Android应用的角度看它看到的就是一个Android系统以及对应的虚拟设备信息。所以你改Build属性、补传感器、调整系统服务都是在模拟器内部进行的跟底层虚拟化技术的关系没有想象中那么大。这就引出一个重要结论在MuMu里做硬件伪装主要阵地是Android系统层而不是虚拟化层。只要你的修改能让Android上层返回的数据看起来像真机很多检测就自然通过了。3.2 改哪些Build属性、怎么改最传统的方式是直接改/system/build.prop。build.prop里保存着系统启动时加载的一批属性键值应用通过android.os.Build系列接口读取到的内容基本都来自这里。你可以把下面这一组值按真机模板替换掉ro.product.model设备型号比如Pixel 7ro.product.brand品牌比如googlero.product.name产品名ro.product.device设备名ro.product.manufacturer制造商ro.hardware硬件平台ro.build.fingerprint指纹App经常拿来做设备唯一标识需要注意ro.开头的属性是只读的在Android系统启动之后不能通过setprop临时修改必须改完build.prop再重启。MuMu模拟器一般有root权限所以挂载为可写后修改是可行的。但修改build.prop有风险改坏了一个字段可能导致系统无法启动所以动手之前最好先备份一份原始文件。除了build.prop还有一个地方是Build类对应的运行时缓存。某些模拟器版本里即使你改了build.prop应用通过Build.MODEL拿到的还是旧值这是因为系统会缓存编译期常量。这种情况需要通过adb或者对应的系统属性写入接口去同步。3.3 硬件信息的伪装思路不止于Build属性Build属性以外硬件信息的伪装是更深入的一步。很多检测来看CPU信息可以通过读取/proc/cpuinfo来知道当前CPU型号。MuMu模拟器在不同架构下的cpuinfo差别很大如果应用的检测逻辑包含了ARM SoC的关键词比如exynos、sm8350而你跑的是x86镜像这就会露馅。一个常见的思路是修改cpuinfo的返回内容把它改成骁龙或麒麟SoC的字符串。但要注意如果你只是改了字符串而应用进一步通过JNI调用原生接口去查询CPU指令集比如检查是否支持ARM NEON、是否支持某些x86专属指令那字符串伪装就会失效。所以更要紧的是在MuMu里尽量使用带ARM兼容的镜像——这类镜像在指令翻译层做了处理应用查到的CPU信息以及指令集支持情况会更接近真机。GPU信息也是同理。模拟器的GPU渲染器通常是软件模拟或者转译应用通过GL_RENDERER拿到的字符串不是Adreno也不是Mali。MuMu模拟器在设置里提供了多种渲染模式比如兼容模式、速度优先、强制使用独立显卡等选项这些都影响GL_RENDERER的输出。在实际操作中如果你发现某个App老是因为GPU信息被拒试着切换渲染模式往往比改配置文件更快。3.4 传感器补全与环境自洽的边界传感器是整个模拟器最容易露馅的地方。前面提到真机有完整的传感器列表而模拟器默认往往只有加速度计或者干脆什么都没有。要让App觉得环境正常可以在模拟器设置里开启传感器模拟也可以借助系统级配置去补一些虚拟传感器。但这一步有它的上限。一是传感器数据本身是否合理——加速度计它有值但你不动手机数值一直精确地停在某个固定值这样的完美静止在真机上是不常见的二是陀螺仪和加速度计的联合数据会被一些高级检测算法拿去算设备姿态如果数据太干净反而会触发风控。所以传感器补全不追求完美能让检测逻辑不报缺少传感器就算基本达标。这里我必须要说一个我在实际测试中反复遇到的怪事有时候你把各个属性都改成和真机一模一样反而被风控识别出来了。原因很简单——你的完美太假了。一台新手机它刚开机时的时区、语言、输入法、系统应用列表、账号数量都是有规律可循的。模拟器里如果只改了Build信息其他配置还是模拟器初始态App的风控引擎一比对就会发现异常。所以硬件伪装的关键不是某一个字段要完全匹配而是字段与字段之间要自洽。比如你改了手机型号是某个高端机型结果系统的内存大小只有2GB这就不自洽改了品牌是三星结果系统语言里没有三星的默认应用商店也不自洽。这个自洽度越高的配置才越难被检测。3.5 一个常见的误区只改Build属性就够了吗我见过太多人改完build.prop就觉得我已经伪装成真机了结果App还是秒识别。原因在于检测逻辑并不只看Build属性它可能同时检查了文件系统里是否有与模拟器相关的文件比如/qemu/、/system/bin/ranchu等、是否有模拟器相关的系统应用、蓝牙或WiFi的MAC地址是否属于虚拟网卡段、摄像头型号是否为空或虚拟设备。如果只改Build属性这些环境痕迹照样会暴露。处理方式是把这些项目也一并检查逐一调整。比如有些模拟器的文件系统里存在/system/bin/ranchu这个文件是模拟器特有的哪怕你改一万个Build字段它也还在那里。可以视情况移除或替换但一定要先确认系统的其他组件不依赖它。4. API欺骗从概念辨析到模拟器环境适配4.1 到底什么是API欺骗为什么它是个敏感词API欺骗这个词在开发者圈子里一般有两种理解。第一种理解是应用调用系统API比如TelephonyManager、SensorManager、PackageManager时返回的数据被篡改或替换让应用相信当前设备是一台真机。第二种理解是开发者直接Hook应用的某个内部API让它返回特定数值从而绕过业务逻辑。第一种属于环境适配在开发测试中是可以接受的第二种属于应用攻击已经明显踩线。这篇文章里讨论的只能是第一种。哪怕是在第一种场景下我也建议你优先考虑通过系统配置让API返回合理数据而不是去装一个Hook框架去篡改返回值。4.2 常见的API返回差异与适配方向来聊几个具体例子。TelephonyManager是重灾区。真机上正常的TelephonyManager会有simState、networkOperator、phoneType、imei等一系列数据。模拟器里这些字段大多是空或者默认值。要让这些API返回合理数据通常需要在模拟器里配置虚拟SIM卡。不少模拟器或者对应的虚拟化层提供了SIM卡模拟功能设置好运营商代码之后TelephonyManager返回的数据会好看很多。SensorManager前面已经说过了。PackageManager的差异也很典型。比如真机里一定会有Google服务框架GMS相关的包名模拟器如果不带GMSApp通过packageManager.getPackageInfo(com.google.android.gms)就会抛异常。反过来有些模拟器预装了很多模拟器特有的包名比如com.android.emulator、com.mumu.locker之类的这类包名在真机上不存在App只需要枚举一遍包名就能发现异常。所以在配置模拟器环境时把不必要的模拟器预装App删掉或者隐藏掉也是环境适配的一部分。还有一个容易被忽略的是Settings.Secure里的ANDROID_ID。真机上每个设备有一个唯一的ANDROID_ID重刷系统后会变化但在一个系统生命周期内是稳定的。模拟器里因为快照、克隆的原因这个值可能与另一个实例相同或者变动得太频繁。这也会被风控当成异常。4.3 为什么我不推荐去Hook系统API说实话市面上有一些工具是专门做API Hook的比如用Xposed或Frida去Hook系统的返回结果。它们确实能绕过很多检测但我强烈不建议你把它们用到正经的开发测试环境里原因有三点。第一它们本身就是检测目标。很多安全SDK专门扫描系统里是否有Xposed框架或Frida端口你装了这些工具相当于主动暴露自己。第二行为不可控。Hook了系统API之后所有调用该API的应用都会受到影响你很难判断某个测试结果是源自你的代码Bug还是Hook引发的副作用。第三你无法保证它在所有Android版本上都稳定。系统更新后Hook的底层实现可能直接失效治标不治本。这道题的底层逻辑是如果你真的只是做开发调试那系统API返回的差异完全可以通过配置模拟器本身来解决根本不需要引入一个更高风险的Hook层。只有当你的目的不是适配环境而是篡改逻辑时你才会需要那种重型工具而那已经不属于技术讨论的范畴了。4.4 API适配的上限什么时候该放弃当你发现一个App的检测逻辑极其严格无论怎么配置模拟器都无法通过时正确的做法不是继续加码伪装而是要评估这个App是不是真的适合用模拟器来做测试。我自己在项目里的边界是如果花了两天还搞不定某个App的模拟器环境适配就直接改用真机测试机。模拟器的价值是批量、并发、低成本而一旦某个应用的安全策略复杂到行为分析层面模拟器的性价比就会急剧下降。5. 避坑指南MuMu模拟器过检测实操中的典型问题与排查技巧这一章是真正的干货汇总。我把自己在MuMu上做环境适配时踩过的坑、看别人踩过的坑统一整理一遍。5.1 改了build.prop后模拟器起不来了这是最高频的失败场景。原因往往是改的时候把某个关键字段写错比如把ro.product.device的值写成了一个包含特殊字符的字符串或者格式没对齐导致系统启动时无法解析。处理办法很简单修改之前先备份。具体操作上可以用adb pull /system/build.prop拿到本地备份一份改完push回去之前先在本地用文本工具确认格式不要直接Windows记事本改完就pushWindows记事本换行符是\r\nLinux下解析会有问题。5.2 改完属性发现应用拿到的还是旧值这通常是因为应用读取的不是Build类而是其他运行时缓存。比如一些应用会通过反射去拿SystemProperties.get(ro.product.model)这个值在系统启动后是只读的修改build.prop后不会立即生效必须重启。如果你重启了还是旧值那就要检查是不是系统在启动过程中会从其他配置源重新加载这部分属性。不同版本的Android系统对属性的读取路径有些差异遇到这种情况建议先确认你的模拟器镜像的Android版本再针对性地查这个版本的属性初始化顺序。5.3 只改了Build属性但App还是秒识别模拟器这种情况在我帮别人排查时非常常见。原因前面也提到了检测逻辑可能同时检查了文件系统痕迹、包名列表、MAC地址、摄像头信息等多维度内容。处理方式是把这些项目也一并检查逐一调整。建议的排查顺序是先看Build属性再看CPU/GPU/内存然后看传感器最后看包名和系统文件。按这个顺序排查大部分问题半小时内都能定位出来。5.4 传感器和电话信息都补了还是被检测那就要看行为层了。比如App可以检查屏幕分辨率是否在真机常见分辨率列表中。很多模拟器为了性能默认分辨率是1280x720或者1920x1080这在几年前是真机常见但现在的旗舰机很多是3200x1440或者2400x1080。这种剂量式的检测点非常多很难用某一个静态配置去骗过。我的建议是把目标拆成按风险等级来做适配。如果一个App只是用Build属性加传感器来检测那么你做好这两块适配就够了如果它已经用了多种行为特征比如MAC地址、分辨率、开机时间、IP归属地那这类App的根本目的是风控不是兼容性测试你用模拟器去适配它的性价比很低。5.5 别忽略了网络环境这一点往往是所有配置都做好之后最后一块绊脚石。模拟器跑在PC上网络出口通常是家宽或机房IP而真机走的是移动网络。一个App如果想通过IP识别模拟器只需要判断IP的ASN归属是不是移动运营商即可。这一点靠模拟器内部配置是改不了的唯一的方式是走一条合规的移动网络出口链路但这又涉及代理的合规性使用我建议谨慎处理。如果你的测试环境必须要用到这种级别的可靠性直接换真机更省心。5.6 常用排查命令速查这里我列几个在模拟器环境适配中经常用到的adb命令方便你自己排查检查项命令常见异常特征Build属性adb shell getprop | grep ro.product出现sdk_gphone、google_sdk、vbox等CPU信息adb shell cat /proc/cpuinfoCPU型号带goldfish、ranchu等分辨率adb shell wm size分辨率不在真机常见范围内传感器adb shell dumpsys sensorservice传感器列表为空或极少包名adb shell pm list packages出现模拟器特有包名设备指纹adb shell getprop ro.build.fingerprintfingerprint包含模拟器标识这些命令可以直接帮你判断App可能从哪里识别出模拟器。比如你用dumpsys sensorservice发现根本没有传感器列表那你就知道下一步该去配置传感器模拟了。6. 写在最后的个人体会6.1 过检测是一场消耗战做模拟器环境适配这几年我最大的感受是这是一个攻防对抗的游戏但我不建议普通人陷入太深。模拟器检测技术在不断进步今天你改好了一组配置明天App更新一版又把检测点换了这是一种无止境的消耗战。真正高效的做法是明确你到底是要解决一个开发测试问题还是想绕过某个App的安全机制。前者值得投入时间后者则充满了法律和伦理风险。6.2 保持边界感最重要如果你是一名开发者我的建议是把精力放在理解检测原理上这能帮你快速判断某个App在模拟器上的行为是否符合预期而不是去写一套万能过检测配置。如果遇到实在过不去的App就果断换真机别在模拟器上赌时间。最后送大家一句话好的配置是让你看起来像一台正常的测试机而不是伪装成某个不想让别人知道身份的机器。两者之间那条线自己心里要清楚。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进