Android SELinux

Android SELinux

第一部分:核心理论与基础概念

1.1 为什么需要 SELinux?从 DAC 到 MAC

在深入 SELinux 的细节之前,我们必须先回答一个根本性的问题:Android(以及Linux)系统原本就有权限控制,为什么还需要引入 SELinux 这一套复杂的安全机制?

要理解这一点,我们必须对比两种根本不同的访问控制模型:DACMAC

1.1.1 DAC(自主访问控制)的舒适区与致命缺陷

1. 什么是 DAC?

DAC 是 Discretionary Access Control 的缩写,意为"自主访问控制"。它是传统 Linux/Unix 系统(以及早期 Android 版本)默认的安全模型。

在 DAC 模型中,一个文件或资源的访问权限由文件的所有者(User) 自主决定。系统通过经典的 rwx(读、写、执行)权限位来控制访问。

  • 判断依据 :进程的 UID (用户ID)和 GID(组ID)。
  • 管理方式 :用户或管理员可以通过 chmodchown 命令自由地修改文件权限。

2. DAC 模型的权限检查流程

下面这张时序图清晰地描绘了当一个 root 进程尝试访问一个设备文件时,DAC 是如何"放行"的。

3. 核心风险:"Root is God"

DAC 模型最大的设计缺陷在于:它无法约束超级用户(root,UID=0)

在 DAC 的规则下:

  • 对于非 root 进程 :如果一个进程的 UID 不是 0,它想访问一个文件,系统会严格检查权限位。比如文件属主是 root,权限是 rw-------,那么这个非 root 进程就会被无情地拒绝。
  • 对于 root 进程系统检查到 UID == 0,无论文件的权限位是什么(哪怕是 000),系统都会无条件放行,赋予最高权限。

为什么这是致命的?

在现代操作系统尤其是 Android 系统中,攻击者的终极目标通常是"提权"(Privilege Escalation)。他们利用系统服务或内核的某个漏洞,让一个普通的、受限的应用进程(如浏览器或聊天软件)获得 root 身份执行代码的能力。

一旦攻击者拿到了一个 root 权限的 shell 或进程,在纯 DAC 环境下,他可以:

  • 读取所有用户的私人数据(联系人、短信、照片)。
  • 修改系统核心文件,植入后门。
  • 直接读写硬件设备(如摄像头、麦克风)。

Example1 :在早期 Android 版本中,如果 App 无法打开 /dev/xxx 设备,开发者往往会直接在 init.rc 或脚本里写一句 chmod 777 /dev/xxx。在 DAC 体系下,这确实解决了"权限不够"的问题,但同时也把设备的大门向所有恶意软件敞开了。

1.1.2 MAC(强制访问控制)与 SELinux 的应对之道

为了解决 DAC "Root 特权过大,无法细粒度约束" 的问题,NSA(美国国家安全局)主导开发了 SELinux(Security-Enhanced Linux),并将其贡献给开源社区。Android 从 4.3 版本开始,正式引入了 SEAndroid(SELinux in Android)。

1. 什么是 MAC?

MAC 是 Mandatory Access Control 的缩写,意为"强制访问控制"。

2. 核心思想:最小权限原则

SELinux 设计的灵魂是 最小权限原则

每个进程(主体)只被授予完成其任务所必需的最小权限,不多也不少。

这意味着,即使一个进程是 root 身份运行的,SELinux 也会在旁边冷冷地加上一句:"等等,先让我看看你的策略文件(Policy)里有没有这一条规则。"

3. 生动的比喻:公司门禁系统

结合《11 SELinux理论基础》文档中的比喻,我们可以这样理解:

  • DAC 模型(旧门禁) :相当于公司只有一个大门。你只要有一张"CEO 金卡"(Root 权限),你就可以在整栋大楼里畅通无阻。你可以进财务室拿钱,进机房拔网线,进 HR 办公室看所有人的工资单。只要你是 CEO,没人拦你。
  • MAC 模型(SELinux 新门禁) :相当于公司在每个办公室门口都安装了独立的门禁读卡器
    • CEO 金卡(Root 权限)现在只是你进大门的凭证。
    • 当你走到研发部 门口时,门禁系统会读取你的工牌信息,然后查询后台的规章制度数据库(SELinux Policy)
    • 如果数据库里没有写"CEO 可以进入研发部代码服务器机房"这一条,就算你是 CEO,门也打不开

4. MAC 如何对抗 Root 攻击?

假设一个恶意软件利用漏洞成功以 root 身份运行了一个进程,企图读取 /data/data/com.android.providers.contacts/databases/contacts2.db(联系人数据库)。

  • 在纯 DAC 下 :进程是 root,文件属主也是 root放行
  • 在 SELinux (MAC) 下
    1. 恶意进程的 SELinux 上下文是 u:r:untrusted_app:s0(假设它是一个普通应用域)。
    2. 联系人数据库文件的上下文是 u:object_r:contacts_data_file:s0
    3. SELinux 策略数据库中并没有 allow untrusted_app contacts_data_file:file read; 这条规则。
    4. 结果:即使进程有 root 身份,SELinux 安全模块(LSM)依然会拦截请求,返回权限拒绝(EACCES)

1.2 SELinux 访问控制模型详解

在理解了"为什么需要 MAC"之后,我们需要深入 SELinux 的内部运行机制,看看它是如何在每一次操作发生时介入并做出判断的。

1.2.1 两大核心组件:主体与客体

在 SELinux 的语言体系中,所有与安全相关的操作都可以归结为 主体 (Subject)客体 (Object) 的访问。

组件 定义 在 Android 中的典型实例
主体 (Subject) 访问操作的主动发起方。在操作系统中,主体几乎总是指进程 init 进程、adbd 守护进程、system_server、任何第三方 App 的进程。
客体 (Object) 被访问的资源。它既可以是一个具体的文件,也可以是一个抽象的服务或通信通道。 普通文件、目录、设备节点 (/dev/*)、Socket、Binder 服务、系统属性 (ro.*)、进程间通信对象等。

思考:为什么需要区分主体和客体?

因为在 SELinux 的策略语言 (TE) 中,我们需要精确地描述:"哪一个"主体,对"哪一类"客体,拥有"哪些"操作许可。 如果不做区分,策略语法将无法描述。

1.2.2 MAC 权限检查的完整流程

让我们结合下面的时序图,拆解当一个进程试图访问一个文件时,系统内部发生了什么。

流程分步解析:

  1. 发起请求 :主体(一个运行在 init 域的进程)通过系统调用(如 open)请求访问客体(一个类型为 device 的字符设备文件)。
  2. 拦截并查询 :系统调用穿过 Linux 内核的 VFS 层,最终触发了 LSM (Linux Security Modules) 的钩子函数。LSM 调用 SELinux 安全服务器 (Security Server) 进行权限裁决。
  3. 策略匹配 :SELinux 安全服务器拿着本次访问的全部上下文信息:scontext=inittcontext=deviceclass=chr_fileperm=write,去访问缓存或加载在内核中的 策略数据库 (Policy)
  4. 命中规则 :策略数据库检查是否存在一条或多条 TE 规则 (Type Enforcement) ,例如 allow init device:chr_file write;
  5. 返回裁决 :策略数据库返回 allow(允许)或 deny(拒绝)给安全服务器。
  6. 执行或拒绝
    • 若允许,LSM 放行系统调用,进程成功获得文件描述符,读写操作正常执行。
    • 若拒绝,LSM 拦截系统调用,直接向进程返回 EACCES (Permission Denied) 错误。同时,内核会生成一条 AVC (Access Vector Cache) 日志,记录下这次失败的访问尝试。这条日志是开发者调试 SELinux 问题的核心线索。
1.2.3 关键知识点补充:AVC 日志

从上图可以看到,每当发生"权限拒绝"时,系统都会记录日志。在 Android 中,我们通常通过 logcat 来捕获它。

bash 复制代码
logcat | grep "avc"

一条典型的 AVC 日志如下:

text 复制代码
avc: denied { read write } for pid=1234 comm="myprocess" name="mydevice" dev="tmpfs" scontext=u:r:myprocess_dt:s0 tcontext=u:object_r:mydevice_t:s0 tclass=chr_file permissive=0
  • denied { read write }: 被拒绝的操作。
  • scontext: 主体上下文。
  • tcontext: 客体上下文。
  • tclass: 客体类别。
  • permissive=0/1: 当前是强制模式(0)还是宽容模式(1)。

1.3 深入理解 SELinux Context (安全上下文)

如果说"主体"和"客体"是舞台上的演员,那么 SELinux Context (安全上下文) 就是贴在他们身上的唯一身份标签。在 SELinux 的世界里,一切皆标签。

1.3.1 标签的定义与标准格式

SELinux 上下文是一个由冒号分隔的字符串,标准格式为:

text 复制代码
user:role:type:mls_level

在 Android 的 SELinux 实现中,这个格式被进一步简化并赋予了特定的含义:

字段 全称 在 Android 中的实际作用
user SELinux User 在 Android 中,该字段几乎总是 u (代表 unconfined 或通用用户)。Android 不依赖于 SELinux 的用户划分来做权限隔离(而是依靠 Linux UID)。
role Role-Based Access Control 在 Android 中,该字段主要用于区分上下文类别: - r :用于进程(主体) 的角色。 - object_r :用于文件/资源(客体) 的角色。
type Type Enforcement (TE) 这是 SELinux 安全策略中唯一真正核心的字段。 所有的 allow 规则都是基于 type 来编写的。
mls_level Multi-Level Security 在 Android 中,该字段被固定为 s0,代表最低敏感级别。Android 没有启用复杂的多级安全分类。

因此,在 Android 的日常开发和调试中,你可以将 SELinux 上下文简单地理解为:"它的类型 (Type) 是什么"

1.3.2 主体上下文 vs. 客体上下文:一眼识别

通过 role 字段的不同,我们可以非常轻松地判断一个上下文标签是贴在一个进程(主体)上,还是贴在一个文件(客体)上。

  • 主体 (进程) 的上下文u:r:进程类型:s0
    • 特点:role 字段是 r
    • 示例:
      • u:r:init:s0 (init 进程)
      • u:r:adbd:s0 (ADB 守护进程)
      • u:r:system_server:s0 (Android 系统服务大管家)
      • u:r:untrusted_app:s0 (普通第三方应用进程)
  • 客体 (文件/资源) 的上下文u:object_r:文件类型:s0
    • 特点:role 字段是 object_r
    • 示例:
      • u:object_r:system_file:s0 (大多数 /system 分区下的文件)
      • u:object_r:vendor_file:s0 (/vendor 分区下的文件)
      • u:object_r:device:s0 (通用的设备节点类型)
      • u:object_r:app_data_file:s0 (应用私有数据目录下的文件)
1.3.3 实战命令:如何查看 Android 系统中的上下文

理论结合实践,以下是在 Android 设备上查看各种对象安全上下文的常用命令。

1. 查看进程的安全上下文

使用 ps -AZ 命令。-Z 选项会显示 SELinux 上下文列。

bash 复制代码
# 进入 adb shell
adb shell

# 查看所有进程的上下文
ps -AZ

# 过滤查看特定进程,例如 system_server
ps -AZ | grep system_server

输出示例解读

bash 复制代码
emulator_x86_64:/ # ps -AZ | grep system_server
u:r:system_server:s0           system         486   289   15212636 302348 do_epoll_wait       0 S system_server
  • 标签 u:r:system_server:s0 确认了 system_server 进程运行在名为 system_server 的 SELinux 域中。

2. 查看文件的扩展属性(安全上下文)

使用 ls -Z 命令。它会显示文件的 SELinux 上下文。

bash 复制代码
# 查看 /vendor/bin/ 目录下的文件及其标签
ls -laZ /vendor/bin/

# 查看某个特定设备节点的标签
ls -lZ /dev/input/event0

输出示例解读

bash 复制代码
emulator_x86_64:/ # ls -lZ /dev/input/event0
crw-rw---- 1 root input u:object_r:input_device:s0  13,  64 2026-04-20 01:45 /dev/input/event0
  • 标签 u:object_r:input_device:s0 表明该文件在文件系统上被打上了 input_device 这个类型标签。

3. 查看系统属性的安全上下文

使用 getprop -Z 命令。

bash 复制代码
# 查看属性的上下文
getprop -Z ro.build.type

输出示例解读

bash 复制代码
emulator_x86_64:/ # getprop -Z ro.build.type
u:object_r:build_prop:s0
  • 这表示 ro.build.type 这个属性的类型是 build_prop

通过熟练掌握这些查看命令,开发者可以快速验证:

  • 我的进程是否运行在了我期望的域中?
  • 我创建的文件是否被系统正确打上了标签?
  • 如果标签不对,问题可能出在 file_contexts 的定义或 restorecon 的执行上。

1.4 安全策略文件的定义与存储

在前面的章节中,我们反复提到了一个关键概念:"打标签"。无论是进程(主体)还是文件(客体),都必须拥有一个正确的 SELinux 安全上下文,SELinux 策略才能发挥作用。

那么问题来了:系统是如何知道哪个文件应该打上哪个标签的?这个"标签映射表"保存在哪里?

答案就在一系列的 *_contexts 文件中。这些文件是 SELinux 策略的 "静态映射字典",它们定义了从具体的资源(文件路径、属性名、服务名)到 SELinux 类型的对应关系。

在 Android 的 AOSP 源码中,这些文件通常位于 system/sepolicy/ 目录及其子目录下。

文件名 作用描述
file_contexts 为普通文件、目录、设备节点等定义默认安全上下文。
genfs_contexts procsysfstmpfs 等虚拟或内存文件系统定义上下文。
property_contexts 为 Android 系统属性(ro., persist. 等)定义上下文。
service_contexts 为 Android Binder Service 定义上下文。
seapp_contexts 为 APK 应用进程定义上下文(基于签名、UID 等)。
1.4.1 file_contexts:普通文件与目录的标签字典

作用 :为 Android 文件系统上的所有普通文件、目录、设备节点指定默认的 SELinux 安全上下文。

格式

text 复制代码
[正则表达式路径] [安全上下文]

示例 (节选自 system/sepolicy/private/file_contexts):

text 复制代码
/system_dlkm(/.*)?  u:object_r:system_dlkm_file:s0
/dev/adf[0-9]*      u:object_r:graphics_device:s0
/data/vendor(/.*)?              u:object_r:vendor_data_file:s0

工作机制

  1. 在系统编译时,file_contexts 文件会被编译进 plat_file_contextsvendor_file_contexts 中。
  2. 在系统启动或调用 restorecon 命令时,init 进程或 restorecon 工具会读取这些编译后的文件,根据路径正则匹配,为文件或目录设置 xattr(扩展属性)中的 SELinux 标签。

查看系统中生效的 file_contexts

bash 复制代码
adb shell cat /system/etc/selinux/plat_file_contexts
adb shell cat /vendor/etc/selinux/vendor_file_contexts
1.4.2 genfs_contexts:虚拟文件系统的标签字典

作用 :为那些不存在于物理磁盘 上的文件系统(如 /proc/sys/devtmpfs)中的文件定义安全上下文。

为什么需要单独的 genfs_contexts

因为 /proc/sys 下的文件是内核动态生成的,它们没有持久化的 xattr 存储空间。因此,SELinux 必须通过一个内存中的映射表来为这些文件在生成时"即时打标签"。

格式

text 复制代码
genfscon [文件系统类型] [路径] [安全上下文]

示例 (节选自 system/sepolicy/private/genfs_contexts):

text 复制代码
genfscon proc / u:object_r:proc:s0
genfscon sysfs / u:object_r:sysfs:s0
genfscon debugfs /tracing/instances                   u:object_r:debugfs_tracing_instances:s0
genfscon binder /binder u:object_r:binder_device:s0

工作机制

  • 内核在挂载这些虚拟文件系统时,SELinux 模块会查询 genfscon 规则,并在每个文件/目录被创建时,自动赋予其预定义的上下文。
1.4.3 property_contexts:系统属性的标签字典

作用 :为 Android 的系统属性(System Properties)指定安全上下文。

背景 :Android 系统属性(通过 getprop/setprop 访问)是一个全局共享的键值对存储。某些敏感属性(如 ro.boot.selinux)只能被特定进程修改或读取。SELinux 通过为属性打标签,并在 TE 策略中限制对该标签的访问来实现细粒度控制。

格式

text 复制代码
[属性名前缀] [安全上下文]

示例 (节选自 system/sepolicy/private/property_contexts):

text 复制代码
ro.ril.                 u:object_r:radio_prop:s0
sys.                    u:object_r:system_prop:s0
dhcp.                   u:object_r:dhcp_prop:s0 
debug.                  u:object_r:debug_prop:s0

查看属性标签

bash 复制代码
adb shell getprop -Z ro.build.type
# 输出: u:object_r:build_prop:s0
1.4.4 service_contexts:Binder 服务的标签字典

作用 :为 Android 系统中注册的 Binder 服务 指定安全上下文。

背景 :在 Android 中,几乎所有的系统服务(如 activitypackagepower)都是通过 Binder 机制提供的。当一个客户端进程想要调用某个服务时,SELinux 需要知道该服务的 SELinux 类型,以便检查 binder_call 权限。

格式

text 复制代码
[服务名] [安全上下文]

示例 (节选自 system/sepolicy/private/service_contexts):

text 复制代码
activity                                  u:object_r:activity_service:s0 
package                                   u:object_r:package_service:s0
power                                     u:object_r:power_service:s0
vold                                      u:object_r:vold_service:s0

工作机制

  • 当服务通过 ServiceManager.addService() 注册时,SELinux 会根据 service_contexts 为其绑定一个上下文。
  • 当客户端通过 ServiceManager.getService() 获取服务并调用时,SELinux 会使用这个上下文来进行权限检查。
1.4.5 seapp_contexts:应用进程的标签字典

作用 :为 Android 应用程序(APK)的进程 动态分配 SELinux 域。

背景 :与 C/C++ 编写的守护进程不同,APK 应用并没有一个固定的可执行文件来让我们提前打标签。应用进程的 SELinux 域是在应用启动时,由 zygote 进程根据 应用的签名、UID、是否预装、是否特权 等多个维度动态计算出来的。

格式(逻辑描述):

text 复制代码
isSystemServer=true domain=system_server
user=system seinfo=platform domain=platform_app
user=_app isPrivApp=true domain=priv_app
user=_app domain=untrusted_app

实际文件示例 (节选自 system/sepolicy/private/seapp_contexts):

text 复制代码
# Input selectors:
#       isSystemServer (boolean)
#       isEphemeralApp (boolean)
#       user (string)
#       seinfo (string)
#       name (string)
#       isPrivApp (boolean)
#       minTargetSdkVersion (unsigned integer)
#       fromRunAs (boolean)
#       isIsolatedComputeApp (boolean)
#       isSdkSandboxNext (boolean)
... ...
#
# Outputs:
#       domain (string)
#       type (string)
#       levelFrom (string; one of none, all, app, or user)
#       level (string)

user=_app seinfo=platform name=com.android.traceur domain=traceur_app type=app_data_file levelFrom=all
user=system seinfo=platform domain=system_app type=system_app_data_file
user=system seinfo=platform isPrivApp=true name=com.android.DeviceAsWebcam domain=device_as_webcam type=system_app_data_file levelFrom=all
user=bluetooth seinfo=bluetooth domain=bluetooth type=bluetooth_data_file

工作机制

  1. Zygote 在 fork 出新的应用进程后,会收集该应用的各种元数据(UID、签名信息等)。
  2. Zygote 将这些信息传递给 libselinux 库。
  3. libselinux 遍历 seapp_contexts 中的规则,从上到下进行匹配,第一个匹配成功的规则决定了新进程的 SELinux 域 (domain) 和该应用私有数据目录的 类型 (type)
1.4.6 总结:五张"地图"协同工作

这五类 *_contexts 文件共同构成了 SELinux 的 "命名服务" 。它们将具体的、开发者容易理解的名字(如路径 /dev/device、属性名 persist.vendor.camera、服务名 vold)映射为 SELinux 策略引擎能够理解和执行的 安全类型 (Type)


第二部分:TE 策略文件编写与语法实战

SELinux 的强大之处在于其灵活而精确的策略描述能力。所有的权限授予与拒绝,都通过一种叫做 TE(Type Enforcement,类型强制) 的语言来编写。TE 策略文件(通常以 .te 为后缀)是开发者与 SELinux 安全服务器对话的"合同条款"。

2.1 核心语法:allow 规则

allow 语句是 TE 策略中最基本、使用频率最高的语句。它的作用非常直接:授予权限

2.1.1 基本格式
text 复制代码
allow <主体类型> <客体类型>:<客体类别> <许可权限集>;

让我们逐一拆解这四个核心组成部分:

组成部分 含义 说明
主体类型 Source Type (scontext) 发起操作的进程所属的 SELinux 类型(域) 。例如 voldinitsystem_server
客体类型 Target Type (tcontext) 被访问的资源所属的 SELinux 类型 。例如 file_contexts_fileblock_devicesystem_data_file
客体类别 Object Class 被访问资源的类别。SELinux 需要知道这是一个普通文件、目录、Socket 还是字符设备,因为不同类别支持的操作是不同的。
许可权限集 Permission Set 一个或多个被允许的操作名称,用空格分隔并包裹在花括号 { } 中,或者使用预定义的
2.1.2 深入理解客体类别 (Object Class)

SELinux 对资源的抽象非常细致。你不能简单地对一个"文件"授权,你必须明确告诉内核:这个客体是 file(普通文件)、dir(目录)、chr_file(字符设备)还是 blk_file(块设备)。

在 Android 策略中,最常见的客体类别包括:

类别 (Class) 描述 典型权限 (Permissions)
file 普通文件 read, write, open, getattr, setattr, execute, link, unlink
dir 目录 read, write, open, getattr, search, add_name, remove_name, rmdir
chr_file 字符设备文件 read, write, open, getattr, ioctl
blk_file 块设备文件 read, write, open, getattr, ioctl
lnk_file 符号链接文件 read, getattr
sock_file UNIX Domain Socket 文件 read, write, open, getattr
tcp_socket / udp_socket 网络 Socket create, bind, connect, listen, accept, read, write
process 进程本身 fork, sigkill, signal, setsched, transition
binder Android Binder IPC call, transfer, impersonate
property 系统属性 read, write, set

思考 :为什么同样都是"写入",dir 的写入和 file 的写入不同?

对于 dir,"写入"意味着在该目录下创建或删除文件 (需要 add_nameremove_name 权限),而 file 的"写入"意味着修改文件内容(需要 write 权限)。SELinux 将这两种操作严格区分,以实现更细粒度的控制。

2.1.3 示例解析(来自 vold.te

让我们以 Android 存储管理守护进程 vold 的真实策略为例,逐行解析。

示例 1:允许 vold 读取 file_contexts 文件

selinux 复制代码
# 允许 vold 进程读取 file_contexts 文件
allow vold file_contexts_file:file r_file_perms;
组成要素 实际值 详细说明
主体类型 vold 指运行在 vold 域中的进程。
客体类型 file_contexts_file 指 SELinux 文件上下文配置文件(如 /system/etc/selinux/plat_file_contexts)的类型。
客体类别 file 指明 file_contexts_file 是一个普通文件
许可权限集 r_file_perms 这是一个预定义宏,代表一组只读权限。

宏展开:r_file_perms 的背后

system/sepolicy/public/te_macros 文件中,r_file_perms 被定义为:

selinux 复制代码
define(`r_file_perms', `{ getattr open read ioctl lock map watch watch_reads }'

因此,上述 allow 语句实际上等价于:

selinux 复制代码
allow vold file_contexts_file:file { getattr open read ioctl lock map watch watch_reads };

这意味着 vold 进程可以打开、获取属性、读取、执行 ioctl 以及锁定 file_contexts_file 类型的文件。

示例 2:允许 vold 在 block_device 目录中创建子目录

selinux 复制代码
# 允许 vold 进程对 block_device 目录拥有创建子目录的权限
allow vold block_device:dir create_dir_perms;
组成要素 实际值 详细说明
主体类型 vold 同上。
客体类型 block_device 这是一个属性(Attribute),代表所有块设备相关的目录。
客体类别 dir 指明操作的对象是目录
许可权限集 create_dir_perms 另一个预定义宏,代表在目录下创建子项所需的权限。

宏展开:create_dir_perms 的背后

system/sepolicy/public/global_macros 中:

selinux 复制代码
define(`create_file_perms', `{ create rename setattr unlink rw_file_perms }')

展开后:

selinux 复制代码
allow vold block_device:dir { create rename setattr unlink rw_file_perms };
  • create:在目录中创建子文件/子目录。

  • rename:重命名目录内的子项

  • setattr:修改目录属性。

  • rw_file_perms包括r_dir_permsw_dir_perms

    selinux 复制代码
    define(`r_dir_perms', `{ open getattr read search ioctl lock watch watch_reads }') 
    define(`w_dir_perms', `{ open search write add_name remove_name lock }')
2.1.4 常用预定义宏速查表

为了方便开发者,AOSP 在 te_macros 中定义了大量常用的权限组合宏。掌握这些宏可以大幅提升策略编写的效率和可读性。

宏名称 展开后的权限集 典型用途
r_file_perms { getattr open read ioctl lock map watch watch_reads } 只读普通文件。
w_file_perms { open append write lock map } 只写(或追加)普通文件。
rw_file_perms { r_file_perms w_file_perms } 读写普通文件。
x_file_perms { getattr execute execute_no_trans map } 不改变自身的安全上下文执行一个可执行文件
rx_file_perms { r_file_perms x_file_perms } 读取并执行普通文件(如 .so 库)。
create_file_perms { create rename setattr unlink rw_file_perms } 创建并写入普通文件。
create_dir_perms { create reparent rename rmdir setattr rw_dir_perms } 创建子目录。
r_dir_perms { open getattr read search ioctl lock watch watch_reads } 遍历并读取目录内容。
w_dir_perms { open search write add_name remove_name lock } 创建、删除或重命名文件/子目录
rw_dir_perms { r_dir_perms w_dir_perms } 在目录中创建/删除文件,并遍历目录。
2.1.5 编写 allow 规则的实战思维

当你在开发过程中遇到 avc: denied 日志时,添加 allow 规则是一个严谨的过程,而非随意堆砌权限。

  1. 读取日志,确定四要素
    • scontext → 主体类型
    • tcontext → 客体类型
    • tclass → 客体类别
    • denied { ... } → 缺失的权限
  2. 选择最小权限集
    • 如果日志显示 denied { read },不要直接给 rw_file_perms。只给 r_file_perms 或更精确的 { open read getattr }
  3. 考虑使用宏
    • 检查所需权限是否恰好对应某个预定义宏。如果是,直接使用宏,代码更简洁。
  4. 添加注释
    • 在每条 allow 规则上方添加注释,说明为什么需要这个权限。这对于后续维护至关重要。2.1.6
2.1.6 使用 audit2allow 快速生成初始策略

在手动分析 AVC 日志并编写规则之前,有一个非常高效的辅助工具可以帮你快速"翻译"日志为 TE 语句:audit2allow

操作步骤:

  1. 清空旧日志,保证抓取内容纯净

    bash 复制代码
    adb shell logcat -c
  2. 复现权限拒绝问题(运行你的程序或触发相关操作)

  3. 抓取 AVC 日志并保存到文件

    bash 复制代码
    adb shell logcat | grep "avc" > avc.txt
  4. 使用 audit2allow 将日志转换为 TE 规则

    bash 复制代码
    audit2allow -i avc.txt > hello.te
  5. 查看生成的策略文件

    bash 复制代码
    cat hello.te

示例输出 (hello.te 内容):

text 复制代码
#============= sedemo_dt ==============
allow sedemo_dt devpts:chr_file { read write };
allow sedemo_dt sedemo_dev_t:file { open read write };

重要提示:

  • audit2allow 生成的策略仅供参考和快速验证 ,它只是机械地将日志"翻译"成规则,不会理解业务语义
  • 切勿直接将输出结果贴进正式策略文件! 你应该:
    1. 分析生成的每一条 allow 规则是否合理。
    2. 检查是否可以使用预定义宏 (如 rw_file_perms)替代展开的权限列表。
    3. 删除重复或不必要的规则。
    4. 添加清晰的注释说明授权原因。
    5. 确保遵循最小权限原则

完整调试闭环流程:

text 复制代码
1. 抓取 AVC 日志 (logcat | grep "avc")
       ↓
2. 分析四要素 (scontext, tcontext, tclass, denied)
       ↓
3. (可选) 使用 audit2allow 生成参考规则
       ↓
4. 手工编写/优化 allow 规则 (最小权限 + 宏)
       ↓
5. 添加注释并整合到 .te 文件
       ↓
6. 编译策略 → 推送 → 验证

2.2 高级语法:属性与域转换

在掌握了基础的 allow 规则之后,面对复杂的系统策略,你很快会发现两个痛点:

  1. 需要对几十个不同的类型授予相同的权限,重复代码太多。
  2. 系统启动时,init 进程(u:r:init:s0)如何将服务进程切换到其专属的受限域(如 u:r:vold:s0)?

解决这两个问题的利器,就是 属性(Attribute)域转换(Domain Transition)

2.2.1 宏 (Macro):简要回顾

宏是 SELinux 策略中的"代码片段",用于封装一组常用权限,避免重复书写,提高可读性。

selinux 复制代码
# 定义宏 (位于 global_macros 或 te_macros)
define(`r_file_perms', `{ getattr open read ioctl lock map watch watch_reads }')

# 使用宏
allow myprocess myfile:file r_file_perms;

本节不再赘述宏的详细展开,后续我们将聚焦于属性和域转换。

2.2.2 属性 (Attribute):批量授权的"标签组"

1. 什么是属性?

属性是一组 SELinux 类型(Type)的集合名称。你可以把属性理解为一个"标签组",将多个具体的类型归入同一个组,然后只需要对这一整个组授权,组内所有类型都会自动获得该权限。

2. 定义和使用属性

属性通常在 attributes 文件中声明,然后在各个 .te 文件中通过 type attribute 将具体类型关联到属性上。

selinux 复制代码
 # All services declared as part of an HAL
 attribute hal_service_type;
 
 # All domains used for apps.
 attribute appdomain;
 
 ubuntu@ubuntu1804:~/aosp/system/sepolicy/public$ grep "type untrusted_app, domain;" ./* -rn
./untrusted_app.te:21:type untrusted_app, domain;

ubuntu@ubuntu1804:~/aosp/system/sepolicy/public$ grep "type platform_app, domain;" ./* -rn
./platform_app.te:5:type platform_app, domain;

ubuntu@ubuntu1804:~/aosp/system/sepolicy/public$ grep "type system_data_file," ./* -rn
./file.te:307:type system_data_file, file_type, data_file_type, core_data_file_type;

3. 对属性进行批量授权

一旦类型被关联到属性,我们就可以在策略中对属性 授权,其效果等同于对该属性下所有类型授权。

selinux 复制代码
# 允许所有应用域读取所有数据文件类型的目录
allow domain data_file_type:dir r_dir_perms;

这条规则一次性覆盖了:其他所有 domaindata_file_type 的组合。

4. 实战意义

属性机制是实现 最小权限原则策略可扩展性 的基石。当 Google 新增一种应用类型(例如 isolated_app)时,只需将其关联到 appdomain 属性,它就会自动继承所有针对 appdomain 授权的规则,无需修改任何现有 allow 语句。


2.2.3 域转换 (Domain Transition):进程的"安全变身"

这是 SELinux 最精妙、也最容易令人困惑的机制。它回答了一个根本问题:一个运行在高权限域(如 init)的进程,如何启动一个运行在受限域(如 vold)的子进程?

1. 问题场景

在 Android 启动过程中,init 进程是所有用户空间进程的祖先。如果 init 直接 fork()exec() 执行 /vendor/bin/vold,按照 Linux 的继承逻辑,新进程会继承父进程 init 的 SELinux 上下文(u:r:init:s0)。但这显然违背了"最小权限原则"------存储守护进程 vold 不应该拥有 init 的全部能力。

SELinux 必须提供一种机制,让新进程在执行特定可执行文件时,自动切换 到一个预定义的、受限的域。这就是 域转换

2. 域转换的三要素

要成功实现一次域转换,必须满足三个条件:

要素 说明 对应规则
入口文件类型 被执行的可执行文件必须具有一个特定的 SELinux 类型(通常以 _exec 结尾)。 file_contexts 中定义:/vendor/bin/vold u:object_r:vold_exec:s0
执行权限 旧域(父进程)必须拥有对该入口文件的 execute 权限。 allow init vold_exec:file execute;
转换规则 必须有一条明确的 type_transition 规则,告诉 SELinux:当旧域执行该入口文件时,新进程应切换到目标域。 type_transition init vold_exec:process vold;

3. 域转换过程图解

4. 实战:使用 domain_auto_trans 宏简化转换

手动编写三要素略显繁琐,AOSP 提供了高级宏 domain_auto_trans,一行搞定。

宏定义(位于 te_macros):

bash 复制代码
#####################################
# domain_auto_trans(olddomain, type, newdomain)
# Automatically transition from olddomain to newdomain
# upon executing a file labeled with type.
#
define(`domain_auto_trans', `
# Allow the necessary permissions.
domain_trans($1,$2,$3)
# Make the transition occur by default.
type_transition $1 $2:process $3;
')

sedemo.te 中的使用示例:

bash 复制代码
# 定义目标域类型和入口文件类型
type sedemo_dt, domain;
type sedemo_dt_exec, exec_type, vendor_file_type, file_type;

# 允许从 shell 域执行 sedemo 时自动转换到 sedemo_dt 域
domain_auto_trans(shell, sedemo_dt_exec, sedemo_dt)

参数说明:

  • $1 = shell:旧域(父进程的域)
  • $2 = sedemo_dt_exec:入口文件类型
  • $3 = sedemo_dt:目标域(新进程的域)

这条宏展开后等价于:

bash 复制代码
# 1. 授予执行权限和基本转换权限
allow shell sedemo_dt_exec:file { getattr open read execute };
allow shell sedemo_dt:process transition;
# 2. 允许目标域继承必要的文件描述符和环境
dontaudit shell sedemo_dt:process noatsecure;
# 3. 设置类型转换规则
type_transition shell sedemo_dt_exec:process sedemo_dt;

5. 域转换生效的前提:进程入口点

需要特别注意的是,域转换只在 exec() 系统调用发生时才会触发。这意味着:

  • 通过 fork() 产生的子进程会原样继承父进程的域,不发生转换。
  • 只有紧跟着调用 exec() 加载新的可执行文件时,内核才会检查 type_transition 规则并执行切换。

因此,启动脚本(如 init.rc)中配置的服务,在 init 执行 execve() 启动它们时,就会自然触发域转换。

2.2.4 小结
机制 核心作用 关键语法/宏
封装常用权限,提高可读性 define(), r_file_perms, create_file_perms
属性 将多个类型分组,实现批量授权 attribute, typeattribute
域转换 控制进程在执行新程序时切换 SELinux 域 domain_auto_trans(), type_transition

第三部分:实战演练 ------ 为自定义进程 sedemo 添加 SELinux 策略

3.1 场景描述

我们有一个 C 程序 sedemo,它需要打开并写入 /dev/sedemo_dev 设备节点。本演练将完整演示如何为其配置 SELinux 策略,使其在强制模式下正常运行。

3.2 核心步骤速览

步骤 关键操作 涉及文件与命令
1 定义设备节点类型 devnode.te type sedemo_dev_t, dev_type;
2 定义进程域与入口文件类型 sedemo.te type sedemo_dt, domain; type sedemo_dt_exec, exec_type, vendor_file_type, file_type; domain_auto_trans(shell, sedemo_dt_exec, sedemo_dt)
3 绑定安全上下文 file_contexts /dev/sedemo_dev u:object_r:sedemo_dev_t:s0 /vendor/bin/sedemo u:object_r:sedemo_dt_exec:s0
4 集成策略到编译 BoardConfig.mk BOARD_SEPOLICY_DIRS += device/hello/sedemo/sepolicy
5 编译并推送 make selinux_policy adb push out/.../vendor/etc/selinux /vendor/etc/ adb push out/.../vendor/bin/sedemo /vendor/bin/
6 宽容模式下验证功能 setenforce 0 touch /dev/sedemo_dev restorecon /dev/sedemo_dev /vendor/bin/sedemo
7 强制模式下捕获 AVC 日志 setenforce 1 `logcat
8 生成并添加权限规则 audit2allow -i avc.txt 将输出的 allow 规则手动添加到 sedemo.te
9 重新编译并验证 重复步骤 5-7,直至无 AVC 拒绝日志

3.3 AVC 日志分析与策略生成

典型拒绝日志

text 复制代码
04-19 22:39:12.668  2079  2079 I sedemo  : type=1400 audit(0.0:25): avc:  denied  { read write } for  path="/dev/pts/0" dev="devpts" ino=3 scontext=u:r:sedemo_dt:s0 tcontext=u:object_r:devpts:s0 tclass=chr_file permissive=1
04-19 22:39:12.672  2079  2079 I sedemo  : type=1400 audit(0.0:26): avc:  denied  { read write } for  name="sedemo_dev" dev="tmpfs" ino=673 scontext=u:r:sedemo_dt:s0 tcontext=u:object_r:sedemo_dev_t:s0 tclass=file permissive=1
04-19 22:39:12.672  2079  2079 I sedemo  : type=1400 audit(0.0:27): avc:  denied  { open } for  path="/dev/sedemo_dev" dev="tmpfs" ino=673 scontext=u:r:sedemo_dt:s0 tcontext=u:object_r:sedemo_dev_t:s0 tclass=file permissive=1

使用 audit2allow 生成修复规则

bash 复制代码
audit2allow -i avc.txt > hello.te

输出

bash 复制代码
#============= sedemo_dt ==============
allow sedemo_dt devpts:chr_file { read write };
allow sedemo_dt sedemo_dev_t:file { open read write };

将该规则加入 sedemo.te 并重新编译策略即可。

3.4 关键调试技巧
  • restorecon :使 file_contexts 中的标签定义立即生效。
  • su 域陷阱userdebug/eng 版本中 adb shell 默认运行在 su 域,权限极大,会掩盖 SELinux 问题。务必确认进程已转换到自己的受限域(用 ps -AZ 检查)。
  • runcon:手动以指定上下文运行程序,用于测试域转换。
bash 复制代码
runcon u:r:sedemo_dt:s0 /vendor/bin/sedemo

dt:s0 tcontext=u:object_r:sedemo_dev_t:s0 tclass=file permissive=1

04-19 22:39:12.672 2079 2079 I sedemo : type=1400 audit(0.0:27): avc: denied { open } for path="/dev/sedemo_dev" dev="tmpfs" ino=673 scontext=u:r:sedemo_dt:s0 tcontext=u:object_r:sedemo_dev_t:s0 tclass=file permissive=1

复制代码
**使用 `audit2allow` 生成修复规则**:

```bash
audit2allow -i avc.txt > hello.te

输出

bash 复制代码
#============= sedemo_dt ==============
allow sedemo_dt devpts:chr_file { read write };
allow sedemo_dt sedemo_dev_t:file { open read write };

将该规则加入 sedemo.te 并重新编译策略即可。

3.4 关键调试技巧
  • restorecon :使 file_contexts 中的标签定义立即生效。
  • su 域陷阱userdebug/eng 版本中 adb shell 默认运行在 su 域,权限极大,会掩盖 SELinux 问题。务必确认进程已转换到自己的受限域(用 ps -AZ 检查)。
  • runcon:手动以指定上下文运行程序,用于测试域转换。
bash 复制代码
runcon u:r:sedemo_dt:s0 /vendor/bin/sedemo
相关推荐
阿巴斯甜2 小时前
Android中项目架构:
android
程序员陆业聪3 小时前
线上监控与防劣化:让启动优化成果不再回退 | Android启动优化系列(五·完结)
android
程序员陆业聪3 小时前
首帧渲染优化:从白屏到内容可见的最后一公里
android
AI玫瑰助手4 小时前
Python基础:字符串的常用内置方法(查找替换分割)
android·开发语言·python
xiangxiongfly9155 小时前
Android 使用WebSocket通信
android·websocket·网络协议·okhttp
su_ym81105 小时前
Android属性系统
android·framework·property
明天就是Friday6 小时前
Android实战项目③ Room+Clean Architecture开发待办事项App 完整源码详解
android
没有了遇见6 小时前
《彻底搞懂 ViewModel:作用、原理与源码分析》
android
Fate_I_C6 小时前
Kotlin 协程:串行/并行请求、async/await、coroutineScope 管理并发、重试机制
android·代码规范