跳至主要內容
蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Kobako:為什麼沙盒裡刻意沒有時間?

這篇文章是 Kobako 系列的一部分。

當我們把 RPC、Handle、Block 等機制完成後,已經是一個很好用的 mruby 腳本環境,但是作為沙盒(Sandbox)最關鍵的能力就是「安全性」也就是執行任意腳本時,都當作有危險去看待,那麼就需要有各種防禦情境。

旁路攻擊

旁路攻擊(Side-Channel Attack)屬於相當有名的攻擊手段,尤其像 Kobako 這類專案可以執行完整的 Ruby 語法時,就更容易有機會被攻擊,以下面作為例子。

 1sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))
 2
 3# ...
 4sandbox.eval(<<~SANDBOX)
 5  start = Time.now
 6  UserRepository.find(email: "...")
 7  Time.now - start
 8SANDBOX
 9
10# => 10ms
11# => 1ms
12# => 1ms
13# => 12ms

隨機選擇四個 Email 依序查詢時,發現兩種時間範圍 10ms 左右和 1ms 左右兩種,此時可以猜到有一半的 Email 可能是真實存在的,因為資料庫查詢命中跟非命中有時間差,或者在透過 Kobako 的繫結機制時,因為解析需要比較多時間。

不管理由是怎樣,只要嘗試足夠多,或者使用網路上已經外洩的信箱嘗試,那麼很快的就可以知道這個系統內部是否存在這些使用者,就有機會更進一步做其他嘗試。

雖然前提是沙盒被利用,但是一個專案少一個攻擊點,整體就會更安全,因此在設計時就需要考量要提供怎樣的機制。

凍結時間

Kobako 預設提供的設定叫做 hermetic(密閉)模式,而且不引入 mruby-time 這個原生的擴充,所有跟時間相關的機制都不存在於沙盒內。

簡單來說,如果想問時間,因為沒有 Time 所以問不到。即使想要用其他方式測量,對沙盒來說只會是「經過多少單位」而不是準確的多少秒、毫秒,那麼想要利用旁路攻擊的機會就會變得非常低。

然而,很多情境我們還是需要有「時間」的存在,在大多數情境中,我們並不需要精確度非常高的時間,至少不會有人問像是「幾點幾分幾秒註冊?」這樣的問題,想要篩選使用者頂多精確到幾日就足夠,那麼可以很簡單的利用原本的繫結(Bind)能力,提供一個低精確度的時間。

1sandbox.bind("Date::Today", -> { Date.today.to_s })

這樣想要做到「某個使用者是否存在」這樣的測量,就會因為「一天」的顆粒太粗,不管怎樣嘗試都無法確認,一定程度上就可以避開。

當然,大多數情境並不需要這麼極端的設計,通常只需要提供到秒為單位,就很難大量利用跟嘗試,至少在那之前會先因為沙盒限制或者限速之類的機制被阻擋下來。

1sandbox.bind("CurrentTime", -> { Time.now.to_i })

基本上只要提供像這樣的設計,就非常足夠大多數情境使用。

安全取捨

這個完全封閉的設計在初期 Kobako 的情境下基本上是安全的,但是為了對應 AI 的使用情境,最近開始支援虛擬檔案系統(Virtual Filesystem)以及網路(Networking)的狀況下,又把這些風險帶回到專案。

 1sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))
 2
 3# ...
 4sandbox.eval(<<~SANDBOX)
 5  File.write('/mnt/.timestamp', '')
 6  start = File.mtime('/mnt/.timestamp')
 7  UserRepository.find(email: "...")
 8  File.write('/mnt/.timestamp', '')
 9  File.mtime('/mnt/.timestamp') - start
10SANDBOX

當我們可以存取檔案時,馬上又變成新的破口,檔案的修改時間會被呈現出來,最終還是無法避免時間被推論出來。

也因此在設計虛擬檔案系統時,如果是在 hermetic 模式的狀況下,Kobako 會把 mtime(修改時間)固定在 1970 年的起點(Epoch),這樣就可以封鎖掉這一條路徑。

不過,還是有很多可能的機會,像是這樣的方式

1sandbox.eval(<<~SANDBOX)
2  start = Net::HTTP.get('https://hacker.com/timestamp').to_i
3  UserRepository.find(email: "...")
4  Net::HTTP.get('https://hacker.com/timestamp').to_i - start
5SANDBOX

假設有一台伺服器可以固定回傳時間,只需要讓沙盒中的程式發起請求就好,不過這點難度實際上困難不少,因為網路傳輸如果不是很剛好在非常接近的位置,那麼就沒辦法在夠短的時間內回傳,很可能還是測量不到。

另一種可能的路徑是針對宿主環境的查詢,這樣延遲大多會落在 1ms 內就有機會被利用,因此大多數沙盒對內部網路的存取通常是封鎖的,除了避免橫向移動(找到弱點,再轉移到目標主機)之外,也可以避免這類路徑的應用。

Kobako 的網路功能目前仍在開發中,針對這類問題也有做許多考量,解決容易使用問題後,最大的挑戰就落到如何保持安全的使用環境,要在安全、快速和輕量之間平衡,會是相當困難的挑戰。