前言
前一个学习私有内存(Private Memory),本篇学习与之相对的映射内存(Mapped Memory),回顾之前见到过的进程Vad树,会发现Mapped Memory总是占据绝大部分。
![](/2020/09/01/Mapped-Memory/whyMappedMemory.png)
仅有少数,例如存储函数与局部变量的栈,malloc申请的堆,属于私有内存,其它情况下,大部分都是映射内存,这也归功于映射内存的优点所致,映射内存可以节约内存资源,更有效率的使用内存。
映射内存主要函数
映射内存主要有两类应用场景,一种是共享物理页,另一种是共享文件。下面来看一下与映射内存相关的主要函数。
![](/2020/09/01/Mapped-Memory/Type.png)
CreateFileMapping
这两类都离不开申请映射内存的核心函数:CreateFileMapping,它的作用是在底层准备好一个物理页/文件,下面是函数原型:
1 | HANDLE CreateFileMapping( |
简要介绍一下参数:
hFile:文件句柄,用于共享文件时此处填写文件名;用于共享物理页时,此处填写INVALID_HANDLE_VALUE。
lpAttributes:安全设置,通常设置NULL,使用默认的安全配置。
flProtect:设置内存保护属性,例如READWRITE,READONLY之类的。取值如下:
Code1
2
3
4
5常数:
1.PAGE_READONLY 2.PAGE_READWRITE 3.PAGE_WRITECOPY
4.PAGE_EXECUTE_READ 5.PAGE_EXECUTE_READWRITE
可组合使用常数:
1.SEC_COMMIT 2.SEC_IMAGE 3.SEC_RESERVEdwMaximumSizeHigh:通常BUFSIZ,该值似乎是FILE默认的buf大小,在头文件“stdlib.h”中定义。
lpName:共享内存的名称,想要和另一个进程共享一块内存时,另一个进程必须知道这块内存是什么,这个参数就是描述这块内存的名称。
返回值:如果执行成功,返回映射对象(物理页/文件)的句柄。
MapViewOfFile
除了CreateFileMapping外,另一个函数MapViewOfFile也相当重要,CreateFileMapping只是在底层准备好一个物理页/文件,想将准备好的物理页/文件与当前进程关联起来,就要依赖MapViewOfFile函数,其原型如下:
1 | LPVOID WINAPI MapViewOfFile( |
hFileMappingObject:CreateFileMapping函数返回的映射对象句柄。
dwDesiredAccess:映射对象的文件数据的访问方式,要与CreateFileMapping中设置的内存保护属性(flProtect)相匹配。取值如下:
Code1
2
3
4
5
6dwDesiredAccess取值 (flProtect对应的值)
1.FILE_MAP_ALL_ACCESS (PAGE_READWRITE)
2.FILE_MAP_COPY (PAGE_WRITECOPY)
3.FILE_MAP_EXECUTE (PAGE_EXECUTE_READ/PAGE_EXECUTE_READWRITE)
4.FILE_MAP_READ (PAGE_READONLY/PAGE_READWRITE)
5.FILE_MAP_WRITE (PAGE_READWRITE)dwFileOffsetHigh:通常填0。
dwFileOffsetLow:通常填0.
dwNumberOfBytesToMap:映射文件的字节数。
返回值:如果执行成功,返回映射物理页/文件的开始地址值。
OpenFileMapping
在已经CreateFileMapping准备好一个物理页/文件后,想要使用这个文件,就不需要再次创建了,通过调用另一个函数OpenFileMapping,就能够获取到该物理页/文件的句柄。进而可以将这个物理页/文件映射到当前内存上。函数原型如下:
1 | HANDLE OpenFileMappingA( |
- dwDesiredAccess:同MapViewOfFile。
- bInheritHandle:如这个函数返回的句柄能由当前进程启动的新进程继承,则这个参数为TRUE,通常填FALSE。
- lpName:同CreateFileMapping。
返回值:如果执行成功,返回映射对象(物理页/文件)的句柄。
共享物理页
申请映射内存
在了解了映射内存的两个关键函数后,下面用一个申请映射内存的实验加深印象。
原理:通过CreateFileMapping函数在底层准备好一个用于共享的物理页,调用MapViewOfFile将物理页与当前进程关联起来。
编译运行如下代码(平台:Windows XP,编译器:VC++6.0)
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char* argv[])
{
printf("申请映射内存之前\n");
getchar();
//准备物理页
HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFSIZ, "共享内存");
//将物理页与线性地址进行映射
LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
*(PDWORD)lpBuff = 0x12345678;
printf("A进程写入地址 - 内容:%p - %x ", lpBuff, *(PDWORD)lpBuff);
getchar();
return 0;
}运行程序,在申请映射内存之前,进入Windbg,记录下进程当前Vad树的情况
等申请完映射内存之后,再观察该进程的Vad树
可以看到多出了一个起始位置为0x3a0000,大小为1个物理页的映射内存。这验证了CreateFileMapping也可以申请内存。
共享资源
映射内存的一大特点,就是它可以通过共享物理页实现共享资源,从而节省不少内存资源,来看接下来这个实验。
首先是刚刚的代码,运行后,可以看到,A进程在0x3a0000处写入数据0x12345678
接着创建一个新的文件(不要关掉之前的进程),编写如下代码:
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char* argv[])
{
printf("读取物理页前\n");
getchar();
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "共享内存");
LPTSTR buffer = (LPTSTR)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
printf("B进程读取:%x", *(PDWORD)buffer);
getchar();
return 0;
}尝试在另一进程中,访问前一个实验创建的物理页。
运行程序,在读取物理页前,停下来,查看该进程的Vad树
可以发现,此时还未读取物理页时,0x3a0000处的内存节点并不在当前进程的Vad树中。
继续运行代码,并再次查看进程的Vad树
可以看到,在新起的进程中,多了0x3a0000处的一个内存节点,大小是一个物理页,并且这块内存是Mapped类型;此外,运行结果也可以看出,我们成功读取出了在另一个进程中存进去的数据。也就是说此时两个进程的内存空间中,都有0x3a00000处这个映射内存节点。
这样就能理解清楚映射内存是怎么回事了,一个进程在底层准备了一个物理页(也可以多个),此时物理页并不可被使用,但是任意进程,只要获得了该物理页的句柄,就可以将其映射到自己的内存空间中,也就可以使用该内存了,例如存储数据或者读取数据(依据创建物理页时设置的属性)。这样也就实现了资源共享。
共享文件
共享文件实验
有了共享资源的基础,再来看共享文件,就容易的多。与共享资源相比,就是多了一步,需要先获取文件句柄(例如创建或者打开一个文件),接下来的步骤与共享资源是一样的。这里直接上代码:
1 |
|
这里就不分P了,直接看结果。
![](/2020/09/01/Mapped-Memory/shareFile.png)
可以看到当前进程通过映射文件的方式共享了一个刚刚创建的txt文件。共享文件的主要好处是能够分享处理大文件,从而减少大文件的反复加载内存与拉伸,节省不必要的资源开支。需要注意一点,如果有一个进程修改了该共享文件,那么所有使用该共享文件的进程都会被影响。
文件写拷贝
通过前面几个实验,多次观察进程的Vad树,可能会发现一点,就是有几个映射文件,有点与众不同。
![](/2020/09/01/Mapped-Memory/WriteCopy_1.png)
可以看到,框出的这几个映射文件,多出一个Exe的属性,并且这几个文件的内存保护属性都是EXECUTE_WRITECOPY。它有什么用呢?来看下面一个实验,编写如下代码:
1 |
|
运行代码后,观察进程的Vad树,会发现notepad.exe也有了与刚刚几个映射文件相同的属性。
结论:
- LoadLibrary函数底层实现,就是利用了映射文件的机制,实现的共享文件。
- LoadLibrary加载的映射文件,会具备EXECUTE_WRITECOPY内存保护属性
- EXECUTE_WRITECOPY用来防止映射文件被其它进程修改,当一个进程试图修改映射文件时,若该文件的内存保护属性是EXECUTE_WRITECOPY,那么操作系统会让该进程指向一个新的物理页,新的物理页存着映射文件的副本,这样进程试图对映射文件修改时就不会影响到真正的映射文件。此等保护机制,可用于防止对系统函数Hook等手段。
关于模块隐藏
这段时间,反复接触了一个结构,就是Vad树。之前,在学习进程结构体的时候,有一个模块隐藏的思路就是通过断链的方式,使得操作系统无法通过进程结构体找到其所加载的模块,但是,有了Vad树后,进程的内存空间,一目了然,加载的模块都逃不掉。所以说,断链只是一个表面上的模块隐藏,仅能够骗骗3环API。
如果考虑删除Vad树上的节点,实现模块隐藏,则是更加不现实的,这样很容易造成程序出问题,因为操作系统是根据Vad树判断当前进程是否占用了这块内存,如果删除了Vad树上的节点,当别的进程使用VirtualAlloc申请内存时,就有可能申请到你原来隐藏模块的内存,这样程序运行就会出错了。
有一种极为困难的办法,就是自己申请内存,拉伸文件,添加PE头。将模块融入代码中,这样的话,就很难检测出来,仅有通过内存搜索方式,才能找到。
参考资料
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83722047 (My classmates-线性地址的管理学习笔记)