Vakuum開發筆記02 核心與安全問題

3.judger核心設計

評測系統最重要部分就是評測核心了(judger)。核心judger負責了編譯、執行、檢查三大部分,也就是評測系統的靈魂所在,因此judger設計的好壞,直接影響到整個評測系統的整體水準。judger的設計要考慮到幾個方面,首先是對安全性要求很高。別忘了,這是一個在線評測系統,任何人都可以提交任何代碼,並在服務器上執行,這意味着給駭客們提供了方便之門。駭客們(注意,不是黑客)總是希望得到一個Shell,並在上面執行想要的命令。而評測系統直接允許了源碼的提交,如果我是駭客的話簡直樂壞了。當然,只有傻瓜纔會對用戶提交的代碼直接編譯、運行,就像編譯自己的程序一樣,限制是必不可少的。

不當的方法

一個愚蠢的方案是直接對代碼進行掃描,然後過濾掉某些字符串,如"system"。當然這個過濾列表可能長得匪夷所思,就像某牆,然而繞過它卻是很容易的,例如以下代碼:

#include <stdlib.h>
#define dosomething sys##tem
int main()
{
    dosomething("shutdown");
    return 0;
}

怎麼辦呢?有人會繼續想到,我可以用g++的-E命令,將預處理以後的代碼輸入到一個文件中,然後再檢查關鍵字。這樣的話上面的方法就不行了,不過更厲害的駭客還有辦法。什麼辦法呢?直接按地址調用函數,代碼如下:

#include <stdlib.h>
int main()
{
    int (*func)(char *);
    func = (int (*)(char*))134513676;
    func("ls");
    return 0;
}

上述代碼中的數值就是 system函數的絕對地址,在不同的環境中不一樣,不過獲取這個數值並不困難的。由此可見,依靠過濾源代碼的方式幾乎是行不通的安全性解決方案。如果不依賴檢查源代碼,就要監控程序的“行爲”,這就需要比較複雜的調試器技術了。在Linux下,有一個ptrace函數,可以在程序運行時監測程序的行爲,捕獲接收到的信號。

解決方案

vakuum的執行器executor,就是用的ptrace的原理。主程序爲兩個進程,子進程用來執行用戶程序,父進程用來監視子進程。子進程每進行系統調用的時候,運行就被中斷,由父進程檢查子進程的行爲是否合法,如果不合法就殺掉子進程。對於open調用,還要檢查用戶打開的文件是否被允許。有了這個程序,監控程序的用戶時間和內存也就不難了,用戶時間的監測需要在每個系統調用後檢查,對於內存,在任何一個與內存分配相關的系統調用後檢查一下/proc/{pid}/statm即可。

如此還有一個問題容易被忽略,就是如果用戶程序中長時間不進行系統調用,只是在做一些運算的時候,如何讓程序暫停下來判斷是否超時呢?設置CPU_Limit爲時間限制顯然是不行的,因爲在監控下的用戶程序可能會運行時間更久。不知道大家在用Cena的時候是否發現過一個現象,例如某個程序本來運行0.8秒,設爲1秒時限以後顯示超時,設爲2秒時間反而顯示成了0.8秒。這是怎麼回事呢?很可能就是設定了不合適的CPU_Limit。穩妥的方法是設置爲時間限制的若干倍,但是到底是幾倍不好估計,而且很多時候還會浪費時間。思考問題的根源還在於長時間沒有被暫停,我的解決方法是給父進程設置一個alarm,每秒向子進程發送一個信號,設置子進程接收到信號以後也會暫停,並檢查時間是否超時。這樣的話,超時的程序最多會在不超過時限1秒的情況下被終止。

在這種嚴密的監控下,很難找到突破的方法,至少我至今還沒有想到如何攻破executor的監控。其代價就是降低執行效率了,不過由於我監視的是用戶時間,用戶程序不會被誤判超時,儘管感覺實際執行時間多了不少。如果你有什麼攻破監控想法,歡迎與我聯繫做安全性測試。

潛在的缺陷

雖然我並不瞭解,但是我還是擔心,如果用戶程序中有彙編代碼嵌入,是否可以獲得底層的權限。如果真的可以,我還沒有想到應對的方法。

相關日誌