英语

Seccomp BPF(带过滤器的安全计算)

介绍

每个用户空间进程都暴露了大量的系统调用,其中许多在进程的整个生命周期中都未使用。随着系统调用的变化和成熟,会发现并消除错误。通过减少可用系统调用的集合,某些用户空间应用程序会受益。由此产生的集合减少了暴露给应用程序的内核总表面。系统调用过滤旨在用于这些应用程序。

Seccomp过滤提供了一种进程指定传入系统调用过滤器的手段。该过滤器表示为伯克利数据包过滤器(BPF)程序,与套接字过滤器一样,不同之处在于操作的数据与正在进行的系统调用有关:系统调用号和系统调用参数。这允许使用具有悠久的用户空间暴露历史以及直接数据集的过滤器程序语言对系统调用进行富有表现力的过滤。

此外,BPF使seccomp的用户不可能成为时间检查-时间使用(TOCTOU)攻击的牺牲品,这些攻击在系统调用介入框架中很常见。BPF程序可能不会取消引用指针,这会将所有过滤器限制为仅直接评估系统调用参数。

它不是什么

系统调用过滤不是沙箱。它提供了一个明确定义的机制来最小化暴露的内核表面。它旨在成为沙箱开发人员使用的工具。除此之外,逻辑行为和信息流的策略应与其他系统强化技术以及您选择的LSM的组合来管理。富有表现力的动态过滤器提供了沿着这条道路的更多选择(避免病态大小或选择允许socketcall()中的哪个多路复用系统调用,例如),这可能会被错误地解释为更完整的沙箱解决方案。

用法

添加了一个额外的seccomp模式,并使用与严格seccomp相同的prctl(2)调用启用。如果该架构具有CONFIG_HAVE_ARCH_SECCOMP_FILTER,则可以如下添加过滤器

PR_SET_SECCOMP:

现在需要一个额外的参数,该参数使用BPF程序指定一个新的过滤器。BPF程序将在struct seccomp_data上执行,该数据反映了系统调用号,参数和其他元数据。然后,BPF程序必须返回可接受的值之一,以通知内核应采取的措施。

用法

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, prog);

“prog”参数是指向struct sock_fprog的指针,其中将包含过滤器程序。如果程序无效,则调用将返回-1并将errno设置为EINVAL

如果@prog允许fork/cloneexecve,则任何子进程都将受到与父进程相同的过滤器和系统调用ABI的约束。

在使用之前,任务必须调用prctl(PR_SET_NO_NEW_PRIVS, 1)或在其命名空间中以CAP_SYS_ADMIN权限运行。如果这些不正确,将返回-EACCES。此要求确保过滤器程序不能应用于具有比安装它们的任务更高的权限的子进程。

此外,如果附加的过滤器允许prctl(2),则可以分层附加过滤器,这将增加评估时间,但允许在进程执行期间进一步减少攻击面。

以上调用在成功时返回0,在错误时返回非零值。

返回值

seccomp过滤器可能会返回以下任何值。如果存在多个过滤器,则给定系统调用评估的返回值将始终使用最高优先级值。(例如,SECCOMP_RET_KILL_PROCESS将始终优先。)

按优先级顺序,它们是

SECCOMP_RET_KILL_PROCESS:

导致整个进程立即退出而不执行系统调用。任务的退出状态(status & 0x7f)将为SIGSYS,而不是SIGKILL

SECCOMP_RET_KILL_THREAD:

导致任务立即退出而不执行系统调用。任务的退出状态(status & 0x7f)将为SIGSYS,而不是SIGKILL

SECCOMP_RET_TRAP:

导致内核向触发任务发送SIGSYS信号而不执行系统调用。siginfo->si_call_addr将显示系统调用指令的地址,并且siginfo->si_syscallsiginfo->si_arch将指示尝试了哪个系统调用。程序计数器将好像发生了系统调用一样(即,它不会指向系统调用指令)。返回值寄存器将包含一个与架构相关的的值 - 如果恢复执行,请将其设置为合理的值。(架构依赖性是因为用-ENOSYS替换它可能会覆盖一些有用的信息。)

返回值中的SECCOMP_RET_DATA部分将作为si_errno传递。

由seccomp触发的SIGSYS的si_code为SYS_SECCOMP

SECCOMP_RET_ERRNO:

导致返回值中的较低16位作为errno传递给用户空间而不执行系统调用。

SECCOMP_RET_USER_NOTIF:

如果在用户空间通知fd上发送struct seccomp_notif消息,如果没有,则返回-ENOSYS。有关如何处理用户通知的讨论,请参见下文。

SECCOMP_RET_TRACE:

返回时,此值将导致内核尝试在执行系统调用之前通知基于ptrace()的跟踪器。如果没有跟踪器存在,则将-ENOSYS返回到用户空间,并且不执行系统调用。

如果跟踪器使用ptrace(PTRACE_SETOPTIONS)请求PTRACE_O_TRACESECCOMP,则将通知跟踪器。将通知跟踪器PTRACE_EVENT_SECCOMP,并且BPF程序返回值的SECCOMP_RET_DATA部分将通过PTRACE_GETEVENTMSG提供给跟踪器。

跟踪器可以通过将系统调用号更改为-1来跳过系统调用。或者,跟踪器可以通过将系统调用更改为有效的系统调用号来更改请求的系统调用。如果跟踪器要求跳过系统调用,则系统调用将显示为返回跟踪器放入返回值寄存器中的值。

通知跟踪器后,seccomp检查将不会再次运行。(这意味着基于seccomp的沙箱绝不能允许使用ptrace,即使是对其他沙箱进程的ptrace,也要格外小心;ptracer可以使用此机制来逃脱。)

SECCOMP_RET_LOG:

导致系统调用在记录后执行。应用程序开发人员应使用它来了解其应用程序需要的哪些系统调用,而无需迭代多个测试和开发周期来构建列表。

只有当“log”出现在actions_logged sysctl字符串中时,才会记录此操作。

SECCOMP_RET_ALLOW:

导致系统调用被执行。

如果存在多个过滤器,则给定系统调用评估的返回值将始终使用最高优先级值。

仅使用SECCOMP_RET_ACTION掩码确定优先级。当多个过滤器返回值的优先级相同时,仅返回最近安装的过滤器的SECCOMP_RET_DATA

陷阱

使用过程中要避免的最大陷阱是在不检查体系结构值的情况下过滤系统调用号。为什么?在支持多种系统调用调用约定的任何体系结构上,系统调用号可能会因特定调用而异。如果不同调用约定中的数字重叠,则过滤器中的检查可能会被滥用。始终检查arch值!

示例

samples/seccomp/目录包含一个特定于x86的示例以及一个用于BPF程序生成的高级宏接口的更通用的示例。

用户空间通知

SECCOMP_RET_USER_NOTIF返回码使seccomp过滤器可以将特定的系统调用传递给用户空间进行处理。这对于容器管理器之类的应用程序可能很有用,这些应用程序希望拦截特定的系统调用(mount()finit_module()等)并更改其行为。

要获取通知FD,请使用SECCOMP_FILTER_FLAG_NEW_LISTENER参数调用seccomp()系统调用

fd = seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);

(成功时)将返回过滤器的侦听器fd,然后可以通过SCM_RIGHTS或类似的方式传递。请注意,过滤器fd对应于特定的过滤器,而不是特定的任务。因此,如果此任务随后派生,则来自两个任务的通知将出现在同一过滤器fd上。对过滤器fd的读写也是同步的,因此过滤器fd可以安全地拥有许多读取器。

seccomp通知fd的接口由两个结构组成

struct seccomp_notif_sizes {
    __u16 seccomp_notif;
    __u16 seccomp_notif_resp;
    __u16 seccomp_data;
};

struct seccomp_notif {
    __u64 id;
    __u32 pid;
    __u32 flags;
    struct seccomp_data data;
};

struct seccomp_notif_resp {
    __u64 id;
    __s64 val;
    __s32 error;
    __u32 flags;
};

struct seccomp_notif_sizes结构可用于确定seccomp通知中使用的各种结构的大小。 struct seccomp_data的大小将来可能会更改,因此代码应使用

struct seccomp_notif_sizes sizes;
seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes);

确定要分配的各种结构的大小。有关示例,请参见samples/seccomp/user-trap.c。

用户可以通过在seccomp通知fd上使用ioctl(SECCOMP_IOCTL_NOTIF_RECV)(或poll())读取以接收struct seccomp_notif,其中包含五个成员:结构的输入长度,每个过滤器唯一的id,触发此请求的任务的pid(如果任务在侦听器的pid命名空间中不可见,则可能为0)。该通知还包含传递给seccomp的data和filters标志。在调用ioctl之前,应将结构清零。

然后,用户空间可以根据此信息做出关于如何操作的决定,并ioctl(SECCOMP_IOCTL_NOTIF_SEND)一个响应,指示应返回给用户空间的内容。struct seccomp_notif_respid成员应与struct seccomp_notif中的id相同。

用户空间还可以通过ioctl(SECCOMP_IOCTL_NOTIF_ADDFD)将文件描述符添加到通知进程。struct seccomp_notif_addfdid成员应与struct seccomp_notif中的id相同。 newfd_flags标志可用于在通知进程中设置O_CLOEXEC之类的标志。如果主管想要注入具有特定编号的文件描述符,则可以使用SECCOMP_ADDFD_FLAG_SETFD标志,并将newfd成员设置为要使用的特定编号。如果该文件描述符已在通知进程中打开,则将其替换。主管还可以添加FD,并通过使用SECCOMP_ADDFD_FLAG_SEND标志自动响应,并且返回值将是注入的文件描述符编号。

可以抢占通知进程,从而导致通知中止。当试图代表通知进程采取长期运行且通常可重试的操作(安装文件系统)时,这可能会出现问题。或者,在过滤器安装时,可以设置SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV标志。此标志使得当主管收到用户通知时,通知进程将忽略非致命信号,直到发送响应为止。在用户空间收到通知之前发送的信号将正常处理。

值得注意的是,struct seccomp_data包含系统调用的寄存器参数的值,但不包含指向内存的指针。合适特权的跟踪器可以通过ptrace()/proc/pid/mem访问任务的内存。但是,应注意避免本文档中提到的TOCTOU:从tracee的内存中读取的所有参数都应在做出任何策略决定之前读入tracer的内存中。这允许对系统调用参数进行原子决策。

Sysctls

可以在/proc/sys/kernel/seccomp/目录中找到Seccomp的sysctl文件。这是该目录中每个文件的描述

actions_avail:

一个只读的seccomp返回值有序列表(请参阅上面的SECCOMP_RET_*宏),以字符串形式表示。从左到右的顺序是从最不严格的返回值到最严格的返回值。

该列表表示内核支持的seccomp返回值集合。用户空间程序可以使用此列表来确定程序构建时在seccomp.h中找到的操作是否与当前运行内核中实际支持的操作集合不同。

actions_logged:

允许记录的seccomp返回值(请参阅上面的SECCOMP_RET_*宏)的读写有序列表。写入文件不需要有序形式,但是从文件读取将以与actions_avail sysctl相同的方式排序。

由于无法记录SECCOMP_RET_ALLOW操作,因此actions_logged sysctl不接受allow字符串。尝试将allow写入sysctl将导致返回EINVAL。

添加体系结构支持

有关权威要求,请参见arch/Kconfig。通常,如果一个体系结构同时支持ptrace_event和seccomp,它将能够通过一些小的修复来支持seccomp过滤器:SIGSYS支持和seccomp返回值检查。然后,它必须仅将CONFIG_HAVE_ARCH_SECCOMP_FILTER添加到其特定于架构的Kconfig中。

注意事项

vDSO可能导致某些系统调用完全在用户空间中运行,从而导致在不同的机器上运行程序时会感到惊讶,这些程序会回退到实际的系统调用。为了最大程度地减少x86上的这些意外情况,请确保您使用设置为类似acpi_pm/sys/devices/system/clocksource/clocksource0/current_clocksource进行测试。

在x86-64上,默认情况下启用vsyscall仿真。(vsyscalls是vDSO调用的旧式变体。)当前,仿真的vsyscall将遵守seccomp,但有一些奇怪之处

  • SECCOMP_RET_TRAP的返回值将设置一个si_call_addr,该地址指向给定调用的vsyscall条目,而不是'syscall'指令之后的地址。任何想要重新启动调用的代码都应意识到(a)ret指令已被仿真,并且(b)尝试恢复syscall将再次触发标准的vsyscall仿真安全检查,从而使恢复syscall几乎毫无意义。

  • SECCOMP_RET_TRACE的返回值将像往常一样向跟踪器发出信号,但是不能使用orig_rax寄存器将syscall更改为另一个系统调用。它只能更改为-1,以便跳过当前仿真的调用。任何其他更改都可能终止该进程。跟踪器看到的rip值将是syscall入口地址;这与正常行为不同。跟踪器绝不能修改rip或rsp。(不要依赖于其他更改来终止该进程。它们可能会起作用。例如,在某些内核上,选择仅在将来内核中存在的syscall将被正确仿真(通过返回-ENOSYS)。

要检测到这种古怪的行为,请检查addr & ~0x0C00 == 0xFFFFFFFFFF600000。(对于SECCOMP_RET_TRACE,请使用rip。对于SECCOMP_RET_TRAP,请使用siginfo->si_call_addr。)不要检查任何其他条件:将来的内核可能会改善vsyscall仿真,并且当前处于vsyscall=native模式的内核的行为会有所不同,但是在这些情况下,0xF...F600{0,4,8,C}00上的指令将不是系统调用。

请注意,现代系统不太可能根本使用vsyscalls - 它们是一项旧式功能,并且它们比标准syscall慢得多。新代码将使用vDSO,并且vDSO发出的系统调用与普通系统调用无法区分。