2. 创建沙盒政策

获得执行器后,您可能需要为 Sandboxee 定义 Sandbox 政策。否则,Sandboxee 仅受默认系统调用政策保护。

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

获得系统调用的列表后,您可以使用 PolicyBuilder 来定义政策。PolicyBuilder 附带许多便捷的辅助函数,可用于执行许多常见操作。以下列表仅列出了部分可用函数:

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

这些便捷函数可将任何相关的系统调用列入许可名单。这样做的好处是,可以在某些系统调用不可用的不同架构上使用同一政策(例如,ARM64 没有 OPEN 系统调用),但可能会启用比必要更多的系统调用,从而带来轻微的安全风险。例如,AllowOpen() 可让沙盒进程调用任何与打开相关的系统调用。如果您只想将一个特定的系统调用列入许可名单,可以使用 AllowSyscall();;如果您想同时将多个系统调用列入许可名单,可以使用 AllowSyscalls()

到目前为止,该政策仅检查系统调用标识符。如果您需要进一步加强政策,并希望定义一项只允许使用特定实参的系统调用政策,则需要使用 AddPolicyOnSyscall()AddPolicyOnSyscalls()。这些函数不仅将系统调用 ID 作为实参,还使用来自 Linux 内核的 bpf 辅助宏将原始 seccomp-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();
}