Online Judge 是如何解決判題端安全性問題的?

如何過濾惡意提交的危險代碼??分析了一下hustoj的實現,貌似只是通過創建一個低磁碟讀寫許可權的linux用戶限制磁碟操作,然後用ptrace去過濾系統調用,但是代碼寫的非常混亂(目測是故意的),幾乎沒有啥借鑒的價值了— —。。。但是網路操作啥的貌似都沒做限制……大家有沒有啥好的建議?


其實這就是在做一個沙盒,而一個可靠的沙盒不是那麼簡單的。我簡單說一些高中時寫 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


參考 OnlineJudge 沙箱實現思路 - virusdefender#x27;s blog

很早就關注這個問題,最近我也自己寫了一個簡單的判題,並沒有想像中的那麼複雜。

整體的思路就是判題 fork 出一個進程然後去 exec 用戶提交的代碼,然後在父進程中獲取子進程的狀態。

  • 限制運行時間和 cpu 時間,使用 setitimer setitimer(2): get/set value of interval timer 父進程中捕獲超時信號
  • 限制內存佔用,使用 setrlimit setrlimit(2): get/set resource limits
  • 獲取內存佔用和 cpu 時間,wait4 有個 rusage 參數,wait4(2) - Linux man page
  • 獲取實際運行時間,這個直接時間戳減一下就知道了

當然上面的這些功能需要沙箱配合,否則用戶代碼中很容易就能取消掉你的計時器或者 kill 掉父進程。

沙箱的設計上面不少人也提到了,目前主流方案就是 ptrace 和 seccomp,不過 ptrace 我一直是聽說是效率很低,但是並沒有測試過,所以就直接使用 seccomp 了。

seccomp 的使用方法比較有侵入性,需要在自己的代碼中載入策略,不像是 ptrace 可以在父進程中控制每個系統調用,參考了一些代碼以後,就決定在 execve 之前載入 seccomp 策略,同時需要為 execve 和 glibc 初始化增加一些系統調用,但是這些系統調用也可能會產生危險行為,所以還是增加參數限制,比如 execve 第一個參數只能是執行的可執行文件路徑,write 只能使用 fd 0 1 2 等。

----------------------
這一段是以前的方案,存在繞過,見評論
使用 Linux 的動態鏈接庫,可以 hook __libc_start_main 函數,這樣就可以在用戶的 main 函數之前載入 seccomp 策略了。這樣的好處還有沙箱和判題可以完全分離,只需要增加一個環境變數LD_PRELOAD,就自動載入了安全策略。
------------------------

對於 c/c++ 的提交來說應該是足夠了。

至於 Java,我是使用的自帶的安全策略,不使用沙箱。

上面提到的代碼都在 QingdaoU/Judger

主要邏輯完成了,還有部分細節需要處理,也暫時沒有文檔。使用方法見 demo.py,如果有任何問題,可以提出~~

開發過程中主要參考了 GitHub - quark-zju/lrun: Run command on Linux with resources limited. 和 daveho/EasySandbox: really simple sandboxin...

lrun 是我們現在在用的,但是因為涉及到 cgroup,需要完成的 root 許可權才行,而我是跑在 docker 中的,這個會導致 docker 的隔離性變弱。EasySandbox 的設計就是 hook __libc_start_main 的思路來源,但是它限制的太嚴了,malloc、exit 之類的函數都是自己實現的,導致過於複雜。

再加個廣告,我們開發的 OnlineJudge 系統 QingdaoU/OnlineJudge: Open source online judge based on Python, Django and Docker. 也開源了,歡迎給個 star。


之前寫過一個這樣的系統,09年的時候,那時候也是同樣遇到這些問題,思路類似,構建一個sandbox,filter syscall。 Online Compiler!

1. Then Google Native Client
後來遇見了chrome,在看其native client的實現,其實就是OJ後台的一套sandbox系統。
核心部分,構建的2套sandbox

? inner sandbox: binary validation
? outer sandbox: OS system-call interception

--&> inner sandbox
Protection Rule For Inner Sandbox

--&> outer sandbox
Based on systrace.

Reference:
http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/34913.pdf
http://www.citi.umich.edu/u/provos/papers/systrace.pdf

2. ZeroVM
zerovm/zerovm · GitHub

APP Sandbo領域新出生的創業公司,核心思路如下

ZeroVM creates a sandbox around a single process,
using technology based on Google Native Client (NaCl). The sandbox ensures that the application executed cannot access data in the host operating system, so it is safe to execute untrusted code.

所以,如果不想自己造輪子,可以用ZeroVM。過幾天,把我那個系統試試移到ZeroVM上~~

3. Qubes OS
看下architecture 感受下,

著名的波蘭美女黑客Joanna Rutkowska設計的,基於Xen、X和Linux的新開源操作。
它充分利用了虛擬化技術(基於安全虛擬機Xen),所有用戶應用程序都運行在AppVM(基於Linux的輕量級虛 擬機)中,彼此隔離。
也能利用,但是用來跑OJ可能有點大材小用了。

歡迎討論~


給ZOJ寫過Patch的來回答一下。

ZOJ的沙箱是ptrace。但是ptrace的規則是硬寫進去的,規則寫的也比較複雜。比如:我記得當時glibc升級之後,由於某些安全功能(貌似是pointer guard?),需要在程序啟動的時候讀取/dev/urandom。

沙箱的內存控制是簡單的setrlimit。

ptrace倒是並不慢,因為OJ的題目大部分很少頻繁的調用system call。如果很頻繁,那恐怕那個程序本身就是攻擊的惡意程序。

前面說用語言環境運行時而非動態沙箱的解決方法,我覺得做起來難度比較大。我對JIT Spraying攻擊不了解,但是個人感覺語言運行時的JIT是一個很大的attack surface。而且還有,OJ里用Java和C#的比較少,部分原因是IO庫比較慢,而且有些OJ問題對性能要求實在是太高了,OJ提交者往往喜歡對一切過程都有控制。

如果說其他的選擇,我會覺得虛擬化是個很好的選擇。唯一的attack surface就是虛擬機和硬體本身


如果提交C#程序的話,可以在運行的時候直接把Assembly的許可權調到最低,基本上只能往屏幕上打字了,什麼都幹不了。


How do sites like codepad.org and ideone.com sandbox your program?
下面有個回答講得很詳細


寫的亂是事實,不是故意的,就是懶啊。iptable補漏在wiki寫了的。
求不黑


曾經在我們學校里做過online judge,用的好像是POJ當時的一個demo,在Windows上弄的。

當時做法也沒考慮太多,先是建立一個guest賬戶,用guest賬戶運行代碼,所有許可權全限制在某一個盤裡,大不了就廢了一個盤,也無所謂。

反對匿名用戶說的不危險,實際上OJ這種東西太危險了,允許上傳+執行許可權,危險特別大。

把網頁部分和代碼分開,我忘記當時我們是用一個賬戶還是兩個賬戶,反正網頁的路徑是一個很古怪的路徑,這樣入侵者也不太好在頁面上掛馬,我記得頁面好像是PHP或者JSP之類的。

在編譯器和連接器上做了點手腳,一共有幾層防禦:

第一層是把標準庫里的頭文件先都注釋掉,包括文件操作、還有system、網路操作等等,對於一般的菜鳥就足夠了,大多數菜鳥沒了頭文件都不知道該怎麼辦。

第二層是徹底幹掉C++,我記得當時我們用的是MinGW,直接不安裝G++組件,因為G++的庫太複雜了,像cin/cout這些不好控制

第三層是修改鏈接庫,當時在大學時候技術也很一般,我記得方法很糙,就是找到lib文件,用winhex之類的工具打開,找到fopen這些直接把所有敏感的字元串全換掉。實際上允許用的就string庫和stdlib這些,這樣連接器也找不到符號。

這樣下來入侵者要是想通過標準庫的話基本就很困難了。

在上傳頁面上也要做限制,比如,禁用彙編內聯,直接通過過濾字元串asm實現,必要的時候做一個WindowsAPI的過濾表,在上傳代碼的時候就過濾掉所有WindowsAPI,但這個很困難,因為代碼里可以不用字元串。

這樣折騰下來,基本上把主要的入口都封死了。然後關鍵的一步:伺服器網卡上關掉所有的埠,僅限於某幾個埠開放(80/8080之類的)。

但是現在想想,並不是特別的安全,比如上傳代碼如果自己實現一套LoadLibrary然後直接調用WindowsAPI的話,還是可以入侵的。

至於說限制運行時間的,這個太困難了,1秒鐘夠執行很多指令了,沒意義。

更穩妥的方法是限制註冊,但這已經不是技術範疇了。

我能記得的就這些了。


自己沒做過這玩藝,不過也稍微想了下。僅考慮目標平台是 Windows + Mingw 的狀況。。
1、頭文件進行詳細審計,Mingw 的 .a進行重編譯 刪除各種可能有危險的東西,以及一些 Windows API的調用
2、重新編譯編譯器,刪除內聯彙編功能
3、在編譯默認參數中打開編譯器和鏈接器的nxbit和ASLR支持,並確認這兩個功能在所在平台有效
4、做一個服務,自動監視進程的內存使用,運行時間之類 發現異常就終止之,


最近正在寫OJ,說下我的經驗。
說下我的解決方法,直接丟到docker上,什麼安全性都不用管了,跑壞了也無所謂,把輸入輸出連到host上就行,把docker當沙箱。
資源限制什麼的其實用rlimit不好,原因很簡單,rlimit在時間上只能限制real cpu time但是不管waitting time,隨便一個sleep就沒辦法了,而且rlimit限制資源,你真的對你面向的語言的內存分配非常了解么?反正我要兼容C和Go就折騰了我好一陣子我還沒考慮java什麼的呢。
而我的方法是檢查/proc/{pid}/stat 目錄下的資源,因為API調用都是看這個目錄的。
至於編譯的情況,還真沒考慮,參考樓上的想法再解決一下。


最徹底的做法是用 bochs + 定做一個最小化的操作系統,編譯器完全設定為交叉編譯,編譯完了用 bochs 跑,用虛擬的串口 IO。這樣做出來的是能防小人的,比如要是有人把某函數指針指向的內存里故意寫 int 80h,這方法能保證防住。


我們大學的時候做的OJ對於這個問題處理極其簡單:
1、使用一個許可權極度少的windows賬號,除了一個隨機名字的臨時目錄以外其他什麼路徑許可權都沒有。
2、屏蔽掉windows.h。和一堆頭文件。
那是懵懂時期,僅供參考


http://www.51nod.com/onlineJudge/judgeStatus.html使用的是windows操作系統。
使用一個無許可權的用戶,僅僅允許讀取與執行目標文件目錄,其它全部設為拒絕訪問。
使用job限制線程與進程的創建,物理內存的使用。
服務運行在host only模式的虛擬機中,隔絕廣域網。


哈哈,知乎處女答。

我來說說我當時做的judge吧。

那時答主還在上大二大三。當時市面上的oj主要應該就是poj和zoj兩家。我們學校的oj是基於Linux的,也是用了ptrace之類的東西。我閑著無聊就說做個Windows的玩玩。

其實一個程序能幹嘛主要還是靠syscall來實現。所以當時的想法是直接截取syscall。這樣一來能訪問哪些文件,每次申請多少內存,禁止不相關的syscall,都可以自由控制。考慮到最初的目的是娛樂,所以並不打算搞個驅動來做這件事。權衡各種方法後,最後的實現是以debug模式載入程序,在程序開始執行前,注入自己的dll,然後修改nt.dll中進syscall的stub,這樣所有系統調用都會先進到我們自己dll里的code。只要是正常編譯的程序(因為提交的是源代碼,直接編譯就可以了),最後系統調用一定是從nt.dll走的。這樣就實現了運行時restrict function call的功能。

關於運行時間,我配置的是每15ms中斷一下,由控制程序決定是否繼續運行,這樣,一個程序最多超額運行15ms,我覺得完全可以接受。

當然,實際使用為了安全性還有其他一些需要注意的地方。首先,要控制程序的編譯時間,還有生成的程序大小。這樣就防止了.data段超過允許內存大小。然後在注入dll的時候還要檢測內存用量,以防一個超大的.bss段。我用的是PrivateBytes。其後的內存分配可在syscall時控制。最後,編譯的時候要禁止彙編,然後還要禁止非代碼段執行,防止程序自己進syscall。

大概就是這樣。憑記憶寫的,如果不對不負責啊哈哈哈。

歡迎討論。

PS C/C++的程序大概允許十幾二十個syscall就可以正常運行了。Java的要多一點,還會載入更多的dll。


linux上有個工具,叫ulimit。
教程鏈接:通過 ulimit 改善系統性能
可以限制內存,CPU時間等。

剩下的如許可權控制,就如樓上幾位大牛說的那樣。


跑在docker容器裡頭我覺得是目前比較好的選擇…


編譯時間/輸出限制(比如有些代碼很短但是能輸出巨量編譯錯誤信息)
限制syscall(用ptrace啥的限制白名單)(這個才是王道,上面啥基於字元串的限制都是浮雲)
限制運行時間啥的太簡單了。
guest低許可權用戶+chroot監獄。

如果還要防止網路部分攻擊的話:
judge server與website之間採用rsa加密傳輸。或者rsa商量個一次一密的對稱加密傳輸。
記錄時間戳防止重放攻擊。

至於其他dDos啥的就太general了……此處不談……


我有點不理解,如果可以信任linux內核是安全的話,那麼只要以一個許可權極低的身份來編譯和運行提交的程序不就可以了嗎?這樣用戶程序能對系統做出的破壞再怎麼也不會超過這個用戶的許可權範圍啊。


Linux下控制許可權的除了常用的DAC許可權系統,還可以使用SELinux的MAC許可權系統。新建一個Linux用戶,將其SELinux用戶設置成user_u就能夠屏蔽該用戶的大多數的許可權操作。


Docker


順便說下
ptrace直接用的效率很垃圾
seccomp的效率很好但是只能殺。
但是seccomp有個叫做trap的
在觸發的時候給一個trap


推薦閱讀:

世界歷代間諜衛星信息(圖片,文字,代碼……)收集後,回傳方式經歷了哪幾個時期?
如何評論阿里云云盾負責人的這篇《危機時刻,我只心疼我們的客戶》?
如何看待白帽子在烏雲網提交世紀佳緣網漏洞後被抓?
如何看待 2017 年 5 月 12 日中國大量高校及公共設備發生電腦中毒,勒索比特幣的事件?
為啥現在大多數手機開機後不能直接使用指紋?

TAG:Linux | 網路安全 | 信息安全 | OnlineJudge |