文章目录
  • 前言
  • SELinux内核态简介
  • SELinux数据库的实现
  • 安全上下文
  • sid
  • sid与安全上下文映射关系的保存
  • class与permission
  • class的实现
  • 公用class
  • 私有class
  • class与permission的关联--selinux_mapping
  • selinux_mapping的初始化
  • class查询的优化
  • 基于class id 查询其结构体class_datum内存地址
  • 基于class id 查询 class的名字
  • class的加载
  • 填充 common(公共class)
  • 填充class表(私有class)

前言

本文适合对SElinux已经有一定了解,能够按需实现.te、.if、.fc文件,对内核中SElinux相关机制实现感兴趣的同事。

此外,本文实现基于4.19内核版本,且本人工作中未使用mls。也将跳过mls代码解析的实现。

SELinux内核态简介

SELinux在内核中分为检查部分以及数据库部分

检查部分也就是我们熟悉的lsm hook机制

Linux Security Modules (LSM) 是Linux内核中的一种框架,用于为操作系统添加可插拔的安全策略。这一机制允许开发者和系统管理员通过插入自定义的安全模块来增强系统的安全性,而不必直接修改内核代码。LSM主要关注访问控制,以防范未授权的活动。

LSM的工作原理围绕“钩子”(hooks)展开。这些钩子是内核中预定义的插入点,分布在各种敏感操作上,比如文件访问、进程创建和信号处理等。每当内核执行这些操作时,都会暂停去调用LSM注册的钩子函数,让安全模块有机会根据其策略来决定是否放行或拒绝请求。

安全模块通过实现这些钩子接口,可以在操作执行前后介入,执行额外的权限验证或审计。例如,在文件打开操作期间,模块可以通过钩子检查请求者是否有权访问目标文件。如果模块判断访问不应被允许,它可以返回一个错误,从而阻止操作。

值得注意的是,LSM是静态编入内核的,意味着要在系统中使用特定的LSM,必须在编译内核时将其包含进去,且同一时间系统只能激活一个LSM模块。这保证了安全策略的统一性和效率,但要求对安全需求有明确的预先规划。

数据库部分则保存的是我们检查过程中需要使用的数据,比如selinux标签对应的编号,class对应的权限类等。

本文章对检查部分涉及较少,有以下两个原因:

一来检查部分除了一个初始化宏较难理解外,其余部分代码部分读懂难度不大。主要难点是在为什么在这里设置检查点以及检查点检查的内容是什么。而这两点更多考察的是对内核各个功能模块实现机制的了解,和安全模块本地检查功能关系不大。

二是如果要细讲,需要将前置技能点《linux内核功能的实现》一一点亮,这与SElinux本身功能实现关联较小。

所以检查部分这块不作为本文重点。本文还是以SElinux数据库部分实现为主。

SELinux数据库的实现

SElinux数据库的核心结构体是selinux_statepolicydb。前者主要保存的是sid与安全上下文的映射关系、检查结果的缓存,policydb主要保存的是检查规则。这些概念看不懂没关系,后续会详细解释这些内容。

不过在此之前,我们需要先了解SELinux数据库结构设计中保存各个数据的最小结构单元:安全上下文、class、

安全上下文

我们应当知道,SELinux的管控是基于扩展属性的标签实现的。其标签格式如下

/var/tinydns/env(/.*)?                             all files          system_u:object_r:svc_conf_t:s0
  • 1.

这4个字段分别表示user、role、type以及mls级别,而这些元素的组合一起就是安全上下文,所以user_u:role_r:type_t:s0就是一个安全上下文。

SElinux使用结构体为strcut context保存安全上下文,定义如下

/*
 * A security context consists of an authenticated user
 * identity, a role, a type and a MLS range.
 */
struct context {
	u32 user;
	u32 role;
	u32 type;
	u32 len;        /* length of string in bytes */
	struct mls_range range;
	char *str;	/* string representation if context cannot be mapped. */
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

这里其中char str表示这个安全上下文的,也就是上文中的user_u:role_r:type_t:s0,而len很明显是这个字符串的长度。

context_struct_to_string我们可以了解SELinux对context->str的用法

static int context_struct_to_string(struct policydb *p,
				    struct context *context,
				    char **scontext, u32 *scontext_len)
{
	char *scontextp;

	if (scontext)
		*scontext = NULL;
	*scontext_len = 0;
	// 如果安全上下文有设置长度,则直接返回context->str
	if (context->len) {
		*scontext_len = context->len;
		if (scontext) {
			*scontext = kstrdup(context->str, GFP_ATOMIC);
			if (!(*scontext))
				return -ENOMEM;
		}
		return 0;
	}
	// 否则则拼接各个字符串合成完整上下文
...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

其次,我们观察struct context可以发现role,type,user,len的类型是u32,所以我们可以合理推测两点

  1. SELinux保存role,type,user,len是通过id保存的。
  2. 用户空间拿到规则te文件后,编译阶段会给这些role,type,user,len来分配id。

sid

既然role,type,user,len是通过id来保存在内核中的,那么对于role,type,user,len,即安全上下文是不是也可以通过id来保存的呢。

结果是必然的。这个id在SELinux中被称为sid。这里以邮政编码,假设role是湖北省、type是宜昌市、user是夷陵区,那么那么实际的上下文对于邮编地址就是湖北省宜昌市夷陵区,就是sid对应的就是身份证前6位:420505

role:type:user:mls  --> sid
湖北省宜昌市夷陵区 --> 420505(身份证号前6位)
  • 1.
  • 2.

这个sid与context上下文映射保存在selinux_ss->sidtab->sidtab哈希表中。此外,为了快速查询,SElinux还会将查询结果缓存,这份缓存保存在selinux_ss->sidtab->cache中。

根据SELinux生成sid部分的代码(sidtab_context_to_sid函数),可以发现这个sid在系统每次运行时候是不一样的。也就是说,sid与安全上下文的映射关系是实时生成的,当某个安全上下文首次需要被检查时候,SELinux将会给其分配一个sid,然后将这个映射关系保存在sidtab->sidtab哈希表中,接着更新sidtab->cache缓存表。

同理,查询的时候也是先查询sidtab->cache,然后查询sidtab->sidtab,如果还找不到,则给这个安全上下文分配一个新的sid。

具体查询实现代码如下

int sidtab_context_to_sid(struct sidtab *s,
			  struct context *context,
			  u32 *out_sid)
{
	u32 sid;
	int ret = 0;
	unsigned long flags;

	*out_sid = SECSID_NULL;
	// 从sidtab->cache中查询context的sid
	sid  = sidtab_search_cache(s, context);
	if (!sid)
		// 从sidtab->sidtab中查询context的sid
		sid = sidtab_search_context(s, context);
	if (!sid) {
		spin_lock_irqsave(&s->lock, flags);
		/* Rescan now that we hold the lock. */
		sid = sidtab_search_context(s, context);
		if (sid)
			goto unlock_out;
		/* No SID exists for the context.  Allocate a new one. */
		if (s->next_sid == UINT_MAX || s->shutdown) {
			ret = -ENOMEM;
			goto unlock_out;
		}
		// 分配新sid
		sid = s->next_sid++;
		if (context->len)
			pr_info("SELinux:  Context %s is not valid (left unmapped).\n",
			       context->str);
		ret = sidtab_insert(s, sid, context);
		if (ret)
			s->next_sid--;
unlock_out:
		spin_unlock_irqrestore(&s->lock, flags);
	}

	if (ret)
		return ret;

	*out_sid = sid;
	return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.