在 SElinux内核态的实现-class中,我们已经知晓了安全上下文、class的概念。 并且已经知晓如何获取其安全上下文对应的id以及class的id.
现在我们可以开始试着理解SELinux如何实现对一个请求进行检查了。
在SELinux设计中,权限检查的核心函数是avc_has_perm
。这个函数在内核代码中随处可见,其内容也很简单:
/**
* avc_has_perm - Check permissions and perform any appropriate auditing.
* @ssid: source security identifier
* @tsid: target security identifier
* @tclass: target security class
* @requested: requested permissions, interpreted based on @tclass
* @auditdata: auxiliary audit data
*
* Check the AVC to determine whether the @requested permissions are granted
* for the SID pair (@ssid, @tsid), interpreting the permissions
* based on @tclass, and call the security server on a cache miss to obtain
* a new decision and add it to the cache. Audit the granting or denial of
* permissions in accordance with the policy. Return %0 if all @requested
* permissions are granted, -%EACCES if any permissions are denied, or
* another -errno upon other errors.
*/
int avc_has_perm(struct selinux_state *state, u32 ssid, u32 tsid, u16 tclass,
u32 requested, struct common_audit_data *auditdata)
{
struct av_decision avd;
int rc, rc2;
rc = avc_has_perm_noaudit(state, ssid, tsid, tclass, requested, 0, &avd);
rc2 = avc_audit(state, ssid, tsid, tclass, requested, &avd, rc, auditdata, 0);
if (rc2)
return rc2;
return rc;
}
相信读者如果阅读过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 是创建文件过程中的一步,主要检查进程创建文件过程中,进程是否对文件有链接的权限
/* Check whether a task can link, unlink, or rmdir a file/directory. */
static int may_link(struct inode *dir,
struct dentry *dentry,
int kind)
{
struct inode_security_struct *dsec, *isec;
struct common_audit_data ad;
u32 sid = current_sid();
u32 tsid2, ssid2;
u32 av;
int rc;
dsec = inode_security(dir);
isec = backing_inode_security(dentry);
......
switch (kind) {
case MAY_LINK:
av = FILE__LINK;
break;
rc = avc_has_perm(&selinux_state, sid, isec->sid, isec->sclass, av, &ad);
......
return rc;
}
我们可以发现,对于进程创建文件的链接请求,avc_has_perm
检查的主体SID来自进程的SID
u32 sid = current_sid();
检查的客体来自于isec,也就是文件inode->i_security
isec = backing_inode_security(dentry);
/*
* Get the security label of a dentry's backing inode.
*/
static struct inode_security_struct *backing_inode_security(struct dentry *dentry)
{
struct inode *inode = d_backing_inode(dentry);
return inode->i_security;
}
所以此时,avc_has_perm
的入参ssid来自进程,tsid与tclass来自文件inode。
那么这里进程与文件的相关信息是何时设置的呢?
考虑到将整个逆向过程粘贴在这整个文章会比较啰嗦,我直接写出实际进程创建阶段SID初始化函数和文件inode创建阶段SID初始化部分函数,如果对整个函数调用逻辑比较感兴趣可以留言我再补充。
进程初始化阶段中的SID初始化
进程的安全上下文保存在bprm->cred->security
中,默认是的父进程的上下文。
bprm是一个中间变量,用于保存执行二进制前,二进制的相关信息,其中就包含此二进制的相关安全信息
SELinux对bprm
的安全规则设置阶段是在selinux_bprm_set_creds
完成的。
static int selinux_bprm_set_creds(struct linux_binprm *bprm){
......
old_tsec = current_security();
new_tsec = bprm->cred->security; //获取bprm中的安全属性
/* Default to the current task SID. */
new_tsec->sid = old_tsec->sid;
new_tsec->osid = old_tsec->sid;
......
}
然后开始配置new_tsec
如果父进程设置了exec_sid,则bprm->cred->security->sid = exec_sid,否则通过security_transition_sid
函数来计算SID,将结果保存在new_tsec
中,即bprm
中
if (old_tsec->exec_sid) {
new_tsec->sid = old_tsec->exec_sid;
......
} else {
/* Check for a default transition on this program. */
rc = security_transition_sid(&selinux_state, old_tsec->sid, isec->sid, SECCLASS_PROCESS, NULL,
&new_tsec->sid);
当执行二进制时候,bprm
结构体将会作为入参传递给二进制处理函数load_elf_binary
而进程的安全上下文的设置,是在load_elf_binary
内的commit_creds
函数实现,commit_creds
将会修改进程的cred结构体,从而修改进程的sid
static int load_elf_binary(struct linux_binprm *bprm){
......
install_exec_creds(bprm);
......
}
void install_exec_creds(struct linux_binprm *bprm)
{
.....
commit_creds(bprm->cred);
.....
}
int commit_creds(struct cred *new){
......
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
......
}
所以对于进程来说,如果没有域切换,其安全上下文继承于父进程的上下文。
但是实际进程一般都是需要域切换的。所以我们来看看security_transition_sid
是如何实现域切换的。
首先我们来看下security_transition_sid
的入参
security_transition_sid(&selinux_state, old_tsec->sid, isec->sid,
SECCLASS_PROCESS, NULL, &new_tsec->sid);
入参 | 含义 |
---|---|
selinux_state | 数据库地址 |
old_tsec->sid | 父进程sid |
isec->sid | 被执行的二进制文件的sid |
SECCLASS_PROCESS | class类型为process(进程类) |
new_tsec-sid | 保存查询到的结果 |
security_transition_sid
的本质是security_compute_sid
的封装
int security_transition_sid(struct selinux_state *state,
u32 ssid, u32 tsid, u16 tclass,
const struct qstr *qstr, u32 *out_sid)
{
return security_compute_sid(state, ssid, tsid, tclass,
AVTAB_TRANSITION,
qstr ? qstr->name : NULL, out_sid, true);
}
再看security_compute_sid
的入参
static int security_compute_sid(struct selinux_state *state,
u32 ssid,
u32 tsid,
u16 orig_tclass,
u32 specified,
const char *objname,
u32 *out_sid,
bool kern)
其入参含义如下
入参 | 含义 | 示例中的入参值 |
---|---|---|
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
if (!state->initialized) {
switch (orig_tclass) {
case SECCLASS_PROCESS: /* kernel value */
*out_sid = ssid;
break;
default:
*out_sid = tsid;
break;
}
goto out;
}
接着SELinux需要在原始安全类和内部表示之间进行转换
if (kern) {
tclass = unmap_class(&state->ss->map, orig_tclass);
sock = security_is_socket_class(orig_tclass);
} else {
tclass = orig_tclass;
sock = security_is_socket_class(map_class(&state->ss->map,
tclass));
}
为什么会有这一步,直白点讲,虽然我们在SElinux内核态的实现-class中说明了selinux_map
中保存了class 与permission的映射关系。貌似selinux_map
内容是用户层编译过程决定的。但是实际SELinux已经指定了selinux_map
内保存的部分class的顺序。其顺序保存在flask.h
中
flask.h文件内容是根据字典secclass_map的内容自动生成,生成脚本位于
scripts/selinux/genheaders/genheaders.c
/*
* Security object class definitions
*/
#define SECCLASS_SECURITY 1
#define SECCLASS_PROCESS 2
#define SECCLASS_SYSTEM 3
#define SECCLASS_CAPABILITY 4
#define SECCLASS_FILESYSTEM 5
#define SECCLASS_FILE 6
....
以SECCLASS_PROCESS
为例,这里SECCLASS_PROCESS
是一个对外公开的、易于理解的标签,它对应着内部的一个数字标识符,表示进程安全类。
当SELinux执行诸如访问控制决策这样的安全操作时,它需要确保使用的是系统内部一致且精确的安全类标识,而非外部用户或应用程序所使用的可能更方便理解的其他标签。因此,在这个函数中,当确定是否初始化了SELinux状态并且是在内核空间(kern为真)执行时,会调用unmap_class
来将传入的外部安全类orig_tclass转换成SELinux内部使用的实际安全类标识tclass。
如果不是内核空间内进程SID的计算,则直接使用传入的class id作为tclass。
接着通过ssid与tsid 查询到源安全上下文与目的安全上下文,这块实现在SElinux内核态的实现-class中说明了
scontext = sidtab_search(sidtab, ssid);
tcontext = sidtab_search(sidtab, tsid);
对入参的处理已经完成,现在是时候填充查询用的新安全上下文了。
首先填充的是user,这里specified
的值为AVTAB_TRANSITION
,所以,如果class中指定了default_user,则新安全上下文为tcontext–>user,否则为 scontext->user
switch (specified) {
case AVTAB_TRANSITION:
case AVTAB_CHANGE:
if (cladatum && cladatum->default_user == DEFAULT_TARGET) {
newcontext.user = tcontext->user;
} else {
newcontext.user = scontext->user;
}
break;
}
然后填充的是role,同样的对default_role做了判断。
当default_role
没有设置的时候,此时SElinux会判断 class类型是否为进程类或者socket类。如果是则使用scontext->role。否则使用OBJECT_R_VAL
OBJECT_R_VAL
是一个预定义的角色,通常分配给非进程类型的对象,如文件、目录等
/* Set the role to default values. */
if (cladatum && cladatum->default_role == DEFAULT_SOURCE) {
newcontext.role = scontext->role;
} else if (cladatum && cladatum->default_role == DEFAULT_TARGET) {
newcontext.role = tcontext->role;
} else {
if ((tclass == policydb->process_class) || (sock == true))
newcontext.role = scontext->role;
else
newcontext.role = OBJECT_R_VAL;
}
接着填充的是type,这里与role类似,不在详述
/* Set the type to default values. */
if (cladatum && cladatum->default_type == DEFAULT_SOURCE) {
newcontext.type = scontext->type;
} else if (cladatum && cladatum->default_type == DEFAULT_TARGET) {
newcontext.type = tcontext->type;
} else {
if ((tclass == policydb->process_class) || (sock == true)) {
/* Use the type of process. */
newcontext.type = scontext->type;
} else {
/* Use the type of the related object. */
newcontext.type = tcontext->type;
}
}
接着是在&policydb->te_avtab
中查询是否有满足条件的标签(type)转换规则
- 如果没有,则在
policydb->te_cond_avtab
中查询是否有bool控制的满足条件的条件标签(type)转换。 - 如果查询到,则将新的安全上下文设置为转换后的标签(type)
这里SELinux的处理逻辑有点重复,如果有标签转换则不需要从cladatum读取type,不过对性能影响不大
/* Look for a type transition/member/change rule. */
avkey.source_type = scontext->type;
avkey.target_type = tcontext->type;
avkey.target_class = tclass;
avkey.specified = specified;
avdatum = avtab_search(&policydb->te_avtab, &avkey);
/* If no permanent rule, also check for enabled conditional rules */
if (!avdatum) {
node = avtab_search_node(&policydb->te_cond_avtab, &avkey);
for (; node; node = avtab_search_node_next(node, specified)) {
if (node->key.specified & AVTAB_ENABLED) {
avdatum = &node->datum;
break;
}
}
}
if (avdatum) {
/* Use the type from the type transition/member/change rule. */
newcontext.type = avdatum->u.data;
}
然后检查对指定文件是否有标签转换的规则
/* if we have a objname this is a file trans check so check those rules */
if (objname)
filename_compute_type(policydb, &newcontext, scontext->type,
tcontext->type, tclass, objname);
最后检查是否有role转换的要求
/* Check for class-specific changes. */
if (specified & AVTAB_TRANSITION) {
/* Look for a role transition rule. */
for (roletr = policydb->role_tr; roletr;
roletr = roletr->next) {
if ((roletr->role == scontext->role) && (roletr->type == tcontext->type) &&
(roletr->tclass == tclass)) {
newcontext.role = roletr->new_role;
break;
}
}
}
最后,计算新的安全上下文的SID并保存至返回值。SELinux将会查询sidtab是否有匹配的SID,如果有则返回,如果没有则分配一个新的SID。
/* Obtain the sid for the context. */
rc = sidtab_context_to_sid(sidtab, &newcontext, out_sid);
out_unlock:
read_unlock(&state->ss->policy_rwlock);
context_destroy(&newcontext);
out:
return rc;
}
小结
到这里,我们理解了进程类的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
static struct inode *alloc_inode(struct super_block *sb)
{
.....
if (ops->alloc_inode)
inode = ops->alloc_inode(sb);
.....
如果文件系统提供了alloc_inode
方法,则调用文件系统的alloc_inode
函数,
到这里 alloc_inode
会有两种场景
- 新建一个文件,需要填充新的安全上下文并分配inode
- 文件已经存在,只是从磁盘上读取相应信息并在内存中创建并填充inode结构体。
新建文件中SID的计算
我们先看下第一种,新建一个文件的逻辑
这里以ext4
文件系统为例。当ext4
文件系统提供的alloc_inode
对应最终实现的函数为__ext4_new_inode
struct inode *__ext4_new_inode(handle_t *handle, struct inode *dir,
umode_t mode, const struct qstr *qstr,
__u32 goal, uid_t *owner, __u32 i_flags,
int handle_type, unsigned int line_no,
int nblocks)
{
...
inode = new_inode(sb);
ei = EXT4_I(inode);
...
// 如果inode还没有初始化其扩展属性(即EXT4_EA_INODE_FL这个标志位没有被设置)
if (!(ei->i_flags & EXT4_EA_INODE_FL)) {
err = ext4_init_security(handle, inode, dir, qstr);
}
}
这里 ext4_init_security
将会调用security_inode_init_security
钩子函数
int ext4_init_security(handle_t *handle, struct inode *inode, struct inode *dir,
const struct qstr *qstr)
{
return security_inode_init_security(inode, dir, qstr,
&ext4_initxattrs, handle);
}
security_inode_init_security
钩子在selinux中对应的就是selinux_inode_init_security
static int selinux_inode_init_security(struct inode *inode, struct inode *dir,
const struct qstr *qstr,
const char **name,
void **value, size_t *len)
{
const struct task_security_struct *tsec = current_security();
struct superblock_security_struct *sbsec;
u32 newsid, clen;
int rc;
char *context;
sbsec = dir->i_sb->s_security;
newsid = tsec->create_sid;
// 确定新 inode 的 SID 存放在newsid中
rc = selinux_determine_inode_label(current_security(),
dir, qstr,
inode_mode_to_security_class(inode->i_mode),
&newsid);
/* Possibly defer initialization to selinux_complete_init. */
// 如果sbsec已经初始化完成
if (sbsec->flags & SE_SBINITIALIZED) {
struct inode_security_struct *isec = inode->i_security;
isec->sclass = inode_mode_to_security_class(inode->i_mode);
isec->sid = newsid;
isec->initialized = LABEL_INITIALIZED;
}
if (name)
*name = XATTR_SELINUX_SUFFIX;
if (value && len) {
// 从sid转换了扩展属性字符串
rc = security_sid_to_context_force(&selinux_state, newsid,
&context, &clen);
if (rc)
return rc;
// 填充文件扩展属性字符串,返回后文件系统将使用此参数作为入参设置扩展文件的属性
*value = context;
*len = clen;
}
return 0;
}
从扩展属性到sid的翻译函数selinux_determine_inode_label
,看看他做了什么
/*
* Determine the label for an inode that might be unioned.
*/
static int
selinux_determine_inode_label(const struct task_security_struct *tsec,
struct inode *dir,
const struct qstr *name, u16 tclass,
u32 *_new_isid)
{
const struct superblock_security_struct *sbsec = dir->i_sb->s_security;
if ((sbsec->flags & SE_SBINITIALIZED) &&
(sbsec->behavior == SECURITY_FS_USE_MNTPOINT)) {
*_new_isid = sbsec->mntpoint_sid;
} else if ((sbsec->flags & SBLABEL_MNT) &&
tsec->create_sid) {
*_new_isid = tsec->create_sid;
} else {
const struct inode_security_struct *dsec = inode_security(dir);
return security_transition_sid(&selinux_state, tsec->sid,
dsec->sid, tclass,
name, _new_isid);
}
return 0;
}
- 首先,它检查父目录所在的文件系统的安全上下文(
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