Windows内存管理学习笔记(一)—— 线性地址的管理
用户空间线性地址的管理
基本概念:每一个进程都有一个4GB的线性地址空间
描述:
- 每个进程都有自己的用户空间需要管理,当我们使用VirtualAlloc等函数申请一块固定的地址空间时,首先需要确认这块空间是否被占用,如果该空间已被占用则申请失败
- 用户空间并非像内核空间一样通过一块链表去管理已占用的线性地址空间(效率低), 而是通过搜索二叉树
申请内存的两种方式:
- 通过VirtualAlloc/VirtualAllocEx申请:Private Memory(独享物理页)
- 通过CreateFileMapping进行映射:Mapped Memory(共享物理页)
进程空间地址划分:
注意:
1)只有用户模式区是用户能够访问的
2)所谓的“无法访问”只是目标地址没有被挂上有效的物理页(详见保护模式章节)
3)通常,当我们需要申请一块内存时,通过系统提供的API去申请
4)当我们申请一块内存时,系统需要对这块内存做记录,表示它已经被申请了,之后再进行申请时,系统就不会再分配这块地址给我们了
5)在内核层,通过一块链表,将所有未进行分配的空间的地址串起来,当我们需要分配空间时,在这一块链表中寻找
6)内核空间线性地址管理相对简单(参考《Windows内核原理与实现》)
7)本章主要介绍用户空间线性地址管理(相对复杂)
实验一:理解用户空间线性地址管理
1)使用WinDbg查看进程列表
命令:!process 0 0
2)定位一个exe程序
3)查看EPROCESS结构体
命令:dt _EPROCESS 86360440
ntdll!_EPROCESS
......
+0x11c VadRoot : 0x8656ef48 Void //入口点,进去后是搜索二叉树
//每一个节点记录了一块已被占用的线性地址空间
//对应_MMVAD结构体
......
4)查看MMVAD结构体
命令:dt _MMVAD 0x8656ef48
nt!_MMVAD
+0x000 StartingVpn : 0x400 //重要,以页为单位,后面添上三个0即是当前节点所描述的线性地址的起始位置
+0x004 EndingVpn : 0x42b //重要,以页为单位,后面添上三个0即是当前节点所描述的线性地址的结束位置
+0x008 Parent : (null) //根节点,不存在父节点
+0x00c LeftChild : 0x86237d60 _MMVAD //左子树
+0x010 RightChild : 0x863e6a20 _MMVAD //右子树
+0x014 u : __unnamed //对应_MMVAD_FLAGS结构体
+0x018 ControlArea : 0x86610220 _CONTROL_AREA //包含该节点所对应的线性地址被谁占用等信息
+0x01c FirstPrototypePte : 0xe11e1048 _MMPTE
+0x020 LastContiguousPte : 0xfffffffc _MMPTE
+0x024 u2 : __unnamed
5)查看CONTROL_AREA结构体
命令:dt _CONTROL_AREA 0x86610220
nt!_CONTROL_AREA
+0x000 Segment : 0xe11e1008 _SEGMENT
+0x004 DereferenceList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x00c NumberOfSectionReferences : 1
+0x010 NumberOfPfnReferences : 0x13
+0x014 NumberOfMappedViews : 1
+0x018 NumberOfSubsections : 6
+0x01a FlushInProgressCount : 0
+0x01c NumberOfUserReferences : 2
+0x020 u : __unnamed
+0x024 FilePointer : 0x863219d0 _FILE_OBJECT //若为空,表示线性地址指向真正的物理页
+0x028 WaitingForDeletion : (null)
+0x02c ModifiedWriteCount : 0
+0x02e NumberOfSystemCacheViews : 0
6)查看FILE_OBJECT结构体
命令:dt _FILE_OBJECT 0x863219d0
ntdll!_FILE_OBJECT
......
+0x030 FileName : _UNICODE_STRING "\Documents and Settings\User\桌面\test\Debug\test.exe"
//线性地址属于主模块
......
7)查看MMVAD_FLAGS结构体
命令:kd> dt _MMVAD_FLAGS 86237d60+0x14
nt!_MMVAD_FLAGS
+0x000 CommitCharge : 0y0000000000000000111 (0x7)
+0x000 PhysicalMapping : 0y0
+0x000 ImageMap : 0y1 //为1表示镜像文件(可执行文件),0表示其他
+0x000 UserPhysicalPages : 0y0
+0x000 NoChange : 0y0
+0x000 WriteWatch : 0y0
+0x000 Protection : 0y00111 (0x7) //表示内存权限为EXECUTE_WRITECOPY
+0x000 LargePages : 0y0
+0x000 MemCommit : 0y0
+0x000 PrivateMemory : 0y0 //0表示Mapped Memory
8)遍历所有节点
命令:!vad 0x8656ef48
VAD Level Start End Commit
86237d60 1 10 10 1 Private READWRITE
85e1adc0 2 20 20 1 Private READWRITE
866df830 3 30 12f 3 Private READWRITE
86320ac0 4 130 132 0 Mapped READONLY Pagefile section, shared commit 0x3
86425598 5 140 23f 4 Private READWRITE
867c49d0 6 240 24f 6 Private READWRITE
862ff800 7 250 25f 0 Mapped READWRITE Pagefile section, shared commit 0x3
864bcad8 8 260 275 0 Mapped READONLY \WINDOWS\system32\unicode.nls
86348070 9 280 2c0 0 Mapped READONLY \WINDOWS\system32\locale.nls
863ec958 10 2d0 310 0 Mapped READONLY \WINDOWS\system32\sortkey.nls
86673f10 11 320 325 0 Mapped READONLY \WINDOWS\system32\sorttbls.nls
85dd0b40 12 330 370 0 Mapped READONLY Pagefile section, shared commit 0x41
86425fe8 13 380 38f 5 Private READWRITE
86588948 14 390 392 0 Mapped READONLY \WINDOWS\system32\ctype.nls
8656ef48 0 400 42b 7 Mapped Exe EXECUTE_WRITECOPY \Documents and Settings\JinXiangcheng\桌面\test\Debug\test.exe
862664c0 2 7c800 7c91d 5 Mapped Exe EXECUTE_WRITECOPY \WINDOWS\system32\kernel32.dll
863e6a20 1 7c920 7c9b2 5 Mapped Exe EXECUTE_WRITECOPY \WINDOWS\system32\ntdll.dll
862f4cb0 3 7f6f0 7f7ef 0 Mapped EXECUTE_READ Pagefile section, shared commit 0x7
8633aa60 2 7ffa0 7ffd2 0 Mapped READONLY Pagefile section, shared commit 0x33
8648b448 3 7ffdb 7ffdb 1 Private READWRITE
86244d88 4 7ffdf 7ffdf 1 Private READWRITE
Total VADs: 21, average level: 6, maximum depth: 14
Total private commit: 0x27 pages (156 KB)
Total shared commit: 0x81 pages (516 KB)
总结:
1)所有的线性地址空间是需要管理的(哪些地址是占用的,哪些地址是未占用的)
2)所有用户线性地址空间通过搜索二叉树进行管理(占用大小,占用方式,内存属性)
4)所有已占用的线性地址可以分为两类
- VirtualAlloc分配的普通内存
- 文件映射(Mapped)的内存(例如dll/exe/txt等)
5)传统的模块隐藏技术,终究会在VadRoot中留下痕迹,假设摘除,操作系统会误认为该地址空间未被分配,从而产生非预期中的错误
Private Memory
描述:通过VirtualAlloc/VirtualAllocEx申请的内存,叫做Private Memory
LPVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的内存区域的地址,填0则随机分配一块符合条件的地址
DWORD dwSize, // 分配的大小,4kb对齐
DWORD flAllocationType, // 分配的类型
// MEM_RESERVE:只保留线性地址,不分配物理页
// MEM_COMMIT:既保留线性地址,又分配物理页
DWORD flProtect // 该内存的初始保护属性
};
实验二:理解Private Memory
1)编译并运行以下代码
#include <stdio.h>
#include <windows.h>
LPVOID lpAddress;
int main()
{
printf("程序运行了...内存还没有申请\n");
getchar();
lpAddress = VirtualAlloc(NULL, 0x1000*2, MEM_COMMIT, PAGE_READWRITE);
printf("内存地址:%x\n", lpAddress);
getchar();
return 0;
}
2)遍历VadRoot
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
......
Failed to get VadRoot
PROCESS 86498ca0 SessionId: 0 Cid: 026c Peb: 7ffd3000 ParentCid: 0b80
DirBase: 0e800380 ObjectTable: e2abfd10 HandleCount: 18.
Image: test.exe
kd> dt _EPROCESS 86498ca0
ntdll!_EPROCESS
......
+0x11c VadRoot : 0x86543ed8 Void
......
kd> !vad 0x86543ed8
VAD Level Start End Commit
8621f160 1 10 10 1 Private READWRITE
866d04e0 2 20 20 1 Private READWRITE
86591aa8 3 30 12f 3 Private READWRITE
862f9658 4 130 132 0 Mapped READONLY Pagefile section, shared commit 0x3
865d7920 5 140 23f 3 Private READWRITE
85decda0 6 240 24f 6 Private READWRITE
862d7068 7 250 25f 0 Mapped READWRITE Pagefile section, shared commit 0x3
862df620 8 260 275 0 Mapped READONLY \WINDOWS\system32\unicode.nls
85e009d0 9 280 2c0 0 Mapped READONLY \WINDOWS\system32\locale.nls
85e4b6b0 10 2d0 310 0 Mapped READONLY \WINDOWS\system32\sortkey.nls
86588988 11 320 325 0 Mapped READONLY \WINDOWS\system32\sorttbls.nls
865e7bb8 12 330 370 0 Mapped READONLY Pagefile section, shared commit 0x41
8642b528 13 380 38f 5 Private READWRITE
8665e1b0 14 390 392 0 Mapped READONLY \WINDOWS\system32\ctype.nls
86543ed8 0 400 42c 8 Mapped Exe EXECUTE_WRITECOPY \Documents and Settings\JinXiangcheng\桌面\test\Debug\test.exe
865feaf8 2 7c800 7c91d 5 Mapped Exe EXECUTE_WRITECOPY \WINDOWS\system32\kernel32.dll
85df5180 1 7c920 7c9b2 5 Mapped Exe EXECUTE_WRITECOPY \WINDOWS\system32\ntdll.dll
86487da0 3 7f6f0 7f7ef 0 Mapped EXECUTE_READ Pagefile section, shared commit 0x7
863b1100 2 7ffa0 7ffd2 0 Mapped READONLY Pagefile section, shared commit 0x33
863ef410 3 7ffd3 7ffd3 1 Private READWRITE
8640f148 4 7ffdf 7ffdf 1 Private READWRITE
Total VADs: 21, average level: 6, maximum depth: 14
Total private commit: 0x27 pages (156 KB)
Total shared commit: 0x81 pages (516 KB)
3)继续运行程序,查看分配到的地址
4)再次遍历VadRoot,观察变化
kd> !vad 0x86543ed8
VAD Level Start End Commit
862f9658 2 130 132 0 Mapped READONLY Pagefile section, shared commit 0x3
865d7920 1 140 23f 3 Private READWRITE
85decda0 3 240 24f 6 Private READWRITE
862d7068 2 250 25f 0 Mapped READWRITE Pagefile section, shared commit 0x3
862df620 4 260 275 0 Mapped READONLY \WINDOWS\system32\unicode.nls
85e009d0 3 280 2c0 0 Mapped READONLY \WINDOWS\system32\locale.nls
85e4b6b0 5 2d0 310 0 Mapped READONLY \WINDOWS\system32\sortkey.nls
86588988 4 320 325 0 Mapped READONLY \WINDOWS\system32\sorttbls.nls
865e7bb8 6 330 370 0 Mapped READONLY Pagefile section, shared commit 0x41
8642b528 5 380 38f 5 Private READWRITE
8665e1b0 6 390 392 0 Mapped READONLY \WINDOWS\system32\ctype.nls
---------------------------------------------------------------------
86600dc0 7 3a0 3a1 2 Private READWRITE //新增节点
---------------------------------------------------------------------
86543ed8 0 400 42c 8 Mapped Exe EXECUTE_WRITECOPY \Documents and Settings\JinXiangcheng\桌面\test\Debug\test.exe
865feaf8 2 7c800 7c91d 5 Mapped Exe EXECUTE_WRITECOPY \WINDOWS\system32\kernel32.dll
85df5180 1 7c920 7c9b2 5 Mapped Exe EXECUTE_WRITECOPY \WINDOWS\system32\ntdll.dll
86487da0 3 7f6f0 7f7ef 0 Mapped EXECUTE_READ Pagefile section, shared commit 0x7
863b1100 2 7ffa0 7ffd2 0 Mapped READONLY Pagefile section, shared commit 0x33
863ef410 3 7ffd3 7ffd3 1 Private READWRITE
8640f148 4 7ffdf 7ffdf 1 Private READWRITE
Total VADs: 19, average level: 4, maximum depth: 7
Total private commit: 0x24 pages (144 KB)
Total shared commit: 0x81 pages (516 KB)
堆
描述:
- 堆是操作系统提前通过VirtualAlloc/VirtualAllocEx申请的一块内存,存在于VadRoot中
- 在C++中,若是想创建一个对象,需要用到new,此时,会在堆中创建一个对象,new的底层实现就是malloc,malloc的底层都是通过HeapAlloc实现的,HeapAlloc要做的事就是在堆中分配一块内存
- 简而言之,操作系统“批发”了一块很大的内存,无论是通过malloc还是new申请的内存,本质上都是从这块内存中“零售”出了一小块
- malloc的底层并没有进0环,因此系统并没有分配出一块新的内存(跟踪malloc实现得知)
- 只有当使用VirtualAlloc/VirtualAllocEx或者Mapped时,才会分配出一块新的内存
实验三:理解堆
1)编译并运行以下代码(在首行设置断点)
#include <stdio.h>
#include <windows.h>
int x = 0x1234;
int main()
{
int y = 0x5678;
int *z = (int*)malloc(sizeof(int)*128);
printf("全局变量:%x\n", &x);
printf("栈:%x\n", &y);
printf("堆:%x\n", z);
getchar();
return 0;
}
2)查看当前进程的VadRoot
3)继续运行程序,观察结果
4)再次查看VadRoot,发现并无任何变化
结论:
- 当在栈中定义变量或者通过malloc申请内存时,系统并没有新分配一块内存,而是从事先分配好的空间中划分出一小块
- 全局变量在编译代码时,地址已经被绑定在了可执行文件中
Mapped Memory
描述:
1)通过CreateFileMapping映射的内存,叫做Mapped Memory,一个进程中大部分的内存都属于Mapped Memory
2)Mapped Memory可以分为两类
- 共享内存:当内存为共享内存时,本质是共享一份物理页
- 共享文件:当内存为共享文件时,这块内存可能被映射到多个进程空间中,但真正占用的物理页只有一份
3)无论是共享内存还是文件,在底层实现都是一样的,当我们需要共享物理页的时候,系统只需将物理页准备好,当我们需要共享文件时,系统先将物理页准备好,然后将物理页与文件进行关联
实验四:理解共享内存
1)编译并运行以下代码(进程A)
#include <stdio.h>
#include <windows.h>
#define MapFileName "共享内存"
int main()
{
//内核对象:1.物理页 2. 文件
//准备一块内存,若第一个参数为INVALID_HANDLE_VALUE(-1)表示共享物理页
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("A进程写入地址,内容:%p - %x", g_lpBuff, *(PDWORD)g_lpBuff);
getchar();
return 0;
}
2)查看进程A的执行结果
3)查看进程A的VadRoot
4)保持进程A运行,编译并运行以下代码(进程B)
#include <stdio.h>
#include <windows.h>
#define MapFileName "共享内存"
int main()
{
//内核对象:1.物理页 2. 文件
HANDLE g_hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, MapFileName);
//将物理页与线性地址进行映射
LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
printf("B进程读取地址,内容:%p - %x", g_lpBuff, *(PDWORD)g_lpBuff);
getchar();
return 0;
}
5)查看进程B执行结果
6)查看进程B的VadRoot
实验五:理解共享文件
1)编译并运行以下代码(在return处设置断点)
#include <stdio.h>
#include <windows.h>
int main()
{
//内核对象:1.物理页 2. 文件
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);
return 0;
}
2)查看g_lpBuff的地址
3)查看当前进程的VadRoot
Mapped Exe
描述:
- 当一个程序通过LoadLibrary进行加载时,此时该文件所在的线性地址空间的属性为Mapped Exe,权限为EXECUTE_WRITECOPY
- 由于权限为EXECUTE_WRITECOPY的地址空间是需要共享给所有程序使用的,因此当我们对权限为EXECUTE_WRITECOPY的线性地址空间的任何位置进行修改时,系统会先给这块内存重新映射一份物理页,然后再进行修改
实验六:理解Mapped Exe
1)编译并运行以下代码(在return处设置断点)
#include <stdio.h>
#include <windows.h>
int main()
{
HMODULE hModule = LoadLibrary("C:\\NOTEPAD.EXE");
return 0;
}
2)查看当前进程的VadRoot
总结
1)线性地址分为三类:私有内存 | 共享内存 | 共享文件
2)共享内存和共享文件本质相同,都是分配了一块物理页,不同的是共享文件将物理页和文件关联了起来
3)传统的模块隐藏技术很难在VadRoot中进行隐藏(脱钩可能会导致程序崩溃),除非通过VirtualAlloc分配私有内存,手动将文件进行拉伸与贴入等一系列操作,此时能够大大增加寻找该模块的难度
思考:当使用VirtualAlloc进行模块隐藏并且抹去特征时,如何找到这个模块?
答案:内存搜索