我想我参加聚会有点晚了,但我觉得到目前为止给出的答案都不是完全正确的。事实上,可以按照您要求的方式限制程序的功能,并且可以明智地这样做。
确实,阻止对任意函数的调用是毫无意义的,虽然也是可能的——这就像一个接一个地密封漏勺。这也不是在问正确的问题——我怀疑你不想阻止编码人员计算数字的平方根,而是阻止他拥有系统。这意味着要防止他让系统做某些事情,这些事情总是会涉及系统调用,所以关注它们而不是函数是有意义的。使用哪个函数打开套接字无关紧要;他们最终都使用socket 系统调用。
对系统调用的访问可以由内核控制。 linux 内核有一个称为 seccomp 的机制,各种大型程序(如 Firefox、Chrome 和 Adobe Flash)使用它来沙箱其代码解释器和一些较小的程序(如 vsftpd)以最小化它们的攻击面攻击者设法找到远程代码执行漏洞的事件(基于漏洞代码会发现自己受到严重限制而无法调用exec 和其他人)。
现在,在我详细介绍之前:如果您要从您不认识(因此无法信任)的人那里获取代码,那么偏执狂就是理智。 Seccomp 在这种情况下很好,但还不够,因为这种情况是攻击者的梦想。最好是堆叠防御,不要为微妙而烦恼。因此,您必须为此做的前三件事是:
- 使用虚拟机
- 使用虚拟机
- 说真的,使用虚拟机。
在虚拟机中运行所有程序会使您的主系统更难利用.有一些免费的实现可以很好地工作并且设置起来并不难。我大部分时间都使用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 系统调用的问题,这是最想禁止的系统调用之一。此外,execv 在main 函数之前需要一堆其他系统调用(例如access、mmap、open、fstat、close、mprotect 和arch_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 仍然是有用的防线。