今天我們探索一個問題: 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:\Windows\SysWow64 和C:\Windows\System目錄中的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模擬層的新增,對地址空間可用性進行了一些額外的考慮,並且這個過程反映了一些這些考慮和及其實現。