前言
只有我们知道了windows如何管理内存空间,才能够得心应手的进行对抗,所以了解windows内存管理是很有必要的。
内存管理
VAD
VAD处于EPROCESS
的0x11c
偏移处,它是一个二叉树结构
查看一下VAD
的结构
dt _MMVAD
这里找一个进程,因为是根节点所以没有父节点
然后往左遍历二叉树,在下一个节点处的父节点指向了上一个二叉树
注意StartingVpn
和EndingVpn
这两个结构,描述了当前页的位置,以4kb为单位,即0x400000到0x488000这一块内存空间已经被占用了
在0x18有一个ControlArea
结构,描述了这块结构体到底被谁占用,这里跟进去看0x24有一个FilePointer
结构,如果这里的值为0就是一个真正的物理页,如果有值继续往里面找
这里对应了Dbgview.exe
在操作系统里面分配的内存只可能有两种类型,一种是VirtualAlloc
自己分配的内存,一种是文件映射使用CreateFileMapping
的内存,当ControlArea
的FilePointer
值为空的时候则是我们自己用VirtualAlloc
分配的内存,还没有对应,如果值不为空则是文件映射的内存
使用!vad address
命令直接遍历
我们如果要了解属性到底是映射还是私有,是否是可读可写的话就需要找到0x14的u
结构,使用dt _MMVAD_FLAGS
查看,这里Protection
表示到底是可读、可读可写、可执行权限,而PrivateMemory
表示内存是Private
还是Mapped
使用dt _MMVAD 86206d50
查看一下属性
对应的相关属性如下
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;
}
首先创建下进程,还没有分配内存空间
看一下此时的vad树
创建进程过后再看一下vad树
在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
malloc成功之后再去看一下vad树
没有任何变化,证明malloc并不分配内存空间
堆里面的地址为3807b8
,对应的是Private
内存
栈的空间是12ff7c
,栈是从大地址往小地址写
全局变量为424d8c
,全局变量内存在运行的时候就是以映射的方式
Mapped Memory
Mapped
分配的内存分为两种,分别是共享物理页面和共享文件
类似于这种就是windows把自己的一些系统文件映射出来供所有进程使用
还有一些Mapped
内存就是物理页
共享物理页
这里我们用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;
}
然后再用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;
}
共享文件
首先创建一个文件,然后进行映射
// 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;
}
可以看到共享文件已经成功
写拷贝
注意到这里还有EXECUTE_WRITECOPY
,即写拷贝,这是因为映射到内存空间里面会有一些dll,而这些dll是不能够随便进行修改的,否则程序会崩溃,所以这里设置成为写拷贝则不能够进行修改,使用LoadLibrary
函数导入的进程或模块都是EXECUTE_WRITECOPY
属性
HMODULE hModule = ::LoadLibrary("C:\\NOTEPAD.EXE");
1、LoadLibrary 就是通过内存映射的方式实现的
2、为了避免影响到别人,属性为:写拷贝
物理内存管理
最大物理内存
10-10-12分页 最多识别物理内存为4GB
2-9-9-12分页 最多识别物理内存为64GB
操作系统限制
为什么在xp中,明明是2-9-9-12分页,单仍然无法超越4GB呢?
实际物理内存
MmNumberOfPhysicalPages
* 4 = 物理内存
首先我们查看一下物理内存的大小
然后通过搜索MmNumberOfPhysicalPages
找到物理页的个数
我们知道物理页是4字节的,这里计算一下就正好是物理内存的大小
那么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
表示
这里如果结构体要对应物理页如下所示
80c9300 -> 0000 第一个物理页
80c9300+1c -> 1000 第二个物理页
80c9300+1c*2 -> 2000 第三个物理页
物理页的状态在u3
这个成员里面表示,一共有6种状态,都是在物理页未被使用的情况下才会有状态
0:MmZeroedPageListHead
1:MmFreePageListHead
2:MmStandbyPageListHead
3:MmModifiedPageListHead
4:MmModifiedNoWritePageListHead
5:MmBadPageListHead
操作系统用六个链表来定位
坏链
<1> MmBadPageListHead
零化链表(是系统在空闲的时候进行零化的,不是程序自己清零的那种)
<2> MmZeroedPageListHead
空闲链表(物理页是周转使用的,刚被释放的物理页是没有清0,系统空闲的时候有专门的线程从这个队列摘取物理页,加以清0后再挂入MmZeroedPageListHead)
<3> MmFreePageListHead
备用链表(当系统内存不够的时候,操作系统会把物理内存中的数据交换到硬盘上,此时页面不是直接挂到空闲链表上去,而是挂到备用链表上,虽然我释放了,但里边的内容还是有意义的)
<4> MmStandbyPageListHead
<5> MmModifiedPageListHead
<6> MmModifiedNoWritePageListHead
__MMPFN
的中存在 Flink
与 Blink
两个成员,但其并不是指针而是ULONG类型,前面说过内存物理页是依次排列的,那么这两个成员有什么用呢?
前面我们说过其存在六个链表,虽然物理页的前后是确定的,但是相同属性的物理页却是通过 Flink
与 Blink
连接起来的,其并不是连接而是在页帧数据库中的位置,这个很好理解。
而MmPageLocationList
数组中存储的各个属性物理页头部的地址,这样整个页帧数据库体系就很容易搭建起来了。
Windbg
遍历pfn
单个 __MMPFN
数据结构
使用windbg
的 !pfn x
命令可以查看其存在多少对应的PFN
对应的物理页的属性:
驱动遍历某一链表
结合我们上面所讲,其Pfn在内存中的布局如下,如果上面都看懂了,这部分应该很好理解,但其中MmPageLocationList
与MmPfnDataBase
并不是导出变量,我们通过IDA特征码定位即可。
如何找到所有的物理页?
首先找到EPROCESS
,定位到Vm
结构
然后再在MMWSL
里面找到VmWorkingSetList
即可找到所有的物理页
缺页异常
当CPU访问一个地址,其PTE的P位为0,此时会产生缺页异常
看一下windows里面分配的虚拟内存
在C盘根目录下的pagefile.sys
文件里面
无效PTE
在windows里面无效PTE有4种情况,如下所示
看一下VirtualAlloc
函数
LPVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的内存区域的地址
DWORD dwSize, // 分配的大小
DWORD flAllocationType, // 类型:MEM_RESERVE MEM_COMMIT
DWORD flProtect // 该内存的初始保护属性
};
MEM_RESERVE:保留线性地址
MEM_COMMIT:可以有物理页,但不是立即有或者一直有
首先用VirtualAlloc
申请一块内存
然后查看PTE为0,在没有使用这块内存的时候windows是没有将这块内存挂上物理页的
然后继续运行程序才会挂上物理页
再去看一下vad树,这里commit
了8块内存
再去u
(_MMVAD_FLAGS
)结构里面看一下CommitCharge
的值也是8
写拷贝
写拷贝就是为了防止一些重要的dll被修改导致其他程序崩溃,就将物理页映射到一个进程中,这里首先windows会将PTE的R/W
为设置为0即只读权限,再把vad树里面的内存设置为写拷贝
当我们想要修改页的内容时因为是只读权限就会导致异常,异常会触发异常处理函数,异常梳理函数发现vad为写拷贝就会挂一个新的物理页