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区域。

(0)

相关推荐

  • 【精品博文】嵌入式高端内存全揭秘

    在一般情况下,Linux在初始化时,总是尽可能的将所有的物理内存映射到内核地址空间中去.如果内核地址空间起始于0xC0000000,为vmalloc保留的虚拟地址空间是128M,那么最多只能有(1G- ...

  • 一个小小指针,竟把Linux内核攻陷了!

    来自公众号:编程技术宇宙 怎样攻进操作系统内核? 这是一个很有意思也很硬核的问题. 黑客通过应用程序的漏洞(如Java.PHP.Apache.IE.Chrome.Adobe.office等)获得执行代 ...

  • Linux内存管理 (3)内核内存的布局图

    专题:Linux内存管理专题 关键词:内核内存布局图.lowmem线性映射区.kernel image.ZONE_NORMAL.ZONE_HIGHMEM.swapper_pg_dir.fixmap.v ...

  • 一次解决Linux内核内存泄漏实战全过程

    2020 年转眼间白驹过隙般飞奔而去,在岁末年初的当口,笔者在回顾这一年程序员世界的大事件后,突然发觉如何避免程序员面向监狱编程是个特别值得一谈的话题. 什么是内存泄漏 程序向系统申请内存,使用完不需 ...

  • 《绝地求生》正式版评测之内存篇:8G刚好,16G更美

    "落地成盒"相信是每一个绝地求生玩家都有过的惨痛经历,特别是那种枯燥并且充满粗言秽语的等待开局过后--迎来却是眼前一黑的孤寂,怎么都会不好受.虽然不是每一个人都拥有异于常人的游戏天 ...

  • 如何让手机内存变大?内存不足解决方法【详解】

    在购买手机的时候,有些人就选择了一些内存比较大的机型,避免日后自己因手机内存小而带来不好的体验.但是使用使用中人们就会发现,再多的手机内存也不够用,随着软件不断升级.手机中使用的垃圾增多等等,人们也开 ...

  • 64g的手机半年就内存不够,手机内存为何越来越不够用?

    为何各大手机厂商都在增加内存,但是好像手机内存在增加反而还不够用了? [64G的手机半年就内存不够,手机内存为何越来越不够用?日前,记者调查发现,众多的APP让手机不堪重负.垃圾挤占内存却很难清理等问 ...

  • MemVerge正式发布大内存软件,实现内存计算架构变革

    "在AI.机器学习和金融数据分析方面已有清晰应用场景." 作者:涂鸦君 编辑:tuya 出品:财经涂鸦(ID:caijingtuya) 据<财经涂鸦>消息,9月24日, ...

  • 19GB内存,实为12GB内存+7GB虚拟内存

    realme真我GT大师版系列将搭载骁龙870芯片与柔性曲面屏,屏幕支持DC调光,采用内存扩展技术的realmeGT大师系列,将支持19GB内存(12GB内存+7GB虚拟内存),将一部分存储空间当作虚 ...

  • 科普文,笔记本电脑内存和台式机的内存,区别有点大

    教育不是灌输知识,教育是养成能力的过程.把知识学习转化成技能获取,是养成教育的重要内容.本文基于<计算机组装与维护>课程内容,实战配机方案.解读笔记本电脑和台式机的内存,区别有点大. 左右 ...

  • 社群变现篇一,社群布局篇(学习社群布局方...

    社群变现篇 一,社群布局篇(学习社群布局方案,做好变现准备) 1,把生意建立在微信之上 (1)改变你的产品形式 (2)改变你的销售方式 (3)改变你的引流方式 (4)重构你的软件系统 (5)重建你的团 ...