英语

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,则导致在用户空间通知 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 程序可以使用此机制来逃脱。)

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 对应于特定的过滤器,而不是特定的任务。因此,如果此任务然后 fork,则来自两个任务的通知将出现在同一个过滤器 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 命名空间不可见的 pid ns 中,则可能为 0)。该通知还包含传递给 seccomp 的 data 和过滤器标志。在调用 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:在做出任何策略决策之前,应将从被跟踪者内存中读取的所有参数都读取到跟踪程序的内存中。这允许对系统调用参数做出原子决策。

Sysctls

Seccomp 的 sysctl 文件可以在 /proc/sys/kernel/seccomp/ 目录中找到。以下是该目录中每个文件的说明

actions_avail:

一个只读的 seccomp 返回值(请参阅上面的 SECCOMP_RET_* 宏)的字符串形式的有序列表。从左到右的顺序是从限制性最小的返回值到限制性最大的返回值。

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

actions_logged:

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

allow 字符串不被 actions_logged sysctl 接受,因为它不可能记录 SECCOMP_RET_ALLOW 操作。尝试将 allow 写入 sysctl 将导致返回 EINVAL。

添加架构支持

有关权威要求,请参阅 arch/Kconfig。一般来说,如果一个架构同时支持 ptrace_event 和 seccomp,它将能够支持 seccomp 过滤器,只需进行少量修复: SIGSYS 支持和 seccomp 返回值检查。然后,它必须仅将其特定于架构的 Kconfig 添加 CONFIG_HAVE_ARCH_SECCOMP_FILTER

注意事项

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

在 x86-64 上,默认情况下启用 vsyscall 模拟。(vsyscall 是 vDSO 调用的旧版本。)目前,模拟的 vsyscall 将遵守 seccomp,但有一些奇怪之处

  • 返回 SECCOMP_RET_TRAP 值将设置一个指向给定调用的 vsyscall 条目的 si_call_addr,而不是指向 ‘syscall’ 指令之后的地址。任何想要重新启动调用的代码都应注意 (a) 已经模拟了 ret 指令,并且 (b) 尝试恢复系统调用将再次触发标准的 vsyscall 模拟安全检查,这使得恢复系统调用几乎毫无意义。

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

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