文章目录
- SElinux如何将安全上下文转换为SID
- 进程初始化阶段中的SID初始化
- SID的计算
- 小结
- 文件inode初始化阶段中SID的初始化
- 新建文件中SID的计算
- 打开已有文件中SID的计算
在
SElinux内核态的实现-class中,我们已经知晓了安全上下文、class的概念。 并且已经知晓如何获取其安全上下文对应的id以及class的id.
现在我们可以开始试着理解SELinux如何实现对一个请求进行检查了。
在SELinux设计中,权限检查的核心函数是avc_has_perm
。这个函数在内核代码中随处可见,其内容也很简单:
相信读者如果阅读过SElinux内核态的实现-class,应当可以理解这个函数的入参。
入参 | 功能 |
struct selinux_state *state, | SELinux数据库入口 |
u32 ssid, | 源上下文 sid |
u32 tsid, | 目的上下文 sid |
u16 tclass, | 目的 class id |
u32 requested, | 请求权限结果 |
struct common_audit_data *auditdata | 审计相关,本文不做讲解 |
在分析这个函数之前,我们需要先了解其入参的来源,换句话说,我们要解答两个疑问:
- 入参的sid 和class id是如何生成的
- SELinux内核如何通过ssid + tsid + class id 查询到其对应的权限的。
在这里本文主要解答第一个问题: avc_has_perm
的入参是如何生成的。
换句话说,内核如何将安全上下文这个字符串转换为sid
SElinux如何将安全上下文转换为SID
这里我们以link文件的过程为例,其中对link的权限检查函数为may_link
。来看看具体实现逻辑,代码已经省略了与本文无关的内容
may_link 是创建文件过程中的一步,主要检查进程创建文件过程中,进程是否对文件有链接的权限
我们可以发现,对于进程创建文件的链接请求,avc_has_perm
检查的主体SID来自进程的SIDu32 sid = current_sid();
检查的客体来自于isec,也就是文件inode->i_security
isec = backing_inode_security(dentry);
所以此时,avc_has_perm
的入参ssid来自进程,tsid与tclass来自文件inode。
那么这里进程与文件的相关信息是何时设置的呢?
考虑到将整个逆向过程粘贴在这整个文章会比较啰嗦,我直接写出实际进程创建阶段SID初始化函数和文件inode创建阶段SID初始化部分函数,如果对整个函数调用逻辑比较感兴趣可以留言我再补充。
进程初始化阶段中的SID初始化
进程的安全上下文保存在bprm->cred->security
中,默认是的父进程的上下文。
bprm是一个中间变量,用于保存执行二进制前,二进制的相关信息,其中就包含此二进制的相关安全信息
SELinux对bprm
的安全规则设置阶段是在selinux_bprm_set_creds
完成的。
然后开始配置new_tsec
如果父进程设置了exec_sid,则bprm->cred->security->sid = exec_sid,否则通过security_transition_sid
函数来计算SID,将结果保存在new_tsec
中,即bprm
中
当执行二进制时候,bprm
结构体将会作为入参传递给二进制处理函数load_elf_binary
而进程的安全上下文的设置,是在load_elf_binary
内的commit_creds
函数实现,commit_creds
将会修改进程的cred结构体,从而修改进程的sid
所以对于进程来说,如果没有域切换,其安全上下文继承于父进程的上下文。
但是实际进程一般都是需要域切换的。所以我们来看看security_transition_sid
是如何实现域切换的。
首先我们来看下security_transition_sid
的入参
入参 | 含义 |
selinux_state | 数据库地址 |
old_tsec->sid | 父进程sid |
isec->sid | 被执行的二进制文件的sid |
SECCLASS_PROCESS | class类型为process(进程类) |
new_tsec-sid | 保存查询到的结果 |
security_transition_sid
的本质是security_compute_sid
的封装
再看security_compute_sid
的入参
其入参含义如下
入参 | 含义 | 示例中的入参值 |
selinux_state | 数据库地址 | 数据库地址 |
ssid | 源sid | 父进程sid |
tsid | 目的sid | 被执行的二进制文件inode的sid |
orig_tclass | class类型 | SECCLASS_PROCESS(进程类) |
objname | 对象名 | 文件名称,非文件绝对路径 |
out_sid | 保存查询到的结果 | 查询到的sid |
kern | 是否是内核态数据 | 是 |
终于到了目标函数了,现在可以详细分析对于进程计算SID的过程,让我们来分段解析下security_compute_sid
的源码
SID的计算
先简单总结下这个函数的处理逻辑
- 判断是否是在SELinux是否初始化完成,如果是则直接使用传入的ssid或者tsid作为计算后的SID
- 申请一个新的安全上下文,根据传入的ssid、tsid、tclass。通过一系列规则计算来填充这个新的安全上下文
- 查询新的安全上下文是否有对应sid,有则将此SID作为计算后的SID,否则分配一个新的SID
函数内删除了与本文无关的内容,比如mls的检查,上下文合法性检查,各类异常处理等
我们知道,SELinux规则库的加载是通过systemd实现的。所以在系统启动中,systemd未介入前。内核是无法知晓存放在磁盘上的规则文件。
所以这里,security_compute_sid
首选判断了initialized
标志位。此标志位表示SELinux是否完成初始化。如果此时是SELInux尚未初始化完成,直接返回源或者目的的SID
接着SELinux需要在原始安全类和内部表示之间进行转换
为什么会有这一步,直白点讲,虽然我们在SElinux内核态的实现-class中说明了selinux_map
中保存了class 与permission的映射关系。貌似selinux_map
内容是用户层编译过程决定的。但是实际SELinux已经指定了selinux_map
内保存的部分class的顺序。其顺序保存在flask.h
中
flask.h文件内容是根据字典secclass_map的内容自动生成,生成脚本位于
scripts/selinux/genheaders/genheaders.c
以SECCLASS_PROCESS
为例,这里SECCLASS_PROCESS
是一个对外公开的、易于理解的标签,它对应着内部的一个数字标识符,表示进程安全类。
当SELinux执行诸如访问控制决策这样的安全操作时,它需要确保使用的是系统内部一致且精确的安全类标识,而非外部用户或应用程序所使用的可能更方便理解的其他标签。因此,在这个函数中,当确定是否初始化了SELinux状态并且是在内核空间(kern为真)执行时,会调用unmap_class
来将传入的外部安全类orig_tclass转换成SELinux内部使用的实际安全类标识tclass。
如果不是内核空间内进程SID的计算,则直接使用传入的class id作为tclass。
接着通过ssid与tsid 查询到源安全上下文与目的安全上下文,这块实现在SElinux内核态的实现-class中说明了
对入参的处理已经完成,现在是时候填充查询用的新安全上下文了。
首先填充的是user,这里specified
的值为AVTAB_TRANSITION
,所以,如果class中指定了default_user,则新安全上下文为tcontext–>user,否则为 scontext->user
然后填充的是role,同样的对default_role做了判断。
当default_role
没有设置的时候,此时SElinux会判断 class类型是否为进程类或者socket类。如果是则使用scontext->role。否则使用OBJECT_R_VAL
OBJECT_R_VAL
是一个预定义的角色,通常分配给非进程类型的对象,如文件、目录等
接着填充的是type,这里与role类似,不在详述
接着是在&policydb->te_avtab
中查询是否有满足条件的标签(type)转换规则
- 如果没有,则在
policydb->te_cond_avtab
中查询是否有bool控制的满足条件的条件标签(type)转换。 - 如果查询到,则将新的安全上下文设置为转换后的标签(type)
这里SELinux的处理逻辑有点重复,如果有标签转换则不需要从cladatum读取type,不过对性能影响不大
然后检查对指定文件是否有标签转换的规则
最后检查是否有role转换的要求
最后,计算新的安全上下文的SID并保存至返回值。SELinux将会查询sidtab是否有匹配的SID,如果有则返回,如果没有则分配一个新的SID。
小结
到这里,我们理解了进程类的SID的计算过程。
- 判断SELinux是否初始化完成,如果否,则直接使用入参的SID
- 对class做处理:如果是内核请求,则将入参的class id转化为实际id。否则使用入参的class id
- 获取源目安全上下文
- 将根据规则与class id,将源目上下文中的内容填充至新的查询用安全上下文中。
- 检查是否有type transition(标签转换) 如果没有则检查是否有bool控制的type transition(标签转换),如果有则将转换后的标签填充至查询用安全上下文中
- 检查是否有role 切换,如果有 则将转换后的role填充至查询用安全上下文中
- 检查查询用安全上下文是否有对应的SID,如果有则返回,如果没有则分配新SID
文件inode初始化阶段中SID的初始化
文件inode的初始化函数为alloc_inode
如果文件系统提供了alloc_inode
方法,则调用文件系统的alloc_inode
函数,
到这里 alloc_inode
会有两种场景
- 新建一个文件,需要填充新的安全上下文并分配inode
- 文件已经存在,只是从磁盘上读取相应信息并在内存中创建并填充inode结构体。
新建文件中SID的计算
我们先看下第一种,新建一个文件的逻辑
这里以ext4
文件系统为例。当ext4
文件系统提供的alloc_inode
对应最终实现的函数为__ext4_new_inode
这里 ext4_init_security
将会调用security_inode_init_security
钩子函数
security_inode_init_security
钩子在selinux中对应的就是selinux_inode_init_security
从扩展属性到sid的翻译函数selinux_determine_inode_label
,看看他做了什么
- 首先,它检查父目录所在的文件系统的安全上下文(
sbsec
)。 - 如果文件系统已经初始化(
SE_SBINITIALIZED
)并且其行为设置为使用挂载点SID(SECURITY_FS_USE_MNTPOINT
),则直接设置_new_isid
为文件系统的挂载点SID(mntpoint_sid
)。 - 如果文件系统使用基于标签的挂载(
SBLABEL_MNT
)并且当前任务有创建SID(tsec->create_sid
),则设置_new_isid
为任务的创建SID。 - 如果上述条件都不满足,则调用
security_transition_sid
函数计算安全上下文的SID。这个函数在进程的SID计算中已经详细解释,其根据当前任务的SID、父目录的SID、目标安全类、以及目标名称来确定一个新的SID。
打开已有文件中SID的计算
打开已有文件的inode初始化的在ext4_link
阶段中的d_instantiate(dentry, inode);
相信读者看完上面的内容后,已经可以详细分析d_instantiate
的处理流程。这里就留为作业吧。
提示:d_instantiate
-> selinux_d_instantiate
-> inode_doinit_with_dentry
-> inode_doinit_use_xattr_withname
-> security_context_to_sid_default