【问题标题】:Compiling a program with limited library access编译具有有限库访问权限的程序
【发布时间】:2015-01-01 14:21:21
【问题描述】:

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

知道我该怎么做吗??

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

谢谢

【问题讨论】:

  • 也许你可以使用预处理器。
  • 你试图做的是一个坏主意。人们总是可以绕过这些限制。
  • @FUZxxl 如果他们不能呢?例如,我有一个项目,我将自己限制为 ISO C89 stdlib 函数,并且不依赖任何常用但特定于操作系统的函数,因为我希望我的代码即使在 Windows 上也能编译 (一方面,它不支持通过snprintf() 进行安全字符串格式化)。
  • @TheParamagneticCroissant 我是从安全的角度说的。人们可以在一些数组中组装自己的函数,然后跳转到那里。#

标签: c linux gcc compilation


【解决方案1】:

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

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

你也可以用你特制的libc链接,使用LD_PRELOADptrace等。

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

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

【讨论】:

    【解决方案2】:

    我想我参加聚会有点晚了,但我觉得到目前为止给出的答案都不是完全正确的。事实上,可以按照您要求的方式限制程序的功能,并且可以明智地这样做。

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

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

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

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

    在虚拟机中运行所有程序会使您的主系统更难利用.有一些免费的实现可以很好地工作并且设置起来并不难。我大部分时间都使用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 或类似的东西修补代码可能是一个想法,但对于安全关键型应用程序来说太不可靠了。在使用execv 调用程序之前禁止系统调用的“安全加载程序”会遇到无法禁止execve 系统调用的问题,这是最想禁止的系统调用之一。此外,execvmain 函数之前需要一堆其他系统调用(例如accessmmapopenfstatclosemprotectarch_prctl)该程序的甚至被输入。那么该怎么办呢?

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

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

    代码如下:

    #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 是包含libmyfilter.so 的目录。

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

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

    【讨论】:

    • 如果在用户代码中定义了自定义的__libc_start_main,你的seccomp_load.so中的__libc_start_main函数将被绕过。
    • @virusdefender:哦,这是一个很好的反对意见。我根本没想到。嗯...你认为在运行加载程序之前检查编译的二进制文件是否有一个未定义的符号__libc_start_main 就足够了吗?尽管现在您已经提到了这一点,但我不确定 gcc 是否会被欺骗生成不会尝试首先运行 __libc_start_main 或根本不会使用某种内联汇编的二进制文件。我会好好思考的。如果我不能想出令人满意的东西,这是一个足够大的洞,我将不得不删除答案,尽管眼含泪水。我真的很喜欢这个。
    • @virusdefender:我认为我有一种方法可以通过依赖运行时链接器调用 seccomp 过滤器代码而不是劫持__libc_start_main 来规避问题。我并不是要强加,但如果您有兴趣并有时间,请您看一下并告诉我您的想法,我将不胜感激。
    • @Wintermute 我的方法是在 execve 之前加载 seccomp 并为 execve 和 libc 初始化添加一些额外的规则。 github.com/QingdaoU/Judger/blob/master/runner.c#L171
    • @virusdefender 我考虑过这种方法,但我真的希望能够禁止 execve。
    【解决方案3】:

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

    您可以做的事情很少:

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

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

    【讨论】:

      猜你喜欢
      • 2011-04-27
      • 2013-07-24
      • 2020-03-10
      • 1970-01-01
      • 2011-12-26
      • 1970-01-01
      • 2020-07-21
      • 2013-01-25
      • 2017-10-08
      相关资源
      最近更新 更多