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

时间:2015-01-01 14:21:21

标签: c linux gcc compilation

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

任何想法我怎么能这样做?

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

由于

3 个答案:

答案 0 :(得分:3)

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

但是,您可以考虑自定义编译器(例如,如果使用最新的GCC进行编译,使用您的MELT扩展名进行自定义)以检测常见情况(但不是所有情况)。这可能意味着开发此类编译器定制的数周工作。

您也可以与经过特制的printf关联,使用libcLD_PRELOAD等。

为了可靠地禁止某些行为,您应该在某个虚拟容器内运行。

PS。 Statically(合理且可靠地)检测到某些源代码永远不会调用给定的一组函数undecidable,因为它等同于halting problem

答案 1 :(得分:2)

请不要那样做。即使你找到一种方法来禁止像execl这样的某些功能,也有很多方法可以解决这些限制。例如,程序可以使用内联汇编或其他技巧自行调用操作系统。

你可以做的事情很少:

  • 锁定环境。
  • 在某种虚拟机管理程序下运行程序,当程序执行您不想要的操作时终止该程序。有一个Linux工具包可以做到这一点,但我忘了它的名字。

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

答案 2 :(得分:2)

我想我在派对上有点迟了,但我觉得到目前为止给出的答案都不完全正确。事实上,可以按照你要求的方式限制程序的功能,并且可以这样做。

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

内核可以控制对系统调用的访问 。 Linux内核有一种称为 seccomp 的机制,它被各种大型程序(如Firefox,Chrome和Adobe Flash)用于沙箱化他们的代码解释器和一些较小的程序如vsftpd,以最大限度地减少其攻击面。攻击者设法找到远程执行代码漏洞的事件(基于漏洞利用代码发现自己严重受限而无法调用exec和其他人)。

现在,在我详细介绍之前:如果你要从不认识的人那里获取代码(因此无法信任),偏执就是理智。 Seccomp很好但在这种情况下还不够,因为这种情况是攻击者梦寐以求的。最好是堆叠防御,而不是精力充沛。所以,你要做的前三件事是:

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

    将Linux系统安装到虚拟机后,制作虚拟机的快照,以便在程序设法销毁它时可以返回该虚拟机。

    完成所有设置?好。现在,seccomp允许进程限制其使用系统调用的能力。按设计,限制是单行道;以后无法重新扩展流程的功能。 seccomp可以放置的限制有点强大;例如,进程不仅可以阻止自己调用write,还可以阻止自己在write以外的任何文件描述符上调用STDOUT_FILENO。由于内核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或类似的东西修补代码是一个人可能会有的想法,但对于安全关键型应用程序而言,它是不可靠的。 A&#34;安全装载机&#34;在使用execv调用程序之前禁止系统调用会遇到无法禁止execve系统调用的问题,这是其中一个想要禁止的系统调用之一。此外,execv需要大量其他系统调用(例如accessmmapopenfstatclose,{{1甚至在输入该程序的mprotect函数之前,还有arch_prctl)。那该怎么办?

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

    一种方法是使用其中没有任何内容的共享库,除了分别在加载和卸载时运行的构造函数和析构函数。链接器将在从二进制文件运行代码之前加载库,因此在用户代码获得控制权之前,将运行构造函数并安装过滤器。

    代码如下:

    LD_PRELOAD

    这需要编译成共享库:

    #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
    

    LD_LIBRARY_PATH=/path/to/myfilter ./untrustworthy_program 是包含/path/to/myfilter的目录。

    因为过滤器库使用libc(和libseccomp)中的函数,所以libc启动内容将在安装seccomp过滤器之前完成。这是故意的(并且是原始尝试背后的基本原理的一部分),因为libc在启动时执行了许多操作,例如打开文件,我们可能希望阻止用户代码执行操作。如果您希望允许使用另一个在启动时执行过滤器应该稍后阻止的库,您可以使用libmyfilter.so使链接器在过滤器之前加载它。

    到目前为止,我不会说这会使漏洞成为不可能,但攻击者如果你设计了你的系统调用过滤器,就必须在Linux中找到可利用的漏洞内核(在seccomp或你允许它使用的内核子集中)和你的VM,这很可能非常困难。在更可能的情况下,我忽略了某些事情(再次),虚拟机将仍然是一个有用的防线。