Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86376375

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

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

(接上文)

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

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

1.png

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

1.png

由於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的內存區域。

1.png

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

1.png

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

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

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

1.png

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

1.png

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

1.png

我們可以看到,前五個元素為:{0,PTE_READONLY,PTE_EXECUTE,PTE_EXECUTE_READ,PTE_READWRITE}。從這裡我們可以確認,以4作為下標訪問這個數組,就能訪問PTE_READWRITE的PTE掩碼,這正是nt!MmWriteableSharedUserData所期望的內存權限,因為我們知道:這應該是KUSER_SHARED_DATA的“新映射視圖”,它是可寫的。同時,別忘了:與“靜態”KUSER_SHARED_DATA關聯的PFN記錄的虛擬地址是通過RDX在函數調用中使用的。

1.png

在函數調用之後,返回值是一個“與PTE兼容”的掩碼,它表示一個可讀和可寫的內存頁面。

1.png

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

1、當前為空的PTE地址;

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

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

1.png

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(因為這個內存用於映射“視圖”)。

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

1.png

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

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

1.png

1.png

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

1.png

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

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

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

1.png

正如我們所知,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。

1.png

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

1.png

1.png

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

1.png

太棒了!但是,我們還需要做最後一件事,那就是將“靜態”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兼容的值。

1.png

1.png

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

1.png

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

1.png

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

1.png

就我們的目的而言,對於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)。

1.png

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

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

最後,祝大家閱讀愉快!