☰
SEAndroid深度解析:SELinux强制访问控制与定制实战
2026/10/3 18:07:14 网站建设 项目流程

最近帮人折腾安卓系统定制,十个里有八个被 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 -Z

getenforce输出 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 denied:

avc: denied { read } for pid=1234 comm="myapp" name="secret.conf" dev="mmcblk0p25" ino=5678 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:system_data_file:s0 tclass=file permissive=0

这里scontext是发起者进程的上下文,tcontext是被访问对象的上下文,tclass是对象类别,花括号里是具体的操作权限。permissive=0表示这次是被 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.selinux=permissive。很多 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 状态下,即使你拥有 uid=0 的 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 denied,scontext 是untrusted_app,tcontext 是unlabeled。

unlabeled的出现说明存储文件在挂载过程中没有被正确打标签。很多外部存储默认使用vfat文件系统,内核会按照挂载选项动态生成标签,常见的类型是vfat或者fuse、sdcardfs。如果挂载时没有指定fscontext=选项,文件可能落到unlabeled,而 untrusted_app 默认策略并没有对unlabeled文件对象开放写权限。

解决办法有两种:一种是在file_contexts里给这个挂载路径设置固定标签,另一种是在 fstab 或 vold 配置中为外部存储挂载增加fscontext=u:object_r:vfat:s0这类选项。当天我直接用 adb 手动验证了第二种方式是否可行,确认后集成进 vold 的 mount flag,问题随即消失。

这个过程没有多高深的技术含量,但输在“先看状态、再查日志、最后动手”这条流水线上。很多人一上来就改策略文件,越改越乱。其实 SEAndroid 的安全上下文规则已经写得相对标准,你只是需要找到不匹配的那一环,然后对症下药。只要掌握ps -Z、ls -Z、dmesg、audit2allow这几个基本工具,再加上对策略文件的熟悉,绝大多数 SELinux 问题都是可以被拆解的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询