最近在研究基於linux的OJ系統,然后想自己寫一系列文章記錄自己這段時間的學習成果。
首先,從原理上講,OJ功能實現並不難,最主要解決的是安全性問題。總結一下,而安全性方面問題主要是用戶可能提交惡意不友好的代碼。關於如何過濾這些不安全的代碼,我從網上收集整理了許多資料,大體上思路如下:
先說錯誤的做法:
1.所有的字符串過濾都是不靠譜兒的,坑人坑自己,C語言強大的宏幾乎沒有繞不過的字符串過濾,而且誤傷也是很常見的,比如,你在程序里要是不小心定義一個叫做fork的變量,那么你的程序別指望可以AC了,因為字符串過濾會把你的fork想成創建子進程的那個fork,而這是不被允許的。
2.手工審計頭文件,去掉某些頭文件或者注釋掉一部分是辛苦的且無用的;做了這樣的工作之后你就幾乎不再想去升級編譯器及頭文件了,更可怕的是這個工作需要你對語言,編譯器,連接器有一定程度的了解,而我認為擁有足夠了解的人都會明白這樣做是毫無道理可言的:就算沒有頭文件,沒有了函數原型,調用系統調用的方法還是有一大把,而且也都不是很麻煩。
再說說准備工作:
1.熟悉你的目標系統(windows or linux):必須要了解這個平台下的原生系統調用API是怎么使用的(不然你怎么屏蔽),最好可以了解到匯編層面。必須要了解這個平台下的用戶系統,權限控制,資源限制。最好了解一下進程跟蹤,調試,監控工具或者系統調用,例如Linux下的ptrace。最好要了解目標系統提供的各種沙盒限制功能。
2.了解你的編程語言及工具鏈:必須要了解你的目標語言的特性,及其在一般的OI/ACM比賽中的規定和限制。必須要了解你的工具鏈的功能及各種參數。
3.擁有足夠的編程功底,對於這樣的小的程序,應嚴格杜絕緩沖區溢出之類的BUG。
最后說說我的做法:我的目標平台是Linux,目標語言是C/C++,Java,Python。
1.操作系統層面:
a.時間資源的限制。
內存:我使用了rlimit進行控制,同時也是方便在運行結束后獲得內存使用情況的數據,不過有一個缺點就是如果聲明了超大的空間但是從未訪問過就會不被統計進來,但是觀察到很多ACM或者OI比賽也都是這樣處理的,所有這也不算是一個問題。
時間:首先同樣也是使用rlimit進行CPU時間控制,注意它只能控制CPU時間,不能控制實際運行時間,所以像是sleep或者IO阻塞之類的情況是沒有辦法的,所以還在額外添加了一個alarm來進行實際的限制。按照大部分比賽的管理,最終統計的時間是 CPU 時間。
文件句柄:同樣可以通過 rlimit 來實現,以保證程序不要打開太多文件。不過其實文件這一塊問題是比較多的,如果可行的話最好還是使用 stdio 然后管道重定向,完全禁止程序的文件 IO 操作。
b.訪問控制:
利用低權限用戶nobody ,將程序限制在指定目錄中運行。由於是比賽程序,使用的動態鏈接庫很有限,所以直接靜態編譯,從而使得運行目錄中連 .so 都不需要。
進行必要的權限控制,例如將輸入文件和程序文件本身設置為程序的運行用戶只讀不可寫。
c.權限控制:
監控程序使用 root 權限運行, 完成必要准備后 fork 並切換為受限用戶(比如 nobody )來運行程序。
rlimit 設置的都是 hard limit ,非 root 無法修改。
正確設置運行用戶之后,nobody 受限用戶是無法逃出的。
d.系統調用控制:
上面這些(尤其是第一步)是有很大問題,就算不是 root ,也還能做到很多事情。且不說 fork 之類的,光是那個 alarm ,就可以很輕松的把計時器取消了或者干脆主動接收這個信號。所以最根本的還是需要使用 ptrace 之類的調試器附着上程序,監控所有的系統調用,進行白名單 + 計數器(比如 exec 和 open )過濾。這一步其實是最麻煩的(不同平台的系統調用號不一樣,我們使用的是 strace 項目里頭整理的調用號)。
e.更進一步:
如果你對操作系統更熟悉,那么還有一些更有趣的事情可以做。比如 Linux 下的 seccomp 功能(seccomp - Wikipedia , Chrome Linux 版就在沙盒中使用了這個技術 ),尤其是后期加入了 seccomp-bpf 之后變得更加易用。還比如 SELinux 也可以作為 defend-by-depth 的一環。另外, cgroup 其實也可以用得上。
2.編譯層面:
a.很多編譯工具都提供了強大的參數控制,允許你進行包括禁用內嵌 ASM 、限制連接路徑之類的一些操作。通讀一遍 manpage 肯定會有幫助的。
b.算法競賽的程序推薦靜態編譯,之后控制起來少了動態鏈接庫會方便許多。
c.小 心編譯期間的一些“高級功能”,比如 C 的 include 其實是有很多巧妙的用法,試試看在 Linux 下 #include "/dev/random" 或者 #include "/dev/tty" 之類的(這兩個東西會把網絡上不少二流 OJ 直接卡死……)。
d.不要使用 root 用戶編譯,越復雜的程序越容易有 bug ,萬一哪天出個編譯器的 0day ……
e.考慮給編譯過程同樣進行時間、資源限制以作為額外防護手段。
3.架構層面:
a.運行在虛擬機/容器中
b.快照
c.心跳檢測