#seccomp #sandbox #jail #linux-kernel

seccompiler

提供易于使用的 seccomp-bpf 禁锢

4 个版本 (破坏性更新)

0.4.0 2023 年 10 月 18 日
0.3.0 2022 年 9 月 14 日
0.2.0 2021 年 10 月 1 日
0.1.0 2021 年 6 月 16 日

#5 in Unix API

Download history 37819/week @ 2024-04-29 38538/week @ 2024-05-06 42200/week @ 2024-05-13 41342/week @ 2024-05-20 54770/week @ 2024-05-27 107910/week @ 2024-06-03 100097/week @ 2024-06-10 107743/week @ 2024-06-17 141252/week @ 2024-06-24 118124/week @ 2024-07-01 104295/week @ 2024-07-08 107194/week @ 2024-07-15 110494/week @ 2024-07-22 112471/week @ 2024-07-29 99212/week @ 2024-08-05 100181/week @ 2024-08-12

每月 425,937 次下载
17 个 crate 中使用(8 个直接使用)

Apache-2.0 或 BSD-3-Clause

140KB
2.5K SLoC

Seccompiler

提供易于使用的 Linux seccomp-bpf 禁锢。

Seccomp 是 Linux 内核的一个安全特性,它允许对进程可以访问的内核级机制进行严格控制。这通常用于在运行不受信任的代码时减少攻击面和暴露的资源。这是通过允许用户为每个进程或线程编写和设置一个 BPF(伯克利包过滤器)程序来实现的,该程序拦截系统调用并决定是否可以安全执行系统调用。

手动编写 BPF 程序很困难且容易出错。此 crate 为处理系统调用过滤提供了高级封装。

支持的平台

由于 seccomp 是特定于 Linux 的功能,因此此 crate 仅支持 Linux 系统。

支持的主机架构

  • 小端 x86_64
  • 小端 aarch64

简短的 seccomp 教程

Linux 支持作为 BPF 程序的 seccomp 过滤器,在每次系统调用之前由内核解释。

它们使用 prctl(PR_SET_SECCOMP)seccomp(SECCOMP_SET_MODE_FILTER) 安装在内核中。

作为输入,BPF 程序接收以下类型的 C 结构体

struct seccomp_data {
    int nr; // syscall number.
    __u32 arch; // arch-specific value for validation purposes.
    __u64 instruction_pointer; // as the name suggests..
    __u64 args[6]; // syscall arguments.
};

作为响应,过滤器返回一个动作,可以是以下之一

#define SECCOMP_RET_KILL_PROCESS 0x80000000U /* kill the process */
#define SECCOMP_RET_KILL_THREAD  0x00000000U /* kill the thread */
#define SECCOMP_RET_TRAP         0x00030000U /* disallow and force a SIGSYS */
#define SECCOMP_RET_ERRNO        0x00050000U /* returns an errno */
#define SECCOMP_RET_TRACE        0x7ff00000U /* pass to a tracer or disallow */
#define SECCOMP_RET_LOG          0x7ffc0000U /* allow after logging */
#define SECCOMP_RET_ALLOW        0x7fff0000U /* allow */

设计

库的核心概念是 过滤器。它是一个抽象,它模拟了一组系统调用映射规则,与匹配和默认动作相结合,逻辑上描述了分配动作(例如允许、陷阱、错误码)的策略,用于处理传入的系统调用。

Seccompiler提供定义过滤器、将它们编译成可加载的BPF程序并在内核中安装的构造。

过滤器可以是用JSON文件定义,也可以使用Rust代码,使用库定义的结构。这两种表示在语义上是等价的,并模拟过滤器的规则。选择哪一种取决于用例和偏好。

该软件包的核心是负责BPF编译的模块。它将用Rust代码表示的seccomp过滤器编译成BPF过滤器,准备加载到内核中。这是seccompiler的后端

将JSON过滤器转换为BPF的过程需要经过额外的反序列化和验证步骤(即JSON前端),然后再达到相同的后端进行BPF代码生成。

因此,Rust表示也是JSON过滤器的中间表示(IR)。这种模块化实现允许在文件格式方面进行扩展。所需的一切只是一个兼容的前端。

以下图表说明了将JSON和Rust过滤器编译成BPF所需的步骤。蓝色方框代表潜在的用户输入。

seccompiler architecture

过滤器定义

让我们更仔细地看看过滤器由什么组成,以及它是如何定义的

过滤器的最小单位是SeccompCondition,它是对当前系统调用应用的比较操作。它由参数索引、参数长度、运算符和实际预期值进行参数化。

再进一步,SeccompRuleSeccompCondition的向量,必须所有条件都匹配才能认为规则匹配。换句话说,规则是一组针对系统调用的和绑定条件。

最后,在顶级,有SeccompFilter。过滤器可以看作是一组与系统调用关联的规则,具有预定义的匹配动作和默认动作,如果没有任何规则匹配,则返回该默认动作。

在过滤器中,每个系统调用编号映射到一个包含或绑定规则的向量。为了使过滤器匹配,只需要与系统调用关联的规则之一匹配。系统调用也可能映射到一个空的规则向量,这意味着无论实际参数如何,系统调用都将匹配。

以下图表模拟了一个简单的过滤器,只允许accept4fcntl(any, F_SETFD, FD_CLOEXEC, ..)fcntl(any, F_GETFD, ...)。对于任何其他系统调用,进程将被终止。

filter diagram

如前所述,有两种方式可以表示过滤器

  1. JSON(在json_format.md中记录);
  2. Rust代码(由库记录)。

以下提供了两种表示方法的示例,用于与上述图表等效的过滤器

示例JSON过滤器

{
    "main_thread": {
        "mismatch_action": "kill_process",
        "match_action": "allow",
        "filter": [
            {
                "syscall": "accept4"
            },
            {
                "syscall": "fcntl",
                "args": [
                    {
                        "index": 1,
                        "type": "dword",
                        "op": "eq",
                        "val": 2,
                        "comment": "F_SETFD"
                    },
                    {
                        "index": 2,
                        "type": "dword",
                        "op": "eq",
                        "val": 1,
                        "comment": "FD_CLOEXEC"
                    }
                ]
            },
            {
                "syscall": "fcntl",
                "args": [
                    {
                        "index": 1,
                        "type": "dword",
                        "op": "eq",
                        "val": 1,
                        "comment": "F_GETFD"
                    }
                ]
            }
        ]
    }
}

请注意,JSON文件需要为每个过滤器指定一个名称。虽然在上面的示例中只有一个(main_thread),但其他程序可能使用多个过滤器。

基于Rust的过滤器示例

SeccompFilter::new(
    // rule set - BTreeMap<i64, Vec<SeccompRule>>
    vec![
        (libc::SYS_accept4, vec![]),
        (
            libc::SYS_fcntl,
            vec![
                SeccompRule::new(vec![
                    Cond::new(1,
                        SeccompCmpArgLen::Dword,
                        SeccompCmpOp::Eq,
                        libc::F_SETFD as u64
                    )?,
                    Cond::new(
                        2,
                        SeccompCmpArgLen::Dword,
                        SeccompCmpOp::Eq,
                        libc::FD_CLOEXEC as u64,
                    )?,
                ])?,
                SeccompRule::new(vec![
                    Cond::new(
                        1,
                        SeccompCmpArgLen::Dword,
                        SeccompCmpOp::Eq,
                        libc::F_GETFD as u64,
                    )?
                ])?
            ]
        )
    ].into_iter().collect(),
    // mismatch_action
    SeccompAction::KillProcess,
    // match_action
    SeccompAction::Allow,
    // target architecture of filter
    TargetArch::x86_64,
)?

示例用法

在应用程序中使用seccompiler是一个两步过程

  1. 编译过滤器(到BPF)
  2. 安装过滤器

编译过滤器

用户应用程序可以在运行时构建时将seccomp过滤器编译成可加载的BPF。

运行时,这个过程很简单,通过利用seccompiler库中的硬编码/基于文件的过滤器函数。

构建时,应用程序可以使用一个cargo构建脚本,将seccompiler作为构建依赖项,并将编译后的过滤器输出到预定义的位置(例如,使用env::var("OUT_DIR"))已序列化为二进制格式(例如,bincode)。然后,可以使用include_bytes!将它们包含到应用程序中,并在安装之前进行反序列化。如果使用低开销的二进制格式,此构建时选项可以用于从应用程序启动时间中减去过滤器编译时间。

无论编译时间如何,过程都是相同的

对于JSON过滤器,使用compile_from_json()函数进行编译到可加载的BPF。

let filters: BpfMap = seccompiler::compile_from_json(
    File::open("/path/to/json")?, // Accepts generic Read objects.
    seccompiler::TargetArch::x86_64,
)?;

BpfMap是库公开的另一种类型,它将线程类别映射到BPF程序。

pub type BpfMap = HashMap<String, BpfProgram>;

请注意,为了使用JSON功能,您需要在导入库时添加json功能。

对于Rust过滤器,只需从SeccompFilterBpfProgram执行try_into()转换即可。

let seccomp_filter = SeccompFilter::new(
    rules,
    SeccompAction::Trap,
    SeccompAction::Allow,
    seccompiler::TargetArch::x86_64
)?;

let bpf_prog: BpfProgram = seccomp_filter.try_into()?;

安装过滤器

let bpf_prog: BpfProgram; // Assuming it was initialized with a valid filter.

seccompiler::apply_filter(&bpf_prog)?;

值得注意的是,安装过滤器不会获得BPF程序的所有权或使其无效,这是由于内核在安装之前对程序执行了copy_from_user

功能文档

docs.rs上的文档不包括功能门控的json功能。

要查看包括可选的json功能的文档,您可以运行:cargo doc --open --all-features

Seccomp最佳实践

  • 在安装过滤器之前,请确保当前的内核版本支持过滤器的操作。这可以通过检查cat /proc/sys/kernel/seccomp/actions_avail的输出或通过调用seccomp(SECCOMP_GET_ACTION_AVAIL)系统调用来检查。

  • 建议使用允许列表方法为seccomp过滤器,只允许应用程序所需的最低系统调用集。这比拒绝列表更安全、更健壮,因为每当内核添加新的危险系统调用时,都需要更新拒绝列表。

  • 在确定应用程序所需的一组系统调用时,建议彻底运行所有代码路径,同时使用straceperf进行跟踪。还重要的是要注意,应用程序很少直接使用系统调用接口。它们通常使用libc包装器,根据实现的不同,它们可能会为同一功能使用不同的系统调用(例如,openopenat)。

  • Linux 支持在线程/进程中安装多个 seccomp 过滤器。它们会按顺序进行评估,并选择最严格的操作。除非您的应用程序需要在线程上安装多个过滤器,否则建议拒绝 prctlseccomp 系统调用,以避免恶意行为者进一步限制已安装的过滤器。

  • Linux vDSO 通常会导致一些系统调用完全在用户空间中运行,绕过 seccomp 过滤器(例如 clock_gettime)。如果使用但未允许这些系统调用,在运行不支持相同 vDSO 系统调用的机器上可能会导致失败。如果可能,建议在未安装 vDSO 的机器上测试 seccomp 过滤器。

  • 为了最小化系统调用开销,建议启用 BPF 即时编译器。在 BPF 程序加载后,内核会将 BPF 代码转换为原生 CPU 指令,以实现最大效率。可以通过以下方式配置:/proc/sys/net/core/bpf_jit_enable

依赖项

~0.4–340KB