【问题标题】:Selective linking of C program with C stdlib functionsC 程序与 C stdlib 函数的选择性链接
【发布时间】:2021-12-19 21:07:32
【问题描述】:

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

我想知道是否有办法传递标志,或配置ld 这样做?我目前的想法是使用ld,仅使其有选择地链接到包含我想要的 libc 实现的静态库文件夹。

【问题讨论】:

  • 不允许他们修改构建命令。包括标题与链接无关。 FWIW,您认为可能导致“邪恶事物”的任何库都可以通过您允许他们编写的代码“模拟”。这些事情应该通过其他方式来防止,例如沙盒。
  • 会有各种有趣的方法来规避这个......
  • 在一个非常受限的 Docker 容器或其他东西中编译参赛者的东西可能是非常明智的。
  • 可能会编译一个非常严格的库版本。

标签: c linux libc


【解决方案1】:

问题的潜在来源不是标准库函数,而是这些函数所做的系统调用。 (实际上,什么可以阻止恶意提交者包含扩展汇编函数来直接调用这些系统调用,并避免您的限制?没什么。如果您使用 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,并且标志是 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 错误)来验证所有允许的系统调用,并测试一些你绝对不想支持的系统调用,然后运行测试每次修改甚至重新编译过滤器时都会发生这种情况。

【讨论】:

  • 谢谢!真的。你为回答这个问题做了很多工作,我不可能接受你的解决方案:-)。
  • 这也是我第一次了解seccomp过滤器,再次感谢详细解答!
  • @BearAqua:不客气!我忘了提,设置seccomp过滤器的相同代码,可以先使用setrlimit()设置资源限制,至少RLIMIT_CPU(最大CPU时间以秒为单位)、RLIMIT_ASRLIMIT_DATA和@987654349 @(限制地址空间、数据和堆栈大小)。我还将使用一对帐户,一个用于编译二进制文件,另一个用于执行它们,编译帐户是执行组的成员,因为确实会发生错误,并且将其风险最小化是有意义的。
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-23
    相关资源
    最近更新 更多