从DbgView探究Windows内存管理笔记

0x00 前言

  讲内存管理单纯的理论比较空洞,所以本文从探究DbgVeiw的内存分布开始,来探究windows系统的内存管理,讨论malloc和VirtualAlloc的差别,和缺页异常。

参考文章

https://blog.csdn.net/weixin_42052102/article/details/83757538

https://blog.csdn.net/weixin_42052102/article/details/83722047

https://blog.csdn.net/weixin_42052102/article/details/83751896

0x01 线性地址的管理

首先在虚拟机中打开一个.exe的文件,这里选择了DbgView,之后到Windbg里查看对应的进程

看一下_KPROCESS结构体

在0x11c的位置的VadRoot处是一个搜索二叉树的入口点,每一个节点都记录被占用的线性地址空间,每一个节点的结构,都是_MMVAD结构体

接下来,先手动找一下这个搜索二叉树的节点  

观察一下1)Parent:因为是根节点所以没有父节点了

               2)LeftChild / RightChild:左子树/右子树,一个是往左边拓展的线,一个是往右边拓展

重复这一过程就可以找到所有的节点选择二叉树而并非链表是因为搜索二叉树查找的效率比链表高很多

               3)StartingVpn && EndingVpn: 这两个值是以页(4kb)为单位的,所以各把这两个数后面添上3个0,就是当前节点的起始位置与结束位置,也此两者之间就是已经被占用的位置,所以想找到那些已被占用和未被占用的线性地址,遍历这个二叉树就可以了。

             4)ControlArea:看一下结构体

     其中的FilePointer如果其中的值是NULL则线性地址对应的是真正的物理页

 

    而如果指向一个_FILE_OBJECT(如上图),那线性地址对应的是某文件影射的内存,我们继续跟下去

         5)_MMVAD+0x14成员u,我们不妨参考一下 ReactOS 里的说明


union {
   ULONG_PTR   LongFlags
 
   MMVAD_FLAGS   VadFlags
 
} u

主要使用的是MMVAD_FLAGS这个结构,跟进这个结构(和实际windows还是有一定差别的,但是主要成员没问题,其中Protection的偏移为0x14)

typedef struct _MMVAD_FLAGS
{
    ULONG_PTR CommitCharge:19
    ULONG_PTR NoChange:1
    ULONG_PTR VadType:3
    ULONG_PTR MemCommit:1
    ULONG_PTR Protection:5//规定文件属性
                          // 1 READONLY  2  EXECUTE  3  EXECUTE _READ  4 READWITER  
	                  // 5 WRITECOPY  6  EXECUTE _READWITER   7 EXECUTE_WRITECOPY  

    ULONG_PTR Spare:2
    ULONG_PTR PrivateMemory:1//是实际物理页(1:private)还是文件映射(0:mapped)
} MMVAD_FLAGS,*PMMVAD_FLAGS;

遍历所有节点,在windbg里也存在这样的指令! vad 0x89473968(这个是根节点的线性地址)

一下列举4种不同的情况

1. 其中的Level就是二叉树中的层级,

2. start和end 后面再加3个0(因为单位为4kb)就是每个节点线性地址的起始与终点位置

3.Private是指线性地址对应真实物理页(由),Mapped是指线性地址对应的是某文件影射的内存

4.文件的属性就是由上诉u里的MMVAD_FLAGS->Protection确定的,其中EXECUTE_WRITECOPY(写拷贝)这一情况下文会单独说明

如果要把DLL模块隐藏的话,不止要断TEB,PEB的各种的链,vad这块也得想办法绕过去,如果直接删除的话一定会造成系统的不稳定,所以可以VAD树隐藏一种是使 _MMVAD.StartingVpn=_MMVAD.EndingVpn,达到隐藏的效果,第二种可以将两个VAD结点融合达到隐藏效果,即p1.EndingVpn = p2.EndingVpn

0x01 Private Memory

  Private Memory,由上文可知线性地址对应的是实际物理页。具有Private Memory性质的线性地址是系统或者用户通过VirtualAlloc这个函数申请到的,那为什么叫Private?大概是因为VirtualAlloc申请的大小,是以物理页为单位的,当前线性地址独享整个物理页。

   在我们写 C或C++时申请内存时常用的是 malloc(C)或new(C++),那与上段的 VirtualAlloc有是什么分别?

(因为new的底层实现就是malloc,所以不讨论new)以下分别说明:

VirtualAlloc:

首先看一下原型:

LPVOID VirtualAlloc
{
	LPVOID lpAddress, 	// 要分配的内存区域的地址
	DWORD dwSize, 		// 分配的大小以物理页为单位
	DWORD flAllocationType, // 分配的类型
	DWORD flProtect 	// 该内存的初始保护属性
};

1. lpAddress:不能再二叉树节点的StartingVpn && EndingVpn之间申请,要在节与节之间申请,如果没有特殊的需要通常填空

2. dwSize:以物理页(4KB)为单位

3.flAllocationType:1.MEM_RESERVE :保留指定线性地址空间,不分配物理内存

                                2.MEM_COMMIT :为指定线性地址地址空间提交物理内存

这里多说以下,即使是commit时,刚刚申请下来,后也不会马上分物理页,而是等到真正使用到它的时候再使用物理页,下文缺页异常处会详细说明)

下面我们自己用VirtualAlloc申请一下内存:

#include <stdio.h>
#include <windows.h>

LPVOID lpAddress;

int main(int argc, char* argv[])
{
	printf("还没有申请");
	getchar();

	lpAddress = VirtualAlloc(NULL, 0X1000, MEM_COMMIT,PAGE_READWRITE);//申请一个物理页 COMMIT 可读可写

	printf("address:%x \n", lpAddress);
        getchar();
	return 0;
}

在还没申请的时候看一下对应进程的vad

继续运行:再看一下分布:

可以看到在3a0的地方多了我们申请的节点,一个物理页3a0,commit是1,如果是reserve的话commit就是0。

malloc 

malloc是在堆里申请内存,实际上,是系统提前利用VirtualAlloc申请了一块很大的内存,malloc是在已经分配好的内存里,拿出一部分来用,VirtualAlloc就像学校到出版社批发一大堆教材,批发就必须有最低单位(物理页4kb),而malloc就像学生从学校批发来的教材中零散的选择自己想要的教材,没有最小的限制。

同样来验证一下:

int main(int argc, char* argv[])
{
        printf("未开始分配");
        getchar();

	int x = 0x12345678;
        printf("栈:%x \n",&x);
        getchar();

	int* y = (int*)malloc(sizeof(int)*128);
	printf("堆:%x \n",y);
        getchar();

	return 0;
}

(可以逆一下malloc这个函数,会发现这个函数没有进入0环,也可以看出,malloc实际没有分配真正的内存)

运行,首先是栈:

运行,堆:

结果都如下图:

没有任何变化

堆在380~38f之间在节内

栈是从大到小的,理论上在30~12f之内。

验证完毕。

0x02 Mapped Memory

上文介绍了线性地址的其中一种(Private),这里来介绍线性地址的第二种mapped,Mapped性质的线性地址对应的是某文件影射的内存,这种线性地址不像Private(堆,栈...)独享物理页,而是同一物理页同时映射在不同的进程中,系统通过CreateFileMapping来申请

mapped性质的线性地址细分为两种:

一种是多个进程共享物理页

另一种是多个进程共享文件(物理页只有一份,物理上的文件映射到多个进程)

图解(嫖的):

看一下函数原型CreateFileMapping

HANDLE WINAPI CreateFileMapping(
_In_HANDLE hFile,//第一个参数如果不添则准备物理页,
                 //如果准备文件句柄,则不止准备物理页,还把文件与物理页关联上
_In_opt_LPSECURITY_ATTRIBUTES lpAttributes,
_In_DWORD flProtect,
_In_DWORD dwMaximumSizeHigh,
_In_DWORD dwMaximumSizeLow,
_In_opt_LPCTSTR lpName);

下面咱们自己实现以下:

以下是text1.exe(第一个进程)

#include <stdio.h>
#include <windows.h>

#define FileName "The_Shared_File"

int main(int argc, char* argv[])
{
	//创建文件或物理页	 我这里创建的是物理页
	HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUFSIZ,FileName);

	//将物理页与线性地址进行映射			
	LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);	
	
	*(PDWORD)lpBuff = 0x12345678;
				
	printf("%p: %x",lpBuff, *(PDWORD)lpBuff);

	getchar();
	return 0;
}

运行:

到windbg里看一下

我们在另起一个进程来读取这个进程的内容

以下是text2.exe(第二个进程)

#include <stdio.h>
#include <windows.h>

#define FileName "The_Shared_File"

int main(int argc, char* argv[])
{
	//打开相同的对象
	HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, FileName);

	//将物理页与线性地址进行映射			
	LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);	
				
	printf("进程2读取到的值: %x",*(PDWORD)lpBuff);

	getchar();
	return 0;
}

运行,成功得到共享物理页的值

上面说的是共享物理页,共享文件和共享物理页相似,下面把共享文件的脚本放一下:

进程1:

#include <stdio.h>
#include <windows.h>

int main(int argc, char* argv[])
{
	HANDLE hFile = CreateFile("C:\\NOTEPAD.EXE",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

	HANDLE hMapFile = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0, BUFSIZ,NULL);
	
	LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);	
	
	printf("共享文件的线形地址:%p",lpBuff);

	getchar();
	return 0;
}

vad里的情况:

但是,可以发现vad里还有如下这种形式

写拷贝

在此详细讲解

写拷贝的类型是由LoadLibrary来映射的,其目的是怕影响到其他进程,以上图为例,如果我们在ntdll里挂钩子,去修改的话,如果不是写拷贝,就会使所有ntdll映射的进程中ntdll改变,造成系统对不稳定,而写拷贝的话,会把ntdll映射到一份新的物理页,即使修改也是修改新物理页的ntdll(副本),和原来物理页的ntdll无关。

脚本:

#include <stdio.h>
#include <windows.h>

int main(int argc, char* argv[])
{
	HMODULE hModule = LoadLibrary("C:\\NOTEPAD.EXE");
    
	getchar();
	return 0;
}

vad的情况就是写拷贝。

线性地址的管理的讨论到此为止,下面是物理内存的管理(要区分开)。

0x03 物理内存管理

物理内存的总物理页数可以在windbg里用MmNumberOfPhysicalPages这个全局变量查看

这个全局变量的单位是物理页(4KB)所以,总共的大小MmNumberOfPhysicalPages* 4

其中每个物理页都对应一个结构体来描述,_MMPFN,它的大小不同系统不一定大,笔者这个的大小是0x18(0x14+4)。

而这些结构体都装在一个大的结构体数组中,此数组的起始位置由MmPfnDatabase查看:

正因为是结构体数组,所以在_MMPFN里没有必要描述物理页的指针,假如要查看第4个物理页的指针,

80C43000(MmPfnDatabase)  + 18h(sizeof(_MMPFN)) * 3,就可以找到。

物理页的状态

既然是物理页的信息,肯定要从_MMPFN结构体入手,这个结构体非常丧心病狂,我们只看和物理页状态相关的。

​​​​union {
   PFN_NUMBER   Flink
 
   ULONG   WsIndex
 
   PKEVENT   Event
 
   NTSTATUS   ReadStatus
 
   SINGLE_LIST_ENTRY   NextStackPfn
 
   SWAPENTRY   SwapEntry
 
} u1 
PMMPTE PteAddress
 
union {
   PFN_NUMBER   Blink
 
   ULONG_PTR   ShareCount
 
} u2
 
union {
   struct {
      USHORT   ReferenceCount
 
      MMPFNENTRY   e1
 
   } 

 
   struct {
      USHORT   ReferenceCount
 
      USHORT   ShortFlags
 
   }   e2
 
} u3
 
union {
   MMPTE   OriginalPte
 
   LONG   AweReferenceCount
 
   PMM_RMAP_ENTRY   RmapListHead
 
}; 

 
union {
   ULONG_PTR   EntireFrame
 
   struct {
      ULONG_PTR   PteFrame:25
 
      ULONG_PTR   InPageError:1
 
      ULONG_PTR   VerifierAllocation:1
 
      ULONG_PTR   AweAllocation:1
 
      ULONG_PTR   Priority:3
 
      ULONG_PTR   MustBeCached:1
 
   } 

 
} u4 
MMWSLE Wsle

看以下 u3的e1的结构MMPFNENTRY:

typedef struct _MMPFNENTRY
{
     USHORT Modified:1
     USHORT ReadInProgress:1
     USHORT WriteInProgress:1
     USHORT PrototypePte:1
     USHORT PageColor:4
     USHORT PageLocation:3//物理页的状态
                          //0:MmZeroedPageListHead
                          //1:MmFreePageListHead
                          //2:MmStandbyPageListHead
                          //3:MmModifiedPageListHead
                          //4:MmModifiedNoWritePageListHead
                          //5:MmBadPageListHead
     USHORT RemovalRequested:1
     USHORT CacheAttribute:2
     USHORT Rom:1
     USHORT ParityError:1
}

windows会把上述6种不同状态的物理页分别串成6个链表(具体解释其中4个)

<1> MmBadPageListHead

坏链

<2> MmZeroedPageListHead

零化链表(是系统在空闲的时候进行零化的,不是程序自己清零的那种)

<3> MmFreePageListHead

空闲链表(物理页是周转使用的,刚被释放的物理页是没有清0,系统空闲的时候有专门的线程从这个队列摘取物理页,加以清0后再挂入MmZeroedPageListHead)

<4> MmStandbyPageListHead

备用链表(当系统内存不够的时候,操作系统会把物理内存中的数据交换到硬盘上,此时页面不是直接挂到空闲链表上去,而是挂到备用链表上,虽然我释放了,但里边的内容还是有意义的) 

ok,我们来跟一下其中一个双向链表(笔者这里选择零化链表),来探究结构

第3,4成员是前者是前一物理页的索引,向前遍历,后者是是后一物理页的索引,向后遍历,笔者这里往前走了(具体结构到ReactOS里看一下,笔者写不动了)

那如何找到具体的位置呢,用上文的公式80C43000(MmPfnDatabase)  + 18h(sizeof(_MMPFN)) * n

重复上一过程:

发现此刻链表的第3个成员刚好是第一个前节点的索引

总结个图:

最后我们来找一下某进程的所有物理页

还是用DbgView为例先找到DbgView对应的_EPROCESS结构体

在+1f8的位置找到VM

进入_MMSUPPORT

其中遍历VmWorkingSetList就可以找到所有物理页了。

还有很多细节可以参考《Windows内核管理与实现》

0x04 缺页异常

PTE的结构:

虽然有2-2-9-12分页和10-10-12分页,但是在属性这块没有太大差别

P位判断当前页面是否有效,当p位为1则有对应物理页,为0则没有物理页

当CPU访问某地址,发现P位为0,则发生缺页异常

正是不断进行的缺页异常大大提高了内存的使用效率,windows只把当前需要使用的线性地址挂上物理页,而暂时不使用的线性地址,windows会把对应物理页的数据转移到硬盘上。

虚拟内存

可以看一下虚拟机的虚拟内存

1.右击“我的电脑”,选择“属性”。点击“高级”选择“性能”→“设置”

2、打开“性能选项”,选择“虚拟内存”就可以改了

把上述的虚拟内存改了之后,在c盘根目录下会生成一个pagefile.sys文件

这个文件的大小就是虚拟内存的大小。

大体上:当我们的物理页占的差不多的时候,windows会把不用的线性地址A的物理页的数据放到pagefile.sys文件文件中,再把这个物理页给别的正在使用的线性地址B使用,使用后并不会释放,如果地址再次访问线性地址A的时候,A此时没有对应的物理页,就会触发缺页异常。

细节:

图a:位于页面文件中
当你的线性地址的物理页被存放到页面文件(pagefile.sys)中的时候,你的PTE就会变成这样
CPU访问了这地址你的p:0,进入缺页异常处理程序,缺页异常位于idt表里的E号中断。
进入缺页异常处理它会回头查看你的这个PTE,这时地址时发现你的1-9位,12-31位都是有值的,它就会知道你这个线性地址是有效的,只不过数据放到页面文件里了
异常处理程序会到pagefile.sys中,按照PTE上描述的页面文件偏移将页的数据取出来挂到一个新的物理页上,将12-31位改为新的物理页地址,再将p=1
图b:要求0页面
页面尚未分配,下次访问时请求一个0页面
图c:转移
页面在物理内存中,但已被转移到某个物理页面链表中,可以通过查询_MMPFN数据库获取实际情况
图d:
缺页异常处理发现你PTE全部为0就会去查VAD,发现这个线性地址已经分配了它会帮你把物理页挂上,如果这个线性地址没有分配就会报0xC0000005。(具体看 “保留与提交的误区” )

保留与提交

详情见这位大佬的文章

使用VirtualAlloc申请时,并不会直接挂上物理页,只有真正使用的时候才会,上文在vad中显示的commit只是说保留了物理页,但比一定挂上。

0x05 总结

本文是我探究内存管理的过程,主要跟着My classment这位博主来的,记录了一些我在学习时的情况,实践比较多,如果有什么问题,望路过的大佬斧正。

猜你喜欢

转载自blog.csdn.net/m0_46362499/article/details/105783085