- 快速SECCOMP入门
- 为什么不能只使用LSM?
- 为什么不能只使用seccomp?
- 结论
假设你已经了解了LSM
内核安全模块,也知道如何使用它们加固系统的安全。但是,你还知道了另一种工具seccomp
(Linux
安全计算)。你可能非常想知道,LSM
和Seccomp
有什么区别?为什么不能将Seccomp
设计为LSM
模块?什么时候使用Seccomp
?接下来,且听我娓娓道来。
Secomp
和LSM
都可以让内核限制进程与系统的交互,但却有大大的不同。也就是说,Seccomp
限制进程发起的系统调用,而LSM
控制对内核对象的访问。
快速SECCOMP入门
Andrea Arcangeli
在2004年首次提案seccomp
,限制用户进程进入内核模式的系统调用,只允许read
、write
、exit
和sigreturn
。Andrea
将其用于CPUShare
项目,其允许人们出租自己未使用的CPU资源,但这需要限制不可信代码的行为。出售者肯定不想购买者能够轻易的通过fork bomb
发起拒绝服务攻击(DoS
)。起初,Andrea
使用一个运行时受限的解释器运行不可信代码,但是为了效率和灵活性,他专门设计了CPUShare
用来直接运行二进制代码。CPUShare
需要限制这种不可信的二进制代码,以便只能读取输入,运算,然后输出结果。除此之外,不允许任何操作。有了Seccomp
,管理进程能够加载不可信的共享对象文件,打开文件进行输入和输出,并进入seccomp
模式,然后,才调用不可信共享对象的入口点,运行二进制代码。之后,受限的进程不能打开任何新文件,更改目录,创建新进程,生成线程,执行新程序。
seccomp
的这种strict
模式带来了很强的隔离性,但是限制太多而无法更普遍的使用。那么就需要一种方法为进程提供策略给内核决定允许那些系统调用。2012年,内核开发者合并了seccomp filter mode
,从而允许进程决定它们自己的系统调用过滤策略。
Seccomp filter mode
允许进程通过prctl
系统调用安装BPF
字节码。一旦安装,此BPF
程序阻止调用进程,或任何子进程发起的系统调用。
让我们来看一下Linux源码中seccomp
测试代码片段,如下所示。该段代码展示了BPF
过滤器会杀死发起getpid
系统调用的进程:
struct sock_filter filter[] = { BPF_STMT(BPF_LD|BPF_W|BPF_ABS, offsetof(struct seccomp_data, nr)), BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_getpid, 0, 1), BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_KILL), BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW), }; struct sock_fprog prog = { .len = (unsigned short)ARRAY_SIZE(filter), .filter = filter, }; ... ret = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
上面的代码创建了一个seccomp过滤器。一旦将该过滤器添加到某个任务中,在对任务进程进行追踪之后,但是在通过系统调用表分配之前,会先运行该过滤器,从而限制某些系统调用。值得注意的是seccomp_data数据结构,如下所示:
/** * struct seccomp_data - BPF程序执行的格式 * @nr: 系统调用号 * @arch: 系统调用的约定,跟架构相关,相关定义位于 * <linux/audit.h>文件中,以AUDIT_ARCH_*为前缀 * @instruction_pointer:指令指针 * @args: 6个系统调用参数,64位值,不管是什么架构 */ struct seccomp_data { int nr; __u32 arch; __u64 instruction_pointer; __u64 args[6]; };
目前,BPF
没有提供解引用指针,或检查用户空间传递进来的数据结构。只有直接拷贝到seccomp_data
结构中的参数才可用。因此,BPF
过滤器不能通过用户空间传递的字符串确定系统调用。
为什么不能只使用LSM
?
LSM
和seccomp
都是增加系统安全的工具。LSM
实现的是强制访问控制(MAC
),保护的内核对象是:文件
,inode
,task_struct
,IPC数据结构
。LSM
会将安全属性插入到这些对象中,根据先前加载的策略进行检查。具有特权的安全管理员(通常是root
)负责加载和管理这些策略。非特权用户不能改变这个策略。
相反,seccomp
允许非特权进程限制自身。与非特权进程放弃自身能力(capability
)一样,seccomp
允许非特权进程放弃某些系统调用的能力。比如说,cat
命令,可以open
和read
命令行中指定的文件,将它们的内容输出到标准输出中。除了stdout(2)
和stderr(3)
之外,它不需要写任何文件描述符。因此,cat
开发者可以使用seccomp
过滤器,当cat
想要写任何除了2
和3
之外的文件描述符时,就会杀死它。
但是,通过LSM
实现相同的功能就会非常复杂,因为进程的标准输出可能会重定向到不同内核对象(设备
,文件
,管道
),而LSM
本身又没有提供将这些对象映射到文件描述符的方法。另外,一个问题就是存在多个LSM
实现:SELinux
, AppArmor
, Tomoyo
, SMACK
,它们都有自己的管理策略的机制,这些机制要么要为每一个LSM
模块生成策略,要么限制用户只能使用一种。
而且,还可以编写自己的LSM
模块,当然,还需要将其编译进内核,并设计一种方法表示你这种特殊的策略,这既耗时又脆弱。在这一点上,seccomp
是有优势的,如果内核配置正确,不管系统如何配置其LSM
模块,都可以使用它。从二进制的角度来说,
最后,因为内核会在系统调用之前检查seccomp
过滤器,也就是减少了攻击者的攻击面。LSM
一般hook
在系统调用底层的内核对象上,不会像seccomp
那样减少攻击面。具体的来说,内核在将系统调用的参数映射到内核对象之后运行这些hook
,本身这段代码可能就包含缺陷。重申一遍:seccomp
减少攻击面。
为什么不能只使用seccomp
?
seccomp
无法对LSM
采用的相同策略进行建模。例如,你不能编写seccomp
过滤器阻止用户进程只打开文件系统中某个位置的文件,比如,/etc/password
。因为seccomp
过滤器不能解引用指针,所以它就不能比较用户传递给open
系统调用的路径参数(像AppArmor
那样),也不能检查通过文件安全属性检查inode
节点(像SELinux
那样)。
另外,也没有机制移除或修改已经附加到进程的过滤器。你只能给进程添加新的过滤器,而且新过滤器的作用也不能与已经存在的过滤器相反。因此,想要从全局给系统创建一组过滤器是非常困难的。最接近的方法是使用systemd
的系统调用过滤器
或者在容器中使用seccomp
过滤器(比如docker
)
即使使用systemd
的系统调用过滤器和docker seccomp
策略,seccomp
过滤器的管理最好还是由应用程序开发者实现,而不是系统管理员。因为应用开发者比系统管理员更清楚他们的应用程序需要哪些系统调用。另外,在应用程序改变时,开发者更清楚在哪里更新这些过滤器。
结论
LSM
和seccomp
都提供了限制进程与系统交互的机制。但它们不是不直接保护你的程序免受攻击的工具,而是阻止攻击者利用某个程序的漏洞进而攻击系统其它部分的工具。LSM
实现的MAC
强制访问控制策略是你实现系统全局细粒度安全控制策略的工具,而seccomp
过滤器是限制非特权进程进行某些系统调用的工具,同时还是常见的进程沙箱技术(如linux container
)的重要组件。