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
每月 425,937 次下载
在 17 个 crate 中使用(8 个直接使用)
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所需的步骤。蓝色方框代表潜在的用户输入。
过滤器定义
让我们更仔细地看看过滤器由什么组成,以及它是如何定义的
过滤器的最小单位是SeccompCondition
,它是对当前系统调用应用的比较操作。它由参数索引、参数长度、运算符和实际预期值进行参数化。
再进一步,SeccompRule
是SeccompCondition
的向量,必须所有条件都匹配才能认为规则匹配。换句话说,规则是一组针对系统调用的和绑定条件。
最后,在顶级,有SeccompFilter
。过滤器可以看作是一组与系统调用关联的规则,具有预定义的匹配动作和默认动作,如果没有任何规则匹配,则返回该默认动作。
在过滤器中,每个系统调用编号映射到一个包含或绑定规则的向量。为了使过滤器匹配,只需要与系统调用关联的规则之一匹配。系统调用也可能映射到一个空的规则向量,这意味着无论实际参数如何,系统调用都将匹配。
以下图表模拟了一个简单的过滤器,只允许accept4
、fcntl(any, F_SETFD, FD_CLOEXEC, ..)
和fcntl(any, F_GETFD, ...)
。对于任何其他系统调用,进程将被终止。
如前所述,有两种方式可以表示过滤器
- JSON(在json_format.md中记录);
- 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是一个两步过程
- 编译过滤器(到BPF)
- 安装过滤器
编译过滤器
用户应用程序可以在运行时或构建时将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过滤器,只需从SeccompFilter
到BpfProgram
执行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过滤器,只允许应用程序所需的最低系统调用集。这比拒绝列表更安全、更健壮,因为每当内核添加新的危险系统调用时,都需要更新拒绝列表。
-
在确定应用程序所需的一组系统调用时,建议彻底运行所有代码路径,同时使用
strace
或perf
进行跟踪。还重要的是要注意,应用程序很少直接使用系统调用接口。它们通常使用libc包装器,根据实现的不同,它们可能会为同一功能使用不同的系统调用(例如,open
与openat
)。 -
Linux 支持在线程/进程中安装多个 seccomp 过滤器。它们会按顺序进行评估,并选择最严格的操作。除非您的应用程序需要在线程上安装多个过滤器,否则建议拒绝
prctl
和seccomp
系统调用,以避免恶意行为者进一步限制已安装的过滤器。 -
Linux vDSO 通常会导致一些系统调用完全在用户空间中运行,绕过 seccomp 过滤器(例如
clock_gettime
)。如果使用但未允许这些系统调用,在运行不支持相同 vDSO 系统调用的机器上可能会导致失败。如果可能,建议在未安装 vDSO 的机器上测试 seccomp 过滤器。 -
为了最小化系统调用开销,建议启用 BPF 即时编译器。在 BPF 程序加载后,内核会将 BPF 代码转换为原生 CPU 指令,以实现最大效率。可以通过以下方式配置:
/proc/sys/net/core/bpf_jit_enable
。
依赖项
~0.4–340KB