Spectre CPU漏洞借着BPF春风卷土重来
By Jonathan Corbet
翻譯整理: 極客重生
https://lwn.net/Articles/860597/
Hi,大家好,昨天不小心看到一篇文章,是關于BPF安全漏洞問題,BPF給內核新功能的開發帶了很大靈活性,尤其是監控和網絡方面,是Linux內核當前最火技術方向之一,但同時也帶來新的安全隱患。?安全和靈活性,總是矛盾的,類似容器和虛擬機的安全之爭,極客們的追求就是不斷挖掘技術的可能性,讓安全性和靈活性都可以滿足。
對eBPF不熟悉可以參考:
Linux網絡新技術基石 |eBPF and XDP
正文
自從披露 Spectre 硬件漏洞(2018年轟動世界的CPU漏洞)以來已經三年多了,但 Spectre 確實是一份不斷給予的"禮物"。當硬件以可預測的方式運行時,編寫正確且安全的代碼就足夠困難了,當處理器可以做隨機和瘋狂的事情時,問題會變得更糟。為了說明所涉及的挑戰,只需查看本公告中描述的 BPF 漏洞即可,該漏洞?已在 5.13-rc7 版本中修復。
對 Spectre 漏洞的攻擊通常依賴于處理器在預測模式下執行一系列在實際執行中不會發生的操作。一個典型的例子是超出范圍的數組引用,即使代碼執行了正確的邊界檢查。一旦處理器發現它錯誤地預測了邊界檢查的結果,錯誤的訪問就會被取消,但預測模式下訪問會在內存緩存中留下可用于竊取數據的痕跡。
在預測模式執行的攻擊方面,BPF 虛擬機一直是一個特別值得關注的領域。大多數此類攻擊依賴于尋找內核代碼片段,當 CPU預測性執行時,該片段可以做出令人驚訝的事情;內核開發人員已經齊心協力消除這些碎片。但是 BPF 的存在是為了能夠從在內核上下文中運行的用戶空間加載代碼;這允許攻擊者制作自己的代碼片段并避免梳理內核代碼的繁瑣任務。
BPF 社區已經做了很多工作來挫敗這些攻擊者。例如,數組索引與位掩碼進行 AND 運算,因此無論它們可能包含什么值,它們甚至無法推測性地到達數組之外。但是很難預測處理器可能會做出令人驚訝的事情的每種情況。
漏洞
例如,考慮以下代碼片段,該代碼片段直接取自?Daniel Borkmann 修復此漏洞的提交:
// r0 = 指向映射數組條目的指針// r6 = 指向可讀棧槽的指針// r9 = 攻擊者控制的標量1: r0 = *(u64 *)(r0) // 緩存未命中2:如果 r0 != 0x0 轉到第 4 行3:r6 = r94: 如果 r0 != 0x1 轉到第 6 行5:r9 = *(u8 *)(r6)6: // 泄漏 r9順便說一下,這個補丁的變更日志(change log)是記錄漏洞及其修復的一個很好的例子,值得一讀:
From 9183671af6dbf60a1219371d4ed73e23f43b49db Mon Sep 17 00:00:00 2001 From: Daniel Borkmann <daniel@iogearbox.net> Date: Fri, 28 May 2021 15:47:32 +0000 Subject: bpf: Fix leakage under speculation on mispredicted branches驗證器僅枚舉有效的控制流路徑并跳過在非推測域中無法訪問的路徑。 因此,它可能會遺漏預測錯誤分支上的推測執行下的問題。例如,以下 精心設計的程序證明了類型混淆:// r0 = 指向映射數組條目的指針 // r6 = 指向可讀堆棧槽的指針// r9 = 由攻擊者控制的標量1: r0 = *(u64 * )(r0) // 緩存未命中2: 如果 r0 != 0x0 轉到第 4 行3: r6 = r9 4: 如果 r0 != 0x1 轉到第 6 行5: r9 = *(u8 *)(r6) 6: // 泄漏 r9由于第 3 行運行 iff r0 = = 0 并且第 5 行運行 iff r0 == 1,驗證器 得出結論,第 5 行的指針取消引用是安全的。但是:如果 攻擊者訓練兩個分支都失敗,從而 推測執行以下內容... r6 = r9 r9 = *(u8 *)(r6) // 泄漏 r9 ... 那么程序將取消引用一個攻擊者控制的值,并可能 通過側信道在推測執行下泄漏其內容。這需要 對分支預測器進行錯誤訓練,這可能相當棘手,因為 分支是相互排斥的。然而,這樣的訓練可以 在用戶空間中使用不 互斥的不同分支在一致的地址上完成。也就是說,通過在用戶空間中訓練分支... A: if r0 != 0x0 goto line C B: ... C: if r0 != 0x0 goto line D D: ... ... 這樣地址 A 和C 分別 與 PHT(模式歷史表)中與 BPF 程序的 第 2 行和第 4 行相同的 CPU 分支預測條目發生沖突。非特權攻擊者可以簡單地 在 PHT 中暴力破解此類沖突,直到觀察到攻擊成功。錯誤訓練分支預測器的替代方法也是可能的 避免暴力破解 PHT 中的沖突。已經 證明了一種可靠的攻擊,例如,使用以下精心設計的程序:// r0 = 指向 [control] 映射數組條目的指針// r7 = *(u64 *)(r0 + 0), training/attack phase // r8 = *(u64 *)(r0 + 8), oob address // [...] // r0 = 指向 [data] 映射數組條目的指針1: if r7 == 0x3 goto line 3 2: r8 = r0 // 精心設計的條件跳轉序列將第193 行中的條件分支與當前執行流程分開3: if r0 != 0x0 goto line 5 4: if r0 == 0x0 goto exit 5: if r0 != 0x0 goto line 7 6:如果 r0 == 0x0 轉到退出[...]187: if r0 != 0x0 goto line 189 188: if r0 == 0x0 goto exit // 加載任何緩慢加載的值(由于階段 3 中的緩存未命中)... 189: r3 = *(u64 *)(r0 + 0x1200) // ...并將其轉換為已知的零以供驗證者使用,同時在執行時緩慢保留加載的依賴項:190: r3 &= 1 191: r3 &= 2 // 推測性地繞過相位依賴項192: r7 + = r3 193: if r7 == 0x3 goto exit 194: r4 = *(u8 *)(r8 + 0) // 泄漏 r4可以看出,在訓練階段(phase != 0x3),第 1 行的條件發生了 變化為 false,因此帶有 oob 地址的 r8 被覆蓋 有效的映射值地址,我們可以在第 194 行中毫無問題地讀出該地址 。然而,在攻擊階段,第 2 行被跳過,并且由于 第 189 行中的緩存未命中,其中映射值(歸零后)添加到 階段寄存器中,第 193 行中的條件由于 先前分支預測器訓練,根據推測,它將 在 oob 地址 r8(此時未知的標量類型)加載字節,然后可能 會通過側信道泄漏。緩解這些問題的一種方法是“分支”一條無法到達的路徑,這意味著 當前驗證路徑一直遵循 is_branch_taken() 路徑 ,我們將另一個分支推送到驗證堆棧。鑒于這是 無法從非推測域訪問,該分支的 vstate 被 明確標記為推測。之所以需要這樣做,有兩個原因: i)?如果僅從推測執行中看到此路徑,那么我們稍后仍 希望消除死代碼以使用?jmp-1s清理這些指令,以及? ii)?確保路徑在非推測域中行走的路徑不會從早期在 推測域中行走的路徑中剪除。此外,為了穩健性,我們 在推測路徑中將作為條件的一部分的寄存器標記為未知, 因為不應對其內容進行任何假設。這里的修復減輕了前面描述的類型混淆攻擊,因為 i)?正在探索的?BPF?程序中的所有代碼路徑以及 ii)?現有的驗證器邏輯已經確保給定的內存訪問指令 引用一個特定的數據結構。在此范圍內也已查看的此修復程序的替代方法是 使用 BPF_JMP_TAKEN 狀態 以及方向編碼(always-goto、always-fallthrough、unknown)在跳轉指令處標記 aux->alu_state , 以便混合不同的always-* 方向本身以及 always-* 與未知方向的混合會導致 驗證器拒絕程序,例如具有像'if ([...]) { x = 0; } else { x = 1; }' 和隨后的 'if (x == 1) { [...] }'。對于無特權者,這 將導致只有單方向始終-* 采取的路徑,并且 允許未知的采用路徑,這樣前者可以從條件 跳轉修補到無條件跳轉(ja)。與這里的這種方法相比,它 有兩個缺點:i) 否則不執行任何 指針運算等的有效程序可能會被拒絕/破壞,以及 ii) 我們 需要關閉非特權的路徑修剪,其中兩個 在這項工作中可以通過將無效分支推送到驗證堆棧來避免。該問題最初是由?Adam?和?Ofek?發現的,后來 作為?Benedict?和?Piotr?的研究工作獨立發現和報告的。 Fixes: b2157399cc98 ("bpf: prevent out-of-bounds speculation") Reported-by: Adam Morrison <mad@cs.tau.ac.il> Reported-by: Ofek Kirzner <ofekkir@gmail.com> Reported-by: Benedict Schlueter <benedict.schlueter@rub.de> Reported-by: Piotr Krysiuk <piotras@gmail.com> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net> Reviewed-by: John Fastabend <john.fastabend@gmail.com> Reviewed-by: Benedict Schlueter <benedict.schlueter@rub.de> Reviewed-by: Piotr Krysiuk <piotras@gmail.com> Acked-by: Alexei Starovoitov <ast@kernel.org> ---kernel/bpf/verifier.c | 44 ++++++++++++++++++++++++++++++++++++++++----1 file changed, 40 insertions(+), 4 deletions(-)diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index af88d9b9c0143..c6a27574242de 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -6483,6 +6483,27 @@ struct bpf_sanitize_info {bool mask_to_left;};+static struct bpf_verifier_state * +sanitize_speculative_path(struct bpf_verifier_env *env, + const struct bpf_insn *insn, + u32 next_idx, u32 curr_idx) +{ + struct bpf_verifier_state *branch; + struct bpf_reg_state *regs; + + branch = push_stack(env, next_idx, curr_idx, true); + if (branch && insn) { + regs = branch->frame[branch->curframe]->regs; + if (BPF_SRC(insn->code) == BPF_K) { + mark_reg_unknown(env, regs, insn->dst_reg); + } else if (BPF_SRC(insn->code) == BPF_X) { + mark_reg_unknown(env, regs, insn->dst_reg); + mark_reg_unknown(env, regs, insn->src_reg); + } + } + return branch; +} +static int sanitize_ptr_alu(struct bpf_verifier_env *env,struct bpf_insn *insn,const struct bpf_reg_state *ptr_reg, @@ -6566,7 +6587,8 @@ do_sim:tmp = *dst_reg;*dst_reg = *ptr_reg;} - ret = push_stack(env, env->insn_idx + 1, env->insn_idx, true); + ret = sanitize_speculative_path(env, NULL, env->insn_idx + 1, + env->insn_idx);if (!ptr_is_dst_reg && ret)*dst_reg = tmp;return !ret ? REASON_STACK : 0; @@ -8763,14 +8785,28 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,if (err)return err;} +if (pred == 1) { - /* only follow the goto, ignore fall-through */ + /* Only follow the goto, ignore fall-through. If needed, push + * the fall-through branch for simulation under speculative + * execution. + */ + if (!env->bypass_spec_v1 && + !sanitize_speculative_path(env, insn, *insn_idx + 1, + *insn_idx)) + return -EFAULT;*insn_idx += insn->off;return 0;} else if (pred == 0) { - /* only follow fall-through branch, since - * that's where the program will go + /* Only follow the fall-through branch, since that's where the + * program will go. If needed, push the goto branch for + * simulation under speculative execution.*/ + if (!env->bypass_spec_v1 && + !sanitize_speculative_path(env, insn, + *insn_idx + insn->off + 1, + *insn_idx)) + return -EFAULT;return 0;} -- cgit 1.2.3-1.el7在正常(非推測性)執行中,上述代碼存在潛在問題。寄存器r9包含攻擊者提供的值;該值在第 3 行分配給r6,然后在第 5 行用作指針。該值可以指向內核地址空間中的任何位置;這正是 BPF 驗證器旨在防止的那種不受約束的訪問,因此人們可能會認為該代碼一開始永遠不會被內核接受。
然而,驗證器通過探索執行 BPF 程序可能采取的所有可能路徑來工作。在這種情況下,沒有可能的路徑同時執行第 3 行和第 5 行。攻擊者提供的指針的分配僅在r0包含零時發生,但該值將阻止第 5 行的執行。因此驗證器得出結論:沒有可以導致用戶提供的指針被間接訪問,并允許加載程序的路徑。
但這種驗證運行確定執行場景,預測執行中有著不同的規則。
上面代碼片段中的第 1 行引用了攻擊者會費心確保當前未緩存的內存,從而導致緩存未命中。然而,處理器將繼續推測,而不是等待內存獲取值,猜測任何涉及r0 的條件語句將如何執行。事實證明,這些猜測很可能是if條件(在第 2 行或第 4 行中)都不會評估為真,因此不會進行任何跳轉。
這個怎么可能?通過猜測r0的值并檢查結果,分支預測不起作用?;相反,它基于該特定分支的最近歷史。該歷史記錄存儲在 CPU 的“模式歷史表”(PHT)中。但是 CPU 不可能跟蹤大型程序中的每條分支指令,因此 PHT 采用哈希表的形式。攻擊者可以定位代碼,使其分支與精心設計的 BPF 程序中的分支位于相同的 PHT 條目中,然后使用該代碼訓練分支預測器以進行所需的猜測。
一旦攻擊者加載了代碼,清除了緩存,并欺騙分支預測器做一些愚蠢的事情,戰斗就結束了;CPU 將推測性地引用攻擊者提供的地址。那么這只是以任何通常的方式泄漏結果的問題。這是一個有點乏味的過程——但計算機擅長遵循這樣的過程而不會抱怨。
值得注意的是,這不是假設性的攻擊。根據公告,當報告此問題時,多個概念證明被發送到?security@kernel.org列表。其中一些不需要訓練分支預測器的步驟(上面鏈接的提交中提供了一個這樣的步驟)。這些攻擊可以讀取內核地址空間中的任何內存;考慮到所有物理內存都包含在其中,因此可以泄露的內容沒有真正的限制。由于非特權用戶可以加載幾種類型的 BPF 程序,因此不需要 root 訪問權限來執行此攻擊。換句話說,這是一個嚴重的漏洞。
修復
這種情況下的修復相對簡單。而不是修剪驗證者“知道”不會執行的路徑,驗證者將推測性地模擬它們。因此,例如,當檢查r0為零的路徑時?,未固定的驗證者只會得出結論,第 4 行中的測試必須為真,而不考慮替代方案。修復后,驗證器將查看錯誤路徑(包括第 5 行),得出正在使用未知指針的結論,并阻止程序加載。
這種變化有可能阻止加載之前可以運行的正確程序,盡管很難想象包含這種模式的真實世界的非惡意代碼。當然,它會減慢驗證過程,因為它需要檢查正常程序執行中不會出現的執行路徑--那些在我們的預測執行的世界里。
此修復已合并到主線中,可以在 5.13-rc7 版本中找到。此后,它已進入 5.12.13 和 5.10.46 穩定更新的版本,但(尚未)進入任何早期的穩定版本。通過此補丁,這些內核可以抵御另一個 Spectre 漏洞,但這個應該不會是最后一個。
- END -
大家好,我是極客君,鵝廠資深工程師,騰訊云網絡核心成員,多次獲得五星員工,專注實戰技術和職場心得,分享技術的本質原理,校招,社招面試技巧和經驗,希望搭建連接大學和工作的橋梁,幫你理解技術實際落地場景,不光幫你拿的BAT offer,還可以幫你獲得高級工程師的視野,爭取拿SP/SSP,希望幫助更多人蛻變重生,期待你的關注,有任何問題,大家都可以加我微信,探討技術,大廠offer,轉行互聯網,還可以交個朋友。
沒有進技術交流群小伙伴,可以進群交流:
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
深入理解編程藝術之策略與機制相分離
C語言登頂!|2021年7月編程語言排行榜
聊聊C語言和指針的本質
總結
以上是生活随笔為你收集整理的Spectre CPU漏洞借着BPF春风卷土重来的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式事务之底层原理揭秘
- 下一篇: 你大学遗憾过吗