如何设计一个安全沙盒

在 OJ 中,沙盒是必不可少的一环,服务器无法相信用户提交的代码,这可能导致很多安全问题。

总体上来说,沙盒需要做到两点:

  • 对系统调用进行限制
  • 对资源使用进行限制

系统调用限制

一般来说,系统调用的拦截有两种办法:

  • ptrace(2)
  • seccomp(2)

ptrace

这是一种开销较大的方法,拦截tracee的系统调用的开始结束,两次切换用户态和内核态。

GDB 的基本调试功能就是这么实现的,不过对于一个追求执行速度的沙盒来说,这个方法不可取。

seccomp

Secure computing,自 Kernel 2.6.12 引入,这是一种内核态下的过滤机制,最初限制程序只能用四个系统调用,用于运行低权限应用

1
read,write,_exit,sigreturn

要对所有系统调用进行拦截,要用到的是是Seccomp-BPF,自 Kernel 3.5 引入,BPF最初是一种网络封包过滤代理,此后被广泛运用,包括这里。

由于其本质上是运行在内核的一个虚拟机,所以需要编写字节码,略微麻烦,所以有libseccomp去帮助人们方便快捷地编写规则

下面是使用libseccomp的一个例子,实现了对execve的禁用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>

int main(void){
// filter context
scmp_filter_ctx ctx;
// using whitelist
ctx = seccomp_init(SCMP_ACT_ALLOW);
// send SIGSYS when this program is calling execve
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
// load filter
seccomp_load(ctx);
// Let's give it a try
char * str = "/bin/sh";
write(1,"hello worldn",12);
// 59 is syscall number of execve
// The program will receive SIGKILL after calling
syscall(59,str,NULL,NULL);
return 0;
}

接下来介绍一些对特定语言的设置

C/CPP

对于 C 和 CPP 这种较为底层的语言,通常使用白名单,对权限进行严格管控

Java/Python

对于带有虚拟机/解释器的语言,通常使用黑名单模式,避免出现错杀情况

一些特殊情况

有时候需要根据沙盒实现进行特殊限制,比如沙盒使用setitimer时,alarm(0);可以直接取消设定好的计时器,从而破坏沙盒功能

系统资源限制

时间

程序的时间可以分为两部分:

  • CPU 时间
  • 实际运行时间

由于操作系统的调度,程序主动sleep,阻塞 I/O 等情况,程序的实际运行时间要大于 CPU 时间

setrlimit

这个调用可以实现对多种资源的限制,对于时间,它只能限制 CPU 时间

  • RLIMIT_CPU

    这个只能精确到秒,可以用但是不建议

  • RLIMIT_RTTIME

    可以精确到微妙,但是只能在特定的调度模式下使用,比较复杂,未使用

setitimer

此调用可以限制 CPU 时间和实际时间

  • ITIMER_REAL

    decrements in real time, and delivers SIGALRM upon expiration.

  • ITIMER_VIRTUAL

    decrements only when the process is executing, and delivers SIGVTALRM upon expiration.

  • ITIMER_PROF

    decrements both when the process executes and when the system is executing on behalf of the process. Coupled with ITIMER_VIRTUAL, this timer is usually used to profile the time spent by the application in user and kernel space. SIGPROF is delivered upon expiration.

计算 CPU 时间时,一般不算入内核态时间,所以使用ITIMER_VIRTUAL

内存

这是一个很麻烦的地方,setrlimit给了两个相关资源

  • RLIMIT_AS

    虚拟内存大小

  • RLIMIT_RSS

    Resident Set Size,常驻内存大小

一般来说,只能限制虚拟内存大小,RSS 不能作为可靠观测指标

一般情况下进程虚拟内存的大小都明显大于实际使用的物理内存,所以需要在限制时给出一个大于目标值的参数,如两倍大小的给定内存数

What is RSS and VSZ in Linux memory management - Stack Overflow

栈内存

使用setrlimit,RLIMIT_STACK

可以这样限制,但是需要特别的方式进行判断,详见 man page

输出大小

这里是指The maximum size of files that the process may create.

所以,这里用来限制重定向 STDOUT 到文件后的输出

使用setrlimitRLIMIT_NPROC

线程数量

使用setrlimitRLIMIT_NPROC

其他一些细节

Guest 环境变量

由于沙盒是通过execve实现的,所以给 Guest 传递什么变量就需要考虑一下

环境变量可以通过编译器提供的外部变量获取

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
#include <stdio.h>
extern char **environ;
int main()
{
// print the environment variables
for (int i = 0; environ[i] != NULL; i++)
printf("%s\n", environ[i]);
}

通过在力扣等多个网站测试,只提供最基本的PATH应该是 OK 的

1
PATH=/usr/local/bin:/usr/bin:/bin

Guest 的 argv[0]

根据标准,它应该是一个 可以代表程序的名字

所以这里完全可以自定义为guest

参考

Author

BakaFT

Posted on

2022-10-21

Updated on

2023-12-28

Licensed under

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×