2. 创建沙盒政策

有了执行器后,您可能需要为 Sandboxee 定义沙盒政策。否则,Sandboxee 仅受默认系统调用政策的保护。

使用沙盒政策的目的是限制沙盒化可以创建的系统调用和参数,以及可以访问的文件。您需要详细了解您计划沙盒化的代码所需的系统调用。观察系统调用的一种方法是使用 Linux 的命令行工具 strace 运行代码。

获得系统调用列表后,您可以使用 PolicyBuilder 定义政策。PolicyBuilder 附带许多便捷函数和辅助函数,支持执行许多常见操作。以下列表只是可用函数的一小部分摘录:

  • 将进程启动期间的任何系统调用列入许可名单:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • 将任何打开的 /read/write* 系统调用列入许可名单:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • 将所有与退出/访问/状态相关的系统调用列入许可名单:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • 将任何与睡眠/时间相关的系统调用列入许可名单:
    • AllowTime();
    • AllowSleep();

这些便捷函数会将任何相关的系统调用列入许可名单。这样做的好处是,可以在某些系统调用不可用的不同架构中使用相同的政策(例如,ARM64 没有开放系统调用),但启用过多的 Sycsall 会造成不必要的安全风险。例如,AllowOpen() 可让沙盒化对象调用任何打开的相关系统调用。如果您只想将一个特定的系统调用列入许可名单,可以使用 AllowSyscall(); 一次允许多个系统调用,也可以使用 AllowSyscalls()

到目前为止,该政策仅检查系统调用标识符。如果您需要进一步强化该政策,并希望定义仅允许使用特定参数的系统调用的政策,则需要使用 AddPolicyOnSyscall()AddPolicyOnSyscalls()。这些函数不仅接受系统调用 ID 作为参数,还接受原始 seccomp-bpf 过滤器,使用来自 Linux 内核的 bpf 帮助程序宏。如需详细了解 BPF,请参阅内核文档。如果您发现自己在编写重复的 BPF 代码,而您认为这些代码应该具有可用性封装容器,请随时提交功能请求。

除了与系统调用相关的函数之外,PolicyBuilder 还提供许多与文件系统相关的函数(如 AddFile()AddDirectory()),用于将文件/目录绑定装载到沙盒中。AddTmpfs() 帮助程序可用于在沙盒内添加临时文件存储空间。

AddLibrariesForBinary() 是一个特别实用的函数,它会添加二进制文件所需的库和链接器。

遗憾的是,设计出列入许可名单的系统调用仍然需要一些手动工作。使用您知道二进制文件需求的系统调用创建政策,并通过常见工作负载运行该政策。如果触发了违规行为,请将系统调用列入许可名单并重复该过程。如果您认为将某个违规行为列入许可名单可能存在风险,并且该程序能够妥善处理错误,您可以尝试使用 BlockSyscallWithErrno() 使其返回错误。

#include "sandboxed_api/sandbox2/policy.h"
#include "sandboxed_api/sandbox2/policybuilder.h"
#include "sandboxed_api/sandbox2/util/bpf_helper.h"

std::unique_ptr<sandbox2::Policy> CreatePolicy() {
  return sandbox2::PolicyBuilder()
    .AllowSyscall(__NR_read)  // See also AllowRead()
    .AllowTime()              // Allow time, gettimeofday and clock_gettime
    .AddPolicyOnSyscall(__NR_write, {
        ARG(0),        // fd is the first argument of write (argument #0)
        JEQ(1, ALLOW), // allow write only on fd 1
        KILL,          // kill if not fd 1
    })
    .AddPolicyOnSyscall(__NR_mprotect, {
        ARG_32(2), // prot is a 32-bit wide argument, so it's OK to use *_32
                   // macro here
        JNE32(PROT_READ | PROT_WRITE, KILL), // prot must be the RW, otherwise
                                             // kill the process
        ARG(1), // len is a 64-bit argument
        JNE(0x1000, KILL),  // Allow single page syscalls only, otherwise kill
                            // the process
        ALLOW,              // Allow for the syscall to proceed, if prot and
                            // size match
    })
    // Allow the openat() syscall but always return "not found".
    .BlockSyscallWithErrno(__NR_openat, ENOENT)
    .BuildOrDie();
}