Windows是如何將64位Ntdll映射到32位進程的

什麼是虛擬地址描述符?

VAD是Windows操作系統跟蹤系統中可用物理內存的許多方法之一。VAD專門跟蹤每個進程用戶模式範圍的保留的和提交的地址。任何時候一個進程請求一些內存,一個新的VAD實力被創建用來跟蹤內存。

VAD被構造成一個自平衡樹,每個節點描述了一段內存範圍。每個節點至多包含兩個子節點,左邊是低地址,右邊是高地址。每個進程被分配一個VadRoot,之後通過遍歷VadRoot來分辨額外用來描述保留或提交的虛擬地址範圍的額外節點。我們需要關注WindDBG中的!vad命令的輸出,因為這是我們將大量使用來跟蹤64位Windows中32位進程的映射的輸出。對於這個練習,不是所有的域對我們來說都是特別有趣的。我們考慮測試程序HelloWorld.exe的輸出。通過!process ProcessObject 命令的輸出來分辨我們進程的VadRoot。

Windows是如何將64位Ntdll映射到32位進程的

一旦我們確定了VadRoot,我將地址輸入到 !vad 命令。(輸出為了容易分析已被截斷)

Windows是如何將64位Ntdll映射到32位進程的

我們看到五列: "VAD", "Level", "Start", "End", 和"Commit".!vad命令 接受VAD實例的地址;在我們的例子中,我們已經為它提供了在此進程中通過使用!process命令獲得的VadRoot。

VAD地址是當前VAD結構體或實例的地址:

等級(Level)描述了這個VAD實例(節點)在所在樹中的級別。Level 0是從上面!process輸出中獲得的VadRoot。

開始(Starting)和結束(Ending)地址值用VPN(Virtual Page Numbers,虛擬頁數量)表示。這些地址可以通過乘以頁面大小(4kb)或者左移3位轉化為虛擬地址。結束VPN會添加一個額外的0xFFF來擴展到頁面末尾。如我們上面例子中的D20->D20000,DD20->DD2FFF。

提交(Commit)是被此VAD實例描述的範圍內提交頁面的數量。

分配類型(type of allocation)告訴我們改特定範圍是否已經被映射或是進程私有的。

訪問類型(Type of access)描述改範圍內的允許訪問。最後是被映射到當前區域對應的名稱。

一個AVD實例可以以多種方式創建。如通過使用映射API(CreateFileMapping/MapViewOfFile)或者內存分配API如VirutalAlloc函數。內存可以是保留或者提交的(或free的),或保留和部分提交的。無論哪一種,一個VAD項被映射到進程的Vad樹來讓內存管理器知道此進程中當前已提交的內存。我們對VAD的觀察將揭示WoW64下運行的32位進程的初始設置。

映射NT子系統DLL

進程初始化的早期,在主可執行文件被映射和初始化之前,Windows為特殊區域確定和保留一些地址範圍。其中包含初始進程地址空間,共享系統空間(_KUSER_SHARED_DATA),控制流守護位圖區域,和NT本地子系統(ntdll)。由於進程初始化整體的複雜性,我們只關注最後一塊,它包含32位ntdll和64位ntdll加載到32位進程地址空間的邏輯。我們關注一系列的API調用和在每個點的內存區域的虛擬地址描述符(VAD)。為了讓內核區分怎樣映射一個新進程,它需要知道是否這是一個WoW64進程。當進程對象最初被創建,內核通過讀取名為_EPROCESS.Wow64Process的未文檔化結構體_EPROCESS結構體的值來實現此操作。

PspAllocateProcess是我們探索開始的地方,但是更具體的說,我們開始在MmInitializeProcessAddressSpace()。MmInitializeProcessAddressSpace()負責與一個新進程地址空間有關的初始化。它調用MiMapProcessExecutable,該函數創建了定義初始進程可尋址內存空間的VAD項,隨後將新創建的進程映射到它的基虛擬地址。

一個特別有趣的函數是PspMapSystemDlls。我們關注在調用PspMapSystemDlls之前的進程地址空間的樣子。在WinDBG中確保我們當前處於我們測試應用程序的上下文中(.process),並尋找當前VadRoot(!vadoutput)。

Windows是如何將64位Ntdll映射到32位進程的

到目前為止我們可以觀察到,我們的進程在32問地址空間中被映射和分配了一個基地址(1200),內核共享內存(0x7FFE0000-0x7FFE0FFF) 和64KB保留內存區域(0x7FFE1000-0x7FFEFFFF) 也已經被映射到他們各自的虛擬地址。

PspMapSystemDlls通過一個包含多個平臺子系統模塊的全局指針迭代。對於x86和x64Windows,這些是分別位於C:\Windows\SysWow64 和C:\Windows\System目錄中的ntdll.dll。

Windows是如何將64位Ntdll映射到32位進程的

一旦PspMapSystemDlls發現要加載的DLL,它調用PspMapSystemDll 來映射他們(DLLs)到進程的地址空間。該函數非常簡短,下面展示了一個片段。為了正確映射本地子系統,需要滿足一些條件。

Windows是如何將64位Ntdll映射到32位進程的

PspMapSystemDll通過調用MmMapViewOfSection實現實際的本地DLL的映射,並保存所佔的基地址。在這兩個DLL映射完成並且他們的VAD項初始化完成後,我們的32位進程地址空間看起來像這樣:

Windows是如何將64位Ntdll映射到32位進程的

所以現在,我們映射完我們的進程(0xc40000-0xcf2fff),內核共享內存空間(0x7ffe0000-0x7ffe0fff),32位地址空間的有效結束區域(0x7ffe1000-0x7ffeffff),和我們的兩個NT子系統DLL。

鎖定地址空間

為了完成32位進程的映射,還有最後一步要做。我們知道一個32位進程最多尋址到2GB的虛擬內存,所以Windows需要屏蔽此進程剩餘的地址空間。對於32位進程,屏蔽在 0x7FFF0000- 0x7FFFFFFF之後;然而,0x7FFeFFFF之後什麼也不可以映射。基於此事實,緊鄰64位NTDLL的內存區域需要保留或者屏蔽。要做到這一點,內核標記剩下的64位地址空間為私有。它通過遍歷當前進程的VAD樹和定位最後可用的虛擬地址來創建此VAD項,然後附加一個新的VAD項。

完成此任務的API是MiInitializeUserNoAccess。該函數接受當前進程句柄和一個虛擬地址。傳遞的虛擬地址是0x7FFF0000,這是32為進程最後可尋址範圍的起始。然後,它遍歷當前的VAD項並執行一個新範圍的插入,該範圍覆蓋了32位進程剩餘的地址空間。在此調用後,我們的進程地址空間看起來像這樣:

Windows是如何將64位Ntdll映射到32位進程的

我們現在可以發現,我們的32位進程已經映射,並且它的合規的內存地址範圍已經被內核保留。涵蓋0x7FFF0- 0x7FFFFED3F和0x7FFFFEF20- 0x7FFFFFFEF 範圍的VAD實例已經被內核保留為私有。隨後任何檢索內存的調用僅僅會發生在允許的32為地址空間內。一旦進程完全加載,我們可以看到額外的已提交的內存出現在進程(0xC40000)附近的地址空間。

Windows是如何將64位Ntdll映射到32位進程的

結束演講

我們觀察到64位Windows下的32位進程的初始映射以及64位ntdll如何被映射到64位區域,隨後64位地址空間被鎖定,防止用戶訪問,我們學到了什麼?

1. 早期初始化邏輯決定我們是否準備映射一個WoW64進程。

2. 分配最初的32位地址空間區域;這包括最高可訪問的32位地址範圍,和進程首選的基虛擬地址。

3. NT子系統DLL被加載到他們各自的地址範圍,32位ntdll加載到32位空間,64位ntdll加載到64位地址空間。

4. MmInitializeUserNoAccess 用來創建與64位ntdll範圍相鄰的西遊範圍。這具有從32位進程鎖定64位可尋址空間的效果。

希望這篇文章提供了一些關於Windows如何允許講32位進程無縫集成到64位Windows操作系統的透明度。隨著WoW64模擬層的添加,對地址空間可用性進行了一些額外的考慮,並且這個過程反映了一些這些考慮和及其實現。

本文由看雪論壇 sudozhange 編譯,來源:Gregory Lindor @ThreatMatrix 轉載請註明來自看雪社區

更多幹貨請關注看雪學院公眾號:ikanxue


分享到:


相關文章: