10. SELinux基础(二):TE规则语法、类型强制、角色与用户
好,我们接着往下聊。上一节我们把SELinux的来龙去脉讲清楚了,这一节直接上手——TE规则怎么写,类型强制怎么玩,角色和用户又是怎么回事。
说实话,我刚接触SELinux那会儿,最头疼的就是TE规则。一堆的allow、neverallow,看着像天书。后来做项目做多了,慢慢就摸到门道了。说白了,TE规则就是一套「谁可以访问谁」的授权清单。
10.1 TE规则的核心语法
TE规则,全称是Type Enforcement规则。它的基本格式其实很简单:
allow 源类型 目标类型:对象类别 { 权限集 };
我来拆解一下:
- 源类型:谁要发起访问?通常是进程的domain
- 目标类型:要访问什么东西?文件、socket、进程等等
- 对象类别:目标属于哪一类?file、dir、socket、process...
- 权限集:具体能做什么?read、write、execute...
举个例子,我在项目中经常写这样的规则:
allow app_domain app_data_file:file { read write open };
这条规则的意思是:允许app_domain这个类型的进程,去访问类型为app_data_file的文件,可以做读、写、打开操作。
关键点:SELinux默认是「拒绝一切」的。你写了allow,它才放行。没写的,一律拒绝。这就是最小权限原则的体现。
10.2 类型强制——你是什么类型,就做什么事
类型强制是SELinux的核心理念。每个主体(进程)和客体(文件、socket等)都被打上了一个「类型标签」。这个标签决定了它能做什么、能被谁访问。
我打个比方:类型就像一个人的身份证。你是「教师」类型,你可以在教室区域活动;你是「学生」类型,你就只能在学生区域活动。类型强制就是靠这个身份证来管控的。
在Android系统里,常见的类型有:
| 类型 | 说明 | 典型对象 |
|---|---|---|
| init | init进程的domain | /init, /sbin/init |
| appdomain | 第三方应用的domain | /data/data/* |
| system_app | 系统级应用的domain | /system/app/* |
| shell | adb shell的domain | /system/bin/sh |
| untrusted_app | 非信任应用的domain | /data/app/* |
你想想看,如果一个app被打上了untrusted_app的标签,它想访问system_app才能碰的文件,SELinux直接一巴掌拍回去。这就是类型强制的威力。
我的经验:调试SELinux问题时,第一步就是确认类型标签对不对。用ls -Z看文件类型,用ps -Z看进程类型。类型错了,后面所有规则都白搭。
10.3 角色与用户——SELinux的另外两个维度
类型强制是SELinux的第一维度,角色和用户是第二、第三维度。三者合起来,构成了SELinux的完整访问控制模型。
10.3.1 角色(Role)
角色是干什么的?它限制了「哪些类型可以被哪些角色使用」。说白了,角色就是类型的一个包装器。
常见的角色有:
- object_r:用于客体(文件、设备等),几乎所有文件都用这个角色
- system_r:用于系统进程
- unconfined_r:不受限制的角色,通常给init进程用
角色和类型的关系,我画个图你就明白了:
嗯,这里要注意:角色不是给普通开发者用的。在Android系统里,大部分进程的角色都是system_r,文件都是object_r。你写TE规则时,基本不用操心角色,专心搞类型就行。
10.3.2 用户(User)
SELinux的用户和Linux系统用户是两码事。SELinux的用户更像是一个「身份容器」,里面可以包含多个角色。
常见的SELinux用户:
- u:普通用户,几乎所有的Android进程都用这个
- system_u:系统用户,给系统服务用
- root:超级用户,很少用
说实话,在Android的SELinux策略里,用户这一层基本被弱化了。大部分场景下,你看到的安全上下文都是u:r:xxx:s0这样的格式。u就是用户,r是角色,xxx是类型,s0是安全级别。
我曾经踩过的坑:有一次我写了一个自定义服务,安全上下文配成了u:r:my_service:s0。结果服务死活起不来,avc日志报错说角色不匹配。查了半天才发现,我忘了在角色声明文件里把my_service类型关联到system_r角色。记住:类型必须关联到角色才能用!
10.4 TE规则的进阶写法
基础的allow规则你会写了,但实际项目中,我们经常需要更灵活的写法。
10.4.1 使用通配符
# 允许所有读取操作
allow app_domain app_data_file:file ~{ write create unlink };
# 允许所有文件操作
allow app_domain app_data_file:file *;
~表示「除了」,*表示「所有」。我个人习惯尽量少用通配符,因为太宽泛了,容易引入安全风险。
10.4.2 使用宏定义
Android的SELinux策略里大量使用了宏,比如:
# 允许appdomain读取system_app的文件
allow appdomain system_app_file:file r_file_perms;
# r_file_perms 展开后就是 { getattr open read ioctl lock }
常用的宏有:
| 宏名 | 展开后的权限 |
|---|---|
| r_file_perms | getattr open read ioctl lock |
| w_file_perms | open append write ioctl lock |
| rw_file_perms | r_file_perms + w_file_perms |
| create_file_perms | create open getattr setattr read write append ioctl link unlink rename |
用宏的好处是:代码更简洁,不容易漏权限。我在项目里都是优先用宏,实在不够用了才手写权限集。
10.4.3 类型转换(Type Transition)
有时候,一个进程创建了一个文件,我们希望这个文件自动带上特定的类型。这时候就需要类型转换规则。
# 当app_domain在app_data_dir目录下创建文件时,文件自动标记为app_data_file
type_transition app_domain app_data_dir:file app_data_file;
这条规则的意思是:app_domain进程在app_data_dir目录下创建文件时,新文件的类型自动变成app_data_file,而不是继承父进程的类型。
我的建议:类型转换规则一定要配合allow规则使用。光有type_transition不行,你还得allow app_domain去创建app_data_file类型的文件。否则SELinux会拒绝创建操作,类型转换也就无从谈起了。
10.5 实战:写一条完整的TE规则
我们来看一个完整的例子。假设我有一个自定义服务叫my_daemon,它需要读取/data/my_config目录下的配置文件。
第一步:定义类型
type my_daemon, domain;
type my_config_file, file_type, data_file_type;
第二步:关联角色
role system_r types my_daemon;
第三步:写allow规则
# 允许my_daemon读取my_config_file
allow my_daemon my_config_file:dir { search };
allow my_daemon my_config_file:file { getattr open read };
第四步:如果需要类型转换
# 当my_daemon在my_config_dir下创建文件时,自动标记为my_config_file
type_transition my_daemon my_config_dir:file my_config_file;
你看,整个流程其实很清晰。定义类型 → 关联角色 → 写allow规则 → 可选类型转换。我在项目里都是按这个步骤来的,基本不会出错。
核心总结:TE规则就是「谁(源类型)可以访问谁(目标类型)的什么(对象类别),做什么(权限集)」。角色和用户是辅助维度,帮你更好地组织策略。写规则时,先想清楚类型,再动手写allow,最后别忘了检查角色关联。
好了,这一节的内容就到这儿。TE规则是SELinux策略的基石,你把它搞明白了,后面讲约束规则、布尔值、调试技巧时就会轻松很多。下一节我们继续深入,聊聊neverallow规则和策略调试工具。
公众号:蓝海资料掘金营,微信deep3321