Analyse linux kernel rust first cve
Author: 堇姬Naup
前言
聽起來噱頭很大,來看一下是甚麼樣的洞
雖然只寫過一點 Rust 對 rust 不熟,如果有錯誤歡迎私訊我更正
Binder & death recipient
這次出問題的地方是在 binder 的地方,他是 android 的一個 IPC 機制,可以用於兩個 process 間的相互通訊
Binder 的詳細原理在以下文章有詳敘
https://hackmd.io/@AlienHackMd/S1qm0vmK5#Binder---Native-Binder-%E5%8E%9F%E7%90%86
這次出問題的地方是在 binder 中負責 DeathRecipient 的部分有關
Binder 是一個有 server-client 概念的東西,當 server 發生意外掛掉的狀況,所有依賴於該 server 的 client 就會收到通知
1 | /// https://elixir.bootlin.com/linux/v6.18-rc1/source/drivers/android/binder/node.rs#L168 |
death_list 是一個 link list,其中會友許多 NodeDeath,這個結構就是表示了通知本身,這個 link list 儲存了所有須要通知的東西
1 | /// https://elixir.bootlin.com/linux/v6.18-rc1/source/drivers/android/binder/node.rs#L896 |
一支 process 可以通過 BC_REQUEST_DEATH_NOTIFICATION 來註冊一個 NodeDeath,並掛到 death_list 中
可以在這裡觀察到
1 | /// https://elixir.bootlin.com/linux/v6.18-rc1/source/drivers/android/binder/process.rs#L1152 |
add_death 就會將 node 加入至 linklist
1 | /// https://elixir.bootlin.com/linux/v6.18-rc1/source/drivers/android/binder/node.rs#L323 |
當完成通知或有其他需要移除 node 會在這裡實現,他會將 node 本身傳入並 remove
1 | /// https://elixir.bootlin.com/linux/v6.18-rc1/source/drivers/android/binder/node.rs#L976 |
漏洞
rust 的編譯器在編譯階段會進行相當多的檢查,基本上有編譯通過,可以說明理論上不會有如 overflow、data race condition、dangling pointer 之類的問題
然而編譯器靜態分析的能力是有限的,並不是所有操作都可以在編譯階段驗證是否正確,而 rust 的準則是,若編譯器檢查了,開發者需要主動的通過 unsafe 來去向編譯器說我驗證過這是安全的,並且在開發社群上,你也需要撰寫 SAFETY 的註解來說明為何是安全的,安全前提條件是甚麼
這次出問題的段落,安全的前提是,NodeDeath 這個節點只可能被插進「它自己的 owner 的 death_list」,所以 NodeDeath 要馬在自己的 death_list,要馬不在
然而出問題的原因就是這件事沒有滿足了
1 | // SAFETY: A `NodeDeath` is never inserted into the death list of any node other than |
原因在 release 功能上
他首先先將 death_list move 到了 stack 上
並且解鎖,然後遍歷這個在 stack 上的臨時鏈表來 set_death
1 | /// https://elixir.bootlin.com/linux/v6.18-rc1/source/drivers/android/binder/node.rs#L536 |
問題就很明顯了,原本聲稱 Node 只會存在在自己的 death_list 中,但現在卻存在在 stack 上的臨時 linklist
也就是說當有兩個線程
線程 1 正在遍歷這個臨時 linklist
線程 2 去對原始 linklist 做 remove
這兩個線程會同時操作到同一塊記憶體的 prev/next pointer
就會破壞這整個 linklist,導致線程 1 戳到不合法 prev/next pointer 而 crash
Patch 的方法也很單純
原本是一次性 mem::take 搬到 stack 上,並解鎖遍歷
改成每次 pop 前都上鎖,pop 後再解鎖
這確保了在遍歷時,list 本身不會被其他線程修改
1 | - let death_list = core::mem::take(&mut self.inner.access_mut(&mut guard).death_list); |
after all
快速 review 完後其實可以發現,由於作業系統需要與硬體、driver 或其他用 C 寫的部分,往往需要更底層的細緻操作,而這是 rust 在編譯期間無法檢查的,也因此需要通過 unsafe 並通過開發者的保證來去確保寫出來的 code 安全性
前陣子看到有蠻多人在吹噓 rust 的,確實他對於 linux kernel 或是其他開發的 project 在安全性上都有相當多的保證,但當遇到編譯器難以保證其安全性時,就需要出動人工來去審視
另外,Rust 雖然在 Linux kernel 中有第一個 CVE,不過這次其實相比起之前 review 其他 CVE,脈絡是相當乾淨的,原因在於一段 unsafe code,會有一個開發者,必須要去承諾這段 code 的安全性,並敘述清楚這段 code 安全的前提,這使得讓這個洞的危害降到最小範圍,並且後續追蹤上,能快速的去定位到問題原因,我認為這也是 Rust 一個相當好的地方
近期嘗試寫了一些 Rust 的 code,體趕上我認為 rust 引入 Linux Kernel 會有其他的問題,不過這邊先不多贅述了,總之這是個很好的例子來去觀察及學習 Rust 在 Linux kernel 中的應用及在安全性設計上與 C 的不同
好玩 ~
最後我要抱怨一件事
就是 https://elixir.bootlin.com/linux
這網站我覺得拿來看 code 相當清楚,但是他的 rust code 沒有辦法用 xref 呀XD