0x00 前言
本文主要是討論Windows 7 x64下的內核虛擬地址空間的結構,可以利用WiinDBG調試的擴展命令"!CMKD.kvas"來顯示x64下的內核虛擬地址空間的整體布局。了解內核的地址布局在某些情況下是很有的,比如說在研究New Blue Pill的源碼和虛擬化的時候。
0x01 基本結構
X64的CPU的地址為64位,但實際上只支持48位的虛擬地址空間供軟件使用。虛擬地址的高16位在用戶模式下總是被設置為0000,而在內核模式下全置為FFFF。
因此用戶模式的地址空間范圍為0x00000000~00000000——0x0000FFFF~ffffffff,內核模式的地址空間范圍為0xFFFF0000~00000000——0xFFFFffff~ffffffff,所以對操作系統可見的內核虛擬地址空間的大小為256TB。Windows操作系統將整個內核地址空間划分為若干個有特定用途的大小固定的虛擬地址空間。下表是關於Windows對於虛擬地址空間具體的划分:
起始地址 |
結束地址 |
內存大小 |
用途 |
FFFF0800`00000000 |
FFFFF67F`FFFFFFFF |
238TB |
未使用 |
FFFFF680`00000000 |
FFFFF6FF`FFFFFFFF |
512GB |
PTE內存空間 |
FFFFF700`00000000 |
FFFFF77F`FFFFFFFF |
512GB |
Hyper內存空間 |
FFFFF780`00000000 |
FFFFF780`00000FFF |
4KB |
系統共享空間 |
FFFFF780`00001000 |
FFFFF7FF`FFFFFFFF |
512GB-4K |
系統cache工作集 |
FFFFF800`00000000 |
FFFFF87F`FFFFFFFF |
512GB |
初始化映射區 |
FFFFF880`00000000 |
FFFFF89F`FFFFFFFF |
128GB |
系統PTE區域 |
FFFFF8a0`00000000 |
FFFFF8bF`FFFFFFFF |
128GB |
分頁池區域 |
FFFFF900`00000000 |
FFFFF97F`FFFFFFFF |
512GB |
會話空間 |
FFFFF980`00000000 |
FFFFFa70`FFFFFFFF |
1TB |
內核動態虛擬空間 |
FFFFFa80`00000000 |
*nt!MmNonPagedPoolStart-1 |
6TB Max |
PFN 數據 |
*nt!MmNonPagedPoolStart |
*nt!MmNonPagedPoolEnd |
512GB Max |
不分頁內存池 |
FFFFFFFF`FFc00000 |
FFFFFFFF`FFFFFFFF |
4MB |
HAL和加載器映射區 |
Windows操作系統用了一些特定的數據結構,比如說Push Locks,Ex Fast Referenced Pointers和Interlocked Slists,對這些數據結構的操作都需要CPU對同一個虛擬地址的數字執行兩遍原子操作。因此雖然64位處理器的虛擬地址是64位,卻必須要有128位長的CMPXCHG指令。但是在早期的64位處理器中是沒有這樣的指令的,在使用上述的數據結構的時候就會引發故障。64位CPU已經將虛擬地址有效位限制為48位,而Windows操作系統則進一步將虛擬地址有效位限制為44位,實際上可以用來存儲上述數據結構的虛擬地址空間大小就是2^44,即64位虛擬地址空間的高8TB的空間,也就是0xFFFFF80000000000 - 0xFFFFFFFFFFFFFFFF。例如之前的”未使用空間”,“PTE內存空間”,“ Hyper內存空間”和“系統cache工作集”都超出了44位虛擬地址的限制,都無法存儲這些特定的數據結構。這些限制也會影響到用戶空間,將用戶空間可用虛擬內存大小限制到了8TB,即0x00000000`00000000 - 0x000007FF`FFFFFFFF,內核空間可用虛擬虛擬內存大小也為8TB,即0xFFFFF000`00000000 - 0xFFFFFFFF`FFFFFFFF。需要說明的一點就是,由Windows操作系統使用的不在FFFF0800`00000000 - FFFFF7FF`FFFFFFFF范圍內的虛擬內存,也並不是都會分配和保存上述的特定數據結構。
64位處理器物理頁大小是4KB,CPU使用PTEs(Page Table Entry頁表項)來完成從虛擬地址到物理地址的映射,因此每個PTE映射4K大小的物理頁。64位處理器下的PTE占64位,也就是8個字節為了兼容更大的物理地址和PFNs(Page Frame Number,頁幀號)。因此單個頁表的物理頁可以容納512個PTE,所有的PTE可以映射2MB(512*4KB)的虛擬地址。同樣的,因為PDEs(Page Directory Entries,頁目錄項)指向頁表的物理頁,所以單個的PDE可以映射2MB的虛擬地址空間。
0x02 內核虛擬空間組成
下面說明內核地址空間的具體組成部分,及其作用。
未使用的 (Unused System Space)
由nt!MmSystemRangeStart開始,這部分在Windows 7 X64下並未使用
PTE空間 (PTE Space)
這部分包含了x64下用戶空間和內核空間的虛擬地址映射的4級頁表。X64下不同頁表頁的映射范圍如下:
PTE Pages FFFFF680`00000000
PDE Pages FFFFF6FB`40000000
PPE Pages FFFFF6FB`7DA00000
PXE Pages FFFFF6FB`7DBED000
Hyper空間 (HyperSpace)
映射進程的工作集。對每一個進程的EPROCESS.Vm.VmWorkingSetList 中包含的地址
0xFFFFF700`01080000就會映射到這片空間。這片空間包括MMWSL(Memory Manager Working Set List)結構和MMWSLE(Memory Manager Working Set List Entry)的數組結構,包括進程工作集的每個物理頁。
需要注意的是雖然函數MiMapPageInHyperSpaceWorker()支持映射物理頁到Hyper空間的虛擬地址,但實際上是將物理頁映射到了PTE空間,而不是真正的Hyper空間。
共享系統頁 (Shared System Page)
這4K大小的頁是由用戶空間和內核空間共享的,主要是用來在用戶層和內核層之前快速的傳遞信息,共享數據的數據結構就是nt!_KUSER_SHARED_DATA。
系統cache工作集(System Cache Working Set)
包含系統cache的虛擬地址的工作集(Working Set)和工作集鏈表項(Working Set List Entries)。
內核變量nt!MmSystemCacheWs指向系統cache工作集的數據結構(即nt!_MMSUPPORT)。想要顯示系統cache的工作集鏈表項可以使用WinDBG命令
"!wsle 1 @@(((nt!_MMSUPPORT *) @@(nt!MmSystemCacheWs))->VmWorkingSetList)"。而這些項會被用來修剪(trim)系統cache的虛擬內存的物理頁。
初始化加載映射區 (Initial Loader Mappings)
Ntoskrnl,HAL和內核調試DLL(KDCOM,KD1394,KDUSB)都會被加載到這片區域。除此之外,這片空間包含idle線程的線程棧,DPC的棧,KPCR和idle線程的數據結構。
分頁池區域 (Paged Pool Area)
分頁池的結束地址保存在變量nt!MmPagedPoolEnd中。而分頁池的大小保存在變量nt!MmSizeOfPagedPoolInBytes。當調用MiVaPagePool()時,MiObtainSystemVa()函數就會從這片區域分配內存,分頁池的內存分配方式由變量nt!MiPagedPoolVaBitMap按位(bit)決定。
PFN數據庫(PFN Database)
對於系統的每個物理頁在PFN中都有對應的項(nt!MmHighestPossiblePhysicalPage+1)。可以在WinDBG中輸入命令'? poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase)'來獲得”PFN Database”的大小。也可以使用命令
'?(poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase))/ @@(sizeof(nt!_MMPFN))'來獲得PFN中項的總數。而這片區域的起始地址保存在nt!MmPfnDatabase中。
不分頁內存池(Non-Paged Pool)
不分頁內存池的區域直接跟在PFN Database后面。不分頁內存池的起始地址保存在nt!MmNonPagedPoolStart中。當調用MiVaNonPagedPool()時,MiObtainSystemVa()就會在這片區域分配內存。內存的分配方式由變量nt!MiNonPagePoolVaBitmap按位決定。
硬件抽象層和加載映射區(HAL and Loader Mappings)
內核全局變量nt!MiLowHalVa包含這片區域的起始地址,即0xFFFFFFFFFFC00000。結束地址和X64內核虛擬地址空間結束地址一致,為0xFFFFFFFFFFFFFFFF。這片區域僅用於系統啟動時,也就是在MmInitSystem()函數中,這片區域中的內存在啟動初始化完畢以后就不可以再被使用了。
在系統初始化函數MmInitSystem()的結尾處調用函數MiAddHalIoMappings()來掃描這片虛擬地址空間來判斷是否有I/O映射到了這片空間,如果有,將會調用函數MiInsertIoSpaceMap()加入到由系統維護的I/O隊列中。而對於每一個I/O映射區域,MiInsertIoSpaceMap()都會用池標簽”Io space mapping trackers“創建一個tracker項,然后將其加入到頭為nt!MmIoHeader的雙向鏈表中,其中的每一項都表示的虛擬內存塊都已經映射了物理地址,而tracker項中的一些字段也包含關於物理內存和虛擬地址映射的信息。
struct _IO_SPACE_MAPPING_TRACKER { LIST_ENTRY Link; PHYSICAL_ADDRESS Pfn; ULONGLONG Pages; PVOID Va; . . . }
會話空間 (Session Space)
關於會話(session)的數據結構,會話池和會話映像都會加載到這片區域。
會話映像包括驅動映像比如Win32k.sys(Windows Manager),CDD.dll(Canonical Display Driver),TSDDD.dll(Frame Buffer Display Driver),DXG.sys(DirectX Graphics Driver)等等。
對於任意一個進程,其EPROCESS->Session指向的MM_SESSION_SPACE就是其所屬的會話結構,而會話池的范圍由MM_SESSION_SPACE->PagesPoolStart 和MM_SESSION_SPACE->PagesPoolEnd指定。
系統PTE (Sys PTEs)
這片區域包括映射的View,MDL,adapter內存,驅動程序的映像和內核棧。當使用MiVaSystemPtes()時,就會調用函數MiObtainSystemVa()在這片區域分配內存。
內核動態虛擬空間(Dynamic Kernel VA Space)
這片區域由系統cache的view,特定的分頁內存池和特定不分頁內存池組成。nt!MiSystemAvailableVa保存動態內核虛擬空間可用的2MB的區域數量。
調用MiObtainSystemVa()的參數是MiVaSystemCache,MiVaSpecialPoolPaged或MiVaSpecialPoolNonpaged時,將會從這片區域分配內存。
0x03 內核虛擬內存的分配
內存管理器使用函數MiObtainSystemVa()來動態的從不同的內核虛擬地址空間分配不同的2MB的內存。當調用MiObtainSystemVa()函數時,調用者需要指定分配的PDE項的總數和系統虛擬內存的分配類型(nt!_MI_SYSTEM_VA_TYPE),而對於此函數有效的類型為MiVaPagedPool,MiVaNonPagedPool,MiVaSystemPtes,MiVaSystemCache,MiVaSpecialPoolPaged,MiVaSpecialPoolNonPaged 。
MiObtainSystemVa()可以滿足不同的內核虛擬空間的分配請求。例如,MiVaPagedPool要求分配分頁池區域(Paged Pool region),MiVaNonPagedPool要求分配不分頁池區域(non-paged pool region),MiVaSystemPtes則分配系統PTE區域(System PTE region),而其他類型的分配請求則是直接分配系統動態虛擬內存(Dynamic System VA region)。內存的釋放則是由函數MiReturnSystemVa()完成。
一個動態內存分配的例子就是MiExpandSystemCache()調用MiObtainSystemVa()來獲取系統cache的view。MiExpandSystemCache()調用MiObtainSystemVa(MiVaSystemCache)來申請存放Cache Manager VACB(Virtual Address Control Block)數據結構的虛擬內存
0x04 系統PTE管理 (SysPTE Management)
由MiObtainSystemVa()從SysPTE區域分配的內存會由MiReservePtes()按照分配要求(nt!MiKernelStackPteInfo和nt!MiSystemPteInfo)進一步的划分為兩類,其目的就是為了防止虛擬內存的碎片化。因為內核棧內存(尤其是system和服務進程的線程)生命期是很長的,而其他的類型分配,例如MDL的生命周期相對短很多。
兩種結構類型nt!MiKernelStackPteInfo和nt!MiSystempteInfo都是屬於nt!_MI_SYSTEM_PTE_TYPE,而這些結構體都是由函數MiInitializeSystemPtes()產生,他們的每一位包含的信息可以影響SysPTE區域的128GB的空間。而函數MiReservePtes()在被調用時需要這些結構體其中一個作為參數來申請SysPTE區域以外的內存,申請的內存由MiReleasePtes()進行釋放。
當虛擬內存地址被nt!MiKernelStackPteInfo和nt!MiSystemPteInfo覆蓋時,則已經耗盡了通過調用MiExpandPtes()(實際調用MiObtainSystemVa(MiVaSystemPtes))擴展的內存區域。
函數MmAllocateMappingAddress()和MmCreateKernelStack()都是申請nt!MiKernelStackPteInfo類型的內存,而函數MiVaildateLamgePfn()和MiCreateImageFileMap(),MiRelocateImagePfn(),MiRelocateImageAgain()申請nt!MiSystemPteInfo類型的內存。