在线评测系统(Online Judge)允许用户提交代码,在评测机上运行,并返回运行结果。而用户提交的代码有时是不安全的,它可能会无限创建进程或文件消耗评测机资源,或者建立到远程服务器的连接,给攻击者提供后门。保证评测机安全的方法之一,是使用沙盒(Sandbox)。
资源的限制
一般的评测系统都会限制程序的运行资源,如内存、时间用量,进程、线程数量。资源的限制通常使用 setrlimit()
(Set Resource Limit)来完成。
setrlimit()
的帮助文档(Man Page)非常详细,此处不多做介绍。但值得一提的是,setrlimit()
限制的时间(RLIMIT_RTTIME
)是 CPU 时间,而线程被挂起的时间并不计算在内 —— 如调用 sleep()
时的延时和 scanf()
等待用户输入的时间。为了正确限制目标程序的时间,我们创建一个监视线程,在指定的时间后将目标进程杀死。
系统调用的限制
程序的一切与操作系统有关的操作,比如输入输出,创建进程,获取系统信息等,都需要系统调用(System Call)。如果不限制系统调用,目标程序也可能会有一些危险的行为。比如,目标程序可以获得文件系统的一部分目录结构,如果它运行在与评测系统或 Web 服务器相同的用户下,可能会获取或修改一些影响服务器安全的信息。
目前常用的两个限制系统调用的方案是 ptrace()
和 seccamp
,前者的原理是在目标程序每次尝试进行系统调用时通知评测程序,如果发现危险的系统调用,可以及时将目标程序杀死。但 ptrace()
在每次系统调用时都会产生两次中断(进入系统调用前一次,系统调用返回后一次),影响效率。相比之下 seccamp
可能是更好的选择。
使用 ptrace()
的评测系统有:HustOJ、UOJ;
使用 seccamp
的评测系统有:QDUOJ、TJudger。
基于 Docker 的沙盒
保护系统安全的另一种思路是将目标程序与系统环境隔离,形成一个沙盒(Sandbox)环境。如 chroot
可以保证程序只能访问某个目录下的内容,而 Docker 则可以实现完全隔离的容器。
每次运行目标程序时,创建一个 Docker 容器,将输入数据与目标程序的可执行文件传入,创建一个监视进程,fork()
出子进程调用 setrlimit()
后使用 exec
载入目标程序。
对于需要运行环境的语言(如基于 .Net 的语言和 Python 等脚本语言),我们需要把系统中的库目录映射到容器中 —— 一般不需要考虑这样做的安全性,因为容器中的目标程序也是以普通用户运行的,对系统目录没有写权限;而库目录中一般没有可以读到的配置文件等敏感信息。
Docker 提供的网络功能可以用来禁止容器中的目标程序联网。
因为命令行参数拼接可能导致命令注入,所以在实现中没有直接调用 docker
命令行工具,而是使用了 dockerode
库与 Docker 服务进行通信。
基于 Docker 的沙盒的优点在于不需要限制系统调用,因为一些语言的运行库(如基于 .Net 的语言)或解释器(如 Python 等脚本语言)需要额外的系统调用,而使用对 C++ 等低级语言的限制方法来限制这些语言的程序,可能会将这些系统调用误认为恶意程序而杀死。 缺点在于,创建 Docker 容器需要一定开销,经过测试每次运行需要额外的大约一秒的时间。并且,Docker 不能在 32 位操作系统下运行。
评测程序的实现
沙盒只是评测系统的一部分,我们还需要一个评测程序来完成与沙盒和后端 Web 服务器的交互。
评测程序的主要工作是:
- 循环请求 Web 服务器获取等待评测的记录;
- 如果需要,从 Web 服务器下载测试数据并解压;
- 根据语言选择编译器进行编译;
- 调用沙盒运行目标程序;
- 检验程序输出并评分;
- 将评测结果上传回 Web 服务器。
最后,用一张图来描述整个评测系统的大致架构: