windows内存管理详解


前言

只有我们知道了windows如何管理内存空间,才能够得心应手的进行对抗,所以了解windows内存管理是很有必要的。

内存管理

VAD

VAD处于EPROCESS0x11c偏移处,它是一个二叉树结构

image-20220329153953780.png

查看一下VAD的结构

dt _MMVAD

image-20220329154007738.png

这里找一个进程,因为是根节点所以没有父节点

image-20220329154131189.png

然后往左遍历二叉树,在下一个节点处的父节点指向了上一个二叉树

image-20220329154230061.png

注意StartingVpnEndingVpn这两个结构,描述了当前页的位置,以4kb为单位,即0x400000到0x488000这一块内存空间已经被占用了

image-20220329154654585.png

在0x18有一个ControlArea结构,描述了这块结构体到底被谁占用,这里跟进去看0x24有一个FilePointer结构,如果这里的值为0就是一个真正的物理页,如果有值继续往里面找

image-20220329155005328.png

这里对应了Dbgview.exe

image-20220329155328329.png

在操作系统里面分配的内存只可能有两种类型,一种是VirtualAlloc自己分配的内存,一种是文件映射使用CreateFileMapping的内存,当ControlAreaFilePointer值为空的时候则是我们自己用VirtualAlloc分配的内存,还没有对应,如果值不为空则是文件映射的内存

使用!vad address命令直接遍历

image-20220329160155229.png

我们如果要了解属性到底是映射还是私有,是否是可读可写的话就需要找到0x14的u结构,使用dt _MMVAD_FLAGS查看,这里Protection表示到底是可读、可读可写、可执行权限,而PrivateMemory表示内存是Private还是Mapped

image-20220329161607204.png

image-20220329161930882.png

使用dt _MMVAD 86206d50查看一下属性

image-20220329162203329.png

对应的相关属性如下

kd> dt _MMVAD_FLAGS
nt!_MMVAD_FLAGS
   +0x000 CommitCharge
   +0x000 PhysicalMapping
   +0x000 ImageMap 
    //1 镜像文件  0 其他
   +0x000 UserPhysicalPages 
   +0x000 NoChange
   +0x000 WriteWatch 
   +0x000 Protection
    //1 READONLY  2  EXECUTE  3  EXECUTE _READ  4 READWITER  
    //5 WRITECOPY  6  EXECUTE _READWITER   7 EXECUTE_WRITECOPY  
   +0x000 LargePages 
   +0x000 MemCommit 
   +0x000 PrivateMemory
    //1 PrivateMemory 2 Map

Private Memory

使用VirtualAlloc申请内存

LPVOID VirtualAlloc{
    LPVOID lpAddress,   // 要分配的内存区域的地址
    DWORD dwSize,       // 分配的大小
    DWORD flAllocationType,     // 分配的类型
    DWORD flProtect     // 该内存的初始保护属性
};
// malloc1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>

LPVOID lpAddress;

int main(int argc, char* argv[])
{
    printf("Not VirtualAlloc");
    getchar();

    lpAddress = ::VirtualAlloc(NULL, 0x1000*2, MEM_COMMIT, PAGE_READWRITE);
    printf("Address : %x\n", lpAddress);

    getchar();
    return 0;
}

首先创建下进程,还没有分配内存空间

image-20220329182944263.png

看一下此时的vad树

image-20220329183013194.png

创建进程过后再看一下vad树

image-20220329183715244.png

image-20220329183054157.png

在C语言中使用malloc和在C++中使用new分配的堆空间并不是真正的内存,new其实就是调用malloc分配内存,而malloc的底层实现是HeapAlloc,但是这个HeapAlloc并没有进0环,而是通过在操作系统一开始用VirtualAlloc已经分配好的一大块空间里面取一块

我们试着分别在全局、堆、栈里面分配空间

// malloc1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

int x = 0x1234;

int main(int argc, char* argv[])
{
    printf("Before malloc");
    getchar();

    int y = 0x5678;
    int* z = (int*)malloc(sizeof(int)*128);

    printf("Golbal x : %x\n", &x);
    printf("Heap y : %x\n", &y);
    printf("Stack z : %x\n", z);

    getchar();

    return 0;
}

首先执行分配空间之前看一下vad

image-20220329185008191.png

image-20220329185100666.png

image-20220329185134645.png

malloc成功之后再去看一下vad树

image-20220329185221622.png

没有任何变化,证明malloc并不分配内存空间

image-20220329185318721.png

堆里面的地址为3807b8,对应的是Private内存

image-20220329185645999.png

栈的空间是12ff7c,栈是从大地址往小地址写

image-20220329185813099.png

全局变量为424d8c,全局变量内存在运行的时候就是以映射的方式

image-20220329200043202.png

Mapped Memory

Mapped分配的内存分为两种,分别是共享物理页面和共享文件

image-20220329200523952.png

类似于这种就是windows把自己的一些系统文件映射出来供所有进程使用

image-20220329200600029.png

还有一些Mapped内存就是物理页

image-20220329200838017.png

共享物理页

这里我们用CreateFileMapping创建一块Mapped内存,这里注意第一个参数如果设置为空windows会默认分配一个物理页,且与文件关联到一起

// Mapped_Memory.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#define MapFileName "Shared_Memory"

int main(int argc, char* argv[])
{
    HANDLE g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUFSIZ,MapFileName);

    // 将物理页与线性地址进行映射
    LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);

    *(PDWORD)g_lpBuff = 0x12345678;

    printf("Address - Word : %p - %x", g_lpBuff, *(PWORD)g_lpBuff);

    getchar();

    return 0;
}

image-20220329202737824.png

image-20220329202949604.png

然后再用B进程去访问物理页

// Mapped_Memory2.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#define MapFileName "Shared_Memory"

int main(int argc, char* argv[])
{
    HANDLE g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUFSIZ,MapFileName);

    // 将物理页与线性地址进行映射
    LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);

    printf("B_Process read A : %x",*(PDWORD)g_lpBuff);

    getchar();

    return 0;
}

image-20220329203233633.png

共享文件

首先创建一个文件,然后进行映射

// Mapped_Memory2.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>


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

    HANDLE g_hMapFile = CreateFileMapping(g_hFile,NULL,PAGE_READWRITE,0,BUFSIZ,NULL);

    // 将物理页与线性地址进行映射
    LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);

    printf("Address : %x\n", g_lpBuff);

    getchar();

    return 0;
}

image-20220329203930777.png

可以看到共享文件已经成功

image-20220329204122840.png

写拷贝

注意到这里还有EXECUTE_WRITECOPY,即写拷贝,这是因为映射到内存空间里面会有一些dll,而这些dll是不能够随便进行修改的,否则程序会崩溃,所以这里设置成为写拷贝则不能够进行修改,使用LoadLibrary函数导入的进程或模块都是EXECUTE_WRITECOPY属性

image-20220329210835378.png

HMODULE hModule = ::LoadLibrary("C:\\NOTEPAD.EXE");

1LoadLibrary 就是通过内存映射的方式实现的

2、为了避免影响到别人,属性为:写拷贝

物理内存管理

最大物理内存

10-10-12分页 最多识别物理内存为4GB

2-9-9-12分页 最多识别物理内存为64GB

操作系统限制

为什么在xp中,明明是2-9-9-12分页,单仍然无法超越4GB呢?

实际物理内存

MmNumberOfPhysicalPages* 4 = 物理内存

首先我们查看一下物理内存的大小

image-20220329212710375.png

然后通过搜索MmNumberOfPhysicalPages找到物理页的个数

image-20220329212758744.png

我们知道物理页是4字节的,这里计算一下就正好是物理内存的大小

image-20220329212828823.png

那么windows该如何管理物理内存呢?

每个物理页都对应一个结构体,如下

kd> dt _MMPFN
nt!_MMPFN
   +0x000 u1               : __unnamed
   +0x004 PteAddress       : Ptr32 _MMPTE
   +0x008 u2               : __unnamed
   +0x00c u3               : __unnamed
   +0x010 OriginalPte      : _MMPTE
   +0x018 u4               : __unnamed

通过MmPfnDatabase找到MMPFN的起始位置,数组长度用MmNumberOfPhysicalPages表示

image-20220329213508413.png

这里如果结构体要对应物理页如下所示

80c9300 ->  0000    第一个物理页
80c9300+1c  ->  1000    第二个物理页
80c9300+1c*2    ->  2000    第三个物理页

物理页的状态在u3这个成员里面表示,一共有6种状态,都是在物理页未被使用的情况下才会有状态

image-20220329214620012.png

0MmZeroedPageListHead
1MmFreePageListHead
2MmStandbyPageListHead
3MmModifiedPageListHead
4MmModifiedNoWritePageListHead
5MmBadPageListHead

操作系统用六个链表来定位

坏链
<1> MmBadPageListHead

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

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

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

<5> MmModifiedPageListHead

<6> MmModifiedNoWritePageListHead

image-20220329221216762.png

image-20220329221230020.png

__MMPFN的中存在 FlinkBlink两个成员,但其并不是指针而是ULONG类型,前面说过内存物理页是依次排列的,那么这两个成员有什么用呢?

前面我们说过其存在六个链表,虽然物理页的前后是确定的,但是相同属性的物理页却是通过 FlinkBlink连接起来的,其并不是连接而是在页帧数据库中的位置,这个很好理解。

1827556-20200423090547453-134322060.png

MmPageLocationList数组中存储的各个属性物理页头部的地址,这样整个页帧数据库体系就很容易搭建起来了。

Windbg遍历pfn单个 __MMPFN数据结构

使用windbg!pfn x 命令可以查看其存在多少对应的PFN对应的物理页的属性:

1827556-20200423083656957-1685826234.png

驱动遍历某一链表

结合我们上面所讲,其Pfn在内存中的布局如下,如果上面都看懂了,这部分应该很好理解,但其中MmPageLocationListMmPfnDataBase并不是导出变量,我们通过IDA特征码定位即可。

1827556-20200423114958214-2079385791.png

如何找到所有的物理页?

首先找到EPROCESS,定位到Vm结构

image-20220329221050678.png

然后再在MMWSL里面找到VmWorkingSetList即可找到所有的物理页

image-20220329221127204.png

1827556-20200423082400531-175836355.png

缺页异常

当CPU访问一个地址,其PTE的P位为0,此时会产生缺页异常

image-20220330101914822.png

看一下windows里面分配的虚拟内存

image-20220330100750664.png

在C盘根目录下的pagefile.sys文件里面

image-20220330100813011.png

无效PTE

在windows里面无效PTE有4种情况,如下所示

image-20220330102131055.png

看一下VirtualAlloc函数

LPVOID VirtualAlloc{
    LPVOID lpAddress,   // 要分配的内存区域的地址
    DWORD dwSize,       // 分配的大小
    DWORD flAllocationType,     // 类型:MEM_RESERVE MEM_COMMIT
    DWORD flProtect     // 该内存的初始保护属性
};

MEM_RESERVE:保留线性地址

MEM_COMMIT:可以有物理页,但不是立即有或者一直有

首先用VirtualAlloc申请一块内存

image-20220330102555528.png

然后查看PTE为0,在没有使用这块内存的时候windows是没有将这块内存挂上物理页的

image-20220330102627451.png

然后继续运行程序才会挂上物理页

image-20220330103013998.png

image-20220330102953379.png

再去看一下vad树,这里commit了8块内存

image-20220330103154494.png

再去u(_MMVAD_FLAGS)结构里面看一下CommitCharge的值也是8

image-20220330103322597.png

写拷贝

写拷贝就是为了防止一些重要的dll被修改导致其他程序崩溃,就将物理页映射到一个进程中,这里首先windows会将PTE的R/W为设置为0即只读权限,再把vad树里面的内存设置为写拷贝

当我们想要修改页的内容时因为是只读权限就会导致异常,异常会触发异常处理函数,异常梳理函数发现vad为写拷贝就会挂一个新的物理页

image-20220330104640041.png

评论

Drunkmars

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

硬件与物联网 文章:40 篇
XSS 文章:34 篇
逻辑漏洞 文章:15 篇
网络协议 文章:18 篇
企业安全 文章:40 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

草莓

收藏一下,目前见过最全的资料了

Y4tacker

Ps:纠错不是每个环境都是那样,之前很多时候/proc/self这些都可以,但是

苦咖啡

感谢大佬分享 863558996@qq.com

m1yuu

H

HaCky

stomppe 这部分有点问题,和官方文档有出入,文档中介绍开启这项功能,则会混

目录