本文基于4.19内核写作。
lsm的原理
操作系统内的一切操作都需要通过系统调用和内核交互后才能使用硬件资源。由于软件是构建在硬件提供的能力上的,因此操作系统中的基本上所有软件都需要通过系统调用才能运行。
理论上讲,能够对所有的系统调用按照需求进行过滤,那么就可以将其中危险的系统调用进行控制,从而提升系统的安全性。
为了让所有的安全钩子函数都有一个统一的调用入口,便于维护,同时降低安全模块的复杂性。内核提供了LSM安全子系统框架。
lsm基础原理(安全子模块钩子函数的调用)
linux 的安全子模块的主要原理是在所有系统调用处调用已注册的安全钩子函数。
为了安全模块的钩子函数入口的一致性,在所有系统调用的关键路径增加security_xxx函数调用,这些函数的所有LSM安全子模块注册的钩子函数的统一入口。
以open函数对应的系统调用为例,其LSM钩子函数定义如下:
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);
}
在vfs_open的路径中有对安全钩子函数的调用:
static int do_dentry_open(struct file *f,
int (*open)(struct inode *, struct file *))
...
error = security_file_open(f);
if (error)
goto cleanup_all;
...
也就意味着在进行open系统调用时,将会调用LSM子模块的函数进行安全管控的判断。
security_file_open
调用了call_init_hook
函数,明显是调用所有的安全钩子函数用的。
对于所有的安全过滤函数,分成void和int两种返回类型:
/*
* Hook list operation macros.
*
* call_void_hook:
* This is a hook that does not return a value.
*
* call_int_hook:
* This is a hook that returns a value.
*/
#define call_void_hook(FUNC, ...) \
do { \
struct security_hook_list *P; \
\
hlist_for_each_entry(P, &security_hook_heads.FUNC, list) \
P->hook.FUNC(__VA_ARGS__); \
} while (0)
#define call_int_hook(FUNC, ...) ({ \
int RC = LSM_RET_DEFAULT(FUNC); \
do { \
struct security_hook_list *P; \
\
hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \
RC = P->hook.FUNC(__VA_ARGS__); \
if (RC != LSM_RET_DEFAULT(FUNC)) \
break; \
} \
} while (0); \
RC; \
})
这两个宏还是比较清晰的,主要是完成对应钩子函数的列表注册的所有函数的调用;注意call_int_hook
最后的RC,这里用到一个C的语言特性,表达式,这样让整个宏的结构变成代码块的结构,实际上是RC表达式的值,相当于做了return RC的操作,但是可以不用单独定义函数,开销也比较小。
其中security_hook_list
定义如下:
union security_list_options {
...
int (*file_open)(struct file *file);
...
};
struct security_hook_heads {
...
struct hlist_head file_open;
...
};
...
/*
* Security module hook list structure.
* For use with generic list macros for common operations.
*/
struct security_hook_list {
struct hlist_node list;
struct hlist_head *head;
union security_list_options hook;
const struct lsm_id *lsmid;
} __randomize_layout;
这个结构体是用来为记录每个回调函数的钩子函数的列表的, 在内核安全模块初始化时候用于注册对应的安全钩子函数用。
这些安全钩子在内核中的呈现形式如下:
)
安全钩子函数的注册
既然钩子函数是一个链表的形式,那么这个链表在哪里初始化和插入对应的元素呢?又在哪里能移除对应的元素呢?
内核的安全挂钩通过security_add_hooks
增加钩子函数的内容,通过security_delete_hooks
移除钩子函数链表的内容。
以知名内核LSM模块selinux为例:
static struct security_hook_list selinux_hooks[] __lsm_ro_after_init = {
...
LSM_HOOK_INIT(file_open, selinux_file_open),
...
};
...
static __init int selinux_init(void)
{
...
security_add_hooks(selinux_hooks, ARRAY_SIZE(selinux_hooks), "selinux");
...
}
...
security_initcall(selinux_init);
上述代码片段是selinux在LSM侧初始化它的钩子函数的的主要逻辑:
在selinux的初始化函数中通过security_add_hooks
函数注册/初始化了相关的钩子函数;
注意这里的__lsm_ro_after_init
标记,这个标记代表这个结构体本身在内核初始化之后就会被设置成只读了.
之所以这样设计,其实是为了在内核运行时,这里的安全钩子函数不会被其他人恶意修改。
它的定义如下:
/* Currently required to handle SELinux runtime hook disable. */
#ifdef CONFIG_SECURITY_WRITABLE_HOOKS
#define __lsm_ro_after_init
#else
#define __lsm_ro_after_init __ro_after_init
#endif /* CONFIG_SECURITY_WRITABLE_HOOKS */
可以看到和内核选项SECURITY_WRITABLE_HOOKS
有关,也可能不是ro的,可以被修改。
/*
* Initializing a security_hook_list structure takes
* up a lot of space in a source file. This macro takes
* care of the common case and reduces the amount of
* text involved.
*/
#define LSM_HOOK_INIT(HEAD, HOOK) \
{ .head = &security_hook_heads.HEAD, .hook = { .HEAD = HOOK } }
本质上,这个操作是把对应的函数放到了LSM对应的钩子函数的链表中, 这样,每次调用security_file_open
这样的LSM回调函数的时候都会来调用注册了的安全模块函数做处理。
* security_add_hooks - Add a modules hooks to the hook lists.
* @hooks: the hooks to add
* @count: the number of hooks to add
* @lsm: the name of the security module
*
* Each LSM has to register its hooks with the infrastructure.
*/
void __init security_add_hooks(struct security_hook_list *hooks, int count,
char *lsm)
{
int i;
for (i = 0; i < count; i++) {
hooks[i].lsm = lsm;
hlist_add_tail_rcu(&hooks[i].list, hooks[i].head);
}
if (lsm_append(lsm, &lsm_names) < 0)
panic("%s - Cannot get early memory.\n", __func__);
}
同样以selinux为例,它的代码中有如下代码片段:
#ifdef CONFIG_SECURITY_SELINUX_DISABLE
int selinux_disable(struct selinux_state *state)
{
...
security_delete_hooks(selinux_hooks, ARRAY_SIZE(selinux_hooks));
...
}
#endif
这个函数会在selinux的一些文件接口的代码路径中调用,它的主要作用是关闭selinux注册的所有回调函数:
static inline void security_delete_hooks(struct security_hook_list *hooks,
int count)
{
int i;
for (i = 0; i < count; i++)
hlist_del_rcu(&hooks[i].list);
}
可以看到是很简单的删除链表内容的操作。
这个security_delete_hooks
函数其实最好不要在安全模块中使用,之所以这里有,只是因为历史原因,这个接口已经向用户提供了。在高版本的linux内核中,为了安全起见,已经把这个接口去除,并保证selinux_hooks
注册的接口在启动之后是只读的了。
安全子系统的初始化
在4.19.20之前,安全子系统通过security initcall初始化;在4.19.20之后则通过DEFINE_LSM宏进行处理,引入了结构体lsm_info
来处理.
这样做的一个好处是能够在LSM的框架中优雅地处理各个安全模块的初始化,例如通过orderd方式初始化,可以处理安全模块的初始化先后顺序,而不是硬编码他们的顺序。
同时内核中也可以统一初始化安全模块的函数.
内核的所有子系统,在内核启动的时候都需要按照某种形式被初始化,一种最直接的初始化方式是提供初始化函数,让内核在start_kernel
函数调用。
/**
* security_init - initializes the security framework
*
* This should be called early in the kernel initialization sequence.
*/
int __init security_init(void)
{
int i;
struct hlist_head *list = (struct hlist_head *) &security_hook_heads;
for (i = 0; i < sizeof(security_hook_heads) / sizeof(struct hlist_head);
i++)
INIT_HLIST_HEAD(&list[i]);
pr_info("Security Framework initialized\n");
/*
* Load minor LSMs, with the capability module always first.
*/
capability_add_hooks();
yama_add_hooks();
loadpin_add_hooks();
/*
* Load all the remaining security modules.
*/
do_security_initcalls();
return 0;
}
这个函数初始化了所有的安全挂钩函数的列表,可以看到capability_add_hooks
的顺序被强制安排在最开始的时候。
这个函数还调用了
static void __init do_security_initcalls(void)
{
int ret;
initcall_t call;
initcall_entry_t *ce;
ce = __security_initcall_start;
trace_initcall_level("security");
while (ce < __security_initcall_end) {
call = initcall_from_entry(ce);
trace_initcall_start(call);
ret = call();
trace_initcall_finish(call, ret);
ce++;
}
}
这个函数主要是负责各个安全子模块的初始化函数的调用的,例如selinux的初始化函数就是这样调用的:
static __init int selinux_init(void)
{
...
}
...
security_initcall(selinux_init);
这里通过security_initcall定义了
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ___define_initcall(fn, id, __sec) \
__ADDRESSABLE(fn) \
asm(".section \"" #__sec ".init\", \"a\" \n" \
"__initcall_" #fn #id ": \n" \
".long " #fn " - . \n" \
".previous \n");
#else
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif
...
#define security_initcall(fn) ___define_initcall(fn,, .security_initcall)
也就是说,通过这样定义的函数将会放入.security_initcall
这个section中。
程序段的定义是通过链接脚本控制的:
arch/x86/kernel/vmlinux.lds.S
SECTIONS
{
...
INIT_DATA_SECTION(16)
...
}
这个宏定义在include/asm-generic/vmlinux.lds.h
中:
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
INIT_DATA \
INIT_SETUP(initsetup_align) \
INIT_CALLS \
CON_INITCALL \
SECURITY_INITCALL \
INIT_RAM_FS \
}
...
#define SECURITY_INITCALL \
__security_initcall_start = .; \
KEEP(*(.security_initcall.init)) \
__security_initcall_end = .;
这就解释了为什么代码中通过__security_initcall_start
和__security_initcall_end
来调用对应的函数。