上周帮朋友看一台定制的安卓一体机,开机卡在品牌 logo 无限重启,串口日志刷得飞快,最后几行停在Signature|privileged permissions not in privapp-permissions whitelist上。他第一反应是"是不是系统镜像烧坏了",第二反应是"是不是我那个预装的应用签名有问题"。折腾了半天,问题其实特别朴素:他把一个应用丢进了/system/priv-app,但忘了给这个应用补 privapp-permissions 授权清单,ro.control_privapp_permissions又是 user 版本默认的 enforce,system_server 在启动扫描阶段直接抛异常,整机就进不去了。
我猜不少人在搜privapp-permissions的时候都被搜索引擎带偏过,旁边自动补全的词全是android studio 安装教程、android studio 汉化、文件权限修复、你需要来自administrators的权限才能删除这类跟本话题八竿子打不着的东西。privapp-permissions 和那些内容之间几乎没有交集,它是Android 特权应用(privileged application)权限授予体系里的一环,解决的是"预装在系统分区里的应用凭什么能拿到系统级权限"这个问题。
如果你在做 ROM 定制、系统预装应用集成、车机/平板/盒子类产品的系统打包,或者你写的应用需要WRITE_SECURE_SETTINGS、PACKAGE_USAGE_STATS、READ_DEVICE_IDENTIFIERS这类只对系统应用开放的能力,那这套机制你绕不开。下面我按自己的理解和工作里的踩坑经历,把 privapp-permissions 从头到尾捋一遍,包含文件怎么写、放哪儿、怎么验证、报错了怎么查,以及它对整个 Android 安全模型带来的连锁影响。
1. 先把话说清楚:privapp-permissions 到底解决什么问题
1.1 从 /system/priv-app 说起:特权应用的历史身份
Android 早期设计了一套很朴素的分区目录约定:普通系统应用放/system/app,而"需要更高权限的系统应用"放/system/priv-app。这个目录不是随便起的名字,PackageManagerService 在扫描 APK 时会根据路径给应用打上ApplicationInfo.FLAG_PRIVILEGED标记,代码里通常叫它 privileged app 或者特权应用。
一个应用一旦被判定为 privileged,它在权限授予环节就拥有了普通应用没有的待遇。最典型的是signature|privileged这个保护级别的权限——普通应用想拿它必须和系统使用同一个签名,而特权应用即使签名不匹配,也有机会被授予。这就解释了为什么历史上有大量厂商和第三方把自己的应用塞进/system/priv-app,因为那是"低成本获得系统能力"的捷径。
这套约定在 Android 8.0 及以前基本是"靠目录说话":只要你的 APK 在 priv-app 目录里,manifest 里声明了signature|privileged权限,系统安装阶段就会直接给。没有额外的审批环节,没有清单登记。对 ROM 开发者来说很方便,对攻击面来说就很难看。
1.2 Android 9 为什么突然"收紧":一个真实的安全缺口
到了 Android 9(API 28),AOSP 引入了 privapp-permissions 机制,规则彻底变了。特权应用不再能"声明即获得"系统级权限,而是必须在系统分区的/etc/permissions/目录下有一份 XML 授权清单,逐条列出"这个包名允许持有哪些特权权限"。清单里没有的权限,系统就拒绝授予;在强制模式下甚至直接把 system_server 干崩。
为什么非要做这件事?站在平台侧想一想就很清楚了。/system/priv-app是只读分区,理论上用户和普通应用改不了,但实际供应链里能往这个目录塞东西的角色太多了:芯片方案商、ODM、品牌方、渠道预装、甚至刷机包制作者。每一个环节都有可能塞一个应用进去,而这个应用只要在 manifest 里写上一行android.permission.WRITE_SECURE_SETTINGS,就能静默修改系统安全设置。整个过程没有任何显式的授权动作,用户也看不到,审计也查不到——这就是这套机制要堵的洞。
引入授权清单之后,往 priv-app 里塞应用这件事仍然可以做,但"给它什么权限"变成了一个必须显式写下来、必须跟着包名走的声明。ROM 编译时这份清单会被打包进系统镜像,谁改了什么权限,diff 一下 XML 就能看出来。审计成本从"逆向 APK 看 manifest"降到了"看一行业务代码式的配置"。
1.3 授权清单的边界:它管什么、不管什么
这里有个认知误区需要提前掰开:privapp-permissions 不是万能的权限总开关,它的管辖范围非常明确。
第一,它只管protectionLevel 里带privileged标志的权限。normal级别权限装完就有,跟清单无关;dangerous级别权限走的是运行时申请流程,用户点同意才有,清单也管不着。真正被清单卡住的,是signature|privileged和纯粹的privileged这两类。
第二,它管的是权限的授予,不是权限的使用。一个应用被授予了WRITE_SECURE_SETTINGS,它拿这个权限去干什么,清单不会去过问。清单只是把"谁可以拥有"这一步显式化了,后续的行为约束得靠 SELinux、AppOps 这些机制。
第三,它不区分应用的功能意图。清单里写android.permission.INTERACT_ACROSS_USERS,系统不会去判断你这个应用到底有没有多用户交互的需求,它只认包名和权限名的匹配关系。所以真正决定"给不给"的,是写清单的人,也就是 ROM 集成方。
2. 权限分级与授权清单的匹配规则
2.1 protectionLevel 四层分级与 privileged 标志位
要理解清单为什么这么写,得先回到 Android 权限的保护级别定义。manifest 里android:protectionLevel的取值可以组合,常见的有:normal、dangerous、signature、signature|privileged、privileged、development。
signature的含义是"申请方必须和被申请方(定义这个权限的一方)签名一致",本质上把权限锁在同一个签名体系内部。privileged是一个附加标志位,它单独出现时含义是"只有特权应用可申请";和signature组合成signature|privileged时,含义变成"签名一致可以拿,或者你是特权应用且在白名单里也可以拿"。这个"或"关系是理解整个机制的关键。
用生活化的说法类比一下:signature像是"公司内部员工卡",只有本公司的卡能刷开本公司的门;signature|privileged则是"内部员工卡可以刷,但如果园区管委会给你发了一张特别通行证(白名单登记),非本公司的人也能进"。privapp-permissions 清单扮演的就是那张特别通行证的登记簿。
2.2 签名匹配优先级:为什么 platform 签名应用不用写清单
很多人第一次接触这套机制时会有个疑问:我的应用已经用 platform 签名了,为什么还报错?答案往往是——你的应用其实没有真正用 platform 签名构建,或者你的应用请求的是纯privileged权限(没有signature部分),那签名再对也没用,必须走清单。
真正用 platform 签名(和android包同签名)的应用,在signature类权限上是靠签名匹配直接通过的,压根不走白名单那条路。所以判断"要不要写清单"的第一步,是先确认应用签名是不是平台签名。判断方法也不复杂:adb shell dumpsys package <包名>看signatures段,或者直接看构建产物是不是用了platform.x509.pem那一套 key。
这里有个坑我踩过:某些平台会在构建后期对 APK 做二次签名或者重签名(尤其是渠道包流程),结果 manifest 里写着要WRITE_SECURE_SETTINGS,最终产物却不是 platform 签名,于是清单里也没写、签名也对不上,权限就静默丢了。这种情况在开机日志里表现得很明显,dumpsys package里那条权限压根不在install permissions分组里。
2.3 哪些目录下的应用会被判定为 privileged
授权清单是按分区目录生效的,所以搞清楚哪些目录会被扫描、哪些目录下的应用算特权应用,是排查问题的前提。Android 10 之后分区结构拆得更细,涉及的目录也更多,我整理成一张表方便对照。
| 目录路径 | 所属分区 | 是否判定为特权应用 | 常见用途 |
|---|---|---|---|
| /system/priv-app | system | 是 | AOSP 自带系统应用 |
| /system_ext/priv-app | system_ext | 是 | 厂商从 system 拆出来的系统组件 |
| /product/priv-app | product | 是 | 产品差异化预装应用 |
| /vendor/priv-app | vendor | 是 | 芯片/方案商相关组件 |
| /odm/priv-app | odm | 是 | ODM 定制组件 |
| /system/app | system | 否 | 无特权需求的系统应用 |
| /data/app | data | 否 | 用户自行安装的应用 |
注意:判断一个应用是不是特权应用,最可靠的依据是
dumpsys package输出里的flags字段是否包含PRIVILEGED,而不是靠看 APK 放在哪个目录的"印象"。有些设备的分区挂载方式比较特殊,路径和分区标识不总是一一对应。
另外要澄清一点:授权清单文件本身不一定要放在"应用所在的那个分区"。系统会把所有分区的/etc/permissions/目录都读一遍并且合并结果。也就是说,一个放在/vendor/priv-app的应用,它的白名单完全可以写在/system/etc/permissions/下,只要包名对得上就行。实际项目里通常还是就近放置,方便模块化管理。
3. XML 授权清单文件的写法与落地位置
3.1 标准文件结构与最小可用示例
privapp-permissions 的 XML 结构本身非常朴素,根节点是<permissions>,里面用<privapp-permissions>元素按包名分组,每个分组里用<permission name="...">列出允许的权限名。
<?xml version="1.0" encoding="utf-8"?> <permissions> <privapp-permissions package="com.example.systemhelper"> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> <permission name="android.permission.PACKAGE_USAGE_STATS"/> <permission name="android.permission.REBOOT"/> <permission name="android.permission.CHANGE_CONFIGURATION"/> </privapp-permissions> <privapp-permissions package="com.example.settings.provider"> <permission name="android.permission.INTERACT_ACROSS_USERS"/> <permission name="android.permission.MANAGE_USERS"/> </privapp-permissions> </permissions>几个容易写错的地方,我按踩坑频率排一下。
第一,package属性填的是包名,不是 APK 文件名,也不是应用显示名称。如果应用用了applicationIdSuffix(比如 debug 和 release 包名不同),要按最终装机用的包名写。
第二,<permission>里的name填的是权限全名,必须和 manifest 里uses-permission声明的字符串逐字一致,包括大小写和包名前缀。写成WRITE_SECURE_SETTINGS这种省略形式是无效的。
第三,绝对不能填权限组名。权限组(permission-group)只是界面上做分类展示用的,跟授予逻辑没关系。我见过有同事把android.permission-group.SYSTEM_TOOLS写进去,结果权限一条都没生效,排查了小半天。
第四,根节点必须是<permissions>,这是SystemConfig解析时的硬性约定。写错根节点会导致整个文件被跳过,日志里能看到解析失败相关的告警。
3.2 放进哪个分区:system / system_ext / product / vendor 的选择逻辑
文件写完之后,放哪儿是个需要想一下的问题,因为它直接关系到 OTA 升级和模块解耦。
从系统读取顺序上说,SystemConfig会扫描各个分区的/etc/permissions/与/etc/sysconfig/目录,把里面的文件逐个解析、合并进内存中的权限配置表。所以从"能不能生效"的角度,放哪个分区都行。但从工程角度,有几个约定俗成的取舍:
- AOSP 原生应用的清单跟着原生应用走,通常在
frameworks/base/packages/相关模块里,最终落进/system/etc/permissions/。 - 厂商自己拆出来的系统组件,如果组件本身放在
system_ext,清单也建议放system_ext/etc/permissions/,保持分区自洽。 - 产品差异化预装应用,清单跟着 product 分区走,这样不同产品线切换时能整块替换。
- 芯片方案商提供的组件,放在
vendor或odm分区,清单同理。
这个"就近放置"原则的实际价值在于:如果哪天要裁掉某个方案商的模块,只要把它的 priv-app 目录和对应的 permissions 文件一起删掉就行,不会留下一堆孤儿配置。
3.3 用 Android.bp 的 prebuilt_etc 集成进 ROM
手工PRODUCT_COPY_FILES也能把 XML 拷进镜像,但 Android 10 之后更推荐用 Soong 的prebuilt_etc模块,好处是能指定安装分区、能参与依赖图、能被PRODUCT_PACKAGES引用。
prebuilt_etc { name: "privapp-permissions-example.xml", src: "privapp-permissions-example.xml", sub_dir: "permissions", filename_from_src: true, } // 如果需要装到 product 分区 prebuilt_etc { name: "privapp-permissions-example-product.xml", src: "privapp-permissions-example-product.xml", sub_dir: "permissions", filename_from_src: true, product_specific: true, }然后在设备配置的 makefile 里加上:
PRODUCT_PACKAGES += \ privapp-permissions-example.xml这里有个细节值得单独拎出来说:prebuilt_etc默认装到system/etc,sub_dir: "permissions"会让最终路径变成/system/etc/permissions/。如果你想放到system_ext,用system_ext_specific: true;想放vendor,用soc_specific: true或者vendor: true。不同分支上这些属性名可能有细微差别,建议先grep一下现有模块的写法再抄。
提示:文件权限和 SELinux 标签也需要正常。用
prebuilt_etc生成的一般会带system_file标签(system 分区)或vendor_file(vendor 分区),手工拷贝的场景要留意别出现标签错配,否则会出现"文件在、内容对、就是不生效"这种玄学现象。
3.4 命名冲突与文件名前缀约束
这是我认为最容易被忽视、但后果最隐蔽的一个点:SystemConfig在解析权限目录时会对文件名做去重。如果system分区和vendor分区下各有一个叫privapp-permissions-custom.xml的文件,只有其中一个会被解析,另一个会被直接跳过并打一条 warning。
这个设计本身有它的道理——避免同名文件互相覆盖造成配置漂移。但对开发者来说就是灾难:你在 vendor 分区里改了半天,其实生效的是 system 分区里那个同名文件。
所以命名上一定要做到全局唯一。建议的命名格式是privapp-permissions-<模块名或厂商名>.xml,带上足够区分度的前缀。顺带提一句,较新版本的 AOSP 对这类文件的命名做了更严格的约束,要求文件名以privapp-permissions-开头,沿用老命名习惯在新分支上可能会触发告警甚至 CTS 失败。这条约束的具体表现各家分支不完全一样,动手前最好在源码里搜一下privapp-permissions相关的字符串确认。
4. enforce 与 log:ro.control_privapp_permissions 的行为差异
4.1 三种取值的实际表现
AOSP 用一个系统属性来控制这套机制的严格程度,属性名是ro.control_privapp_permissions,它有三个有效取值,行为差别非常大。
| 取值 | 行为 | 适用场景 |
|---|---|---|
| disable | 完全跳过检查,特权权限按老规则直接授予 | 极老的兼容场景,基本不该用 |
| log | 只记录告警日志,权限照常拒绝,但不影响开机 | userdebug/eng 调试 |
| enforce | 缺失清单时抛异常,system_server 崩溃重启 | user 正式版本默认 |
log模式的价值在于:它把"缺清单"这件事从"开机即死"降级成了"日志里能看到"的软失败。你在 userdebug 上刷机调试,即使漏了某条权限,系统也能正常起来,然后你去 logcat 里搜关键字,把缺口一次性补齐。等清单补全了,再切到 enforce 模式验证一次。
enforce模式的杀伤力就大得多。它在启动扫描阶段直接抛IllegalStateException,异常从 system_server 主线程抛出来,进程崩溃,Zygote 重启,再崩,再重启,最终表现就是开机动画循环转圈,或者卡在 logo 不动。很多人第一次遇到会误判成硬件故障或者镜像损坏,其实去看一眼内核/串口日志,答案就写在异常信息里。
4.2 构建类型默认值:user 与 userdebug/eng 的差异
如果你在设备上跑getprop ro.control_privapp_permissions发现是空的,别慌,这不是异常。属性没显式设置时,系统会根据构建类型给一个默认行为:可调试构建(userdebug、eng)默认走log,正式user构建默认走enforce。
这个默认值差异造成的经典事故是:userdebug 上验得好好的一版固件,出 user 版本就开不了机。因为开发阶段大家习惯了log模式的"宽容",清单缺几条根本感知不到,直到切到 user 构建才暴露。我在项目里养成的习惯是,只要动过 priv-app 相关的东西,一定单独出一个 user 版本的验证包,装完至少完整重启两次,确认没有 boot loop。
要显式指定这个属性,可以在设备配置里注入:
PRODUCT_PRODUCT_PROPERTIES += \ ro.control_privapp_permissions=log注意这是只读属性,运行时用setprop改是改不动的,必须编译期注入或者走设备树 overlay。改完之后adb shell getprop ro.control_privapp_permissions确认一下实际生效的值,不同分支上属性优先级可能有覆盖关系,别只信配置文件。
4.3 调试期临时放开的正确姿势与风险
调试期临时放开检查,一般就是把它设成log。这个过程有几个坑要提醒。
第一,别用persist.sys前缀的变体乱试。虽然某些分支上确实存在persist.sys.control_privapp_permissions这类调试用的覆盖开关,但这不是所有版本都支持的,属于"源码里恰好有就赚到"的东西。稳妥做法还是编一个log版本的镜像刷进去调试,或者用 userdebug 构建(本身默认就是 log)。
第二,disable这个取值虽然存在,但我强烈建议不要在正式固件里用。它的效果是把整套机制关掉,等于回到了 Android 8.0 之前的裸奔状态,前面说的那些安全收益全部归零。更要命的是,CTS 有对应的用例会在系统里做同样的检查,正式认证场景这样做基本过不了。
第三,改回enforce之后一定要做完整的开机验证,不能只看"能进桌面"。因为 system_server 抛异常的位置在启动早期,有时候异常发生在桌面起来之后(比如某些延迟初始化的模块),这时候表现可能是"用几分钟突然重启",更隐蔽。
5. 实操:从 logcat 报错到清单补全的完整闭环
5.1 抓日志:定位缺哪些权限
整个排障闭环的第一步是拿到准确的缺失清单。最直接的入口是 logcat 里的关键字,异常信息和告警信息都带着privapp-permissions字样。
# 抓当前缓冲区里所有相关日志 adb logcat -b all -d | grep -i "privapp-permissions" # 如果是反复重启的设备,抓串口日志更靠谱 # 串口终端里直接看 system_server 崩溃前的最后几十行典型的 enforce 模式报错长这样:
java.lang.IllegalStateException: Signature|privileged permissions not in privapp-permissions whitelist: {com.example.systemhelper: android.permission.WRITE_SECURE_SETTINGS}log 模式的告警则是这个味道:
W PackageManager: Signature|privileged permissions not in privapp-permissions whitelist: {com.example.systemhelper: android.permission.REBOOT}花括号里的内容就是答案:冒号前面是包名,后面是缺失的权限名。如果一次缺很多条,会看到多个包名和权限混在一起,用逗号分隔。这时候最省事的做法是把整段日志复制到编辑器里,按{和}拆开逐条整理。
需要注意的是,一次开机只能暴露"当前扫描到的那批"。补齐、重编、重启之后,可能又冒出新的——因为扫描顺序和依赖关系会导致某些应用晚一步才处理。所以这个流程通常要跑两到三轮才能收敛,别指望一把过。
5.2 用 AOSP 自带脚本批量生成清单
手工对着日志抄权限名很枯燥,而且容易抄漏。AOSP 在development/tools/privapp_permissions/下提供了一个脚本,能扫描构建产物并自动输出缺失的清单内容。
cd $ANDROID_BUILD_TOP source build/envsetup.sh lunch <你的目标设备>-userdebug # 跑一遍脚本 development/tools/privapp_permissions/privapp_permissions.py脚本会把out/target/product/<device>/下各个分区的 priv-app 目录扫一遍,比对已有的权限清单文件,然后打印出"哪些应用请求了哪些特权权限但没有清单覆盖"。输出的内容基本可以直接贴进 XML 文件里。
几点使用心得:
- 脚本必须在完整编译过一遍之后运行,因为依赖 out 目录下的产物。如果只编了 system 分区,vendor 下的应用它扫不到。
- 脚本读取的清单文件来源也是 out 目录,所以你改了源码里的 XML 一定要先重新编一遍再跑,否则比对结果不准。
- 不同 Android 版本的脚本参数有差异,
--help看一眼总没错。有的版本支持输出成完整 XML 片段,有的只输出列表。 - 脚本只能告诉你"缺什么",不能替你决定"该不该给"。有些应用请求了明显不该有的权限,这时候正确的做法是去改应用的 manifest,而不是往清单里加。
如果你想手工核对某一个具体应用申请了哪些权限,用aapt2更直接:
aapt2 dump permissions \ out/target/product/<device>/system/priv-app/SystemHelper/SystemHelper.apk输出里的uses-permission列表就是潜在需要写进清单的候选集。不过要注意的是,只有那些 protectionLevel 带privileged的权限才需要写清单,普通权限写进去不但没用,还会让清单变得很脏。想确认某个权限的保护级别,去frameworks/base/core/res/AndroidManifest.xml里搜权限名,看它声明的 protectionLevel 就行。
5.3 手写清单的校验与验证步骤
清单文件写好、集成进构建之后,怎么确认它真的生效了?我一般走这四步。
第一步,确认文件真的进了镜像。adb shell ls -l /system/etc/permissions/ | grep privapp,如果文件不在,那是构建集成的问题,跟权限逻辑无关。
第二步,确认系统解析到了这个文件。logcat 里搜SystemConfig,解析失败会有告警,比如根节点不对、XML 语法错误。语法错误特别隐蔽,一个没闭合的标签就能让整个文件被跳过,而且不会有很显眼的报错。
第三步,确认权限真的授予了。用dumpsys看具体的权限状态:
adb shell dumpsys package com.example.systemhelper | grep -A 20 "install permissions"授权成功的权限会出现在install permissions分组里,格式是权限名: granted=true。如果某个权限压根不在这个分组里,说明授予环节就被拒绝了。
第四步,确认应用能看到这个权限。这一步经常被跳过,但很关键。应用侧可以用checkSelfPermission()或者PackageManager.getPackageInfo()里的requestedPermissions来确认。有些情况下权限显示已授予,但应用因为 SELinux 上下文或者 AppOps 拦截,实际调用系统接口时依然被拒,这时候要看dmesg或者logcat里的avc: denied日志。
5.4 一次真实的排障记录
说一个印象比较深的案例。某款车载设备,OTA 升级到基于 Android 11 的新版本之后,一部分用户反馈"网络设置页面打不开",点进去闪退。开发同学第一反应是应用 bug,但在本地用同一版 APK 测是好的。
拿到设备之后,我的排查顺序是这样的:先看闪退日志,发现是SecurityException,抛出来的位置在修改网络配置的接口上;再看权限,应用请求了WRITE_SECURE_SETTINGS,在dumpsys package里也是granted=true。到这一步看着很正常,于是继续往下查。
转而去看系统侧日志,logcat -b all | grep privapp,果然有告警:
W PackageManager: Signature|privileged permissions not in privapp-permissions whitelist: {com.vendor.networkhelper: android.permission.NETWORK_SETTINGS}原来是这个应用除了WRITE_SECURE_SETTINGS(这条在清单里有),还额外请求了NETWORK_SETTINGS(这条没写)。WRITE_SECURE_SETTINGS拿到了,所以应用能启动,但真正要改网络配置的时候,需要的是NETWORK_SETTINGS,这条被静默拒绝了,于是抛异常闪退。因为构建类型是 userdebug,走的是 log 模式,所以只告警不崩溃,问题就这么一路漏到了用户手里。
这个案例给我的教训有两条。一是不要只看"应用能不能起来",要看"功能能不能跑通"。log 模式下权限缺失是软失败,应用启动阶段往往感知不到,只有走到具体功能路径上才爆。二是排查权限问题时,抓日志比看 dumpsys 更直接。dumpsys只能告诉你已授予了什么,告警日志才能告诉你被拒绝的是什么。
顺带说一句,这次问题的根因其实是应用在新版本里加了个新功能、引入了一条新权限,但集成方不知道这个约定,清单没同步更新。所以流程上应该约定:任何预装应用改 manifest 里的权限声明,都必须同步更新清单文件,最好在 CI 里加一道检查。
6. 常见问题与避坑清单
排障排得多了,很多现象其实是重复的。我把高频问题整理成一张表,遇到类似情况可以直接对号入座。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 开机循环重启,卡 logo | enforce 模式下清单缺失 | 抓串口/logcat 里的 IllegalStateException,补齐清单 |
| 同一份固件,userdebug 正常,user 不行 | 构建类型默认值差异 | 显式检查ro.control_privapp_permissions实际值 |
| 清单文件改了但完全没效果 | 文件没进镜像,或 XML 语法错误 | ls确认路径,logcat 搜 SystemConfig |
清单加了权限,dumpsys里仍没有 | 包名写错,或权限名拼写不一致 | 逐字比对 manifest 里的 uses-permission 字符串 |
| 某个分区改了配置没生效 | 不同分区存在同名文件被去重 | 全局搜索同名文件,统一命名加前缀 |
用pm grant授权被拒绝 | 运行时授权不支持 signature/privileged 类权限 | 只能改清单,运行时命令无效 |
| 清单里写了权限组名 | 混淆了权限组与权限名 | 改成具体的权限全名 |
| 应用有平台签名却仍报缺失 | 实际产物不是平台签名,或被二次签名 | dumpsys package看 signatures |
| 重启后权限状态不对 | 旧权限状态被缓存在 data 分区 | 清除 data 后重启验证 |
关于最后一条,补充一点实操体会。改完清单之后如果重启发现权限状态还是老的,先别怀疑清单写错了,很可能只是/data分区里缓存了上一次的权限授予结果。这种情况下做一次恢复出厂设置(清 data)再验证,往往就正常了。我在项目里遇到过两三次,每次都是先怀疑人生、再怀疑配置、最后清个 data 就好了。
另外还有一个经验性的提醒:不要往清单里"多加一点"。有些同学为了省事,把一个应用可能用到的所有系统权限都写进清单,想着"反正加多了也不影响"。这个想法在这套机制里是不成立的。每一行清单都是一次显式的权限授予,权限面越大,被滥用的风险越高,同时也会让安全审计和 CTS 检查里多出需要解释的内容。正确做法是按需最小化,应用实际调用了哪个接口、需要哪条权限,就写哪条。
7. 影响范围:这件事到底牵动了谁
7.1 对 ROM 集成与 CTS 认证的影响
从 ROM 集成的角度看,privapp-permissions 改变的是工作流程。在 Android 8.0 之前,集成一个预装应用基本是"把 APK 拷进 priv-app 目录、在 makefile 里加一行",收工。现在多了一步"梳理这个应用请求了哪些特权权限、写进清单、验证"。这一步没有技术难度,但它是强制性的,漏了就是开不了机。
对 CTS 认证的影响更直接一些。CTS 里有对应的测试项会检查系统分区里特权应用的权限授予情况,检查逻辑和系统启动时的检查本质是同一套。这意味着在正式认证的设备上,你没法靠log模式蒙混过关——该暴露的问题在认证阶段一定会暴露出来。反过来说,在开发早期就把清单补干净,认证阶段可以少踩一个坑。
再往上层看,这套机制其实也在慢慢改变分区职责的划分。以前很多厂商愿意把应用塞进system/priv-app图方便,现在因为要走清单流程,大家更倾向于评估一下"这个应用到底需不需要特权身份",不需要的就挪到普通系统应用目录,整个系统的攻击面反而收窄了。
7.2 对应用开发者的影响
对纯第三方应用开发者来说,这套机制基本无感,因为第三方应用本来就装不到 priv-app 目录里。真正受影响的是那些"给方案商或者设备厂商做预装应用"的团队。
这类团队常见的困惑是:"我在真机上跑得好好的,客户拿到固件说权限没了"。原因往往很简单:开发阶段用的是把 APK 手动 push 到/data/app或者adb install的方式,压根不是特权应用;而客户那边是把 APK 编进priv-app的,走的是特权应用的授权逻辑。两种状态下权限获取路径完全不同。
所以给这类团队的建议是:开发阶段就模拟真实部署方式。把 APK 放进priv-app目录、写好清单、刷进设备验证,而不是等交付的时候才发现权限对不上。同时要明确告诉集成方"我这个应用需要哪几条特权权限",最好在交付文档里直接给出可粘贴的 XML 片段。
7.3 对安全模型的影响
从更大的视角看,privapp-permissions 是 Android 权限体系从"隐式信任"向"显式声明"演进的一环。类似的思路在别的领域也能看到:早期是"把东西放到可信目录就默认可信",后来变成"可信目录只是前提条件之一,还得有一份登记"。
这个变化的实际收益是审计成本的下降。以前要确认一台设备上的预装应用有没有多余的系统权限,得逐个 APK 反编译看 manifest;现在只要把系统分区里的 permissions 目录打包拿出来,所有特权权限的授予关系一目了然,甚至可以做成自动化 diff,检测固件升级前后权限面的变化。我在项目里就用这种方式抓出过"某次升级静默给一个应用多加了两条系统权限"的情况,如果靠人工看 APK,几乎不可能发现。
7.4 一个容易被忽略的连带影响:动态化预装
现在有些厂商会做"动态预装",也就是应用主体在 data 分区、只有壳或者配置在系统分区,然后靠特权身份去拿权限。这种玩法在有了清单机制之后会变得很别扭:清单是按包名写的,包名对得上,但应用的实际代码可能来自可更新的部分。这意味着清单授权的是一个"身份",而这个身份背后的代码是可能变化的。
从安全角度,这等于在系统里留了一个可以随时替换内容的特权入口。我的建议是,如果一个应用需要特权权限,尽量让它保持静态预装,不要搞动态化,至少不要让它成为唯一持有某些高危权限的角色。这个判断没有绝对的对错,但值得在架构评审阶段拿出来讨论一下。
最后分享一个我自己在用的小习惯。每次做完 priv-app 相关的改动,我会在设备上跑几个命令组成一条检查链:先getprop ro.control_privapp_permissions确认模式,再logcat -b all -d | grep -i privapp确认没有遗留告警,最后打开应用走一遍主功能路径。这三步加起来不到五分钟,但能挡住大部分"看起来没问题、实际有隐患"的情况。踩过几次坑之后我是真的相信,权限这类问题上,多花五分钟验证,比事后花五小时排查划算得多。