Windows内存篇Ⅱ x64内核内存布局的迁移演变
FaEry 看雪学院 3天前
本文为看雪论精华文章
看雪论坛作者ID:FaEry
前言
内存扫描一直都是比较根本的威胁检测手法,对于顽固型木马外挂而言,依赖于操作系统API相关的传统检测方式,模块扫描,窗口扫描,进程扫描以及文件扫描等都显得较为吃力,应用层而言如此,到内核层亦是如此。
对于x86内核而言,0x80000000~0xFFFFFFFF 2GB内存扫描检测不是什么难事,因为整个区域就这么大。
但是对于x64内核空间而言,就没有这么简单,尽管64位地址高16位一直都是保留字节0xffff,48位地址虚拟内存总量也大的有些离谱。或许有人会说物理内存就这么大,现在见的比较大的是16GB、32GB内存条,直接扫描物理内存不就好了。
起初我也这么想,但是最主要的是要访问物理内存本质上需要通过虚拟内存经过页表转换到MMU单元才能完成对物理页的访问,而且抛开这个层面,尽管可以直接访问物理内存最终我们也是需要转为虚拟内存地址。
那么本篇就来探索下从Win7开始到Win10 x64内核的虚拟地址空间布局以及经历了哪些更新变化。
项目传送门
黑暗之门
https://github.com/FaEryICE/MemScanner
一. Win7/Win8 x64固定布局
Win7相关资料也比较多,Win8总体上与Win7很像,就放在一起介绍了。推荐下面两篇文章
原文版本:Kernel Virtual Address Layout
翻译版本:内核虚拟地址空间布局
引用下这张图:
PTE Space 顾名思义页表所在区域;
Hyper Space 姑且称为超区域,主要是操作系统做中转用的,不同页表场景下需要用这块区域临时映射;
Shared System Page 共享内存空间,会映射到每一个进程空间,应用层地址即可访问内核层数据;
System Cache Working Set 系统缓存工作集,没怎么研究过,还有个SystemCache区域,老容易搞混...
Initial Loader Mappings 最初的加载器映射区域, 由加载器初始化。在系统引导阶段,winload.exe 将 NTOSKRNL.EXE,HAL.DLL,以及内核调试器 DLL(KDCOM, KD1394, KDUSB)加载到此区域。该区域还包含了空闲线程的栈,DPC(延迟过程调用)的栈,以及 KPCR(内核处理器控制区,Kernel Processor Control Region)和 Idle 线程的数据结构。
Sys PTEs 系统页表条目区域重点哦,MDL映射的虚拟内存,驱动映像都在这里。
Paged Pool Area 分页内存区域注意区分SpecialPagedPool。
Dynamic Kernel VA Spacce 动态内存区域 MiVaSystemCache(注意前面还有一个缓存工作集), MiVaSpecialPoolPaged(特殊池,特殊池还会细分PagedPool与NonpagedPool)
PFN Database PFN 数据库 别的我不知道,MmGetVirtualForPhysical找虚拟地址的时候访问PFN数据库容易蓝屏
Non-paged Pool 非分页内存区域 不会被倒换到磁盘空间,只要有物理内存映射,只要不被申请者释放一般都很稳定,注意这个地址需要获取!
那我们看看怎么获取MmNonPagedPoolStart与MmNonpagedPoolEnd吧。
固定思路先从MiInitializeNonPagedPool函数找到未导出变量MmNonPagedPoolStart,发现他在MiCreatePfnDatabase里初始化。
自己算不太现实,有几个因子也不好确定。查看MmNonpagedPool的引用发现数据段上有个引用:
最上面有个KdDebuggerDataBlock好像跟调试有关,发现导出函数KeCapturePersistentThreadState有用到这个,记得Blackbone有用到这个函数,可以获取一堆未导出变量地址:
#ifndef _WIN64#define DUMP_BLOCK_SIZE 0x20000#else#define DUMP_BLOCK_SIZE 0x40000#endif #ifndef _WIN64#define KDDEBUGGER_DATA_OFFSET 0x1068#else#define KDDEBUGGER_DATA_OFFSET 0x2080#endif VOID InitializeDebuggerBlock(){ CONTEXT context = { 0 }; context.ContextFlags = CONTEXT_FULL; RtlCaptureContext(&context); PDUMP_HEADER dumpHeader = (PDUMP_HEADER)ExAllocatePoolWithTag(NonPagedPool, DUMP_BLOCK_SIZE, MMS_POOL_TAG); if (dumpHeader) {KeCapturePersistentThreadState(&context, NULL, 0, 0, 0, 0, 0, dumpHeader);g_KdDebuggerDataBlock = dumpHeader->KdDebuggerDataBlock; RtlCopyMemory(&g_KdBlock, (PUCHAR)dumpHeader + KDDEBUGGER_DATA_OFFSET, sizeof(g_KdBlock));ExFreePool(dumpHeader); }}
dumpHeader->KdDebuggerDataBlock就是KdDebuggerDataBlock地址,另外在0x2080偏移处还有另外一份Copy。
那么拿到内存区域,就可以扫描一些自己想要的东西了,我最想扫的就是PG代码了...... 也可以扫隐藏驱动,隐藏进程。
这里实际的应用我就不再做阐述了,简单的做个验证吧,找了几种可以在内核申请内存的方式:
可以得出MmMapViewInSystemSpace是直接映射到System Ptes区域,与驱动映像属于同一块区域;
MDL申请出来的虚拟内存也是在System Ptes区域;
MmAllocateContiguousMemory申请的连续非分页物理内存 对应的虚拟内存空间 也是在SystemPtes区域。
二. Win8.1~Win10 TH2(10586)
x64 内核内存布局
说起这个阶段,印象最为深刻的就是这两个函数了:
MiInitializeDynamicRegion 与 MiInitializeDynamicBitmap
MiInitializeDynamicRegion看着就以为好多区域都动态了,不再静态了,实际上呢?
就拿这三个区域说事,唬人还是可以的。
不过,引入的这两个结构管理内存区域,算是很值得称赞的,内存块全部用Bitmap管理,优化了性能(_MI_DYNAMIC_BITMAP Win8.1没有符号)。
//0x68 bytes (sizeof)struct _MI_SYSTEM_PTE_TYPE{ struct _RTL_BITMAP_EX Bitmap; //0x0 struct _MMPTE* BasePte; //0x10 ULONG Flags; //0x18 enum _MI_SYSTEM_VA_TYPE VaType; //0x1c ULONG* FailureCount; //0x20 ULONG PteFailures; //0x28 union {ULONGLONG SpinLock; //0x30 struct _FAST_MUTEX* GlobalMutex; //0x30 }; struct _MMSUPPORT* Vm; //0x38 volatile ULONGLONG TotalSystemPtes; //0x40 ULONGLONG Hint; //0x48 struct _MI_CACHED_PTE* CachedPtes; //0x50 volatile ULONGLONG TotalFreeSystemPtes; //0x58 volatile LONG CachedPteCount; //0x60}; //0x50 bytes (sizeof)struct _MI_DYNAMIC_BITMAP{ struct _RTL_BITMAP_EX Bitmap; //0x0 ULONGLONG MaximumSize; //0x10 ULONGLONG Hint; //0x18 VOID* BaseVa; //0x20 ULONGLONG SizeTopDown; //0x28 ULONGLONG HintTopDown; //0x30 VOID* BaseVaTopDown; //0x38ULONGLONG SpinLock; //0x40 struct _MMSUPPORT* Vm; //0x48};
其实比较难找的还是NonPagePool区域,因为从Win8.1开始,KdDebuggerDataBlock就不再导出MmNonPagedPoolStart内核变量了,再来Win8.1开始就直接去掉了MmNonPagedPoolStart变量...真令人发指!
跟进去MiInitializeNonpagedPool函数看看,由于Win8.1结构体没符号,拿10240作为样例分析:
这个qword_140340930原名叫做MiNodeInformation,Win10把这个变量符号拿掉了。
这个函数其实是对 0x100000000000 大小的NonpagedPool内存进行等分(份数就是KeNumberNodes的值,这个值是干什么的呢?目前我还没搞懂)
那么NonpagedPool基地址是多少呢?
是 ((0x8000000 / KeNumberNodes) << 9 * Index - 0x2000000000) << 12 (Release版本优化才会这么难看)。
其实地址就是 Index 为0的时候,正好是 0xFFFFE000`00000000
整理之后,得到:
NTSTATUS MmsInitMemoryLayoutForWin8_1ToWin10TH2(IN OUT PDYNAMIC_DATA pData){ if (!pData) { return STATUS_INVALID_ADDRESS; } pData->MmPteSpaceStart = (PVOID)0xFFFFF68000000000; pData->MmPteSpacecEnd = (PVOID)0xFFFFF6FFFFFFFFFF;pData->MmHyperSpaceStart = (PVOID)0xFFFFF70000000000; pData->MmHyperSpaceEnd = (PVOID)0xFFFFF77FFFFFFFFF; pData->MmSharedSystemPageStart = (PVOID)0xFFFFF78000000000; pData->MmSharedSystemPageEnd = (PVOID)0xFFFFF78000000FFF; pData->MmSystemCacheStart = (PVOID)0xFFFFB00000000000;pData->MmSystemCacheEnd = (PVOID)0xFFFFBFFFFFFFFFFF; pData->MmPagedPoolStart = (PVOID)0xFFFFC00000000000; pData->MmPagedPoolEnd = (PVOID)0xFFFFCF7FFFFFFFFF;pData->MmSpecialPoolStart = (PVOID)0xFFFFCF8000000000; pData->MmSpecialPoolEnd = (PVOID)0xFFFFCFFFFFFFFFFF; pData->MmSystemPtesStart = (PVOID)0xFFFFD00000000000;pData->MmSystemPtesEnd = (PVOID)0xFFFFDFFFFFFFFFFF; pData->MmNonpagedPoolStart = (PVOID)0xFFFFE00000000000; pData->MmNonpagedPoolEnd = (PVOID)0xFFFFF00000000000;//等分成KeNumberNodes块 pData->MmDriverImageStart = (PVOID)0xFFFFF80000000000;pData->MmDriverImageEnd = (PVOID)0xFFFFF87FFFFFFFFF; pData->MmSessionSpaceStart = (PVOID)0xFFFFF90000000000; pData->MmSessionSpaceEnd = (PVOID)0xFFFFF97FFFFFFFFF;pData->MmDynamicVASpaceStart = (PVOID)0xFFFFF98000000000; pData->MmDynamicVASpaceEnd = (PVOID)0xFFFFFA70FFFFFFFF; pData->MmPfnDatabaseStart = (PVOID)0xFFFFFA8000000000; pData->MmPfnDatabaseEnd = (PVOID)((ULONG_PTR)pData->MmNonpagedPoolStart - 1); return STATUS_SUCCESS;}
看看Win8.1 测试结果
结果有些不一样,MmMapViewInSystemSpace映射的地址空间在DriverImage,而MDL与连续的物理页面映射的虚拟地址都在SystemPtes。
其实从Win8.1开始,DriverImage不再纳入System Ptes空间,而是归入Initial Loader Mapping区域,统一改名为DriverImage,MapViewOfSection这种方式就会在DriverImage中申请虚拟内存空间。
三. Win10 RS1~Win10 20H1(19041)
x64 内核内存布局
要是说Win8.1与早期的Win10版本没有真正意义上的内存布局动态化的话,那么Win10RS1以后的就是真正的动态化了,这个动态化从调试来看不是nt内部做的初始化,是更上一层,推测应该是Hal或者是Hyper-V进行的初始化布局。
之前研究过这篇文章 Win10 1909 反向计算windows内核内存布局及代码实现 知道了内核有个0x100大小的Mark标记数组,可以通过数组计算每个区域的具体位置,可惜的是到Win10 2004就已经找不到Mark的踪迹了。
观前顾后,我找到了MiQuerySystembase函数用到的一个未导出变量:
我是怎么找到他的呢?很简单,还是看的MiInitializeNonpagedPool,发现用了这个数组的0号成员,很明显这个数组就是存放各种区域的地址范围的,并且Nt初始化时这个数组也早已被初始化了。
通过一些猜测与符号的查找,发现这个数组结构体以及宏定义如下:
enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSession = 10, AssignedRegionSecureNonPagedPool = 11,AssignedRegionSystemImages = 12, AssignedRegionMaximum = 13}; typedef struct _MI_SYSTEM_VA_ASSIGNMENT{ VOID* BaseAddress; //0x0 ULONGLONG NumberOfBytes; //0x8} MI_SYSTEM_VA_ASSIGNMENT, * PMI_SYSTEM_VA_ASSIGNMENT;
在NT内核初始化时,会将Assignment数组里的地址范围全部应用到系统中。
那么找这个数组就用MiQuerySytemBase的特征码就行了:
MmsScanSection(".text",(PCUCHAR)"\x48\x63\xC1\x48\x8D\x0D\xCC\xCC\xCC\xCC\x48\x03\xC0\x48\x8B\x04\xC1\xC3", 0xCC, 18, (PVOID)&lpTargetAddr); if (!lpTargetAddr){ DbgPrint("MemScanner: %s: MmsScanSection Failed\n", __FUNCTION__); return STATUS_NOT_FOUND;} lpMiSystemVaAssignment = (PMI_SYSTEM_VA_ASSIGNMENT)((PUCHAR)lpTargetAddr + *(PULONG)((PUCHAR)lpTargetAddr + 6) + 10);for (ulIndex = 0; ulIndex < AssignedRegionMaximum; ulIndex++){ DbgPrint("MemScanner: %s: Names:%s, BaseAddr:%I64x, Size:%I64x\n", __FUNCTION__, g_szAssignedRegionNames[ulIndex],lpMiSystemVaAssignment[ulIndex].BaseAddress,lpMiSystemVaAssignment[ulIndex].NumberOfBytes);}
不过,最让人头疼的事情是:
Win10 RS1enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionPageTables = 8, AssignedRegionSpecialPool = 9,AssignedRegionSession = 10, AssignedRegionWorkingSetPagedPool = 11,AssignedRegionWorkingSetSystemCache = 12, AssignedRegionSystemImages = 13,AssignedRegionMaximum = 14}; Win10 RS2enum _MI_ASSIGNED_REGION_TYPES{AssignedRegionNonPagedPool = 0, AssignedRegionPagedPool = 1,AssignedRegionSystemCache = 2, AssignedRegionSystemPtes = 3,AssignedRegionUltraZero = 4, AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionPageTables = 8,AssignedRegionSpecialPoolPaged = 9, AssignedRegionSpecialPoolNonPaged = 10,AssignedRegionSession = 11, AssignedRegionSystemImages = 12, AssignedRegionMaximum = 13}; Win10 RS3enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSpecialPoolPaged = 10, AssignedRegionSpecialPoolNonPaged = 11,AssignedRegionSession = 12, AssignedRegionSystemImages = 13, AssignedRegionMaximum = 14}; Win10 RS4enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSpecialPoolPaged = 10, AssignedRegionSpecialPoolNonPaged = 11,AssignedRegionSession = 12, AssignedRegionSystemImages = 13, AssignedRegionMaximum = 14}; Win10 RS5enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSpecialPoolPaged = 10, AssignedRegionSpecialPoolNonPaged = 11,AssignedRegionSession = 12, AssignedRegionSystemImages = 13, AssignedRegionMaximum = 14}; Win10 19H1enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSession = 10, AssignedRegionSystemImages = 11, AssignedRegionMaximum = 12}; Win10 19H2enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSession = 10, AssignedRegionSystemImages = 11, AssignedRegionMaximum = 12}; Win10 20H1enum _MI_ASSIGNED_REGION_TYPES{ AssignedRegionNonPagedPool = 0,AssignedRegionPagedPool = 1, AssignedRegionSystemCache = 2,AssignedRegionSystemPtes = 3, AssignedRegionUltraZero = 4,AssignedRegionPfnDatabase = 5, AssignedRegionCfg = 6, AssignedRegionHyperSpace = 7, AssignedRegionKernelStacks = 8, AssignedRegionPageTables = 9,AssignedRegionSession = 10, AssignedRegionSecureNonPagedPool = 11,AssignedRegionSystemImages = 12, AssignedRegionMaximum = 13};
尽管8号之前没什么变化,但是后面序号一直在变...
最后看下Win10的测试结果吧,由于序号变得快,调试信息有不准的地方我也没做修改了...
结论还是与Win8.1一致,MmMapViewInSystemSpace映射在DriverImage区域,MDL与连续物理页的虚拟页映射在SystemPtes区域。