SElinux内核态的实现-SID的计算篇


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_PROCESSclass类型为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_tclassclass类型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

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值