在這篇文章中,我們繼續為讀者分享與SMM漏洞相關的知識、工具和方法。
(接上文)
TOCTOU攻擊類型說明有時,即使在嵌套指針上調用SmmIsBufferOutsideSmmValid()也不足以保證SMI處理程序的安全。其原因是,SMM在設計時沒有考慮到並發性,因此,它受到一些固有的競態條件漏洞的影響,最突出的是針對通信緩衝區的TOCTOU攻擊。因為通信緩衝區本身駐留在SMRAM之外,因此,它的內容可以在SMI處理程序執行時發生改變。這一事實具有嚴重的安全影響,因為它意味著從它那裡兩次獲取的值不一定相同。
為了解決這個問題,多進程環境中的SMM採用了所謂的“SMI會合(SMI rendezvous)”技術。簡而言之,一旦某個CPU進入SMM,一個專門的軟件序言將向系統中所有其他處理器發送一個處理器間中斷(IPI)。這個IPI將使它們也進入SMM,並在那裡等待SMI的完成。只有這樣,第一個處理器才能安全地調用處理函數來實際服務SMI。
該方案在防止其他處理器在使用通信緩衝區時干擾通信緩衝區方面非常有效,但當然,CPU並不是唯一有權訪問內存總線的實體。正如任何操作系統入門課程所教導的那樣,現在許多硬件設備都能夠作為DMA代理運行,這意味著它們可以直接讀/寫內存而根本不需要通過CPU。從性能上講,這些都是好消息,但就固件的安全性而言,卻是一個可怕的壞消息。
DMA感知硬件可以在SMI執行時修改通信緩衝區的內容
為了了解DMA操作如何幫助成為漏洞利用的幫兇的,讓我們來看看以下摘自實際SMI處理程序中的代碼片段:
易受TOCTOU攻擊的SMI處理程序
可以看到,這個處理程序至少在3個不同的位置引用了一個我們命名為field_18的嵌套指針:
首先,從通信緩衝區中檢索其值,並將其保存到SMRAM中的局部變量中。
然後,對局部變量調用SmmIsBufferOutsideSmmValid函數,以確保該變量不與SMRAM重疊。
如果被認為是安全的,則從通信緩衝區中重新讀取嵌套指針,然後將其作為目標參數傳遞給CopyMem函數。
正如前面提到的,沒有什麼能保證從通信緩衝區中連續讀取的內容一定會產生相同的值。這意味著,攻擊者可以通過讓指針引用SMRAM外部的、完全安全的位置來處理該SMI:
處理SMI時通信緩衝區的初始佈局
但是,就在SMI驗證嵌套指針之後、再次獲取它之前,存在一個小的機會窗口,DMA攻擊可以修改其值,使其指向其他地方。由於攻擊者知道,這個指針很快就會傳遞給CopyMem()函數,因此,可以設法讓指針指向要破壞的SMRAM中的地址。
惡意DMA設備可以修改CommBuffer中的指針,使其指向其他地方,當然,也可以指向SMRAM內存
緩解措施如果固件配置正確的話,SMRAM應該可以防止被DMA設備所篡改。為了確保您的機器上的配置是正確的,請通過CHIPSEC運行smm_dma模塊。
檢查系統是否為SMRAM提供了針對DMA攻擊的保護
因此,可以通過將數據從通信緩衝區復製到位於SMRAM中的局部變量來緩解TOCTOU漏洞。
將數據從通信緩衝區復製到SMRAM中的局部變量中
一旦以這種方式將所需的全部數據都複製到SMRAM中,DMA攻擊就無法影響SMI處理程序的執行流了:
如果配置正確,SMRAM就能免受DMA設備的篡改
檢測方法為了檢測SMI處理程序中的TOCTOU漏洞,首先需要重建通信緩衝區的內部佈局,然後計算每個字段被提取的次數。如果同一個字段被同一個執行流程取用了兩次或更多,那麼相應的處理程序就有可能容易受到這種攻擊的影響。這些問題的嚴重性在很大程度上取決於各個字段的類型,其中指針字段是最嚴重的。同樣,正確地重建通信緩衝區的結構對評估潛在的風險也是有很大幫助的。
僅能感知CSEG的處理程序類型說明正如本系列文章之前提到的,SMRAM內存的事實標準位置是“頂部內存段”,通常縮寫為TSEG。然而,在許多機器上,由於兼容性的原因,通常會有一個單獨的SMRAM區域被稱為CSEG(兼容段),並且是與TSEG並存的。與TSEG不同,它在物理內存中的位置可以由BIOS以編程方式確定,而CSEG區域的位置則被固定在0xA0000-0xBFFFF的地址範圍。一些傳統的SMI處理程序在設計時只考慮了CSEG,這一事實可能會被攻擊者所濫用。下面就是這樣的處理程序的例子。
具有一些CSEG特定保護的SMI處理程序
與我們到目前為止審查的處理程序不同,這個SMI處理程序並不是通過通信緩衝區來獲得其參數的。相反,它使用EFI_SMM_CPU_PROTOCOL從SMM的狀態保存區讀取相應的寄存器,該狀態是由CPU在進入SMM時自動創建的。因此,在這個例子中,潛在的攻擊面並非通信緩衝區,而是CPU的通用寄存器,在處理SMI之前,其值幾乎可以隨意設置。
處理程序的操作過程如下所示:
首先,它從狀態保存區讀取ES和EBX寄存器的值。
然後,利用上面的值計算線性地址,相應的公式為:16 * ES + (EBX0xFFFF)。
最後,它檢查計算出來的地址是否位於CSEG的範圍內。如果該地址被認為是安全的,則將其作為參數傳遞給0x3020處的函數。
請注意,該處理程序基本上重新實現了常見的實用函數,如SmmIsBufferOutsideSmmValid(),只是它的實現方式很差,完全忽略了CSEG之外的SMRAM段。理論上講,攻擊者可以設置ES和BX寄存器,使計算出的線性地址指向一些其他的SMRAM區域,如TSEG,從而繞過處理程序所施加的安全檢查。
然而,在實踐中,這種漏洞很可能無法被實際利用。這是因為,我們能達到的最大線性地址為16*0xFFFF+0xFFFF==0x10FFF,而經驗表明,TSEG通常位於更高的地址。儘管如此,了解這樣的處理程序及其帶來的危害還是有好處的。
緩解措施緩解這些漏洞的措施,完全取決於SMI處理程序的開發人員。
檢測方法確定這些漏洞的一個不錯的策略是尋找SMI處理程序,這些處理程序通常會利用“魔術數字”來表示CSEG的一些獨特特徵,其中包括直接值,如0xA0000(CSEG的物理基址)、0x1FFFF(其大小)和0xBFFFF(最後一個可尋址字節)。根據我們的經驗,使用兩個或更多這些值的函數可能具有某些特定於CSEG的行為,必須仔細檢查以評估其潛在風險。
基於SetVariable()函數的信息洩露類型說明到目前為止,所有描述的漏洞類別都集中在劫持SMM執行流程和破壞SMM內存方面。然而,另一個非常重要的漏洞類別是圍繞著洩露SMRAM的內容展開的。眾所周知,SMRAM不能從SMM的外部讀取,這就是為什麼它有時被固件用來存儲必須對外界保密的秘密。除此之外,洩露SMRAM的內容還可以幫助利用其他需要準確了解內存佈局的漏洞。
當SMM代碼試圖更新NVRAM變量的內容時,就會容易發生SMRAM洩露。在UEFI中,更新NVRAM變量的操作並不是一個原子操作,而是由以下步驟組成的複合操作:
分配一個堆棧緩衝區,用來存放與該變量相關的數據。
使用GetVariable()服務,將變量的內容讀入堆棧緩衝區。
對堆棧緩衝區進行所有必要的修改。
使用SetVariable()服務將修改後的堆棧緩衝區寫回NVRAM。
用於更新UEFI變量的UEFI代碼
當調用GetVariable()時,注意第4個參數是作為輸入輸出參數使用的。在進入該函數時,這個參數表示調用方要讀取的字節數,而在返回時,它被設置為實際從NVRAM讀取的字節數。如果變量的實際大小與預期的一致,兩個值應該是一樣的。
當開發者隱含地假設一個變量的大小是不可改變的,問題就出現了。由於這種假設,他們完全忽略了GetVariable()讀取的字節數,而只是在寫入更新的內容時將一個硬編碼的大小傳遞給SetVariable()函數。
上面的代碼隱含地假設CpuSetup的大小總是0x101A,所以,它並沒有檢查GetVariable()實際讀取的字節數。
由於一些NVRAM變量(至少是那些具有EFI_VARIABLE_RUNTIME_ACCESS屬性的變量)的內容可以從操作系統中進行修改,它們可以被濫用來觸發SMM中的信息洩露漏洞,同時也可以同時作為滲出渠道。讓我們看看在實踐中是如何做到這一點的。
首先,攻擊者會使用操作系統提供的API函數,如SetFirmwareEnvironmentVariable()來截斷該變量,從而使其比預期的短。然後,它將繼續觸發易受攻擊的SMI處理程序。 SMI處理程序將:
分配基於堆棧的緩衝區。像其他基於堆棧的內存分配一樣,這個緩衝區默認是未初始化的,這意味著它保存了以前發生在SMM中的函數調用的剩餘部分。
NVRAM變量和堆棧緩衝區的並列示意圖(第1階段)
調用GetVariable()服務,將變量的內容讀入堆棧緩衝區。通常情況下,變量的大小等於堆棧緩衝區的大小,但由於攻擊者剛剛截斷了NVRAM中的變量,緩衝區肯定會更長。這又意味著,即使在GetVariable()返回後,它還會繼續保存一些未初始化的字節。
NVRAM變量和堆棧緩衝區的並列示意圖(第2階段)
修改內存中的堆棧緩衝區。
NVRAM變量和堆棧緩衝區的並列示意圖(第3階段)
調用SetVariable()服務,將修改後的堆棧緩衝區寫回NVRAM中。因為這個調用是使用堆棧緩衝區的硬編碼、恆定大小來完成的,所以它也會將其未初始化的部分寫入NVRAM中。
NVRAM變量和堆棧緩衝區的並列示意圖(第4階段)
為了完成這個過程,攻擊者現在可以使用API函數,如GetFirmwareEnvironmentVariable()來完全洩露變量的內容,包括來自未初始化部分的字節。
緩解措施這個故事的寓意是,不能盲目相信NVRAM變量,在分析處理程序的攻擊面時應考慮到這一點。如果可以的話,最好使用相應的編譯器標誌,如InitAll,以確保堆棧緩衝區將被零初始化。更有技巧的是,當更新NVRAM變量的內容時,代碼必須始終考慮到變量的實際大小,而不是依賴靜態的、預先計算的值。
然而,緩解這些問題的另一個可能方向是限制對NVRAM變量的訪問。這可以通過完全刪除EFI_VARIABLE_RUNTIME_ACCESS屬性或使用EDKII_VARIABLE_LOCK_PROTOCOL等協議來實現,使變量成為只讀的。
檢測方法我們有理由認為,NVRAM變量的更新操作會在一個函數的執行過程中發生。這意味著我們通常可以忽略一個函數讀取變量而另一個函數寫入變量的場景。要找到這些函數,可以在用efiXplorer分析完輸入文件後,導航到“services”選項卡,搜索SetVariable()後面緊跟著GetVariable()的調用對。
搜索由GetVariable()和SetVariable()組成的調用對
對於每一對這樣的調用,請檢查是否滿足下列條件:
兩個調用都來自同一個函數;
兩個調用都對同一個NVRAM變量進行操作;
傳遞給SetVariable()的大小參數是一個立即值。
檢測SMRAM信息洩漏的簡單啟發式方法
識別庫函數這篇文章大量地引用了諸如FreePool()和SmmIsBufferOutsideSmmValid()等庫函數,並天真地假設我們可以不費吹灰之力地找到它們。問題是這些函數是靜態鏈接到二進製文件中的,通常SMM映像在發送給終端用戶之前會去除所有的調試符號。因此,在IDA數據庫中定位它們是相當具有挑戰性的。
在我們的工作過程中,我們研究了多種方法來解決這個問題,包括使用Diaphora進行自動比對,以及使用一些不太知名的插件進行實驗,如rizzo和fingermatch。最終,我們決定堅持KISS原則,考慮到目標函數的一些獨特特徵,我們決定使用簡單明了的啟發式方法進行匹配。下面是一些用於匹配前面提到的函數的經驗法則。注意,我們假設二進製文件已經被efiXplorer分析過了,這可以讓事情變得更輕鬆。
FreePool函數識別FreePool()是非常簡單的。只需要掃描IDA數據庫滿足下列條件的函數即可:
接收一個整型參數;
有條件地調用gBs-FreePool()或gSmst-FreePool()中的一個(但絕不會同時調用);
將其輸入參數轉發給這兩個服務。
準確定位FreePool()的簡單啟發式方法
SmmIsBufferOutsideSmmValid函數對SmmIsBufferOutsideSmmValid()函數來說,識別起來就有點棘手了。為了成功地完成這個任務,我們需要掌握一些名為EFI_SMM_ACCESS2_PROTOCOL的UEFI協議的背景信息。這個協議被用來管理和查詢平台上SMRAM的可見性。因此,它提供了相應的方法來打開、關閉和鎖定SMRAM。
EFI_SMM_ACCESS2_PROTOCOL的接口定義
除了這些,這個協議還導出了一個叫做GetCapabilities()的方法,客戶端可以用它來弄清SMRAM在物理內存中的確切位置。
GetCapabilities()函數的說明文檔
返回時,該函數會填充一個EFI_SMRAM_DESCRIPTOR結構體數組,告訴調用方SMRAM的哪些區域是可用的,它們的大小和狀態是什麼,等等。
使用EFI_SMM_ACCESS2_PROTOCOL查詢SMRAM範圍的示例程序的輸出
在EDK2中,通常的做法是將這些EFI_SMRAM_DESCRIPTORS存儲為全局變量,以便其他函數在未來可以輕鬆地訪問它們。正如你可能猜到的,這些函數之一不是別的,而是SmmIsBufferOutsideSmmValid(),它用以遍歷描述符列表,以確定調用方提供的緩衝區是否安全。
SmmIsBufferOutsideSmmValid的源代碼
考慮到這一點,我們識別SmmIsBufferOutsideSmmValid()函數的策略將是反向查找:首先,我們要找到由EFI_SMM_ACCESS2_PROTOCOL初始化的全局SMRAM描述符,然後才根據使用它們的函數,推斷哪個最可能是SmmIsBufferOutsideSmmValid()函數。
從技術上講,只要按照下面的簡單步驟就可以做到這一點:
進入efiXplorer的“protocols”選項卡標籤,雙擊EFI_SMM_ACCESS2_PROTOCOL。這樣,IDA將跳到使用這個GUID的位置(通常是對LocateProtocol的調用)。
在IDA中搜索EFI_SMM_ACCESS2_PROTOCOL
點擊協議的接口指針(EFISmmAccess2Protocol),點擊“x”來搜索其交叉引用。
列出EfiSmmAccess2Protocol的交叉引用
對於每個對GetCapabilities()的調用,檢查第3個參數(SMRAM描述符)是否是一個全局變量。如果是的話,執行下列操作:
點擊“n”,根據一些命名慣例(例如,SmramDescriptor_XXX,其中XXX是一個序號)重新命名它,以便於將來參考。
點擊“y”,將其變量類型設置為EFI_SMRAM_DESCRIPTOR *。