编译具有有限库访问权限的程序

Compiling a program with limited library access

我想用 gcc 和 glibc(或任何其他 c 库)编译一个 C 程序 但我想限制程序访问某些功能 例如,如果程序使用套接字或信号处理函数,则不应编译该程序。

知道我该怎么做吗?

顺便说一句,我想在简单的编程竞赛评委中使用它

谢谢

请不要那样做。即使您找到了禁止某些函数(如 execl)的方法,也有很多方法可以绕过这些限制。例如,程序可以使用内联汇编或其他技巧自行调用操作系统。

您可以做的事情很少:

  • 封锁环境。
  • 运行 某种管理程序下的程序会在程序执行您不希望的操作时终止程序。有一个 Linux 工具包可以做到这一点,但我忘记了它的名字。

如果您只是想检测程序是否使用了不允许的函数,您可以 运行 nm 在编译器创建的二进制文件上检查是否出现任何不允许的函数名称。请注意,并非所有函数的符号名称都与其名称相同。

您不能可靠地限制对某些功能的访问,因为积极的开发人员可以总是找到解决方法。例如,他可以使用 dlsym 在 运行 时间找到某个函数的地址,或者使用 asm 代码调用一些系统调用(或使用缓冲区溢出技术)或假设特定版本的libc 二进制文件并计算一些函数指针(例如,通过使用内置偏移量偏移一些合法 libc 函数的地址,如 printf),或转换一些文字字符串(包含合适的机器操作码)到函数指针等等......

但是,您可以考虑自定义编译器(例如,如果使用最近的 GCC, customize it with your MELT 扩展进行编译)以检测常见情况(但不是所有情况)。这可能意味着需要数周的时间来开发此类编译器定制。

您也可以 link 使用特制的 libc,使用 LD_PRELOADptrace,等等

要可靠地禁止某些行为,您应该运行在一些虚拟容器中。

PS。 Statically (soundly & reliably) detecting that some source code would never call a given set of functions is undecidable, since equivalent to the halting problem.

我想我来晚了一点,但我觉得目前给出的 none 答案是完全正确的。事实上,可以按照您要求的方式限制程序的功能,而且要理智地这样做。

确实,防止调用任意函数虽然也可能是毫无意义的——这就像一个孔一个孔地密封漏勺。它也没有提出正确的问题——我怀疑,你不是想阻止编码员计算数字的平方根,而是想阻止他拥有系统。这意味着要防止他让系统做某些事情,这些事情总是涉及系统调用,所以关注它们而不是函数是有意义的。使用哪个函数打开套接字并不重要;他们最终都使用了 socket 系统调用。

对系统调用的访问可以由内核控制。 linux 内核有一个称为 seccomp 的机制,它被各种大型程序(如 Firefox、Chrome 和 Adob​​e Flash)用来沙盒它们的代码解释器和一些较小的,例如 vsftpd,以在攻击者设法找到远程代码执行漏洞的情况下最大限度地减少攻击面(基于漏洞利用代码会发现自己受到严重限制而无法调用 exec 和其他) .

现在,在我详细介绍之前:如果您要从不认识的人(因此不能信任)那里获取代码,偏执狂就是理智。 Seccomp 很好,但在这种情况下还不够,因为这种情况是攻击者梦寐以求的。最好是重重防御,不要计较微妙。因此,您必须为此做的前三件事是:

  1. 使用虚拟机
  2. 使用虚拟机
  3. 说真的,使用虚拟机。

运行 虚拟机中的所有程序使得利用您的主系统变得更加困难,因为攻击者除了 之外还必须突破 VM 否则他将不得不做的其他事情。有免费的实现可以很好地工作并且设置起来不是很困难。我大部分时间都使用Virtualbox

在您的 VM 中安装 Linux 系统后,制作 VM 的快照 以便在某个程序设法破坏它时您可以返回它.

设置好了吗?好的。现在,seccomp 允许进程限制其使用系统调用的能力。按照设计,限制是一条单行道;以后不可能重新扩展流程的功能。 seccomp 可以设置的限制有些强大;例如,进程不仅可以防止自己调用 write,还可以防止自己对 STDOUT_FILENO 以外的任何文件描述符调用 write。由于内核 API 相当笨重,我将在下面的代码示例中使用 libseccomp。它有一组非常有用的手册页,可以帮助您了解详细信息,并且您的发行版可能有它的软件包,除非它很旧。一个简单的例子来说明这是什么:

#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>

int main() {
  scmp_filter_ctx ctx;

  puts("foo");                // works as usual. (needed here because it forces
  fputs("bar\n", stderr);     // some initialisation. More on that later)

  ctx = seccomp_init(SCMP_ACT_KILL);              // default action: kill process
  seccomp_rule_add(ctx, 
                   SCMP_ACT_ALLOW,                       // allow
                   SCMP_SYS(write),                      // calls to write
                   1,                                    // under one condition:
                   SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO)); // if the first argument
                                                         // is STDOUT_FILENO
  seccomp_load(ctx);

  puts("foo");                      // this will still work
  fputs("bar\n", stderr);           // this will make the kernel kill the process
  fprintf(stderr, "bar\n");         // so would this
  fputc('b', stderr);               // and this
  write(STDERR_FILENO, "bar\n", 4); // and this
                                    // and any other write to anything but stdout

  return 0;
}

因此我们对允许的系统调用进行了合理的细粒度控制,这很好。它留下了识别需要允许程序正常运行的系统调用的问题,其中有几个是非常重要的决定。这是一个您必须自己回答的设计问题。系统调用在 /usr/include/asm/unistd_64.h.

中列出

那么我们如何将其应用于来自不可信来源的一段代码?

sed 或类似的东西修补代码是一个想法,但它太不可靠,对于安全关键应用程序没有意义。一个 "safe loader" 在用 execv 运行 调用程序之前禁止系统调用的问题是它不能禁止 execve 系统调用,这是人们想要的系统调用之一禁止大多数。此外,execv 需要一堆其他系统调用(例如 accessmmapopenfstatclosemprotect, 和 arch_prctl) 在该程序的 main 功能被输入之前。那怎么办?

重要更新: 本节最初包括使用 LD_PRELOAD 加载 seccomp 代码的尝试; @virusdefender 正确地指出它有一个明显的漏洞,因为用户代码可以控制函数是否实际上是 运行。新方法使 运行time 链接器调用我们的函数,关闭那个漏洞。

一种方法是使用一个共享库,除了加载和卸载时分别为 运行 的构造函数和析构函数外,它什么都没有。链接器将在 运行 二进制代码之前加载库,因此构造函数将为 运行 并且在用户代码控制之前安装过滤器。

代码如下:

#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static scmp_filter_ctx ctx;

// Macro just to make error handling simple. Error handling is
// very important here. You don't want this to silently fail.
#define ADD_SECCOMP_RULE(ctx, ...)                      \
  do {                                                  \
    if(seccomp_rule_add(ctx, __VA_ARGS__) < 0) {        \
      perror("Could not add seccomp rule");             \
      seccomp_release(ctx);                             \
      exit(-1);                                         \
    }                                                   \
  } while(0)

// Constructor. This sets up the seccomp filter.
static void __attribute__((constructor)) seccomp_load_init(void) {
  ctx = seccomp_init(SCMP_ACT_KILL);

  if(ctx == NULL) {
    perror("Could not open seccomp context");
    exit(-1);
  }

  // Rules for system calls here.
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit      ), 0);
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write     ), 1, SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO));
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write     ), 1, SCMP_A0(SCMP_CMP_EQ, STDERR_FILENO));
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read      ), 1, SCMP_A0(SCMP_CMP_EQ, STDIN_FILENO));

  // This is needed for dynamic memory allocation
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk       ), 0);

  // These are needed for stdio initialisation. Workarounds to this are ugly, and the
  // syscalls are not terribly critical because they require file descriptors. We
  // restrict the program's ability to obtain those.
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap      ), 0);
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat     ), 0);

  if(seccomp_load(ctx) < 0) {
    perror("Could not load seccomp context");
    exit(-1);
  }
}

// Destructor; run at unload time. Just cleanup here.
static void __attribute__((destructor)) seccomp_load_free(void) {
  seccomp_release(ctx);
}

这需要编译成共享库:

gcc -fPIC -shared -o libmyfilter.so myfilter.c

并且需要链接到不可信代码,以便链接器在程序启动时加载它:

gcc -o untrustworthy_program untrustworthy_code.c -L/path/to/myfilter -lmyfilter -lseccomp

然后你可以不安全地调用不可信的程序 (在你的 VM 中!)

LD_LIBRARY_PATH=/path/to/myfilter ./untrustworthy_program

其中 /path/to/myfilter 是包含 libmyfilter.so.

的目录

因为过滤器库使用来自 libc(和 libseccomp)的函数,所以 libc 的启动工作将在安装 seccomp 过滤器之前完成。这是有意的(并且是最初尝试背后的基本原理的一部分),因为 libc 在启动时会做很多事情,例如打开文件,我们可能希望阻止用户代码做这些事情。如果您希望允许使用另一个在启动时执行筛选器稍后应阻止的操作的库,您可以使用 LD_PRELOAD 让链接器在筛选器之前加载它。

我不会冒昧地说这将使攻击变得不可能,但如果你理智地设计你的系统调用过滤器,攻击者将不得不在 Linux 内核(在 seccomp 中或您允许它使用的内核子集中)和您的 VM,这很可能非常困难。在更可能的情况下,我(再次)忽略了一些事情,VM 仍然是一道有用的防线。