12. Android SELinux(一):Android SELinux架构、init.rc与sepolicy集成

好,咱们今天聊聊SELinux在Android里的落地。说实话,我早年刚接触Android安全时,觉得SELinux就是个“黑盒子”——打开强制模式就崩,关掉又怕被骂。后来啃了几个月源码,才慢慢摸清门道。

这一讲,咱们聚焦三个核心问题:SELinux在Android里到底长什么样?init.rc怎么跟它配合?sepolicy文件又是怎么组织起来的?

12.1 Android SELinux整体架构

先看一张我手绘的架构图,帮你建立整体认知。

Android SELinux 架构分层 应用层(App Domain) 每个App拥有独立的安全上下文(如 u:r:untrusted_app:s0) 框架层(System Server / Service Domain) system_server、mediaserver、surfaceflinger 等核心服务 内核层(Kernel Domain) LSM Hook + SELinux LSM 模块 + 安全策略数据库 策略层(sepolicy) te文件、file_contexts、property_contexts、genfs_contexts 图12-1 Android SELinux 四层架构

这张图我画了四层。你可能会问:为什么不是三层?因为策略层虽然运行在内核之上,但它的配置和编译完全独立。我个人习惯把策略层单独拎出来,这样排查问题时思路更清晰。

核心要点:Android SELinux 是“用户态策略 + 内核态强制访问控制”的组合。策略文件在用户态编译成二进制,然后加载到内核。内核里的LSM Hook会在每次系统调用时检查权限。

12.2 init.rc 与 SELinux 的集成

init.rc 是Android启动的“总指挥”。SELinux的初始化,其实就藏在init进程的启动流程里。

我记得第一次看init.rc时,被一堆on early-initon init搞晕了。后来才明白,SELinux的启动顺序非常讲究:

  1. early-init阶段:挂载selinuxfs,设置内核安全上下文
  2. init阶段:加载sepolicy文件,进入 enforcing 模式
  3. late-init阶段:为关键服务设置安全上下文

来看一段典型的init.rc片段:

on early-init
    # 挂载selinux文件系统
    mount selinuxfs selinuxfs /sys/fs/selinux
    # 设置内核安全上下文
    write /sys/fs/selinux/enforce 0

on init
    # 加载编译好的sepolicy
    selinux_load_policy()
    # 进入强制模式
    write /sys/fs/selinux/enforce 1

on late-init
    # 为系统服务设置安全上下文
    setcon u:r:init:s0
    # 启动关键服务
    class_start core

避坑指南:我曾经在某个项目里,因为init.rc中write /sys/fs/selinux/enforce 1写在了selinux_load_policy()之前,导致系统启动时直接panic。原因是策略还没加载,内核不知道该怎么强制。记住:先加载策略,再开强制

12.3 sepolicy 文件组织

sepolicy 不是单个文件,而是一个目录树。Android 10之后,它被拆成了多个子目录。我整理了一张表格,方便你对照:

目录/文件 作用 典型内容
system/sepolicy/ 系统级策略,所有设备通用 domain.te, app.te, file_contexts
vendor/sepolicy/ 厂商定制策略,硬件相关 vendor_init.te, hal_xxx.te
product/sepolicy/ 产品级策略,特定产品定制 product_app.te
device/<oem>/<device>/sepolicy/ 设备专属策略,最底层 device.te, file_contexts

你可能会问:为什么要分这么多目录?说白了,是为了解耦。Google更新系统时,只动system/sepolicy;厂商改硬件驱动时,只动vendor/sepolicy。互不干扰。

12.4 核心策略文件详解

咱们挑几个最重要的文件说说。嗯,这里要注意,不是所有.te文件都平等——有些是“骨架”,有些是“血肉”。

12.4.1 domain.te —— 所有进程的“祖宗”

domain.te 定义了所有进程域共有的权限。比如:

# 所有进程都可以读取自己的安全上下文
allow domain domain:process getattr;

# 所有进程都可以向logd写日志
allow domain logd:unix_dgram_socket sendto;

# 禁止所有进程直接访问内核对象
dontaudit domain kernel:system *;

我个人习惯把domain.te比作“宪法”——它规定了底线。任何新加的进程域,都必须继承domain属性。

12.4.2 app.te —— 应用沙箱的边界

应用的安全策略全在app.te里。Android用untrusted_appplatform_app来区分普通应用和系统应用:

# 普通应用:只能访问自己的数据目录
allow untrusted_app self:file read write;
allow untrusted_app app_data_file:dir create_dir_perms;

# 系统应用:可以访问更多系统资源
allow platform_app system_app_data_file:dir search;
allow platform_app radio:unix_stream_socket connectto;

警告:千万不要随意给untrusted_app添加额外的allow规则。我在一个OEM项目里见过,厂商为了“方便调试”,给所有应用加了allow untrusted_app system_file:file read,结果被Google CTS测试直接判fail。SELinux的粒度越细,安全性越高。

12.4.3 file_contexts —— 文件的安全标签

文件的安全上下文不是自动继承的,而是通过file_contexts文件静态指定。比如:

/system/bin/init        u:object_r:init_exec:s0
/system/bin/servicemanager u:object_r:servicemanager_exec:s0
/data/app/              u:object_r:apk_data_file:s0
/data/data/             u:object_r:app_data_file:s0

这里有个细节:_exec后缀表示“可执行文件的安全上下文”。当init执行/system/bin/servicemanager时,内核会根据servicemanager_exec的标签,自动将进程域切换为servicemanager。这就是SELinux的“域转换”机制。

12.5 策略编译与加载流程

最后,咱们走一遍策略从源码到内核的完整流程。我画了个流程图:

步骤1:编写.te文件 步骤2:编译为cil 步骤3:链接为二进制 步骤4:加载 编译工具链:checkpolicy -> secilc -> 生成 sepolicy 二进制文件 加载路径:/system/etc/selinux/ -> /sys/fs/selinux/policy 关键点:策略编译在构建时完成,运行时只加载 init进程通过 selinux_android_load_policy() 加载预编译策略 加载失败则系统进入 permissive 模式(或直接panic)

这个流程里,最容易出问题的是步骤2到步骤3的转换。我记得有一次,我在.te文件里写了个allow规则,但忘了声明对应的type,结果编译出来的cil文件直接报错。排查了半天才发现是type声明缺失。

调试技巧:如果你在开发板上调试,可以用adb shell执行dmesg | grep avc查看SELinux的拒绝日志。格式通常是:avc: denied { read } for pid=1234 comm="xxx" scontext=u:r:app:s0 tcontext=u:object_r:system_file:s0 tclass=file。这个日志会直接告诉你:谁想访问什么,被拒绝了。

12.6 小结

这一讲,咱们把Android SELinux的骨架搭起来了。从四层架构,到init.rc的集成,再到sepolicy的文件组织,最后走了一遍编译加载流程。说白了,SELinux在Android里就是一套“规则引擎”——规则写在.te文件里,编译成二进制,内核照着执行。

下一讲,咱们会深入具体的.te文件语法,教你如何编写自己的SELinux策略。到时候,我会分享几个我踩过的“血泪坑”。


公众号:蓝海资料掘金营,微信deep3321