1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        LWN:用戶空間的影子堆棧!

        共 5859字,需瀏覽 12分鐘

         ·

        2022-03-09 19:46

        關(guān)注了就能看到更多這么棒的文章哦~

        Shadow stacks for user space

        By Jonathan Corbet
        February 21, 2022
        DeepL assisted translation
        https://lwn.net/Articles/885220/

        對系統(tǒng)進行攻擊的時候,為了攻破運行中的某個進程,攻擊者最喜歡的目標就是 call stack 了。只要他們能找到一種方法來將 stack 上的返回地址(return address)改寫掉,那么就可以將系統(tǒng)的控制權(quán)重定向到他們精心選擇的代碼上,從而可以讓 "游戲結(jié)束"。因此,人們在保護堆棧這個方面做了大量的工作。其中一種很有希望的技術(shù)就是影子堆棧(shadow stock),于是在各種處理器里面都開始添加影子堆棧的支持。要想利用影子堆棧來保護用戶空間的應(yīng)用程序,則還需要更多時間。目前內(nèi)核社區(qū)內(nèi)正在進行這個話題的討論,但看起來比人們預(yù)想的要棘手。此外,由于這些補丁已經(jīng)存在挺長時間了,所以它們自己也出現(xiàn)了一些向后兼容問題。

        Shadow-stack basics

        每當一個函數(shù)調(diào)用另一個函數(shù)時,被調(diào)用函數(shù)里的信息,包括所有參數(shù)以及函數(shù)完成工作后應(yīng)該跳轉(zhuǎn)回去的地址都會被放到調(diào)用棧(call stack)里。隨著函數(shù)調(diào)用逐層深入,堆棧中的返回地址的數(shù)量也在迅速增加。通常一切都能按部就班地進行,但只要堆棧內(nèi)容有任何一點損壞,都可能會導(dǎo)致一個或多個返回地址被改寫掉,從而導(dǎo)致 CPU 執(zhí)行代碼跳轉(zhuǎn)到一個意料之外的地方。運氣好的情況下也會導(dǎo)致應(yīng)用程序崩潰,運氣不好的話,也許這個錯誤的數(shù)據(jù)是故意準備好的,那么系統(tǒng)的執(zhí)行過程繼續(xù)下去就會導(dǎo)致更加棘手的問題。

        shadow stack 試圖給堆棧創(chuàng)建一個副本來解決這個問題,這個副本里面(通常)只包含返回地址這類數(shù)據(jù)。每當一個函數(shù)被調(diào)用時,返回地址會被同時寫入常規(guī)堆棧以及影子堆棧中。當該函數(shù)返回時,返回地址需要從兩個堆棧中都提取出來進行比較,如果不匹配的話系統(tǒng)就會給出紅色警報,并(可能)kill 掉相關(guān)進程。影子堆??梢酝耆密浖绞絹韺崿F(xiàn),盡管影子堆棧也是可被改寫的,但也也提高了攻擊者的攻擊門檻,他們現(xiàn)在必須要能破壞兩個內(nèi)存區(qū)域了,其中一個不容易確定是在什么位置。不過,硬件支持使影子堆棧的話可以使其更加強大。

        英特爾處理器(還有其他一些廠商)就可以提供這種支持。如果一個影子堆棧正確建立起來(這是一個特權(quán)操作),那么后續(xù)將返回地址入棧的操作以及在函數(shù)返回時進行比較的操作全都是由 CPU 硬件自己完成的。同時,影子堆棧通常不能被應(yīng)用程序?qū)懭耄ㄖ挥型ㄟ^函數(shù)調(diào)用以及 RET 指令方式才能寫入),因此不會被攻擊者破壞。硬件還要求影子堆棧本身要有一個特別的 "restore token",用來確保兩個進程不會共享同一個影子堆棧,因為這種情況也被利用來作為攻擊突破口。

        Supporting user-space shadow stacks

        當前版本的影子堆棧支持 patch 是由 Rick Edgecombe 發(fā)布的,其中大部分 patch 本身是由 Yu-cheng Yu 編寫的,這項工作的許多早期版本都是他發(fā)布的。要啟用這個功能需要 35 個規(guī)模很大的 patch,而且這個問題還沒有完全解決。人們可能會想知道這里有什么難點,畢竟影子堆棧似乎是一個大多數(shù)代碼中幾乎可以完全忽略的功能,但生活從來沒有那么簡單。

        可以想到,內(nèi)核里必須要準備代碼來管理用戶空間的影子堆棧。這其中包括在處理器上啟用該功能、為每個特定的進程來進行處理。每個進程都需要有自己的影子堆棧,并設(shè)置好自己的 restore token,然后修改影子堆棧指針寄存器(這是個特權(quán)操作)來指向它。還需要處理那些 fault,包括正常的 page fault,也包括比如違背了完整性校驗的那些 integrity-violation trap 等。還需要管理許多關(guān)于上下文切換的信息。對于這種新功能來說,這些都是很正常的工作。

        為影子堆棧本身所分配的內(nèi)存空間必須要特別對待。它是屬于用戶空間的,但通常不允許用戶空間的代碼對其進行寫入。處理器也必須要能識別出這些專門用于影子堆棧的內(nèi)存,所以在頁表中要有特別的標記,這就導(dǎo)致事情變得有點復(fù)雜了。在每個頁表項(PTE, page-table entry)中預(yù)留了許多 bit 用來描述相應(yīng)的保護措施以及其他各種狀態(tài),但 X86 架構(gòu)定義中未包括影子堆棧相關(guān)的 bit。這里有一些 PTE bit 是留給操作系統(tǒng)使用的,Linux 并未用完所有的 PTE bit,因此可以為這個目的而留出一個 bit,但顯然有一些其他的操作系統(tǒng)中沒有多余的 PTE bit,所以如果為這個目的來占用一個 bit 的計劃并不受歡迎。

        硬件工程師得出的解決方案看起來可能有點 hack。如果某個 page 的 write-enable bit 是 0(表明它不能被寫入),但 dirty bit 又是 1(表明它已經(jīng)被寫入過了),那么 CPU 就判定這些 page 是影子堆棧的一部分。因為這個組合在正常使用中應(yīng)該是沒有意義的,所以這種做法看起來很合理。

        不幸的是,Linux 內(nèi)核開發(fā)者們在許多年前就得出了類似的結(jié)論,所以 Linux 對 PTE bit 出現(xiàn)這種組合的情況已經(jīng)有了自己特有的解釋。內(nèi)核用這種方法來標記寫時復(fù)制的頁面(copy-on-write page)。如果某個進程試圖寫入該頁,因為缺乏寫入權(quán)限所以會觸發(fā)一個 trap,而 dirty 位是 1 就讓內(nèi)核知道需要對該頁進行一次復(fù)制,并把寫入權(quán)限賦予這個進程。這個機制一直運行得很好,但是現(xiàn)在 CPU 開始對這種組合給出了它自己特有的解釋。所以大部分的 patch 都是用來為一個新增的 _PAGE_COW flag 來尋找一個尚未使用的 PTE bit,并修改內(nèi)存管理代碼從而適配起來。

        當然,影子堆棧還帶來了其他一些復(fù)雜的問題。如果一個進程調(diào)用了 clone(),那么就必須為子進程分配一個新的影子堆棧,內(nèi)核會自動處理好這個任務(wù)。但是 signal 又一次來 kernel 開發(fā)者們添加了麻煩,因為 signal 也涉及到各種對堆棧的操作。如果某個進程用 sigaltstack() 來給 signal handler 處理程序設(shè)置了一個替代堆棧,情況就更糟糕了。當時的 patch set 根本就無法處理這種情況。因此基于這些細節(jié)(有很多)出發(fā),引出了這個很長的 patch 系列。

        ABI issues

        影子堆棧的使用,對于大多數(shù)應(yīng)用程序來說應(yīng)該是完全透明的。畢竟,開發(fā)人員正常情況下很少考慮 call stack。但是總有一些應(yīng)用程序會對它們的 stack 做一些特別的操作。首先是多線程程序,它們會明確管理每個線程的堆棧區(qū)域。還有一些程序可能會把他們自己特別制作的 thunk 代碼(參見?https://en.wikipedia.org/wiki/Thunk?) 放到堆棧上,甚至可能還會放其他一些更隱蔽的內(nèi)容。如果沒有特別處理的話,在這些程序用在影子堆棧環(huán)境下就會出問題。這種大規(guī)模的 regression 問題也正是安全功能(security features)不受歡迎的原因,所以開發(fā)者采取了各種措施來避免這種情況。

        他們深思熟慮之后,提出的計劃是對準備用影子堆棧運行的應(yīng)用程序進行標記(在.note.gnu.property ELF section 有一個特殊的 property)。沒有對堆棧進行特殊使用的應(yīng)用程序可以直接重新編譯一下,后續(xù)就可以在運行時支持影子堆棧了。對于那些更復(fù)雜的情況,我們定義了一組 arch_prctl() 操作來實現(xiàn)對影子堆棧的顯式操作。GNU C 庫也添加了功能,從而可以在應(yīng)用程序啟動時使用這些調(diào)用來正確配置好環(huán)境,而內(nèi)核將在運行這些標記好的程序的時候啟用影子堆棧。包括 Fedora 和 Ubuntu 在內(nèi)的一些發(fā)行版,已經(jīng)針對影子堆棧構(gòu)建好了他們平臺上的二進制文件,他們所需要的只是一個合適的內(nèi)核來運行額外的保護。

        不過,根據(jù)尚未被接受和合并的內(nèi)核功能來發(fā)布代碼,總是很容易出問題的做法。這次影子堆棧又變成了典型的例子。根據(jù)當前 patch 系列的封面所說,arch_prctl() API "因為太奇怪而被放棄了"。但是,那些部署在世界各地的系統(tǒng)上的針對影子堆棧準備好的二進制文件在構(gòu)建時就認為是一定會有那個 API 的,才不管它是不是奇怪。如果內(nèi)核會審查 ELF 文件中的標記從而為這些程序來啟用影子堆棧,那么其中一些程序就會出問題。這就會導(dǎo)致全世界的系統(tǒng)管理員至少在 2040 年之前都會禁用影子堆棧,從而使整個工作的目的完全無法落實了。

        解決這個問題的一個明顯辦法是,永遠不要認可當前 ELF 中的影子堆棧特有標記,而是新創(chuàng)建一個標記來利用內(nèi)核實際會支持的接口。然而,這里的最終決定是讓內(nèi)核完全不去處理識別二進制文件是否支持影子堆棧,而是讓 C 庫來處理這個功能。所以,如果這個版本的 ABI 被采用的話,內(nèi)核就永遠不會啟用影子堆棧了,除非用戶空間顯式要求。

        The proposed interface

        對影子堆棧功能的整體控制是通過一個(人們假設(shè)這個接口并不奇怪)arch_prctl() 的調(diào)用來實現(xiàn)的:

        status = arch_prctl(ARCH_X86_FEATURE_ENABLE, ARCH_X86_FEATURE_SHSTK);

        還有一個 ARCH_X86_FEATURE_DISABLE 操作用來關(guān)閉影子堆棧,以及 ARCH_X86_FEATURE_LOCK 用來防止未來的改動。

        雖然大多數(shù)應(yīng)用程序不需要擔(dān)心影子堆棧,但其中有一些需要有能力創(chuàng)建新的影子堆棧。使用 makecontext() 系列接口的應(yīng)用程序就是一個很顯著的例子。創(chuàng)建影子堆棧需要有內(nèi)核的支持,而相關(guān)的內(nèi)存又必須有上述的特殊 page bit 組合設(shè)置,而且還必須包括 restore token。所以針對這些操作要有一個新的系統(tǒng)調(diào)用:

        void *map_shadow_stack(unsigned long size, unsigned int flags);

        所需堆棧的大小就是 size 指定的,而 flags 只可以是 SHADOW_STACK_SET_TOKEN,用來請求在堆棧中存儲一個 restore token。成功后的返回值就是這個堆棧的基地址。

        在使用這個新的堆棧的時候,需要執(zhí)行 RSTORSSP 指令來進行切換,這很可能是作為線程之間的用戶空間上下文切換的一部分來完成。該指令會在進行切換前先對 page 權(quán)限以及 restore token 進行必要驗證。它還會將新的影子堆棧上的 token 標記為此時在使用中,從而防止該堆棧被其他進程使用。

        那些做了特別棘手的操作的應(yīng)用程序可能就需要能對影子堆棧進行寫入了。由于眾所周知的原因,通常不允許這種操作,但正如 Edgecombe 所指出的,這 "限制了那些潛在的有用的應(yīng)用程序,他們可能想犧牲一點安全,以此為代價來實現(xiàn)一些奇怪的工作"。對于這些特殊情況,可以通過 arch_prctl() 來打開另一個特性(LINUX_X86_FEATURE_WRSS),它可以啟用 WRSS 指令,用它就能寫入影子堆棧的內(nèi)存區(qū)域了。在這種情況下,仍然不可以直接根據(jù)指針來對該內(nèi)存進行寫入。

        What next?

        這項工作并不是個新的工作,它的早期版本在 2018 年的 LWN 文章中就有所涉及。影子堆棧 patch set 之前經(jīng)過了 30 個版本。關(guān)于 control-flow integrity 的工作(indirect branch tracking)也達到了第 29 版本,不過這個當前已經(jīng)被擱置了(盡管 Peter Zijlstra 剛剛提出了另一種實現(xiàn))。隨著有一個新的開發(fā)者領(lǐng)導(dǎo)這項工作,以及縮小了的目標,還有一些人們要求要做的改動,我們希望這項工作能夠最終合入 mainline。

        有很多因素讓這個希望看起來很可能會落實。雖然有人對這套 patch 的各個部分都提出了一些意見,似乎目前沒有多少人反對它的工作方式。不過,開發(fā)者們確實對缺乏支持另一個 signal stack 而感到擔(dān)憂。這個功能在某種程度上來說肯定是必要的,所以在這個功能被合并之前,還是需要先看看如何解決這個問題。

        還有一個另一個討論,是關(guān)于用戶空間中的檢查點/恢復(fù)(CRIU, Checkpoint/restore in user space)的,這個功能為了實現(xiàn)目標而采用了一些不正當?shù)氖侄?。checkpoint 生成過程中牽涉到將 "parasite" 代碼注入到需要抓取快照的目標進程中,來抓取所需的信息,然后完成一個特殊的 return 操作來恢復(fù)正常執(zhí)行。這正是影子堆棧所要防止的那種控制流篡改問題(control-flow tampering)。我們討論了各種可能的解決方案,但目前還沒有落實到代碼上。正如 Thomas Gleixner 所說,在影子堆??梢员缓喜⒅?,也需要先解決這個問題:"我們不能因為內(nèi)核升級而破壞 CRIU 機制"。

        最后,這個功能所支持的硬件范圍肯定需要再擴大一些。有一些 AMD 的 CPU 也實現(xiàn)了影子堆棧,看起來實現(xiàn)方式可以兼容,但是在這個 patch set 中只支持了英特爾的 CPU。人們認為原因可能是無法進行測試。這一點至少是需要改變的,才能讓工作繼續(xù)推進下去。影子堆棧在 32 位系統(tǒng)上也無法支持。要解決這個問題可能更加困難,不清楚人們是否有動力去做這項工作。不過,無論是否支持 32 位系統(tǒng),在這段代碼進入 mainline 之前都仍有工作要做。不要期望它能在不久的將來出現(xiàn)在 Linux 的某個版本中。

        全文完
        LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。

        歡迎分享、轉(zhuǎn)載及基于現(xiàn)有協(xié)議再創(chuàng)作~

        長按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~



        瀏覽 59
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            77777亚洲午夜久久多人 sandrarusso精品艳妇 | 69精品人人人人人人人人人 | 天天做天天爱综合网 | 久久久久黄片 | 操逼视频免费的 | 中文在线√天堂 | 男女啪啪做爰 | 亚洲无码专区视频在线 | 男人天堂色色 | 日本丰满白嫩bbrbbr |