C 程序与 C stdlib 函数的选择性链接

Selective linking of C program with C stdlib functions

我正在编写一个编码竞赛评分器,我想在其中使用 gcc 来编译参赛者的代码,并 link 它只包含 C 标准库函数的一个受限子集。例如,我只希望参赛者能够使用来自 stdlib.hstring.h 和少数其他 stdlib 头文件的函数,但不能使用例如包括 sys/sysinfo.h,这可能会让他们做出邪恶的事情。

我想知道是否有办法传递一个标志,或者配置 ld 这样做?我目前的想法是玩弄 ld 只使其 link 有选择地针对包含我想要的 libc 实现的静态库文件夹。

将参赛者的代码编译为未链接的对象模块后,使用“objdump”打印其未解析的引用。使用一个小脚本根据您允许的事物(函数、变量等)列表检查它。

您应该阅读 objdump 的文档,但选项“-r”可能是一个好的开始。

问题的潜在根源不是标准库函数,而是这些函数执行的系统调用。 (事实上​​ ,什么可以阻止恶意提交者包含扩展汇编函数来直接调用这些系统调用,并避免你的限制?没有。如果你使用 GCC 或 clang,你甚至不能禁用扩展汇编支持。)

您可以毫不费力地实现 seccomp filter 只允许您认为安全的系统调用。在 x86-64 上,您的过滤器也确实需要处理可能的 32 位系统调用(例如,通过扩展的汇编函数);我个人只允许在 x86-64 上进行一组基本的 64 位系统调用(因此请先在过滤器中检查体系结构编号),也许只是 exitexit_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,并且 flags 是 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 错误)来验证所有允许的系统调用,并测试一些你绝对不想支持的系统调用,并且 运行每次修改甚至重新编译过滤器时都要测试用例。