今天我們探索一個問題: 64位的ntdll是如何被載入到WoW64下的32位進程?今天的旅程將會帶領我們進入到Windows內核邏輯中的未知領域,我們將會發現32位進程的內存地址空間是如何被初始化的。

WoW64是什麼?

來自MSDN:

WOW64是允許32位Windows應用程序無縫運行在64位Windows的模擬器。

換句話說,隨著64位版本Windows的引進,Microsoft需要拿出一種允許在32位時代的Windows程序與64位Windows新的底層組件無縫交互的解決方案。特別是64位內存定址和與內核直接交流的組件。

兩個NT層,一個內核

在32位的Windows系統中,要調用Windows API的應用程序需要經過一系列的動態鏈接庫(DLL)。然而,所有的系統調用最終會定向到ntdll.dll,它是在用戶模式下將用戶模式API傳遞給內核的最高層。以調用CreateFileW為例,這個API調用源於用戶模式下的kernel32.dl,隨後它以NtCreateFile傳遞給ntdll,隨後NtCreateFile通過系統調度程序將控制權傳遞給內核。

在32位Windows下這是非常簡單的,然而,在WoW64下需要額外的步驟。32位的ntdll不可以直接將控制權交給內核,因為內核是64位的,只接受遵循64位ABI的類型(譯者註:ABI,Application Binary Interface,應用二進位介面)。正因為如此,一個翻譯層以幾個標準的命名為wow64.dll,wow64cpu.dll和wow64win.dll的DLL的形式被添加到64位Windows。這幾個DLL負責將32位調用轉換成64位調用。那些調用最終被定向到映射到每個32位進程中的64位ntdll。許多關於這種從32位系統調用到64位系統調用(1)的神奇轉換的信息是可獲得的,所以我們不會從這裡進入。我們最關注的是內核何時和怎樣將64位版本的ntdll映射到一個32位進程。看起來像這樣:

我們特別關注倒數第二項。我們能發現ntdll被映射到地址是64位地址範圍(7FFFFED40000-7FFFFEF1FFFF),而且它的位置在Windows

64位系統文件所在的System32路徑下。然而,我們知道32位進程不可以訪問或者運行在64位內存空間。

為了理解上面輸出的內容,我們首先討論VAD(Virtual Address Descriptor,虛擬地址描述符)是什麼和它將如何幫助我們理解載入64位dll到32位進程的機制的。

什麼是虛擬地址描述符?

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

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

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

我們看到五列: "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(!vad

output)。

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

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

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

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

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

結束演講

我們觀察到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模擬層的添加,對地址空間可用性進行了一些額外的考慮,並且這個過程反映了一些這些考慮和及其實現。

[翻譯]Windows是如何將64位Ntdll映射到32位進程的-『外文翻譯』-看雪安全論壇?

bbs.pediy.com
圖標

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


推薦閱讀:
相关文章