Linux 內核是一個不斷迭代的代碼庫,截至2021 年,其代碼行數已超過3000 萬行。然而,由於用戶通常不會運行最新的頂層Linux 內核,因此從用戶的角度來看,它也是一個非常分散的代碼庫,這對安全性有重要影響。一致的安全屬性(跨內核版本、處理器架構、編譯器版本)是grsecurity一個重要目標,但頂層通常不會將添加的預防性安全檢查或功能向後移植到舊支持的內核,並且這些措施的狀態沒有很快被最終用戶採用,實際上,必須是有經驗的專注於安全的內核開發人員才能確定他們自己系統的狀態。
作為系統管理員,我們首先要擔心的問題是一個系統的安全性和穩定性,其次才是它的效能和效率。一個系統是否安全,一般來說與系統的複雜性有直接關係。對於必須開放訪問的系統來說,成熟而可靠的系統保護措施是十分重要的。而對作為各種程序運行平台的操作系統而言,系統越複雜可能隱藏的攻擊面也就越多。
受copy_*_user 中的檢查影響的漏洞類型在過去已經出現過幾次,例如, Mathias Krause 在2013 年的修復報告。在討論這些檢查時,最有意義的是談論當前的頂層狀態,之後我們可以討論代碼的歷史以及它是如何隨著時間的推移而演變的。
當前頂層copy_*_user() 檢查在當前的頂層內核中,copy_from_user(用於將數據從用戶級複製到內核)和copy_to_user(用於將數據從內核複製到用戶級)實現如下:
因此,如果check_copy_size() 通過,則將調用較低級別的copy_*_user 例程。這些較低級別的例程可以內聯或不內聯,具體取決於處理器架構,但它們的實現方式保持不變:
access_ok() 是一個古老的例程,其目的是驗證用戶空間中提供的範圍(作為copy_from_user 的源,copy_to_user 的目標)是否實際駐留在用戶空間中。這個API 最近發生了變化,從thread_info 結構中刪除了addr_limit 並將內核拆分為內核副本到它自己的API。以前,在addr_limit 恢復之前的set_fs(KERNEL_DS) 或惡意修改的addr_limit(過去幾年的常見漏洞利用技術)之後運行的代碼可以(ab)使用copy*user 進行任意內核讀/寫。
should_fail_usercopy() 與錯誤注入(模糊增強)有關,可以忽略。 instrument_* 同樣可以忽略,因為它與KASAN(調試/模糊測試增強)有關。出於生產安全目的,我們感興趣的唯一代碼是access_ok() 和check_copy_size()。
讓我們稍微解釋一下這裡顯示的內容:__compiletime_object_size()是一個宏,它利用了編譯器的__builtin_object_size()。如果內核中用於復制的源或目標(適當)的對象的大小能夠在編譯時確定,這將返回該對象的大小。否則,編譯器版本不支持__builtin_object_size(),它返回-1。
當內核對象的大小是靜態已知的並且要復制的長度也是恆定的時,__bad_copy_from() 和__bad_copy_to() 都是編譯時出現的錯誤,這種情況在實踐中不太可能會變成安全問題,除非代碼從未測試/使用過。當對像大小靜態已知但複製長度不是編譯時常量時,Copy_overflow()是一個運行時警告。
如果復制長度大於INT_MAX,“WARN_ON_ONCE(bytes INT_MAX)”檢查將產生運行時警告。由於這是在無符號size_t 類型上計算的,因此當在有符號int 或long 類型上解釋時(即oss-sec 上報告的錯誤的情況),這具有拒絕負長度的附加效果。稍後會詳細介紹此檢查。
check_object_size() 來自頂層的USERCOPY 功能的有限版本。不會對其返回值進行檢查,因為它不會像其他檢查那樣簡單地使復制失敗,而是在usercopy_abort() 中執行BUG() ,這在簡單的情況下將簡單地終止所涉及的進程,但在更複雜的場景中,例如在用戶空間副本周圍保留互斥鎖可能會導致某些代碼路徑鎖定,或者在panic_on_oops 場景中,會導致系統崩潰。
關於oss-sec 報告中討論的漏洞的實際影響,由於2018 年6 月的'fsi: Add cfam char devices'中針對Linux 4.19 引入了錯誤的CFAM 更改,因此cfam_write() 案例將進入copy_from_user()使用負長度,到達check_copy_size() ,其中__compiletime_object_size() 將返回4 用於復製到的__be32 數據變量,然後由於字節不是編譯時常量,將調用copy_overflow(),觸發其中包含的WARN(),並錯誤地中止複制操作。
為了簡潔起見,我們將把分析限制在這些方面,而不會深入raw_copy_*_user()本身的實現,它已經看到了自己的體系結構特定的發展,包括SMAP/PAN/等的引入和Spectre的發現。
在繼續之前要注意的最後一點是memset()只存在於_copy_from_user()中,內核註釋如下:
注意:1.只有copy_from_user() 在短複製的情況下將目標置零。 2.__copy_from_user() 和__copy_from_user_inatomic() 都不為零。 3.他們的調用者絕對必須檢查返回值。
注意__copy_*_user(兩個下劃線)和_copy_*_user(一個下劃線)是不同的。這個memset()的原因大概是為了解決Linux歷史上copy_*_user被標記為__must_check屬性之前的情況,要求調用者檢查返回值是否錯誤。考慮一種常見的情況,即從用戶級複製結構,內核更改了某些字段,然後將結構複製回用戶級。如果來自用戶級的副本或內核設置字段沒有寫入的區域被複製回用戶級,且未初始化,它們可能會將之前的內存內容洩漏到用戶級(信息洩漏)。用戶級還可以通過使部分複制範圍包括未映射的地址或操作的無效權限來強制低級複製例程中的部分失敗。
check_object_size() 的歷史這個檢查第一次出現在2016年6月的Linux 4.8版本中,通過commit“mm: Hardened usercopy”。它的提交信息如下:
“這是將PAX_USERCOPY移植到主線內核的開始。這是由CONFIG_HARDENED_USERCOPY控制的第一組特性。這項工作是基於PaX Team和Brad Spengler的代碼,以及Casey Schaufler的早期移植。其他非平板頁面測試來自Rik van Riel。”
check_object_size() 通過引用堆和其他元數據來工作,以便在運行時盡可能驗證複製操作是否發生在單個對象的範圍內。
儘管上面提到的提交消息是移植完整功能的開始,但在5年裡,除了禁用上述添加的頁面測試(grsecurity中不存在)以外,該功能沒有出現其他重大變化,這破壞了內核的幾個方面。 PAX_USERCOPY最初發佈於2009年,比有限的上游版本早了大約7年。強化後的用戶複製代碼沒有向後移植到早期版本,包括當時的活動LTS 版本。因此,頂層4.4 XLTS。
__compiletime_object_size()/__builtin_object_size() 的歷史__builtin_object_size()在2005年Arjan van de Ven編寫的FORTIFY_SOURCE補丁中首次使用。它最初關注的是FORTIFY_SOURCE在用戶級中也涵蓋的典型的str*和mem* api。 2009 年,在研究FORTIFY_SOURCE 在內核中的實際覆蓋範圍時,我將Arjan 的工作擴展到對更多函數執行檢查,並增加了它對一些[k|v]malloc 對像大小的編譯時知識,發現它只檢測了約30% 的涵蓋API 實例,不過從安全角度來看,是不太可能出現被攻擊的。
這些str* 和mem* API 的覆蓋範圍是在2017 年7 月為4.13 內核提交“include/linux/string.h: add the option of fortified string.h functions”,但沒有提及早期的工作,或者任何使用一些動態分配對像大小的改進知識,將其有效覆蓋率降低到我最初調查的30% 以下。
2009 年Arjan van de Ven 通過提交“x86: Use __builtin_object_size() to validate the buffer size for copy_from_user()”為x86 頂層添加了用於copy__*_user 的__builtin_object_size()。這個Linux 2.6.34的初始版本只覆蓋了copy_from_user,僅從2013 年10 月開始涵蓋copy_to_user,其中Jan Beulich 為Linux 3.13 提交了'x86: Unify copy_to_user() and add size checking to it'。它看到了許多重構,最終通過Al Viro 在2017 年3 月為4.12 版本的Linux 提交'generic .copy_._user primitives'產生了一個獨立於架構的變體。
正如我們之前提到的,__builtin_object_size() 是由編譯器提供的。 2013 年4 月,為了響應內核在某些GCC版本上現有使用內置函數產生的編譯時錯誤,Guenter Roeck 合併了“gcc4: disable __compiletime_object_size for GCC 4.6+”,使得整個練習對受影響的編譯器毫無用處版本(當時,最新發布的GCC 版本是4.8.0)。
“我想指出,儘管__compiletime_object_size() 在4.6 之前被限制為gcc,但整個構造將變得越來越沒有意義。 然而,我會質疑commit 2fb0815c9ee6b9ac50e15dd8360ec76d9fa46a2 ('gcc4: disable__compiletime_object_size for GCC 4.6+') 確實是必要的,相反,這應該像從一開始就在這裡做的那樣處理。”
然而,直到2016年8月Josh Poimboeuf的Linux 4.8 commit:“mm/usercopy: get rid of CONFIG_DEBUG_STRICT_USER_COPY_CHECKS”,對GCC=4.1和4.6使用__builtin_object_size()的限制被更改為GCC=4.1。在這次提交時,GCC 6.2是最新的編譯器版本。 Josh的提交也消除了啟用調試選項以獲得功能的需要。
綜上所述,在大約三年的時間裡,除了少數內核開發人員對內核進行檢查之外,用戶都沒有進行過有關操作。還應該注意的是,放寬對__builtin_object_size() 版本限制的4.8 提交從未向後移植到早期的內核,這意味著即使對於今天最新的4.4 XLTS 版本,任何涉及此的檢查對於幾乎任何現代用戶都是完全無實操可能性的。
你可能想知道,Clang在構建Linux時是如何發揮作用的,儘管谷歌在2018 年底開始使用Clang 構建內核的4.4 內核。 Clang 歷史上(甚至在最新版本中)偽造了4.2.1 的GCC 版本,因此不受這些更改的影響。
WARN_ON_ONCE 的歷史(bytes INT_MAX)這種檢查最早是Linus Torvalds 本人在2005 年2 月對2.6.11 內核通過BUG_ON() 對i386 進行的檢查。由於Andi Kleen 的提交“[PATCH] i386: Remove copy_*_user BUG_ONs for (size 0)”,兩年多後,這些檢查在2.6.22版本的內核中被刪除,並錯誤地解釋為“access_ok檢查這種情況,不需要檢查兩次”。這種解釋是錯誤的,因為雖然access_ok()確實有效地檢查了相同的情況,但BUG_ON()避免了後續memset()的執行,此後memset()就可以執行了。
在grsecurity 中,2.6.29 版本的Linux 在2009 年8 月時在Linux 支持的幾乎所有架構中安全地實施了此檢查,沒有BUG_ON()。重要的是,此檢查是在access_ok() 之前執行的,並且在copy_from_user() 的情況下避免了稍後將討論的易受攻擊的memset()。
在頂層,這項檢查是由Kees Cook 在2019 年12 月針對Linux 5.5通過“uaccess: disallow INT_MAX copy sizes”引入的。由於只有2 行更改,因此一個月後它被反向移植到Linux 5.4。然而,由於copy_*_user API 在前幾年經歷了相當大的變動,這個簡單的改變並沒有被進一步的反向移植,因此在上游的4.4、4.9、4.14或4.19 XLTS內核中也沒有出現,而這些內核現在仍然支持XLTS。
copy_from_user memset 的歷史在x86 上,至少可以追溯到2002 年之前,只有在access_ok() 成功時才對失敗的copy_from_user 進行歸零。例如當copy_from_user的大小不是編譯時常量時調用__generic_copy_from_user 的實現。後來,情況發生了變化,從Linux 2.4.3.4 的這個提交開始,從隨後的2.4.35版本的這個改變開始,該版本在access_ok()失敗時添加了歸零設置。
Andrew Morton在2003年6月發布了一個x86補丁,提到了在access_ok()失敗的情況下重新放置memset()。
最有趣的是,早在2002年的Linux 2.4.4.4中,ARM就有以下變化:
目前尚不清楚該評論是指它緩解了前面描述的安全漏洞還是引入了一個漏洞,該漏洞可能由攻擊者控制長度的緩衝區歸零,然而,這一改變確實帶來了一個潛在漏洞。考慮到由於計算中的一些溢出而導致n=-1 的情況:access_ok() 在32 位ARM 上將失敗,因為n 表示為添加到任何用戶空間的無符號值地址將涵蓋包括內核空間在內的範圍。 else 情況將被觸發,導致長度為0xffffffff 的memzero(),肯定會導致系統無法恢復的DoS。
目前該問題仍然存在於頂層中。
由於2019年添加的禁止INT_MAX副本大小的頂層檢查是在check_copy_size()中實現的,因此它失敗了,避免了對_copy_from_user()的調用,這將執行錯誤的memset(),與十年前的grsecurity更改相同。然而,由於未知的原因,在提交消息或它所引用的郵件列表討論中,根本沒有提到修復這個漏洞。
如上所述,由於頂層的更改未向後移植到4.4、4.9、4.14 或4.19版本種,因此可以將負長度傳遞給copy_from_user() 的內核版本中存在的錯誤可能會導致大量內存損壞並保證系統DoS。如果有人接受某些人提出的緩解建議並啟用panic_on_warn,2019年進行的頂層更改也會通過恐慌導致DoS。
Recommended Comments