最近帮人折腾安卓系统定制十个里有八个被 SELinux 整得焦头烂额动不动就是 avc denied、进程起不来、系统一直 bootloop。其实很多人没搞明白安卓里的 SELinux 已经是被 Google 深度改造过的一套强制访问控制系统大家都习惯叫它 SEAndroid。它跟 Linux 桌面上的 SELinux 同源但策略模型、初始化流程、权限粒度和编译方式都重新设计过绝对不能直接套用 Linux 的玩法。SEAndroid 解决的核心问题是让安卓系统里的每个进程都只能做自己分内的事情。比如一个普通的第三方 App 没资格直接读你的通讯录一个后台服务也没权限随意操作摄像头这些看似靠权限请求来控制的东西在底层其实都有一层 SELinux 的强制策略兜底。就算你拿到了 root 权限如果 SELinux 处于 enforcing 状态照样会被拦得明明白白。这篇文章我不打算讲教科书式的大道理而是把 SEAndroid 的架构拆开从安全上下文、策略文件、编译集成到实际调试一步步带你走一遍。适合做系统定制、ROM 移植、安全评估和安卓逆向的兄弟参考也适合那些只听说过 SELinux 但没真正动过手的朋友至少看完之后能知道遇到 avc denied 时该从哪里下手。1. SEAndroid 到底是个什么东西1.1 从 SELinux 说起SELinux 是 Linux 内核里的一个安全模块最早由 NSA 贡献给开源社区它把系统安全从传统的 DAC自主访问控制升级成了 MAC强制访问控制。传统 Linux 下文件有 owner、group、others 三种权限root 用户基本可以无视一切限制但 MAC 不一样它要求系统里的每一个主体进程和客体文件、套接字、属性等都有安全上下文然后依靠一套全局策略决定“主体能不能对这个客体做这个操作”。打个比方传统的 DAC 就像公司大门钥匙只要你是员工拿着一把万能钥匙可以进大部分房间。MAC 更像每个房间门口都有独立的门禁规则你的工牌上写着“你是后勤人员”门禁系统说后勤人员只能在第三层活动那就算你有万能钥匙也刷不开别的门。SELinux 提供了两种模式permissive 和 enforcing。permissive 下它会记录所有违规尝试但不阻断enforcing 下则真正强制执行策略。还有 disable 模式但现代系统里几乎没人用因为一旦禁用系统可能连启动都成问题。1.2 Android 为什么要单独做一套安卓最早用的是 Linux 传统 DAC 加上应用沙箱机制靠 uid/gid 隔离各个 App。但后来大家发现仅靠 uid 隔离远远不够。很多系统服务运行在高权限 uid 下一旦被漏洞利用整个系统就裸奔了。Google 从 Android 4.3 开始引入 SELinux到 Android 5.0 全面进入 enforcing 模式从此以后安卓系统的安全模型变成了“应用沙箱 MAC 强制访问控制”双保险。SEAndroid 的核心目标是最小权限原则也就是说系统里每个进程都被塞进一个“域”domain每个文件、设备节点、socket 都被打上“类型”type然后通过一堆允许规则控制域和类型之间的交互。这样就算某个进程被攻破了攻击者也只能在这个域允许的权限范围内活动没办法横向渗透到别的域。很多做 ROM 的兄弟应该深有体会加一个自定义服务、改一个文件权限、定义一个新的设备节点如果不去写对应的 SELinux 策略即使你给了 777 权限系统也照样拒绝访问。这不是安卓故意为难你而是 SEAndroid 的强制访问控制根本不吃传统权限那一套。2. 核心设计拆解标签、策略与域2.1 安全上下文是怎么标出来的SEAndroid 里的每一个对象都有一个安全上下文常用的格式是user:role:type:level。安卓里 user 和 role 一般固定是u和r最重要的其实是type后面可能还跟着 MLs 的level用于多级别安全。比如u:r:system_server:s0表示 system_server 进程的上下文u:object_r:system_file:s0表示一个系统文件。这个标签就像每个进程和文件的“身份证”策略引擎在做判断时根本不看路径和 uid只看这两个安全上下文匹配不匹配。所以你会发现即使你把一个文件从/system复制到/data只要标签没变它依然按照原标签的规则被访问。反过来如果你给一个 App 数据目录打上了 system_file 的标签那系统就可能把它当作系统文件对待。文件标签的初始化是在开机阶段完成的主要由file_contexts文件定义路径与标签的映射关系。每次 root 后或者修改文件后你可能会发现restorecon这个命令特别好用它就是按file_contexts重新给文件打标签的命令。2.2 策略语言与域迁移SELinux 策略语言最核心的就是 allow 规则。举例allow system_server app_data_file:file { read write open getattr map };这条规则的意思是允许 system_server 域对 app_data_file 类型的文件对象进行读、写、打开、读取属性、映射五种操作。如果漏了某一种权限比如缺少search那即使你有read权限目录的search查不到也一样访问不了。所以写策略时经常遇到“权限来回补”的情况。域迁移是另一个重要概念。一个进程默认跑在自己的域里但有时候它需要启动另一个域的程序比如 init 进程会根据 init.rc 里的配置启动各种服务并在启动时通过seclabel字段指定新进程的域。域迁移不是随意的必须有type_transition规则允许否则目标进程还是会启动为默认域。安卓的system_server、zygote、surfaceflinger、vold这些都有独立的域。zygote 在 fork 子进程的时候会根据 App 的包名和 seinfo 来转换到对应的 untrusted_app 域不同 targetSdk 和不同 user 的 app 甚至会被拆分到不同的域里比如untrusted_app_25、untrusted_app_27等目的就是更细粒度地控制。2.3 neverallow 规则的底线价值SEAndroid 策略文件里有一大堆 neverallow 规则这类规则让编译过程直接检查某些危险操作是否被允许。比如说一个普通 App 域永远不可能有直接写 system_file 的能力任何 allow 规则如果撞上了 neverallow都会被编译期拒绝。有人觉得 neverallow 很讨厌因为有时候你只是想放开一个权限却发现跟 neverallow 冲突。但从安全角度讲这些规则正是系统的最后底线。你想测试某些恶意行为是否可行往往会发现明明设备已经 root但 SELinux enforcing 下依然寸步难行大多数情况下就是 neverallow 在起作用。我见过不少人在修改 ROM 时为了省事直接setenforce 0或者在策略里把所有权限全放给某个域。这能解决一时的功能问题但会把 SELinux 的防护能力拉低到零。做正常定制的思路应该是尽量补细粒度策略而不是一刀切关闭。3. 从头到尾跑通一次自定义策略3.1 先学会看状态在动手之前先确认当前设备 SELinux 的工作状态和上下文。常用命令adb shell getenforce adb shell setenforce 0 adb shell setenforce 1 adb shell dmesg | grep avc adb shell dumpsys security adb shell ls -Z /data/data adb shell ps -Zgetenforce输出 Enforcing 或 Permissive如果是 Disabled 那得看一下内核是否支持。ps -Z能看到每个进程的安全上下文ls -Z能看文件和目录的标签。这些命令是排查一切 SELinux 问题的基础我用它们已经数不清多少次了。如果你在 AOSP 源码环境里可以用adb shell setenforce 1切回 enforcing。注意很多 userdebug 固件默认是 permissive某些功能在 userdebug 下跑得通但 user 版就挂就是因为 enforcing 模式下严格了很多。3.2 编写策略文件常见的策略文件放在device/厂商/设备/selinux/下主要包含system、vendor、public、private等子目录。比如你需要给一个自定义服务mydaemon添加存取/data/mydata权限步骤大概是首先定义类型type mydata_file, file_type, data_file_type;然后在 file_contexts 里声明路径映射/data/mydata(/.*)? u:object_r:mydata_file:s0接着写允许规则allow mydaemon mydata_file:dir { create read write open getattr search setattr }; allow mydaemon mydata_file:file { create read write open getattr setattr map }; allow mydaemon mydata_file:sock_file create_sock_perms;如果你的服务需要通过 init 启动在 init.rc 里给服务指定 selinux 域service mydaemon /system/bin/mydaemon class main user root seclabel u:r:mydaemon:s0这样服务启动后就会运行在 mydaemon 域。如果忘了定义域或者没有对应的 allow 规则init 可能拒绝对该服务设置安全上下文服务直接起不来。3.3 编译与集成在 AOSP 环境下selinux 策略文件是通过 BoardConfig 里BOARD_SEPOLICY_DIRS变量收集的。你可以在你自己的 device 目录里加上BOARD_SEPOLICY_DIRS device/xxx/yyy/selinux然后编译时 policy 就会被合成到最终的sepolicy文件里。生成的文件在out/target/product/设备/obj/ETC/sepolicy_intermediates/policy.conf这样的目录中也可以直接看编译时的 audit2allow 输出。有一点特别值得注意安卓 10 以后system 和 vendor 的 SELinux 策略是分开的vendor 进程用的是 vendor 特有策略system 进程不能直接访问 vendor 的 unreachable domains。你给 vendor 模块写策略要放在 vendor 的 sepolicy 目录给 system 模块写则放在 system 的 sepolicy 目录。这俩搞混了轻则编译警告重则运行时还是 avc denied。3.4 调试技巧调试 SELinux 的第一原则先把设备切成 permissive再复现问题看 log 里有没有 avc denied。如果 permissive 下功能正常且无 avc就说明问题不在 SELinux如果 permissive 下功能正常但有 avc那大概率是因为这些操作本来就属于违规但 permissive 只是放行而不记录需要手动确认。最常用的日志位置是logcat -b events或者dmesg很多旗舰机上 avc 消息会进内核 buffer。实际排查中我会先跑adb root adb shell dmesg dmesg.log grep avc dmesg.log也可以使用audit2allow工具自动生成 allow 规则。首先要抓一份 avc denied 日志然后cat dmesg.log | audit2allow它会输出一串建议的 allow 规则。不过这个工具只帮你生成候选不代表加进去就万事大吉还得检查是否违反 neverallow、是否有 type 未定义等。4. 常见问题速查与避坑实录4.1 AVC Denied 日志到底怎么读先看一条典型的 avc deniedavc: denied { read } for pid1234 commmyapp namesecret.conf devmmcblk0p25 ino5678 scontextu:r:untrusted_app:s0:c512,c768 tcontextu:object_r:system_data_file:s0 tclassfile permissive0这里scontext是发起者进程的上下文tcontext是被访问对象的上下文tclass是对象类别花括号里是具体的操作权限。permissive0表示这次是被 enforcing 状态拦下来的。你要做的就是允许untrusted_app域对system_data_file类型文件执行read操作。如果只是单个 App 的问题有时候更快的办法是调整文件标签而不是加域规则。比如某个文件确实应该给 App 读把它标记成app_data_file或者media_rw_data_file解析肯定比你补权限更合理。记住SELinux 讲究的是标签匹配路径只是标签映射的一部分。4.2 加完策略还是被拒绝这种情况特别常见。你可能已经写了 allow 规则但问题依旧。首先确认规则是不是真的加进了最终的 policy。很多 ROM 构建系统有缓存特别是单独 make 某个模块时不会重新生成 sepolicy。需要先make sepolicy或者 clean 后重新编译。其次要检查类型是否存在。如果你在规则里写了一个mydata_file但 file_contexts 或者 type 定义没有正确声明那么这个类型会被当成未知类型规则可能没有生效。建议编译后在板子上用sesearch查询当前 selinux policy 是否包含这条规则sesearch -A -s mydaemon -t mydata_file -c file如果输出为空说明你的规则确实没进去回源头查编译顺序。还有一个非常隐蔽的问题很多服务是在 init 进程中启动的init 在 fork 时可能因为setexeccon失败导致进程仍停留在 init 域。你看到的进程可能只是 init 域而不是你想要的 mydaemon 域那自然跑的是 init 的策略你给 mydaemon 写的规则全是白写。用ps -Z看一眼进程的实际域比什么都管用。4.3 Enforcing 模式下启动崩溃ROM 移植或刷机后在 enforcing 模式下开机卡在 logo 或者直接 bootloop这是最头疼的。遇到这种情况不要盲目重刷。先尝试进入 recovery如果 recovery 支持 adb直接adb shell setenforce 0再重启。如果不支持可以用一个临时用过内核参数的方法adb shell KLIPSE? # 这不是标准命令具体看你的内核是否支持其实更常见的做法是修改 kernel cmdline 追加androidboot.selinuxpermissive。很多 bootloader 支持修改 cmdline或者用 mboot 等工具重新打包 boot.img。这样系统启动时就被置为 permissive可以开机后排查。进入系统后第一件事就是dmesg里搜 avc denied把那些涉及关键服务vold、zygote、surfaceflinger的 denied 全部记录下来逐个补策略。如果实在不清楚哪些必须放行最土的办法是启动到 permissive 后跑一遍完整功能然后抓 log自动生成规则再合并进去。4.4 修改系统权限时的常见误区我刚入门时犯过一个蠢错为了让自己编译的守护进程能访问/sys/class/misc下的设备节点我直接把某个 system_file 标签的权限放开给了所有域结果编译直接挂掉因为撞了 neverallow。后来才明白对于/sys这种敏感路径正确做法是给这个设备节点单独定义一个类型然后只给你的守护进程域添加针对这个类型的规则。还有个很多人踩过的坑file_contexts里定义只写文件名路径但没考虑子目录。比如只写了/data/mydata(/.*)?后面没有根目录标签结果/data/mydata目录本身标签不对导致无法进入目录。实际应该对目录本身和内部文件分别打标签最好用正则覆盖到目录和文件。还有一点当你chown或chmod了某个文件发现依旧访问不了先别急着拿 SELinux 背锅因为它还有可能卡在 Linux DAC 权限上。SELinux 和 DAC 是两套独立的矩阵只有两者都允许时操作才成功。排查时可以ls -lZ同时看模式和安全上下文两个都正常才行。5. 现实场景中的 SEAndroid 经验5.1 系统定制里的应用隔离在做系统定制时经常会遇到要给某个系统应用单独划分数据目录或者限制一个应用访问网络。SEAndroid 可以让这种隔离做得很干净。比如你希望某款 ODM 预装应用只能访问特定目录不能访问 GPS 数据完全可以通过给该应用单独设一个域并只定义必要的 allow 规则。我实际做过一次给系统内置支付应用单独开了payment_app域只允许它访问自己的/data/payment目录和有限的 IPC 通道。因为系统有很多公共的 binder service 以及property_service只靠基础规则没有被定义运行起来比想象中复杂。尤其是 binder 相关的 allow 很容易漏一旦漏了服务调用就会因 avc denied 失败。建议刚开始做这类定制时先把目标应用在 permissive 下跑通记录下 binder 相关 avc 日志用 audit2allow 生成初始规则然后再人工收窄。千万不要把整个 framework 域的所有权限都赋予新域那样等于直接降级安全。5.2 刷机与 Root 场景下的 SELinux 状态玩刷机的朋友对setenforce肯定不陌生。Magisk 这种 root 方案里root 进程通常会运行在magisk域并且默认策略里给这个域补齐了很多权限。有些人刷完模块后运行功能报错就以为是 SELinux 问题实际上是模块脚本没有正确处理文件上下文导致新加的文件标签全错了。Root 不等于随便访问任何文件。在 enforcing 状态下即使你拥有 uid0 的 root 权限如果进程的域是magisk但你访问的是unlabeled或者system_file等敏感类型一样没有权限。这里最典型的例子是修改/system分区的文件如果你只是 mount 为 rw但没有 restorecon 给改动后的文件打标签那么前面提到的system_file标签可能已经被保留但由于 SELinux 策略限制 root 域还是无法访问。我自己的习惯是在刷机完成之后先用restorecon -R /system或者restorecon -R /data修正标记再重启。很多莫名奇妙的 avc denied 都是因为刷机脚本没正确处理上下文导致的。5.3 SELinux 之上还能做什么SEAndroid 并不是终点它底层还有 Linux 内核的其他防护比如 seccomp、capabilities、dm-verity、fs-verify 等。SEAndroid 管的是 MAC 访问控制capabilities 管的是内核权限拆分seccomp 限制的是系统调用能力三者叠加才构成完整的安卓安全隔离体系。如果你打算做工作区隔离、多用户空间或者支撑 AI oT 设备的边缘场景SEAndroid 的策略可以按用户类型做细分。我见过有人为不同 profile 定制不同的seinfo然后在 zygote 里根据seinfo把 App 分派到差异化的域中从而实现多维度的安全隔离。这种思路在强管控设备上非常有用但一定要合理规划策略文件结构否则后期维护是灾难。我在多次定制中最后悔的一次就是前期偷懒把所有 vendor 模块都塞进一个域后期要拆分时改了整整两个星期才把新旧功能理清。SEAndroid 的命名和定义一定要一开始就规划好宁可多做几个域也别图省事。6. 实战补遗从 bug 到修复的一次完整经历最后分享一个真实的小故事。当时我给一款盒子设备做系统定制客户反馈三方 App 无法写入 TF 卡根目录的某个配置文件。拿到样机后我先getenforce确实是 Enforcing再抓 log发现 App 在访问vfat文件系统上的某个文件时被 avc deniedscontext 是untrusted_apptcontext 是unlabeled。unlabeled的出现说明存储文件在挂载过程中没有被正确打标签。很多外部存储默认使用vfat文件系统内核会按照挂载选项动态生成标签常见的类型是vfat或者fuse、sdcardfs。如果挂载时没有指定fscontext选项文件可能落到unlabeled而 untrusted_app 默认策略并没有对unlabeled文件对象开放写权限。解决办法有两种一种是在file_contexts里给这个挂载路径设置固定标签另一种是在 fstab 或 vold 配置中为外部存储挂载增加fscontextu:object_r:vfat:s0这类选项。当天我直接用 adb 手动验证了第二种方式是否可行确认后集成进 vold 的 mount flag问题随即消失。这个过程没有多高深的技术含量但输在“先看状态、再查日志、最后动手”这条流水线上。很多人一上来就改策略文件越改越乱。其实 SEAndroid 的安全上下文规则已经写得相对标准你只是需要找到不匹配的那一环然后对症下药。只要掌握ps -Z、ls -Z、dmesg、audit2allow这几个基本工具再加上对策略文件的熟悉绝大多数 SELinux 问题都是可以被拆解的。