搞安卓底层开发的朋友,应该都知道一个尴尬的现状:很多App对运行环境的检测越来越“狠”,尤其是银行类、支付类、游戏类应用。过去靠Xposed模块改改build.prop就能蒙混过关,现在基本行不通了。Momo这个App会检测一堆系统异常,AIDA64则能把你的真实硬件信息翻个底朝天。想要真正改掉设备指纹,最彻底的办法就是从系统镜像入手,直接编译一个属于自己的AOSP定制ROM,从底层把设备信息“重新捏造”一遍。
这篇文章就完整记录我基于AOSP定制ROM,绕过Momo和AIDA64检测的实战过程,从Ubuntu编译环境搭建、AOSP源码下载编译,到改Build属性、改内核信息、调SELinux策略、最终刷机验证,全程带细节和踩坑记录。适合已经会基本刷机、想深入理解安卓设备指纹原理,或者正在做系统安全研究的朋友。另外我先把话说在前面:所有操作请放在自己的测试机上,在合法合规的场景下进行,本文内容是纯粹的技术研究和设备隐私保护探讨。
1. 项目背景与核心思路
1.1 为什么非要定制ROM,而不是用Xposed模块一键解决
很多新手上来就装个Magisk插件、打个Xposed模块,比如“设备模拟器”之类的,想靠Hook函数返回假数据。这思路确实绕过了部分Java层检测,但对底层信息的检测几乎无效。
原因很简单:Momo和AIDA64这类工具,不仅能读到常规的Build属性(ro.product.model这种东西),还会去遍历Linux内核信息、SELinux状态、SELinux上下文、init进程的环境变量、su文件是否存在、系统应用签名,甚至通过sysfs节点读取真实硬件路径和序列号。这些信息在Java层Hook很难完全覆盖,而且检测方也会交叉比对:比如Build里写的型号是某台厂商机,但内核版本字符串却不对,传感器节点路径对不上,或者SELinux没有强制模式,一眼就露馅。
所以真正彻底的方案就是直接改ROM。AOSP是开源的,把系统源码拉下来,修改你想改的所有文件,重新编译出一个干净的镜像,刷进设备后,系统从底层呈现的就是你想要的样子。这不需要Hook,不需要root暴露,整条链路都是自洽的。
1.2 Momo与AIDA64到底在检测什么
先拆目标。Momo是一款知名的安卓环境检测工具,它主要检查这几点:
- root状态:包括su二进制、Magisk、Superuser、特定包名、init进程不安全属性等
- 系统安全:SELinux是否Enforcing、系统分区是否可写、SELinux上下文是否异常
- 异常Activity:非官方系统界面、Bootloader解锁状态
- 开发调试点:ADB是否开放、USB调试是否开启、开发者选项状态
- 存在可疑的Xposed或Riru/Zygisk模块
AIDA64则完全是另一条路线,它通过查询系统属性、读取/proc/cpuinfo、/proc/meminfo、/sys/devices等节点,把CPU型号、内核版本、存储序列号、屏幕分辨率、传感器型号、电池信息、摄像头型号全扒出来。如果你想改设备画像,必须把这些信息和Build里的型号一致。
我的目标是:让Momo显示“未检测到异常”,同时让AIDA64展示一套完整的、逻辑自洽的“虚拟设备信息”。项目环境是Pixel 4,目标镜像换成一加7 Pro的画像。
2. 环境搭建:从零开始编译AOSP
2.1 编译机配置和系统准备
AOSP编译是个吃硬件的事,千万别拿笔记本贸然开整,很浪费时间。我用的是一台二手服务器,配置是E5-2680 v4双路、64GB内存、1TB NVMe固态,Ubuntu 20.04 LTS。内存低于16GB会卡到怀疑人生,最少建议32GB,磁盘至少400GB空闲空间。另外网络必须能稳定访问Google的源码仓库,这个前置条件我就不展开说了,懂的都懂,自己解决网络问题,但绝对不要用非法手段。
装完系统后,先更新基础依赖包,AOSP官方要求装一堆库,直接执行这些命令:
sudo apt-get update sudo apt-get install -y git-core gnupg flex bison build-essential zip curl zlib1g-dev \ libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev \ lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig python3注意Ubuntu 20.04默认没有libncurses5,只有libncurses6,所以需要单独加源或者手动装老包,否则编到一半报错找不到ncurses头文件。
另外要安装repo工具和配置Git身份:
mkdir ~/bin curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo chmod a+x ~/bin/repo export PATH=~/bin:$PATH git config --global user.name "Your Name" git config --global user.email "you@example.com"2.2 拉取AOSP源码与分支选择
这里有个决策点:到底拉哪个安卓版本。我之前用Android 12(android-12.1.0_r16)做过一版,但Momo对这种版本检测特别严格,长期安全性补丁会暴露系统太旧的问题。后来发现跟我的目标设备也脱节——目标一加7 Pro出厂是Android 10,所以我把源码切到了对应的Android 10分支,稳定的android-10.0.0_r47版本。
拉取命令:
mkdir aosp cd aosp repo init -u https://android.googlesource.com/platform/manifest -b android-10.0.0_r47 repo sync -j16repo sync时间取决于网速,我拉了将近3小时,包括git历史接近80GB。建议用repo sync -c跳过历史只同步当前分支的代码,能省一半空间和时间:
repo sync -c -j16拉取完成后,先测试能否正常编译原版镜像。这一步很重要,先保证环境没问题,再改东西,否则后期排查起来分不清是环境问题还是改动问题。
此时需要下载对应设备的驱动二进制。因为我是Pixel 4,对应的驱动包可以在Google官方页面下载,解压后是一个Shell脚本,执行后会生成vendor目录。
source build/envsetup.sh lunch aosp_flame-userdebug make -j32第一次编译大概2小时,CPU满负载,中间可能会有内存不足被OOM杀掉的情况。建议确认swap开个16GB,编译指令后面加-j32或者-j16,不要盲目用-j64。
耐心等到看到#### build completed successfully,说明底层环境通了。
2.3 定制ROM的编译流程
AOSP编译输出的img文件在out/target/product/flame/目录下,包括boot.img、system.img、vendor.img、vbmeta.img等。定制ROM时,真正高频修改的是system分区和vendor分区,所以咱们核心关注system里的build.prop、framework、等等。
编译完原版后,先整一个干净的基线。后面每次修改,我都遵循三步流程:
- 修改源码文件
- 重新执行make
- 用fastboot刷对应分区验证
但要注意AOSP增量编译有时候不生效,因为很多属性已经打进img里。稳妥起见,在修改build.prop或framework后,执行make systemimage或make -j16,它会自动rebuilt依赖的部分,不要天天clean,否则编译时间直接翻倍。
3. 设备指纹修改:核心实现
3.1 修改Build属性,让AIDA64看到假“皮”
AIDA64读取的第一层信息就是系统Build属性,也就是/system/build.prop文件。AOSP源码中,这些默认属性定义在build/make/target/product/下的mk文件中。最直接的方法是修改device支持目录下的mk文件,也可以后期直接改生成的build.prop再重新打包system.img。我推荐用源码级修改,可维护性更强。
对应的设备mk文件路径是device/google/flame/flume.mk(Pixel 4的内部代号flame),里面有PRODUCT_MODEL等定义,但是很多ro.*属性是在build/make/core/sysprop.mk等地方默认生成的。为了精确改,我找到/system/build.prop里的关键行,再反查源码变量。
最终我改的字段是这个列表:
| Build属性 | 原值(Pixel 4) | 修改后(假装一加7 Pro) |
|---|---|---|
| ro.product.model | Pixel 4 | OnePlus7Pro |
| ro.product.brand | oneplus | |
| ro.product.name | flame | OnePlus7Pro |
| ro.product.device | flame | OnePlus7Pro |
| ro.product.manufacturer | OnePlus | |
| ro.build.fingerprint | google/flame/flame:10/QD1A.190821.007/... | oneplus/OnePlus7Pro/OnePlus7Pro:10/QKQ1.190716.003/... |
| ro.build.version.release | 10 | 10 |
| ro.build.version.security_patch | 2020-01-05 | 2020-05-01 |
| ro.build.display.id | QD1A.190821.007 | QKQ1.190716.003 |
| ro.build.flavor | flame-userdebug | OnePlus7Pro-user |
| ro.com.google.clientidbase | android-google | android-oneplus |
| ro.oem_unlock_supported | 1 | 0 |
这里坑很多。只改model和manufacturer没用,AIDA64的“设备信息”页面还会读取ro.product.name和ro.product.device,Momo则通过Build.FINGERPRINT交叉校验整条指纹链。如果fingeprint里写的brand是google,但product.name不是flame,它就会怀疑是修改过的。所以我直接把fingerprint整条替换成了真实一加7 Pro的指纹串,同时把ro.product.flavor改成OnePlus7Pro-user。这样系统自洽性更好。
修改位置我统一放在device下的device.mk中,通过PRODUCT_PROPERTY_OVERRIDES强制覆盖。举个例子:
PRODUCT_PROPERTY_OVERRIDES += \ ro.product.model=OnePlus7Pro \ ro.product.brand=oneplus \ ro.product.name=OnePlus7Pro \ ro.product.device=OnePlus7Pro \ ro.product.manufacturer=OnePlus \ ro.build.fingerprint=oneplus/OnePlus7Pro/OnePlus7Pro:10/QKQ1.190716.003/28H.191007.113:user/release-keys \ ro.build.display.id=QKQ1.190716.003 \ ro.build.version.security_patch=2020-05-01注意fingerprint里的版本字符串日期必须和ro.build.version.*匹配,AIDA64虽然不校验,但Momo会读Build.VERSION的各个字段做内部一致性判断。
3.2 修改内核信息、SELinux状态和调试标记
Build属性只是表面,Momo最恶心的一点是会通过/proc自己读内核参数。几个关键节点:
/proc/cmdline:里面写了androidboot.veritymode=enforcing之类的,Momo会检查有没有可疑参数/proc/version:Linux内核编译信息,包含编译器版本、内核版本号/proc/selinux下相关节点:检查selinux的状态和上下文
内核版本字符串是在编译内核时生成的。AOSP的内核源码和系统源码是分开的,Pixel 4的内核在kernel/google/msm-4.14,但通常是由预先编译好的内核二进制放到vendor里。要改/proc/version,要么重新编译内核,要么直接修改kernel二进制里的字符串,我试过后一种方法:
strings kernel | grep 'Linux version'找到类似“Linux version 4.14.190-g3c131a0c”的字符串,用010 Editor或Python脚本替换成目标内核字符串,注意保持长度一致,否则会破坏二进制的符号表和字符串表。这个操作有风险,建议在虚拟机里备份kernel文件再操作。我替换成了:
Linux version 4.14.180-g1f4f4d1 (oneplus@swdev) (Android clang version 9.0.3) #1 SMP PREEMPT Thu May 21 09:23:03 CST 2020长度与原串不同时,我用十六进制编辑器小心地填充到相同长度,或者干脆在源码级别编译一个内核。如果你用的是已经root的设备,还可以直接把修改后的/proc/version字符串强制挂载隐藏,但ROM定制里还是建议改二进制。
SELinux状态方面,Momo会检测SELinux是否处于Enforcing,或者getenforce返回值。AOSP的userdebug/eng版本默认SELinux是Permissive或部分Permissive,这就会触发Momo告警。我直接把目标设备定义成user版本,同时修改BoardConfig.mk里的SELinux定义:
BOARD_KERNEL_CMDLINE += androidboot.selinux=enforcing BOARD_VERITY_MODE := enforcing另外还在device.mk中删除了debug相关的persist属性,把ro.debuggable从1改成0,ro.secure改成1,ro.adb.secure改成1。否则Momo检测到adb调试开启、Android Debug属性为1,直接标红。
3.3 修改硬件信息,把AIDA64的所有传感器都归一
AIDA64能读出主板型号、内存大小、存储容量、电池状态、屏幕分辨率、摄像头型号、传感器列表,这些信息不全在Build属性里,很多是通过sysfs节点从内核暴露出来的。比如/sys/devices/soc下的soc_id,/sys/block/mmcblk0/device/等存储信息,摄像头型号在/sys/devices/platform/下。
这些节点的修改最耗时间,因为你得先逐一查看原设备和目标设备读取到的值。我建议直接暴力一点:把目标设备对应的sysfs信息做成一个映射表,在ROM里把原有设备的节点内容替换掉。但节点的读写权限由SELinux管理,直接改文件内容需要root。所以我在kernel里通过修改dts设备树和驱动代码来改变节点输出。这么说吧,如果只是想骗过AIDA64,不需要每个节点都一样,AIDA64主要靠Linux标准接口,比如:
- CPU型号:从
/proc/cpuinfo中的Hardware和Processor字段读取 - 主板信息:从
/sys/devices/soc/soc_id读取 - 内存大小:从
/proc/meminfo读取MemTotal - GPU信息:从
/sys/kernel/gpu读取 - 屏幕分辨率:从DisplayInfo或者读取
/sys/class/graphics/fb0参数
我实践下来优先级最高的是/proc/cpuinfo。很多检测App都拿它做基准。Pixel 4用的是骁龙855,cpuinfo里Hardware为sm8150,而一加7 Pro同款骁龙855,实际Hardware都是sm8150,所以不用改。但如果你用Pixel 4模拟一台骁龙888的设备,那就得改内核的machine_descriptor或dts里的compatible字段,工作量非常大。建议选同平台的目标设备进行伪装,这样CPU、GPU、soc相关信息天然一致,只需要改掉品牌型号就够。
内存大小方面,AIDA64显示的总内存和/proc/meminfo的MemTotal一致,但MemTotal是内核根据实际情况计算出来的,修改它需要给内核传参mem=命令或者改dts的memory节点。例如内核cmdline里加mem=8192M可以强制限制内存大小,但往大了加没门。所以改ROM时要选内存比目标机小或相等的硬件平台,否则AIDA64露馅。比如Pixel 4有6GB RAM,想伪装成12GB的一加7 Pro,单纯刷入的RO属性根本没用,内核看到的MemTotal是6GB,检测方一算就知道对不上。正确思路是选一个目标机型,它的内存<=自己物理内存。一加7 Pro的6GB版就勉强可行,但要把内核cmdline中强制指定为6GB,否则还是8GB原值。
其他的传感器和摄像头,AIDA64通常只显示设备型号,这些信息走的也底层。想省事的话,可以在RRO叠加层或者framework层拦截查询,但这就又变成Hook了,非ROM内置方案,Momo扫Hook更容易出问题。所以我的策略是:放弃在AIDA64的“摄像头”标签页做假信息,直接通过修改AOSP的CameraProvider配置把非法camera设备屏蔽掉,让AIDA64显示“No supported cameras”或只显示前置、后置官方命名。检测工具看到没有摄像头反而不会警觉,因为它确实是一台“开发机”。
3.4 绕过Momo检测的关键点
Momo不同于硬件信息工具,它的重点在“系统环境异常”。我实测下来,想让它报绿色,下面几点必须做到:
必须使用user编译类型,而不是userdebug或eng。userdebug自带root漏洞、ro.debuggable=1,Momo对这些敏感度极高。把lunch的目标切到aosp_flame-user即可,同时把
persist.sys.root_access设为0。去掉所有root相关二进制。AOSP默认没有su,但很多定制ROM都会有。确认整个system/vendor里没有su二进制,关键文件如
/system/xbin/su、/system/bin/su、/system/app/Superuser.apk都不存在。SELinux必须是Enforcing,且上下文不能异常。Momo会检查
getenforce结果,还检查/sys/fs/selinux/enforce节点。另外还会检查avcdenied日志数量,如果SELinux一直Permissive,大量SELinux日志堆积必然暴露。所以不能单纯把selinux置为enforcing,还要保证策略正确、无大量denied。这对AOSP定制ROM其实是个挑战,因为我们改了不少东西,新的策略可能需要调整。我的做法是在framework/base/services/core/java/com/android/server/pm/SELinuxMMAC.java等尽量用默认策略,不改动系统源码的SELinux domains,否则很容易产生一堆denied log。系统分区必须只读且验证启动正常。Momo会检查system分区是不是rw挂载,检查verifyboot状态。定制ROM如果关闭了dm-verity,很容易被标记。所以要么保留原版bootloader的verity,要么自己生成匹配的vbmeta签名,否则fastboot会显示orange状态,Momo一样报告bootloader已解锁。这里我踩了大坑,后面单独讲。
避免系统应用context异常。比如你加了一个新系统应用,但没给它写sepolicy,它运行时SELinux报avc denied,Momo可能去检查应用的安全上下文异常,很容易告警。解决方法是额外封装一个系统应用,给它分配一个已有的platform签名,并添加相应的sepolicy。
理论上做完这些,Momo应该能到全绿。但实际很多ROM连目标机型的vendor指纹都对不上,所以我需要把vendor的build.prop也一并改了。
其实vendor分区里也有一个build.prop,路径/vendor/build.prop,包含了ro.vendor.*属性。AIDA64和Momo同样会读取这里。我在源码里把device/google/flame/device.mk和device/google/flame/board-info.txt都做了修改,让vendor的属性也回到一加7 Pro的vendor build信息。
修改完这些源码,我再执行:
source build/envsetup.sh lunch aosp_flame-user make -j32等编译输出后,用fastboot刷入。
4. 刷机验证与效果评估
4.1 刷机流程与bootloader解锁风险
Pixel 4解锁bootloader很简单,但注意解锁后bootloader状态就是unlocked,任何Open Bootloader状态对Momo来说都是原罪。这就是自制ROM和原厂ROM最大的区别——原厂解锁是有记录的,Momo检测到bootloader unlocked会亮红。要绕过这一点,不能在最后验证时保持unlocked,需要重新锁定bootloader,但锁定后没法刷自制镜像,所以常规方案是:
- 刷机阶段用fastboot刷完所有分区,并且使用
fastboot oem lock重新上锁 - 上锁后的设备启动时验证签名,如果签名不匹配就会变砖
所以如果你要做一个自洽的、让Momo认为bootloader已锁定的ROM,必须自己生成一套可信的AVB(Android Verified Boot)密钥,并把公钥烧进bootloader/keymaster,这个操作风险较高,且设备商家不认第三方密钥。我这次不敢对主力机做这么深的修改,所以用另一台设备做了验证,验证过程中保持bootloader解锁状态,那么Momo的bootloader项会亮黄,但这已经代表绝大多数系统环境通过了,剩下的偏差是解锁状态本身的物理体现。
如果真想彻底绿,可以研究“伪造locked状态”的方案:修改init进程开机时读取的/sys/devices/soc下的bootloader状态节点。但这涉及把关键function挂到hook里,又是Hook路线,容易被扫描。所以我的结论是,Momo全绿需要理论上具备生成合法签名的密钥体系,否则较难完美。
4.2 Momo检测结果对比
刷完我的定制ROM,我直接安装最新版Momo,打开一看:
- root检查:全绿
- 系统检查:全绿
- SELinux检查:全绿
- 开发调试点:因为我关了adb调试,绿
- 异常Activity:绿
- bootloader状态:黄
整体相比原版Pixel 4刷魔改Magisk时一堆红,已经改善很多。虽然没有全绿,但这个黄是上锁问题,说明系统环境已经几乎无可挑剔。
4.3 AIDA64设备画像对比
AIDA64读出的信息基本“以假乱真”。设备型号成了OnePlus7Pro,品牌成了OnePlus,硬件信息里的平台依旧是Qualcomm SM8150,和真实一加7 Pro一致。内存显示6GB,我用了mem=6G内核参数,CPU信息和原机一致,存储型号则是闪存本身的真实厂商,因为AIDA64读取的是底层的/sys/block/mmcblk0/device/vendor等节点,这个我没改,所以显示为Samsung,和一加7 Pro普遍用的Samsung闪存倒也对得上。
屏幕分辨率方面,Pixel 4是1080x2340,而一加7 Pro是1440x3120。AIDA64直接读驱动层的显示模式,我没改dts里的display panel,所以显示的还是Pixel 4规格。这个如果想改,得替换整个display panel驱动,工作量很大。Momo不关注这个,但AIDA64披着一个一加7Pro的外壳,屏幕参数确实是最大破绽。
摄像头和传感器同理,显示出来是Pixel 4的型号。所以从严格意义上讲,我这个ROM的“设备画像”并非100%复制一加7 Pro,而是在主要表层参数上做了同化。对于一般的风控检测来说,最重要的品牌型号、指纹串、系统状态、CPU主板这些核心点都已经通过了。
5. 常见问题与避坑
5.1 编译失败和产物异常怎么排查
我遇到过最频繁的编译问题是:
No rule to make target xxx,多半是驱动包没有正确解压到vendor目录。重新执行驱动脚本后make clean再试。Out of memory,加swap,或者降低-j参数。Jack server相关报错在老版本上常见,Android 10默认用Jack编译Java代码,需要设置JACK_SERVER_VM_ARGUMENTS=-Xmx4096m,或者干脆升级到更高版本避免这问题。- 修改了build.prop后编译不生效,因为product包缓存。执行
make clean然后重新编译镜像。 - 烧录后开机无限重启,很可能是SELinux策略问题,先用
fastboot -w清数据,不行就回滚到改动之前刷。
5.2 修改内核字符串导致kernel panic
替换/proc/version字符串时,我不小心改错了长度,导致内核启动崩溃。这个没有什么好办法,劝导所有人都要备份原kernel,并且用Python脚本控制长度一致再写入。举个例子:
import binascii, re, sys with open("kernel", "rb") as f: data = bytearray(f.read()) old = b"Linux version 4.14.190-g3c131a0c" new = b"Linux version 4.14.180-g1f4f4d1 " # 长度一致,不足补空格 if len(old) != len(new): raise SystemExit("length mismatch") idx = data.find(old) if idx == -1: raise SystemExit("string not found") data[idx:idx+len(old)] = new with open("kernel_new", "wb") as f: f.write(data)这里的kernel是boot.img中解包出来的内核镜像。解包工具用mkbootimg或magiskboot均可。替换完再用mkbootimg --kernel kernel_new --ramdisk ramdisk.img --cmdline "..." --base 0x...重新打包成boot.img。注意boot.img的dtb和header参数不同,直接用原boot.img解包再打包最稳妥。
5.3 Momo检测的红黄项各自怎么处理
我自己遇到的检测项和对策整理成一张速查表,方便大家对照:
| 检测项 | 触发原因 | 对策建议 |
|---|---|---|
| su文件检测 | /system/bin/su或Magisk相关残留 | 彻底移除root方案,或使用magisk deny list,但ROM内最好无 |
| SELinux状态 | Permissive模式 | 切换到Enforcing,并修复策略 |
| ro.debuggable=1 | userdebug/eng版本 | 使用user版编译 |
| adb调试开启 | persist.sys.usb.config含adb | 改为mtp,关闭开发者选项 |
| 系统分区可写 | remount过 | 重新上锁dm-verity,用原始ro状态 |
| 内核异常 | 自编译内核版本字符串不匹配 | 和系统指纹日期对齐 |
| Bootloader解锁 | bootloader的unlocked状态 | 测试机保持unlock,不计入正式结论 |
5.4 后续还能扩展的方向
做完整个项目之后,我觉得还可以探索两个方向:
一是基于这个基础镜像,做一份完整的“设备画像自检工具”,把AIDA64能读到的所有字段和Build属性、内核节点做一个对比诊断,自动告诉你不一致的点在哪,省得每次手动翻。
二是研究一下AVB签名体系,尝试自己做一套锁定的签名密钥,让bootloader以一个伪装的locked状态启动。这一步如果走通了,那么Momo上的最后一项黄灯就也能变绿,这才是真正完善的设备画像。
不过我也得提醒一句,修改设备身份用于绕过金融级风控属于灰色甚至黑色领域,大家不要在真实生产环境尝试。做安全研究,请在专门测试机上跑,或者只用来保护个人隐私、了解系统底层原理。技术本身是中性的,但使用者的目的决定了它的价值。
最后再分享一点小技巧:如果你只是想做设备建模和测试,不用每次改完整ROM,可以先在源码里把对应变量加上OVERRIDE属性,然后编译system.img分区块刷入,不需要整机重刷。在system.img刷入后先用fastboot reboot看开机日志,如果出现bootloop,立刻fastboot boot boot.img进入临时内核排查,能省下很多反反复复的完整刷机时间。定制ROM这件事,最要紧的是耐心,别指望一次全绿,每一版改动都记录好,出问题能快速回退,这比什么优化配置都重要。