LSM 是(Linux Security Module)的简称,它是Linux 从2.6 内核版本开始增加的一个通用的访问控制框架。
LSM 是一个轻量的通用访问控制框架,它必须满足两方面的要求:对不需要它的人保持透明;让需要它的人从内核代码中解放出来,专注于控制策略和安全模块。所以LSM 在设计之初,就明确了其原则:
(1)真正的通用性,使用不同的安全模型仅仅通过加载不同的内核模块实现,而不用改变已有的内核代码。
(2)概念简单,不涉及具体的安全策略,尽量减少对原有内核的修改,对内核的运行效率不会产生大的影响。
(3)支持已有的 POSIX.1e capabilities 机制,将其分离出来,作为可选的安全模块。
为了满足这些目标,LSM 在内核资源访问的源代码中,放置了钩子函数,来仲裁对内部资源的访问,这些资源包括文件、inode 节点、进程、网络套接字等。Linux 已经实现了UNIX 经典的访问控制机制,即基于权限位的自主访问控制。LSM 钩子函数通常放在内核自主访问控制后,在内核进行真正内部资源访问前。也就是说,经过了自主访问控制的检查,还需要经过LSM 钩子函数的判定,才能访问到资源。
由于内核版本不同,LSM的注册、初始化实现方式可能不同,底层实现差异不大,因此本文针对内核版本5.4进行LSM框架分析
一、内核拉起LSM
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
debug_objects_early_init();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_disabled = true;
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them.
*/
...
//lsm early init
early_security_init();
...
//lsm init
security_init();
...
}
二、LSM初始化
对于内核怎么拉起lsm不是我们关注的重点,下面重点看看security.c里面做了什么
struct security_hook_heads {
struct hlist_head binder_set_context_mgr;
struct hlist_head binder_transaction;
struct hlist_head binder_transfer_binder;
struct hlist_head binder_transfer_file;
struct hlist_head ptrace_access_check;
struct hlist_head ptrace_traceme;
struct hlist_head capget;
struct hlist_head capset;
struct hlist_head capable;
struct hlist_head quotactl;
struct hlist_head quota_on;
struct hlist_head syslog;
struct hlist_head settime;
struct hlist_head vm_enough_memory;
struct hlist_head bprm_set_creds;
struct hlist_head bprm_check_security;
struct hlist_head bprm_committing_creds;
struct hlist_head bprm_committed_creds;
struct hlist_head fs_context_dup;
struct hlist_head fs_context_parse_param;
struct hlist_head sb_alloc_security;
struct hlist_head sb_free_security;
struct hlist_head sb_free_mnt_opts;
struct hlist_head sb_eat_lsm_opts;
struct hlist_head sb_remount;
struct hlist_head sb_kern_mount;
struct hlist_head sb_show_options;
struct hlist_head sb_statfs;
struct hlist_head sb_mount;
struct hlist_head sb_umount;
struct hlist_head sb_pivotroot;
struct hlist_head sb_set_mnt_opts;
struct hlist_head sb_clone_mnt_opts;
struct hlist_head sb_add_mnt_opt;
struct hlist_head move_mount;
...
}
struct lsm_info {
const char *name; /* Required. */
enum lsm_order order; /* Optional: default is LSM_ORDER_MUTABLE */
unsigned long flags; /* Optional: flags describing LSM */
int *enabled; /* Optional: controlled by CONFIG_LSM */
int (*init)(void); /* Required. */
struct lsm_blob_sizes *blobs; /* Optional: for blob sharing. */
};
int __init early_security_init(void)
{
int i;
struct hlist_head *list = (struct hlist_head *) &security_hook_heads;
struct lsm_info *lsm;
for (i = 0; i < sizeof(security_hook_heads) / sizeof(struct hlist_head);
i++)
INIT_HLIST_HEAD(&list[i]);
for (lsm = __start_early_lsm_info; lsm < __end_early_lsm_info; lsm++) {
if (!lsm->enabled)
lsm->enabled = &lsm_enabled_true;
prepare_lsm(lsm);
initialize_lsm(lsm);
}
return 0;
}
early_security_init
early_security_init主要做了几件事情:
1.将lsm的钩子函数都放到一个链表内,里面的有些钩子函数可能是通用的,有的钩子函数是特定框架才有的,如binder_set_context_mgr只有selinux下才有,而bprm_committing_creds则除了selinux外apparmor框架也有,但是此时lsm还不知道系统当前支持哪些安全框架,因此选择全部进行初始化
2.根据__start_early_lsm_info以及__end_early_lsm_info结构体查看当前使能了哪些lsm模块,在编译内核的时候需要配置一些宏,如CONFIG_SECURIRY_SELINUX、CONFIG_SECURITY_DEFAULT="selinux"等,最终会编译打包到镜像内,有兴趣的可以去解析一下vmlinux.lds和System.map这两个编译生成物。
3.根据当前设定的default lsm model去调用对应安全框架的初始化函数
security_init
security_init主要是用于内存空间申请,并且调用对应的安全模块初始化函数
/* Initialize a given LSM, if it is enabled. */
static void __init initialize_lsm(struct lsm_info *lsm)
{
if (is_enabled(lsm)) {
int ret;
init_debug("initializing %s\n", lsm->name);
ret = lsm->init();
WARN(ret, "%s failed to initialize: %d\n", lsm->name, ret);
}
}
三、安全模块初始化(以SELINUX为例)
static __init int selinux_init(void)
{
pr_info("SELinux: Initializing.\n");
memset(&selinux_state, 0, sizeof(selinux_state));
enforcing_set(&selinux_state, selinux_enforcing_boot);
selinux_state.checkreqprot = selinux_checkreqprot_boot;
selinux_ss_init(&selinux_state.ss);
selinux_avc_init(&selinux_state.avc);
/* Set the security state for the initial task. */
cred_init_security();
default_noexec = !(VM_DATA_DEFAULT_FLAGS & VM_EXEC);
avc_init();
avtab_cache_init();
ebitmap_cache_init();
hashtab_cache_init();
security_add_hooks(selinux_hooks, ARRAY_SIZE(selinux_hooks), "selinux");
if (avc_add_callback(selinux_netcache_avc_callback, AVC_CALLBACK_RESET))
panic("SELinux: Unable to register AVC netcache callback\n");
if (avc_add_callback(selinux_lsm_notifier_avc_callback, AVC_CALLBACK_RESET))
panic("SELinux: Unable to register AVC LSM notifier callback\n");
if (selinux_enforcing_boot)
pr_debug("SELinux: Starting in enforcing mode\n");
else
pr_debug("SELinux: Starting in permissive mode\n");
fs_validate_description(&selinux_fs_parameters);
return 0;
}
selinux_init最重要的部分就是security_add_hooks,将各个操作都与对应的钩子函数进行绑定,例如LSM_HOOK_INIT(file_open, selinux_file_open),将file_open与selinux_file_open进行绑定。每次调用file_open时会调用selinux_file_open,进行权限检查。如下
1.文件系统初始化时会设置.open的实现为ovl_open
const struct file_operations ovl_file_operations = {
.open = ovl_open,
.release = ovl_release,
.llseek = ovl_llseek,
.read_iter = ovl_read_iter,
.write_iter = ovl_write_iter,
.fsync = ovl_fsync,
.mmap = ovl_mmap,
.fallocate = ovl_fallocate,
.fadvise = ovl_fadvise,
.unlocked_ioctl = ovl_ioctl,
.compat_ioctl = ovl_compat_ioctl,
.splice_read = generic_file_splice_read,
.splice_write = ovl_splice_write,
.copy_file_range = ovl_copy_file_range,
.remap_file_range = ovl_remap_file_range,
};
2.ovl_open会调用到ovl_open_realfile
static int ovl_open(struct inode *inode, struct file *file)
{
struct file *realfile;
int err;
err = ovl_maybe_copy_up(file_dentry(file), file->f_flags);
if (err)
return err;
/* No longer need these flags, so don't pass them on to underlying fs */
file->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
realfile = ovl_open_realfile(file, ovl_inode_realdata(inode));
if (IS_ERR(realfile))
return PTR_ERR(realfile);
file->private_data = realfile;
return 0;
}
3.ovl_open_realfile会再去调用open.c内的open_with_fake_path
static struct file *ovl_open_realfile(const struct file *file,
struct inode *realinode)
{
struct inode *inode = file_inode(file);
struct file *realfile;
const struct cred *old_cred;
int flags = file->f_flags | OVL_OPEN_FLAGS;
old_cred = ovl_override_creds(inode->i_sb);
realfile = open_with_fake_path(&file->f_path, flags, realinode,
current_cred());
ovl_revert_creds(inode->i_sb, old_cred);
pr_debug("open(%p[%pD2/%c], 0%o) -> (%p, 0%o)\n",
file, file, ovl_whatisit(inode, realinode), file->f_flags,
realfile, IS_ERR(realfile) ? 0 : realfile->f_flags);
return realfile;
}
4.open.c内的do_dentry_open就是实际上进行file_open操作的入口,通过上面的1、2、3这几个api调用后,拿到file_path,在对文件进行操作之前,会调用security_file_open进行selinux的权限检查。
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
static const struct file_operations empty_fops = {};
int error;
...
f->f_op = fops_get(inode->i_fop);
if (WARN_ON(!f->f_op)) {
error = -ENODEV;
goto cleanup_all;
}
error = security_file_open(f);
if (error)
goto cleanup_all;
error = break_lease(locks_inode(f), f->f_flags);
if (error)
goto cleanup_all;
/* normally all 3 are set; ->open() can clear them if needed */
f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
...
}
5.security.c内通过事先做好的钩子函数绑定,去调用对应的处理函数。
#define call_int_hook(FUNC, IRC, ...) ({ \
int RC = IRC; \
do { \
struct security_hook_list *P; \
\
hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \
RC = P->hook.FUNC(__VA_ARGS__); \
if (RC != 0) \
break; \
} \
} while (0); \
RC; \
})
int security_file_open(struct file *file)
{
int ret;
ret = call_int_hook(file_open, 0, file);
if (ret)
return ret;
return fsnotify_perm(file, MAY_OPEN);
}
6.对应的selinux钩子函数(selinux_file_open)会去判断当前节点是否有权限,再一层一层返回上去。至此,内核在执行真正的file_open之前的selinux权限检查结束
static int selinux_file_open(struct file *file)
{
struct file_security_struct *fsec;
struct inode_security_struct *isec;
fsec = selinux_file(file);
isec = inode_security(file_inode(file));
/*
* Save inode label and policy sequence number
* at open-time so that selinux_file_permission
* can determine whether revalidation is necessary.
* Task label is already saved in the file security
* struct as its SID.
*/
fsec->isid = isec->sid;
fsec->pseqno = avc_policy_seqno(&selinux_state);
/*
* Since the inode label or policy seqno may have changed
* between the selinux_inode_permission check and the saving
* of state above, recheck that access is still permitted.
* Otherwise, access might never be revalidated against the
* new inode label or new policy.
* This check is not redundant - do not remove.
*/
return file_path_has_perm(file->f_cred, file, open_file_to_av(file));
}
内核的selinux处理目前是比较完善的,虽然实际调试上不会用到太多内核的LSM框架部分,但是还是了解一下比较好。