Online Judge 是如何解决判题端安全性问题的?

如何过滤恶意提交的危险代码??分析了一下hustoj的实现,貌似只是通过创建一个低磁盘读写权限的linux用户限制磁盘操作,然后用ptrace去过滤系统调用,但是代码写的非常混乱(目测是故意的),几乎没有啥借鉴的价值了— —。。。但是网络操作啥的貌似都没做限制……大家有没有啥好的建议?
关注者
909
被浏览
41,931
其实这就是在做一个沙盒,而一个可靠的沙盒不是那么简单的。我简单说一些高中时写 OJ 获得的经验,抛砖引玉。

几个错误做法:
  1. 所有的字符串过滤都是耍流氓,坑人坑自己:C语言强大的宏几乎没有绕不过的字符串过滤,而且误伤也是很常见的(我就见过小白 OIer 问为什么程序老是被判非法,结果一看里头有个变量叫做 fork )。
  2. 手工审计头文件,去掉某些头文件或者注释掉一些部分是辛苦且无用的:做了这样的工作之后,你就几乎再也不会想去升级编译器及头文件了,更可怕的是——这个工作需要你对语言、编译器、连接器有一定程度的了解,而我认为拥有足够了解的人都应该知道这是不靠谱的:就算没有头文件、没有了函数原型,调用系统调用的方法还是有一大把而且都不是很麻烦。

准备工作:

  1. 熟悉你的目标系统(Windows or Linux):
    1. 必须要了解这个平台下的原生系统调用 API 是怎么使用的(不然你要怎么屏蔽?),最好可以了解到汇编层面。
    2. 必须要了解这个平台下的用户系统、权限控制、资源限制。
    3. 最好要了解一下进程跟踪/调试/监控工具或者系统调用,例如 Linux 下的 ptrace 。
    4. 最好要了解目标系统提供的各种沙盒限制功能。
  2. 了解你的编程语言及工具链:
    1. 必须要了解你的目标语言的特性,及其在一般的 OI / ACM 比赛中的规定、限制。
    2. 必须要了解你的工具链的功能及各种参数。
  3. 拥有足够的编程功底,对于这样小的程序,应当严格杜绝缓冲区溢出之类的 bug 。

然后我再说说我的做法,在其中大家就可以看到上面列的这些“准备知识”是如何派上用场的。我的目标平台是 Linux ,目标语言是 Pascal 、 C 、 C++ 。


我采取了以下措施:

  1. 操作系统层面:
    1. 时间、资源的限制:
      1. 内存:我使用了 rlimit 进行控制,同时也方便在运行结束后获得内存使用情况的数据,不过有一个“缺点”就是如果是声明了一个超大的空间但从未访问使用就不会被统计进来(经过观察发现很多 ACM 或者 OI 比赛也都是这么处理的,所以应该不算是一个问题)。
      2. 时间:首先同样也是使用 rlimit 进行 CPU 时间控制。注意它只能控制 CPU 时间,不能控制实际运行时间,所以像是 sleep 或者 IO 阻塞之类的情况是没有办法的,所以还在额外添加了一个 alarm 来进行实际时间的限制。按照大部分比赛的管理,最终统计的时间是 CPU 时间。
      3. 文件句柄:同样可以通过 rlimit 来实现,以保证程序不要打开太多文件。不过其实文件这一块问题是比较多的,如果可行的话最好还是使用 stdio 然后管道重定向,完全禁止程序的文件 IO 操作。
    2. 访问控制:
      1. 通过 chroot 建立一个 jail ,将程序限制在指定目录中运行。由于是比赛程序,使用的动态链接库很有限,所以直接静态编译,从而使得运行目录中连 .so 都不需要。
      2. 进行必要的权限控制,例如将输入文件和程序文件本身设置为程序的运行用户只读不可写。
    3. 权限控制:
      1. 监控程序使用 root 权限运行, 完成必要准备后 fork 并切换为受限用户(比如 nobody )来运行程序。
      2. rlimit 设置的都是 hard limit ,非 root 无法修改。
      3. 正确设置运行用户之后,之前由 root 创造的 jail 受限用户是无法逃出的。
    4. 系统调用控制:
      上面这些(尤其是第一步)是有很大问题,就算不是 root ,也还能做到很多事情。且不说 fork 之类的,光是那个 alarm ,就可以很轻松的把计时器取消了或者干脆主动接收这个信号。所以最根本的还是需要使用 ptrace 之类的调试器附着上程序,监控所有的系统调用,进行白名单 + 计数器(比如 exec 和 open )过滤。这一步其实是最麻烦的(不同平台的系统调用号不一样,我们使用的是 strace 项目里头整理的调用号)。
    5. 更进一步:
      如果你对操作系统更熟悉,那么还有一些更有趣的事情可以做。比如 Linux 下的 seccomp 功能(seccomp - Wikipedia , Chrome Linux 版就在沙盒中使用了这个技术 ),尤其是后期加入了 seccomp-bpf 之后变得更加易用。还比如 SELinux 也可以作为 defend-by-depth 的一环。另外, cgroup 其实也可以用得上。
  2. 编译层面:
    1. 很多编译工具都提供了强大的参数控制,允许你进行包括禁用内嵌 ASM 、限制连接路径之类的一些操作。通读一遍 manpage 肯定会有帮助的。
    2. 算法竞赛的程序推荐静态编译,之后控制起来少了动态链接库会方便许多。
    3. 小心编译期间的一些“高级功能”,比如 C 的 include 其实是有很多巧妙的用法,试试看在 Linux 下 #include "/dev/random" 或者 #include "/dev/tty" 之类的(这两个东西会把网络上不少二流 OJ 直接卡死……)。
    4. 不要使用 root 用户编译,越复杂的程序越容易有 bug ,万一哪天出个编译器的 0day ……
    5. 考虑给编译过程同样进行时间、资源限制以作为额外防护手段。
  3. 架构层面:
    1. 运行在虚拟机/容器中
    2. 快照
    3. 心跳检测

……


你会发现,其实主要的限制都是在操作系统层面完成的。我认为,这样做才能带来更高的安全性,因为引发、启动危险操作的方法有很多,很难一一杜绝(包括源码分析、编译时限制等),但最后要让这些危险操作起效几乎都需要落回系统调用上,所以直接从这里下手也许会是个更好的办法。


我对于 Windows 不了解,不知道 Windows 下该如何实现以上的类似功能,或者是否情况完全不同,欢迎大家补充。


最后是我之前写的沙盒项目,写得很丑,尤其是 ptrace 一块目前还比较坑(64位系统下好像还无法正常工作),总的来讲还只能算是一个 demo 而已:Hexcles/Eevee · GitHub