我想用gcc和glibc(或任何其他c库)编译C程序 但我想限制程序访问某些功能 例如,如果程序使用套接字或信号处理函数,则不应编译程序。
任何想法我怎么能这样做?
顺便说一下,我想在一个简单的编程竞赛评判中使用它
由于
答案 0 :(得分:3)
你不能可靠地限制对某些功能的访问,因为有动力的开发人员总是找到解决方法。例如,他可以使用dlsym
在运行时查找某个函数的地址,或使用asm
代码调用某些系统调用(或使用缓冲区溢出技术)或假设特定版本的{{1}二进制并计算一些函数指针(例如通过使用内置偏移量抵消某些合法libc
函数的地址,如libc
),或者将一些文字字符串(包含合适的机器操作码)转换为函数指针等等......
但是,您可以考虑自定义编译器(例如,如果使用最新的GCC进行编译,使用您的MELT扩展名进行自定义)以检测常见情况(但不是所有情况)。这可能意味着开发此类编译器定制的数周工作。
您也可以与经过特制的printf
关联,使用libc
或LD_PRELOAD
等。
为了可靠地禁止某些行为,您应该在某个虚拟容器内运行。
PS。 Statically(合理且可靠地)检测到某些源代码永远不会调用给定的一组函数undecidable,因为它等同于halting problem。
答案 1 :(得分:2)
请不要那样做。即使你找到一种方法来禁止像execl
这样的某些功能,也有很多方法可以解决这些限制。例如,程序可以使用内联汇编或其他技巧自行调用操作系统。
你可以做的事情很少:
如果您只想检测某个程序使用了不允许的函数,您可以对编译器创建的二进制文件运行nm
,并检查是否出现任何不允许的函数名称。请注意,并非所有函数都具有与其名称相同的符号名称。
答案 2 :(得分:2)
我想我在派对上有点迟了,但我觉得到目前为止给出的答案都不完全正确。事实上,可以按照你要求的方式限制程序的功能,并且可以这样做。
确实,防止对任意函数的调用虽然也可能是毫无意义的 - 但这就像用孔密封漏勺一样。它也没有提出正确的问题 - 我怀疑你没有想要阻止编码人员计算数字的平方根,而是阻止他拥有系统。这意味着要防止他让系统做某些事情,这些事情总是涉及系统调用,因此关注它们而不是功能是有意义的。用于打开套接字的功能并不重要;他们最终都使用socket
系统调用。
内核可以控制对系统调用的访问 。 Linux内核有一种称为 seccomp 的机制,它被各种大型程序(如Firefox,Chrome和Adobe Flash)用于沙箱化他们的代码解释器和一些较小的程序如vsftpd,以最大限度地减少其攻击面。攻击者设法找到远程执行代码漏洞的事件(基于漏洞利用代码发现自己严重受限而无法调用exec
和其他人)。
现在,在我详细介绍之前:如果你要从不认识的人那里获取代码(因此无法信任),偏执就是理智。 Seccomp很好但在这种情况下还不够,因为这种情况是攻击者梦寐以求的。最好是堆叠防御,而不是精力充沛。所以,你要做的前三件事是:
在虚拟机中运行所有程序会使得利用主系统变得更加困难,因为除了之外,攻击者还必须打破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
需要大量其他系统调用(例如access
,mmap
,open
,fstat
,close
,{{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,这很可能非常困难。在更可能的情况下,我忽略了某些事情(再次),虚拟机将仍然是一个有用的防线。