在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

在本文中,我們將為讀者詳細介紹在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化。下面,我們開始進入本文的下篇部分!

(接上文)

知道了這些,我們就會明白:在呼叫nt!MiReservePtes之前,就可以計算出對應於“靜態”的KUSER_SHARED_DATA的PFN資料庫的適當索引。這實質上意味著我們正在從PFN資料庫中檢索相應PFN記錄(一個MMPFN結構)的虛擬地址。

我們可以把它看作是PFN資料庫的基址,在本例中是0xffffc38000000000,它參與了相關操作。而最終的虛擬地址0xffffc380002df8a0(與“靜態”KUSER_SHARED_DATA關聯的PFN記錄的虛擬地址)可以在下面的RBP中看到。將來,它將用作nt!MiMakeProtectionPfnCompatible函式呼叫的第二個引數。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

我們可以透過將上述虛擬地址解析為MMPFN結構體來驗證這一點,以檢視PteAddress成員是否對應於“靜態”KUSER_SHARED_DATA的已知PTE。我們知道,PTE位於0xffffb7fbc0000000。

由於PFN結構體的PteAddress成員與“靜態”KUSER_SHARED_DATA關聯的PTE的虛擬地址是對齊的,這說明它就是與“靜態”KUSER_SHARED_DATA關聯的PFN記錄。

然後,這個值被用於對nt!MiReservePtes的呼叫,我們可以透過前面的兩張圖來確認這一點。根據__FastCall呼叫約定,該函式的第一個引數將進入RCX暫存器。這個引數實際上是一個nt!_MI_SYSTEM_PTE_TYPE結構體。

根據CodeMachine的文章來看,當對nt!MiReservePtes的呼叫發生時,這個結構體被用來定義如何進行記憶體分配,以便為正在建立的PTE預留記憶體。當用nt!MiReservePtes請求分配記憶體時,可能暗示了從系統PTE區域分配一塊虛擬記憶體。系統PTE區域被用於記憶體的對映檢視、記憶體描述符列表(MDL)和其他內容。有了這一資訊,結合我們對兩個虛擬地址是如何被對映到同一物理記憶體頁的瞭解,就能確定:系統正在使用記憶體的不同 “檢視”(例如,兩個虛擬地址對應一個物理地址,所以,儘管兩個虛擬地址包含相同的內容,但可能具有不同的許可權)。此外,我們可以確認分配的記憶體來自系統PTE區域,因為nt!_MI_SYSTEM_PTE_TYPE結構體的VaType成員被設定為9,這是一個與MiVaSystemPtes對應的列舉值。這意味著,在這種情況下,分配的記憶體將來自系統PTE的記憶體區域。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

我們可以看到:在呼叫發生後,返回值是一個核心模式的地址,位於系統PTE區域的同一地址空間內,並且是由BasePte成員定義的。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

此時,OS基本上已經以未填充的PTE結構體的形式從系統PTE區域分配了記憶體,該區域通常用於對映記憶體的多個檢視。下一步將是正確配置該PTE,並將其分配給一個記憶體地址。

之後,將繼續呼叫nt!MiMakeProtectionPfnCompatible。如前所述,該函式的第二個引數將是來自PFN資料庫的PFN記錄的虛擬地址,該記錄與應用於“靜態”KUSER_SHARED_DATA的PTE相關聯。

傳遞給nt!MiMakeProtectionPfnCompatible的第一個引數是常數4。這個價值從何而來?看一下ReactOS,我們可以看到兩個常數,它們用於描述PTE強制執行的記憶體許可權。

根據ReactOS的說法,還有一個名為MI_MAKE_HARDWARE_PTE_KERNEL的函式,也利用了這些常數;其原型和定義可以在下文中看到。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

該函式提供了nt!MiMakeProtectionPfnCompatible和nt!MiMakeValidPte(稍後將看到的函式)所公開的功能的組合。而值4或MM_READWRITE實際上是名為MmProtectTopTemask的陣列的索引。該陣列負責將請求的頁面許可權(4,或MM_READWRITE)轉換為與PTE相容的掩碼。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

我們可以看到,前五個元素為:。從這裡我們可以確認,以4作為下標訪問這個陣列,就能訪問PTE_READWRITE的PTE掩碼,這正是nt!MmWriteableSharedUserData所期望的記憶體許可權,因為我們知道:這應該是KUSER_SHARED_DATA的“新對映檢視”,它是可寫的。同時,別忘了:與“靜態”KUSER_SHARED_DATA關聯的PFN記錄的虛擬地址是透過RDX在函式呼叫中使用的。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

在函式呼叫之後,返回值是一個“與PTE相容”的掩碼,它表示一個可讀和可寫的記憶體頁面。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

到目前為止,我們已經掌握了:

1、當前為空的PTE地址;

2、PTE的“骨架”(例如,可提供可讀/可寫的掩碼)

考慮到這一點,現在讓我們將注意力轉向對nt!MiMakeValidPte的呼叫。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

nt!MiMakeValidPte實際上提供了前面所說的ReactOS函式MI_MAKE_HARDWARE_PTE_KERNEL的“其餘”功能。並且,nt!MiMakeValiePte需要以下資訊:

1、 新建立的空PTE的地址(這個PTE將被應用到nt!MmWriteableUserSharedData的虛擬地址);目前這個地址位於RCX中。

2、 一個PFN;目前位於RDX中(例如,不是來自PFN資料庫的虛擬地址,而是原始的PFN“值”)。

3、 一個“相容PTE的”掩碼(例如,我們的讀/寫屬性);目前位於R8中。

所有這些資訊都可以在下面的螢幕截圖中看到。

就“將同一物理記憶體對映到不同檢視”而言,這裡最重要的組成部分是RDX中的值,它是KUSER_SHARED_DATA的實際PFN值(原始值,而不是虛擬地址)。讓我們首先回憶一下,PFN乘以一個頁面的大小(0x1000位元組,或4KB)後,實際上就是一個物理地址。這是真的,特別是在我們的案例中,因為我們正在處理最細化的記憶體型別:4KB對齊的記憶體塊。由於沒有更多的分頁結構需要索引——這是PFN最常見的用途,因此,這就意味著:在這種情況下,PFN將被用來獲取最終的、4KB對齊的記憶體頁。

我們知道,這其實就是在正在執行的函式(nt!MiProtectSharedUserPage)裡面建立了一個PTE(透過nt!MiReservePtes和nt!MiMakeValidPte)。正如我們所知,這個PTE將被應用於一個虛擬地址,並用於將所述虛擬地址對映到一個物理頁面,本質上是透過與PTE相關的PFN實現的。目前,將用於這種對映的PFN被儲存在RDX中。在較低的水平上,RDX中的這個值乘以一個頁面的大小(4KB),就是虛擬地址被對映到的實際物理頁面。

有趣的是,RDX中的這個值(在第二次呼叫nt!MI_READ_PTE_LOCK_FREE後保留下來的值),就是與KUSER_SHARED_DATA相關的PFN! 換句話說,我們給這個新建立的PTE分配的虛擬地址(最終應該是nt!MmWriteableUserSharedData)將對映到KUSER_SHARED_DATA結構體所在的物理記憶體,因此,當nt!MmWriteableUserSharedData的內容被更新時,該物理記憶體也將被更新。由於“靜態”的KUSER_SHARED_DATA(0xfffff78000000000)也位於相同的物理記憶體中,它也會隨之更新。實際上,即使只讀的“靜態”KUSER_SHARED_DATA不允許執行寫操作,它仍然會收到nt!MmWriteableUserSharedData的更新,也就是說:它是可讀和可寫的。這是因為這兩個虛擬地址都會對映到同一個物理記憶體,所以,只要對其中一個執行寫操作,另一個也會隨之發生變化!

既然如此,也就沒有很好的理由讓“正常的”KUSER_SHARED_DATA結構地址(例如0xfffff78000000000)不是隻讀的,因為現在有另一個記憶體地址可以用來代替它。這樣做的好處是,可寫的“版本”或“對映”(即nt!MmWriteableUserSharedData)是隨機的!

現在繼續,我們告訴作業系統:我們需要一個有效的、可讀和可寫的PTE,它由KUSER_SHARED_DATA的PFN(用於所有意圖和目的的物理地址)提供支援,並且將被寫入我們已經從系統PTE區域分配的PTE(因為這個記憶體用於對映“檢視”)。

在執行該函式後,我們可以看到情況就是這樣的!

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

下一個函式呼叫nt!MiPteInShadowRange實際上只是進行邊界檢查,看看我們的PTE是否位於影子空間中。回想一下前面的內容,對於核心虛擬地址影子(KVAS)的實現來說,分頁結構是獨立的:一組用於使用者模式,一組用於核心模式。通常來說,“影子空間”(也稱為用於使用者模式定址的結構體)是位於nt!MiPteInShadowRange的檢查範圍內的。不過,由於我們處理的是一個核心模式頁面,因此,它所對應的PTE肯定不在“影子空間”內。就我們的目的而言,這並不是我們真正感興趣的東西。

在這個函式呼叫之後,將會執行mov qword ptr[rdi],rbx指令。這將使用nt!MiMakeValidPte函式所建立的相應位,來更新我們分配的PTE(它之前是空白的)!這樣,我們就得到了一個有效的PTE,並被儲存到位於虛擬地址0xFFFF78000000000處的KUSER_SHARED_DATA所在的同一段物理記憶體中!

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

此時,我們離目標符號nt!MmWriteableUserSharedData僅有幾條指令,該符號剛才用新的ASLR對映的KUser_Shared_Data檢視進行了更新。然後,就可以將“靜態”KUSER_SHARED_DATA設為只讀(回想一下,在載入時,它還是可讀/寫的!)。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

目前,透過RDI,我們就能得到用於新的、可讀/寫的PTE的地址和KUSER_SHARED_DATA的隨機對映檢視(透過nt!MiReservePtes生成)。上面的截圖顯示,會對RDI執行一些位運算,同時,我們可以看到頁表項的基址也會參與運算。這些都是簡單的編譯器最佳化,用於將一個給定的PTE轉換為PTE所應用的虛擬地址。

這是一個必要的步驟,回顧一下,到此為止,我們已經成功地從系統PTE區域生成了一個PTE,並將其標記為讀/寫,告訴它使用“靜態”的KUSER_SHARED_DATA作為虛擬記憶體對應的物理記憶體,但是我們並沒有將其實際應用於虛擬記憶體地址,該地址將由這個PTE來描述和對映!我們要應用這個PTE的虛擬地址,將是我們要儲存在nt!MmWriteableUserSharedData中的值!

讓我們再次回顧一下把新的PTE轉換為相應虛擬地址的位運算。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

正如我們所知,RDI暫存器存放的就是目標PTE的地址。同時我們還知道,檢索與給定虛擬地址相關的PTE的步驟如下所示(即透過適當的索引訪問PTE陣列):

1、 透過將虛擬地址除以一個頁面的大小(在標準Window系統上為0x1000位元組),將虛擬地址轉換成虛擬頁面號(VPN)。

2、 用上述數值乘以PTE的大小(在64位系統上為0x8位元組)。

3、 把這個值加到頁表條目陣列的基址上。

這相當於以PteBaseArray[VPN]方式來訪問PTE陣列。由於我們知道如何從虛擬地址轉換為PTE,因此,只要將這些步驟倒過來,就能檢索與給定PTE相關的虛擬地址。

知道PTE後,“反轉”過程如下所示:

1、 將RDI中的PTE(目標PTE)減去PTE陣列的基址,以提取PTE陣列的索引;

2、 用這個值除以PTE的大小(0x8位元組)來獲取虛擬頁面號(VPN);

3、 用這個值乘以一個頁面的大小(0x1000)來檢索虛擬地址。

我們還知道編譯器會生成一條sar rdi,10H指令,對上述步驟生成的值進行算術右移,注意,這個過程其符號並不會發生變化。如果在WinDbg中復現這個過程,我們可以看到,最終值(0x0000A580A4002000)將轉換為地址0xFFFFA580A4002000。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

將計算得到的值與核心生成的值進行比較,我們可以看到,它就是對應於PTE的虛擬地址,該地址將對映到KUSER_SHARED_DATA所在的物理記憶體,並且兩個地址最多匹配到0xffffa580a4002000!我們可以斷定,這些位運算屬於將PTE轉換為虛擬地址的宏的一部分,這是編譯器最佳化過的程式碼!

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

該功能在ReactOS中以名為mi_write_valid_pte的函式形式提供。正如我們所看到的,它本質上不僅將PTE內容寫入PTE地址(在本例中是透過nt!MiReservePtes從系統PTE區域分配記憶體),而且還透過函式miptetoaddress獲取與PTE相關聯的虛擬地址。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

太棒了!但是,我們還需要做最後一件事,那就是將“靜態”KUSER_SHARED_DATA的地址轉換為只讀的。我們已經看到,當前正在排隊等待呼叫nt!MiMakeProtectionPfnCompatible函式。在儲存記憶體許可權常量的RCX中,我們可以看到其值為1,或者MM_READONLY的值——還記得之前為KUSER_SHARED_DATA的讀/寫對映建立的相容PTE的掩碼嗎?換句話說,該頁面所擁有的唯一記憶體“許可權”,就是讀取。

在RDX中,存放的是我們對PFN陣列的索引;透過將“靜態”KUSER_SHARED_DATA的PTE的虛擬地址(位於0xffffb7fbc000000的PTE)與位於PFN結構MMPFN中的PTE進行比較,我們可以確定:我們已經得到了與“靜態”KUSER_SHARED_DATA相關聯的PFN,從而得到了一個與PTE相容的值。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

與上次一樣,現在只是有了一個只讀頁面,我們還需呼叫nt!MiMakeValidPte,透過其PTE的虛擬地址(0xFFFFB7C000000000)為“靜態”KUSER_SHARED_DATA分配只讀許可權。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

呼叫成功後,會生成一個 PTE,以用於只讀頁面。

“靜態”KUSER_SHARED_DATA結構體也是透過前面提到的相同方法(ReactOS中提供的方法稱為MI_WRITE_VALID_PTE)進行更新的。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

就我們的目的而言,對於nt!MiProtectSharedUserPage所做的各種事情,我們感興趣的就是這些!我們現在有兩個虛擬地址都對映到KUSER_SHARED_DATA所在的物理記憶體(一個地址是隻讀的,對應於0xfffff78000000000處的“靜態”KUSER_SHARED_DATA結構體,另一個則對應於新的nt!MmWriteableUserSharedData版本,它是隨機化的,並且是可讀/寫的)!

例如,我們現在可以在IDA中看到,KUSER_SHARED_DATA的更新過程,是透過隨機化和可寫的新符號來完成的。下圖取自nt!KiUpdateTime,在這裡我們可以看到KUSER_SHARED_DATA的幾個偏移量被更新了(即變為0x328和0x320)。同樣,在同一張圖中,我們還可以看到,當讀取來自KUSER_SHARED_DATA的成員時,Windows將使用舊的“靜態”硬編碼地址(在本例中,它們是0xfffff78000000008和0xfffff78000000320)。

在Windows 11內部預覽版中,KUSER_SHARED_DATA結構體發生了哪些新變化?(下)

小結

很明顯,濫用這個程式碼洞的原語已不可用,之前被攻擊者利用的靜態結構體現在已經得到了安全加固。然而,對於如今的漏洞利用過程來說,要想實現程式碼執行,必須首先設法繞過kASLR機制——儘管這不是非常困難,但是,如果攻擊者無法繞過kASLR,就無法將程式碼寫入記憶體。不言而喻,如果攻擊者能夠在核心載入過程中透過競態條件或其他原語儘早將程式碼寫入記憶體,比如將程式碼寫入靜態的0xfffff78000000000+0x800處的KUSER_SHARED_DATA程式碼洞中,就能繞過這個障礙,因為我們知道:當核心第一次被對映到記憶體時,這個結構體仍然是可讀和可寫的。然而,當核心載入完成後,這個區域將變成只讀的。但是,儘管如此,這仍然是可能的,因為初始化發生在核心載入過程中。實際上,有一些已公開的exploit就是利用了這個原語,例如chompie1337的SMBGhost概念驗證就是如此,所以,作為防禦方,不僅需要提高攻擊者的門檻,還需要了解公開exploit的最新動向。雖然本文介紹的內容是一個相當小眾的變動/緩解措施,但我認為它非常有趣,至少在此過程中學到了很多關於系統PTE區域和記憶體檢視的知識。

如果您有任何意見、疑問、更正或建議,請隨時聯絡我。

最後,祝大家閱讀愉快!

參考及來源:https://connormcgarr。github。io/kuser-shared-data-changes-win-11/

頂部