linux能力和seccomp之间的区别
我想知道 linux 功能和 seccomp 之间的确切区别。
我将在下面解释确切的区别,但这里是一般解释: 功能涉及对系统调用可访问的内核函数的各种检查。如果检查失败(即进程缺乏必要的能力),系统调用通常会返回错误。检查可以在特定系统调用的开始处进行,也可以在内核中更深的区域进行,这些区域可能通过多个不同的系统调用(例如写入特定的特权文件)可以到达。
Seccomp 是一个系统调用过滤器,在运行之前应用于所有系统调用。一个进程可以设置一个过滤器,允许他们撤销他们运行某些系统调用的权利,或者某些系统调用的特定参数。过滤器通常采用eBPF字节码的形式,内核使用它来检查该进程是否允许系统调用。一旦应用了过滤器,它就不能放松,只会变得更严格(假设仍然允许负责加载 seccomp 策略的系统调用)。
请注意,某些系统调用不受 seccomp 或功能的限制,因为它们不是真正的系统调用。vDSO调用就是这种情况,它是几个不需要内核的系统调用的用户空间实现。由于这个原因,试图阻止getcpu()
或gettimeofday()
徒劳无功,因为无论如何进程将使用 vDSO 而不是本机系统调用。值得庆幸的是,这些系统调用(及其相关的虚拟实现)在很大程度上是无害的。
也是linux功能在内部使用seccomp还是相反,或者它们都是完全不同的。
它们在内部实现完全不同。我在其他地方写了另一个关于各种沙盒技术当前实现的答案,解释了它们的不同之处以及它们的用途。
能力
许多执行特权操作的系统调用可能包括内部检查以确保调用进程具有足够的能力。内核存储了一个进程拥有的能力列表,一旦一个进程放弃了它的所有能力,它就无法恢复它们。例如,除非调用系统调用的进程具有能力,否则尝试打开/dev/cpu/*/msr
写入将失败,即使字符设备的权限允许写入访问。此检查与用于打开它的系统调用无关(例如,无法绕过此限制)。这可以在负责修改 MSR(低级 CPU 特性)的内核源代码中看到:open()
CAP_SYS_RAWIO
openat()
static int msr_open(struct inode *inode, struct file *file)
{
unsigned int cpu = iminor(file_inode(file));
struct cpuinfo_x86 *c;
if (!capable(CAP_SYS_RAWIO))
return -EPERM;
if (cpu >= nr_cpu_ids || !cpu_online(cpu))
return -ENXIO; /* No such CPU */
c = &cpu_data(cpu);
if (!cpu_has(c, X86_FEATURE_MSR))
return -EIO; /* MSR not supported */
return 0;
}
如果不存在正确的功能,则某些系统调用根本不会运行,例如vhangup()
:
SYSCALL_DEFINE0(vhangup)
{
if (capable(CAP_SYS_TTY_CONFIG)) {
tty_vhangup_self();
return 0;
}
return -EPERM;
}
功能可以被认为是可以有选择地从进程或用户中删除的广泛类别的特权功能。具有能力检查的特定函数因内核版本而异,内核开发人员之间经常就给定函数是否需要能力才能运行而争吵。通常,减少进程的功能可以通过减少它可以执行的特权操作的数量来提高安全性。请注意,某些功能被视为与 root 等效,这意味着即使您禁用所有其他功能,在某些情况下,它们也可以用于重新获得完全权限。grsecurity 的创建者 Brad Spengler给出了许多示例。一个明显的例子是CAP_SYS_MODULE
允许加载任意内核模块。另一个CAP_SYS_ADMIN
是几乎等同于 root 的包罗万象的能力,并允许采取许多管理操作。
模式 1 秒补偿
seccomp 有两种类型:模式 1(严格)和模式 2(过滤器)。模式 1 非常严格,一旦启用,只允许四个系统调用。这些系统调用是read()
、write()
、exit()
和rt_sigreturn()
。SIGKILL
如果进程尝试使用不在白名单上的系统调用,它会立即从内核发送致命信号。该模式是原始的 seccomp 模式,不需要生成 eBPF 字节码并将其发送到内核。进行了一个特殊的系统调用,之后模式 1 将在进程的生命周期内处于活动状态:seccomp(SECCOMP_SET_MODE_STRICT)
或prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)
. 一旦激活,就无法关闭。
以下是安全执行返回 42 的字节码的示例程序:
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/seccomp.h>
/* "mov al,42; ret" aka "return 42" */
static const unsigned char code[] = "\xb0\x2a\xc3";
void main(void)
{
int fd[2], ret;
/* spawn child process, connected by a pipe */
pipe(fd);
if (fork() == 0) {
close(fd[0]);
/* enter mode 1 seccomp and execute untrusted bytecode */
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
ret = (*(uint8_t(*)())code)();
/* send result over pipe, and exit */
write(fd[1], &ret, sizeof(ret));
syscall(SYS_exit, 0);
} else {
close(fd[1]);
/* read the result from the pipe, and print it */
read(fd[0], &ret, sizeof(ret));
printf("untrusted bytecode returned %d\n", ret);
}
}
模式 1 是原始模式,添加的目的是为了能够执行不受信任的字节码进行原始计算。代理进程将派生一个子进程(并可能通过管道建立通信),并且子进程将启用 seccomp,阻止它执行任何操作,除了读取和写入已经打开的文件描述符并退出。然后这个子进程可以安全地执行不受信任的字节码。使用这种模式的人并不多,但在 Linus大声抱怨到杀死它之前,谷歌 Chrome 团队表示希望将它用于他们的浏览器。这引起了对 seccomp 的新兴趣,并使其免于过早死亡。
模式 2 秒补偿
第二种模式,过滤器,也称为 seccomp-bpf,允许进程向内核发送细粒度的过滤器策略,允许或拒绝整个系统调用,或特定的系统调用参数或参数范围。该策略还指定了发生违规时会发生什么(例如,是否应该终止进程,或者是否应该仅仅拒绝系统调用?)以及是否应记录违规。由于 Linux 系统调用保存在寄存器中,因此只能是整数,因此无法过滤系统调用参数可能指向的内存内容。例如,尽管您可以防止open()
使用可写O_RDWR
或O_WRONLY
标志,您不能将打开的单个路径列入白名单。这样做的原因是,对于 seccomp,路径只不过是指向包含以 null 结尾的文件系统路径的内存的指针。没有办法保证保存路径的内存没有被 seccomp 检查传递和被取消引用的指针之间的兄弟线程更改,除非将其放入只读内存并拒绝与内存相关的系统调用访问它。经常需要使用像 AppArmor 这样的 LSM。
这是一个使用模式 2 seccomp 以确保它只能打印其当前 PID 的示例程序。这个程序使用了 libseccomp 库,这使得创建 seccomp eBPF 过滤器变得更容易,尽管也可以在没有任何抽象库的情况下以艰难的方式完成它。
#include <seccomp.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
void main(void)
{
/* initialize the libseccomp context */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
/* allow exiting */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
/* allow getting the current pid */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);
/* allow changing data segment size, as required by glibc */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
/* allow writing up to 512 bytes to fd 1 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 2,
SCMP_A0(SCMP_CMP_EQ, 1),
SCMP_A2(SCMP_CMP_LE, 512));
/* if writing to any other fd, return -EBADF */
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EBADF), SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_NE, 1));
/* load and enforce the filters */
seccomp_load(ctx);
seccomp_release(ctx);
printf("this process is %d\n", getpid());
}
创建模式 2 seccomp 是因为模式 1 显然有其局限性。并非每个任务都可以分离成一个纯字节码进程,该进程可以在子进程中运行并通过管道或共享内存进行通信。这种模式的功能要多得多,而且它的功能还在继续慢慢扩展。但是,它仍然有其缺点。安全地使用模式 2 seccomp 需要对系统调用有深入的了解(想要阻止kill()
杀死其他进程?糟糕的是,你也可以杀死进程fcntl()
!)。它也很脆弱,因为对底层 libc 的更改可能会导致损坏。例如, glibcopen()
函数不再总是使用该名称的系统调用,而是可能使用openat()
,破坏仅将前者列入白名单的策略。
Seccomp 基本上是 linux 上系统调用的防火墙作为一般概念,您可以将它们视为一组系统调用的功能。请记住,seccomp 很难配置,因为您需要深入了解应用程序的系统调用,有时这很难,但是如果您想限制系统调用以创建套接字(基本上是 sys_socket)并检查参数以查看是否是原始套接字您可以在 seccomp 以及功能上执行此操作。