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类型的内存。