问题的潜在来源不是标准库函数,而是这些函数所做的系统调用。 (实际上,什么可以阻止恶意提交者包含扩展汇编函数来直接调用这些系统调用,并避免您的限制?没什么。如果您使用 GCC 或 clang,您甚至无法禁用扩展汇编支持。)
您可以毫不费力地实现seccomp filter,它只允许您认为安全的系统调用。在 x86-64 上,您的过滤器还需要处理可能的 32 位系统调用(例如,通过扩展的汇编函数);我个人只允许在 x86-64 上使用一组基本的 64 位系统调用(所以请先在过滤器中检查架构编号),也许只是 exit 和 exit_group 用于正常结束进程,read/ readv/preadv2 用于读取打开的文件描述符,write/writev/pwritev2 用于写入打开的文件描述符。
我会将提交的代码编译成一个目标文件,并使用objdump -t 检查它不是否包含.init_array 符号(ELF 构造函数,使用 GCC/clang 的函数__attribute__((__constructor__))属性,因此它们在main()) 之前运行,但确实包含main 符号。
如果通过了,那么我会将目标文件中提交的代码与包含合适的 ELF 构造函数的目标文件结合起来,以设置安全计算环境。使用 GCC 或 Clang,它将类似于以下内容:
#define _GNU_SOURCE
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <errno.h>
#define BPF_SYSCALL_NR (offsetof (struct seccomp_data, nr))
#define BPF_ARCH_ID (offsetof (struct seccomp_data, arch))
#if defined(__amd64__) || defined(__x86_64__)
#define ALLOW_ARCH_ID AUDIT_ARCH_X86_64
#elif defined(__i386__)
#define ALLOW_ARCH_ID AUDIT_ARCH_I386
#else
#error Unsupported architecture.
#endif
__attribute__((__constructor__))
static void setup_seccomp_filter(void)
{
struct sock_filter filter[] = {
/* Only allow syscalls using the specified architecture. */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, BPF_ARCH_ID),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, ALLOW_ARCH_ID, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
/* Only allow specific syscalls. */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, BPF_SYSCALL_NR),
/* Allow reading from an open file descriptor. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_read, 18, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_readv, 17, 0),
/* Allow writing to an open file descriptor. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_write, 16, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_writev, 15, 0),
/* Allow obtaining open file descriptor information. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_fstat, 14, 0),
/* Allow seeking. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_lseek, 13, 0),
/* Allow memory allocation.
NOTE: mmap should really check PROT and FLAGS, and
mremap should really check flags. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_brk, 12, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_mmap, 11, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_mremap, 10, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_munmap, 9, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_madvise, 8, 0),
/* Allow POSIX clock access and nanosleep. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_clock_getres, 7, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_clock_gettime, 6, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_clock_nanosleep, 5, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_gettimeofday, 4, 0),
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_nanosleep, 3, 0),
/* Allow syscall restart (used by the C library). */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_restart_syscall, 2, 0),
/* Allow program to exit normally. */
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, __NR_exit_group, 1, 0),
/* Deny all other syscalls with ENOSYS. */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (SECCOMP_RET_DATA & ENOSYS)),
/* Allow syscall */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
};
struct sock_fprog desc = {
.len = sizeof filter / sizeof filter[0],
.filter = filter,
};
/* If exec is ever allowed, never gain new privileges via exec. */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
exit(98);
}
/* Install the filter. */
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &desc, 0, 0)) {
exit(97);
}
}
最好在 mmap() 的情况下附加带有检查的过滤器,以验证 prot 是 PROT_READ | PROT_WRITE,并且标志是 MAP_PRIVATE | MAP_ANONYMOUS。同样,mremap() 应该只允许标志为零或MREMAP_MAYMOVE。这些将阻止提交的程序尝试分配可执行内存,以及其他类似的技巧。在任何情况下,即使是这样的技巧也不会让进程使用任何其他系统调用,除了 seccomp 过滤器明确允许的那些。
BPF_JUMP(BPF_JMP | BPF_K | BPF_JEQ, value, trueskip, falseskip) 宏包含由之前的BPF_STMT(BPF_LD,...) 加载到value 的值。如果两者匹配,则将跳过以下 trueskip 条目。否则,将跳过以下falseskip 条目。
以这种形式维护 seccomp 过滤器非常敏感(尤其是跳过计数!),因此可能更喜欢根据更人性化的描述自动构建过滤器。对于大量允许的系统调用,可以实现一种二分搜索算法来加速。在任何情况下,我都强烈建议实施一个单元测试用例(期望来自不受支持的系统调用的ENOSYS 错误)来验证所有允许的系统调用,并测试一些你绝对不想支持的系统调用,然后运行测试每次修改甚至重新编译过滤器时都会发生这种情况。