簡介不久前,我看到了一條有趣的推文,其中談到了Windows 11的內部預覽版本中KUSER_SHARED_DATA即將發生的一些變化。
這引起了我的極大興趣,因為KUSER_SHARED_DATA是一個位於靜態虛擬內存空間的結構體,在傳統的Windows內核中,它位於0xfffff78000000000處。從漏洞利用的角度來看,由於其靜態特性,攻擊者經常通過它來攻擊系統內核,特別是在遠程入侵內核的時候。雖然KUSER_SHARED_DATA結構體既沒有包含指向ntoskrnl.exe的指針,也不可執行,但有一段內存與KUSER_SHARED_DATA結構體位於同一內存頁內,並且該頁中沒有包含任何數據,因此,它可用作具有靜態地址的代碼洞。
撰寫本文時,在最新版本的windows 11 內部預覽版中,KUSER_SHARED_DATA結構體的長度為0x738字節。
在Windows上,一個給定的內存“頁面”的長度通常為0x1000字節,即4KB。由於KUSER_SHARED_DATA結構體的長度為0x738字節,所以,內存頁中仍有0x8C8字節的內存空間可供攻擊者濫用。因此,這些未使用的字節仍然具有與KUSER_SHARED_DATA結構體其他部分相同的內存權限,即RW,或讀/寫權限。這意味著: “KUSER_SHARED_DATA代碼洞”不僅是一個可讀可寫的代碼洞,而且,還具有靜態地址。實際上,Morten Schenk在BlackHat 2017的演講中早就講過這種技術,我之前也寫過一篇文章,就濫用這種結構體來執行代碼的漏洞進行了簡單介紹。
如果這個代碼洞得到了適當的處理,攻擊者就需要在內存中找到另一個位置來存放其shellcode。另外,具有讀/寫原語的攻擊者可以破壞對應於KUSER_SHARED_DATA的頁表項(PTE),從而使內存頁變為可寫的。然而,為了實現這一點,攻擊者需要繞過kASLR並將一個原語寫入內存——這意味著:攻擊者基本上已經完全控制了系統。緩解這一代碼漏洞的方法,就是迫使攻擊者首先得繞過kASLR,然後,才能將惡意代碼寫入內存,從而提高漏洞利用的門檻。如果攻擊者無法直接寫入靜態地址,則需要定位其他內存區域。因此,我們可以將其歸類為一種更小、更專用的緩解措施。無論如何,我仍然覺得這是一個有趣的研究課題。
最後,在開始之前,本文探討的內容都是在ntoskrnl.exe的上下文中進行的;當啟用基於虛擬化的安全特性(VBS)時,這些內容並不適用於VTL1級別安全內核。正如Saar Amar所指出的,這種結構體的地址,在VTL1中實際上是隨機化的。
0xfffff78000000000現在變為只讀的了對於KUSER_SHARED_DATA可能的變化,我的第一個想法是內存地址最終(不知何故)將被完全隨機化。為了驗證這一點,我將KUSER_SHARED_DATA結構體的靜態地址傳遞給了WinDbg中的dt命令,令我驚訝的是,該結構體在解析後仍然位於0xfffff78000000000處。
我的下一個想法是,嘗試以0x800為偏移量,對KUSER_SHARED_DATA結構體進行寫入操作,以查看是否發生任何意外行為。執行該操作後,通過檢查PTE,我們發現KUSER_SHARED_DATA現在變成只讀的了。
下面提供的地址0xfffffe7bc0000000是與虛擬地址0xfffff78000000000或KUSER_SHARED_DATA結構體關聯的PTE的虛擬地址。在您的系統上,可以使用Windbg的!pte0xfffff78000000000命令來查找該地址。為了提高可讀性,這裡省略了這些命令,不過,我們將告訴讀者哪些地址對應於哪些結構體,以及如何在自己的系統上面查找這些地址。
後來,有次跟同事Yarden Shafir聊天,他告訴我KUSER_SHARED_DATA中有一些東西(例如SystemTime成員)會不斷更新,同時,他還鼓勵我繼續深挖,因為很明顯,KUSER_SHARED_DATA結構體肯定是通過只讀PTE進行寫入/更新的。正如我後來發現的那樣,這也是有意義的,因為與KUSER_SHARED_DATA對應的PTE的Dirty位被設置為0,這意味著該內存頁還沒有被寫入。那麼,這到底是怎麼發生的呢?
帶著這些信息,我開始借助IDA尋找任何有趣的東西。
nt!MmWriteableUserSharedData來救場了!在IDA中搜索了0xFFFF78000000000或“usershared”之類的關鍵詞後,我偶然發現了一個我以前從未見過的符號——nt!mmwriteableusershareddata。在IDA中,這個符號似乎被定義為0xFFFF78000000000。
然而,在查看實時內核調試會話時,我注意到地址似乎有所不同。不僅如此,在重啟之後,這個地址也發生了變化!
我們還可以看到,0xFFFF78000000000靜態地址和新符號都指向相同的內存內容。
然而,是否存在這種情況:兩個單獨的內存頁面指向兩個單獨的結構體,並且其中包含相同的內容?或者它們是以某種方式交織在一起的?在查看了這兩個PTE之後,我確認了這兩個虛擬地址雖然不同,但都使用了相同的頁幀號(PFN)。此外,我們可以通過以下命令找到“靜態”KUSER_SHARED_DATA結構體和新符號nt!MmWriteableSharedUserData的PTE:
!pte0xfffff78000000000
!pte poi(nt!MmWriteableSharedUserData)
如上所述,與“靜態”KUSER_SHARED_DATA結構體相對應的PTE的地址是0xfffffe7bc000000。而地址0xffffcc340c47010正好是與nt!MmWriteableSharedUserData的PTE相對應的虛擬地址。
PFN乘以頁的大小(在Windows上通常為0x1000)將得到相應虛擬地址的物理地址(就PTE而言,它用於獲取4KB對齊頁的“最終”分頁結構)。由於這兩個虛擬地址都包含相同的PFN,這意味著當將PFN轉換為物理地址(本例中為0xfc1000)時,兩個虛擬地址將映射到相同的物理頁面! 我們可以通過查看映射到每個虛擬地址的物理地址的內容以及虛擬地址本身來確認這一點。
我們這裡有兩個虛擬地址,並且具有不同的內存權限(一個是只讀的,另一個是讀/寫的),它們由一個物理頁面提供支持。換句話說,有兩個虛擬地址具有相同物理內存的不同視圖。這怎麼可能?
內存段圍繞KUSER_SHARED_DATA實現的變動,這裡的“要點”是內存段的概念。這意味著一段內存實際上可以由兩個進程共享(內核也是如此,就像我們的例子一樣)。其工作方式是,相同的物理內存可以映射到一系列虛擬地址。
在本例中,KUSER_SHARED_DATA與nt!MmWriteableUserSharedData(一個虛擬地址)的新隨機讀/寫視圖,由與“靜態”KUSER_SHARED_DATA(另一個虛擬地址)共享同一段物理內存。這意味著,現在這個結構體具有兩個“視圖”,具體如下所示:
這意味著:只要更新其中一個虛擬地址(例如nt!MmWriteableSharedUserData)的內容,將同時更新另一個虛擬地址(0xfffff78000000000)的內容。這是因為對其中一個虛擬地址處內容的改變將更新物理內存的內容。由於這段物理內存的內容供兩個虛擬地址共享,所以,兩個虛擬地址的內容都將收到更新。這為Windows提供了一種方法:在保持舊的KUSER_SHARED_DATA地址的同時,也允許一個新的映射視圖是隨機的,以“緩解”傳統上在KUSER_SHARED_DATA結構體中發現的靜態讀寫代碼洞。0xfffff78000000000的“舊”地址現在可以被標記為只讀,因為這個內存有一個新的視圖可以用來代替它,這個視圖是隨機的!接下來,我們開始介紹更複雜、更低層次的實現細節。
nt!MiProtectSharedUserPage在繼續分析之前,請允許我介紹兩個術語。當我提到內存地址0xFFFF78000000000(KUSER_SHARED_DATA的靜態映射)時,我將使用術語“static”KUSER_SHARED_DATA。當我提及新的“隨機化映射”時,我將使用符號名nt!MmWriteableSharedUserData。這樣的話,每次都能指出我所談論的'版本'。
在WinDbg中進行了一些動態分析後,我終於搞明白了KUSER_SHARED_DATA的更新到底是如何實現的。為此,我首先在正在加載的ntoskrnl.exe上設置一個斷點。在現有的內核調試會話中,可以使用以下命令來實現這一點:
sxe ld nt
.reboot
在斷點被命中後,我們實際上可以看到新發現的符號nt!MmWriteableUserSharedData指向了“靜態”的KUSER_SHARED_DATA地址。
這顯然表明,這個符號在加載過程中會進一步更新。
在通過IDA逆向分析的過程中,我注意到在函數nt!MiProtectSharedUserPage中,對nt!MmWriteableSharedUserData有一個交叉引用,這引起了我們的極大興趣。
當執行仍然處於暫停狀態時,由於ntoskrnl.exe觸發了斷點,我趁機在上述函數nt!MiProtectSharedUserPage上設置了另一個斷點,並發現,在到達新的斷點後,nt!MmWriteableSharedUserData符號仍然指向舊的0xfff78000000000地址。
更有趣的是,“靜態的”KUSER_SHARED_DATA'在加載過程中的這一時刻仍然是靜態的,可讀的,可寫的! 下面的PTE地址0xffffb7fbc0000000是與虛擬地址0xfff78000000000相關的PTE的虛擬地址。由於我們重新啟動系統,導致ntoskrnl.exe的加載中斷,PTE地址也發生了變化。如前所述,這個地址可以通過命令!pte0xfffff78000000000找到,並且不同的系統,這個地址可能會有所差異:
因為我們知道0xfffff78000000000,這個“靜態”的KUSER_SHARED_DATA結構體的地址,在某一時刻會變成只讀的,這說明這個函數可能負責改變這個地址的權限,並且動態地填充nt!MmWriteableSharedUserData,特別是基於命名約定。
深入研究nt!MiProtectSharedUserPage的反彙編代碼,我們可以看到nt!MmWriteableSharedUserData這個符號在這個指令執行時被更新為RDI的值。但是這個值是從哪裡來的呢?
讓我們來看看這個函數的開頭部分。首先引起我們注意的就是內核模式地址和對nt!MI_READ_PTE_LOCK_FREE和nt!Feature_KernelSharedUserDataAaslr__private_IsEnabled的調用(這對我們的目的來說,興趣不大)。
上圖中內核模式地址0xfffffb7000000000,在WinDbg的反彙編窗口中用紅框標出,實際上是頁表項的基址(例如PTE數組的地址)。第二個值,即常量0x7bc00000000,是用來索引這個PTE數組的值,以獲取與“靜態”的KUSER_SHARED_DATA相關的PTE。這個值(PTE數組的索引)可以通過以下公式得到:
1、將目標虛擬地址(本例中為0xfff78000000000)轉換成虛擬頁號(VPN),方法是用地址除以一個頁面的大小(本例中為0x1000)。
2、將VPN乘以一個PTE的大小(64位系統=8字節)
我們可以用上述公式來處理虛擬地址0xfffff78000000000,得到的值就是PTE數組相應的索引,從而獲得與“靜態”的KUSER_SHARED_DATA結構體相關的PTE。這可以在上面的WinDbg的命令窗口中看到。
這意味著與“靜態”的KUSER_SHARED_DATA結構體相關的PTE將被傳入nt!MI_READ_PTE_LOCK_FREE。上述PTE的地址為0xffffb7fbc0000000。
簡單來說,nt!MI_READ_PTE_LOCK_FREE將解除對PTE內容的引用,並將其返回,同時,還會對作用域內的頁表項進行檢查,看看它們是否位於PML4E數組的已知地址空間內,其中包含用於PML4分頁結構的PML4頁表條目數組。回顧一下,PML4結構是基本分頁結構。所以,換句話說,這確保了所提供的頁表項駐留在分頁結構的某個地方。這可以在下面看到。
然而,稍有細微差別的是,該函數實際上是在檢查頁表項是否位於“用戶模式分頁結構”中,該結構又稱為“影子空間”。回想一下,在KVA Shadow的實現(即Microsoft的內核頁表隔離(KPTI)實現)中,現在有兩套分頁結構:一套用於內核模式執行,另一套用於用戶模式。這種緩解措施被用於防禦Meltdown漏洞。但是,這個檢查很容易被“繞過”,因為PTE顯然被映射到了內核模式的地址,所以,肯定不是通過“用戶模式分頁結構”來表示的。
如果PTE不在“影子空間”中,則nt!MI_READ_PTE_LOCK_FREE將返回PTE解引用的內容(例如,PTE的各個“比特”)。如果PTE確實位於“影子空間”中,在返回內容之前,還將對PTE進行一些檢查,以確定KVAS是否被啟用。從漏洞利用的角度來看,這對我們關注的整體變化不是太重要,但它仍然是整個“過程”的一部分。
此外,對我們來說nt!Feature_KernelSharedUserDataAslr__private_IsEnabled並不是很有用——利用它,我們只能通過命名規則了解是否走在正確的道路上。這個函數似乎主要是為了收集關於這個功能的指標和遙測數據。
在第一次調用nt!MI_READ_PTE_LOCK_FREE後,“靜態”的KUSER_SHARED_DATA結構體的PTE的內容將被複製到一個堆棧地址:RSP,其偏移量為0x20。類似的,這個堆棧地址也用於對另一個函數(即nt!MI_READ_PTE_LOCK_FREE)的調用。再說一次,這對我們來說並不是特別重要,但它卻是這個過程的一部分。
然而,更有趣的是,nt!MI_READ_PTE_LOCK_FREE解除了對PTE內容的引用,並通過RAX返回它們。由於定義內存的屬性/權限的“靜態”的KUSER_SHARED_DATA結構體的PTE“比特”位於RAX中,所以,需要對其進行相應的位運算,以便從“靜態”的KUSER_SHARED_DATA的PTE中提取頁幀號(PFN)。這個值在PTE中的偏移量是0xf52e,其值是0x800
000000000f52e863。
這個PFN將在以後調用nt!MiMakeValidPte時用到。現在,讓我們繼續前進。
現在,我們可以將注意力轉向對nt!MiMakeValidPte的調用。
請允許我簡單介紹一下PFN記錄:PFN的“值”在技術上只是一個抽象的值,當它乘以0x1000(一個頁面的大小),就會得到一個物理內存地址。在內存分頁過程中,它通常是下一個分頁結構的地址,或者,如果被用於“最後一個”分頁表,即PT(page table)時,可以用來計算最後一個4KB對齊的物理內存頁。
除此之外,PFN記錄還被存儲在一個虛擬地址數組中。這個數組被稱為PFN數據庫。這樣做的原因是,內存管理器可以通過線性(虛擬)地址訪問頁表項,這就提高了性能,因為MMU不需要不斷遍歷所有的分頁結構來獲取PFN、頁表項等。實際上,這就為記錄的引用提供了一種簡單的方法,即通過一個索引訪問數組。這適用於所有的“數組”,包括PTE數組。同時,像nt!MiGetPteAddress這樣的函數,也能夠通過索引訪問相應的頁表數組,比如PTE數組(對應於nt!MiGetPteAddress),PDE數組(PDPT條目,通過nt!MiGetPdeAddress進行訪問),等等。
小結在本文中,我們為讀者詳細介紹了在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化。由於篇幅較長,我們分為上下兩篇進行發布。更多精彩內容,敬請期待!
(未完待續!)