avatar

Catalog
Mapped Memory

前言

前一个学习私有内存(Private Memory),本篇学习与之相对的映射内存(Mapped Memory),回顾之前见到过的进程Vad树,会发现Mapped Memory总是占据绝大部分。

仅有少数,例如存储函数与局部变量的栈,malloc申请的堆,属于私有内存,其它情况下,大部分都是映射内存,这也归功于映射内存的优点所致,映射内存可以节约内存资源,更有效率的使用内存。

映射内存主要函数

映射内存主要有两类应用场景,一种是共享物理页,另一种是共享文件。下面来看一下与映射内存相关的主要函数。

CreateFileMapping

这两类都离不开申请映射内存的核心函数:CreateFileMapping,它的作用是在底层准备好一个物理页/文件,下面是函数原型:

c
1
2
3
4
5
6
7
8
HANDLE CreateFileMapping(
HANDLE hFile, //物理文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect, //保护设置
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
LPCTSTR lpName //共享内存名称
);

简要介绍一下参数:

  • hFile:文件句柄,用于共享文件时此处填写文件名;用于共享物理页时,此处填写INVALID_HANDLE_VALUE

  • lpAttributes:安全设置,通常设置NULL,使用默认的安全配置。

  • flProtect:设置内存保护属性,例如READWRITE,READONLY之类的。取值如下:

    Code
    1
    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_RESERVE
  • dwMaximumSizeHigh:通常BUFSIZ,该值似乎是FILE默认的buf大小,在头文件“stdlib.h”中定义。

  • lpName:共享内存的名称,想要和另一个进程共享一块内存时,另一个进程必须知道这块内存是什么,这个参数就是描述这块内存的名称。

返回值:如果执行成功,返回映射对象(物理页/文件)的句柄。

MapViewOfFile

除了CreateFileMapping外,另一个函数MapViewOfFile也相当重要,CreateFileMapping只是在底层准备好一个物理页/文件,想将准备好的物理页/文件与当前进程关联起来,就要依赖MapViewOfFile函数,其原型如下:

c
1
2
3
4
5
6
7
LPVOID WINAPI MapViewOfFile(
  __in HANDLE hFileMappingObject,
  __in DWORD dwDesiredAccess,
  __in DWORD dwFileOffsetHigh,
  __in DWORD dwFileOffsetLow,
  __in SIZE_T dwNumberOfBytesToMap
);
  • hFileMappingObject:CreateFileMapping函数返回的映射对象句柄

  • dwDesiredAccess:映射对象的文件数据的访问方式,要与CreateFileMapping中设置的内存保护属性(flProtect)相匹配。取值如下:

    Code
    1
    2
    3
    4
    5
    6
    dwDesiredAccess取值 (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,就能够获取到该物理页/文件的句柄。进而可以将这个物理页/文件映射到当前内存上。函数原型如下:

c
1
2
3
4
5
HANDLE OpenFileMappingA(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCSTR lpName
);
  • dwDesiredAccess:同MapViewOfFile。
  • bInheritHandle:如这个函数返回的句柄能由当前进程启动的新进程继承,则这个参数为TRUE,通常填FALSE。
  • lpName:同CreateFileMapping。

返回值:如果执行成功,返回映射对象(物理页/文件)的句柄。

共享物理页

申请映射内存

在了解了映射内存的两个关键函数后,下面用一个申请映射内存的实验加深印象。

原理:通过CreateFileMapping函数在底层准备好一个用于共享的物理页,调用MapViewOfFile将物理页与当前进程关联起来。

  1. 编译运行如下代码(平台:Windows XP,编译器:VC++6.0)

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include "stdafx.h"
    #include "windows.h"

    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;
    }
  2. 运行程序,在申请映射内存之前,进入Windbg,记录下进程当前Vad树的情况

  3. 等申请完映射内存之后,再观察该进程的Vad树

    可以看到多出了一个起始位置为0x3a0000,大小为1个物理页的映射内存。这验证了CreateFileMapping也可以申请内存。

共享资源

映射内存的一大特点,就是它可以通过共享物理页实现共享资源,从而节省不少内存资源,来看接下来这个实验。

  1. 首先是刚刚的代码,运行后,可以看到,A进程在0x3a0000处写入数据0x12345678

  2. 接着创建一个新的文件(不要关掉之前的进程),编写如下代码:

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include "stdafx.h"
    #include "windows.h"

    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;
    }

    尝试在另一进程中,访问前一个实验创建的物理页。

  3. 运行程序,在读取物理页前,停下来,查看该进程的Vad树

    可以发现,此时还未读取物理页时,0x3a0000处的内存节点并不在当前进程的Vad树中。

  4. 继续运行代码,并再次查看进程的Vad树

    可以看到,在新起的进程中,多了0x3a0000处的一个内存节点,大小是一个物理页,并且这块内存是Mapped类型;此外,运行结果也可以看出,我们成功读取出了在另一个进程中存进去的数据。也就是说此时两个进程的内存空间中,都有0x3a00000处这个映射内存节点。

    这样就能理解清楚映射内存是怎么回事了,一个进程在底层准备了一个物理页(也可以多个),此时物理页并不可被使用,但是任意进程,只要获得了该物理页的句柄,就可以将其映射到自己的内存空间中,也就可以使用该内存了,例如存储数据或者读取数据(依据创建物理页时设置的属性)。这样也就实现了资源共享。

共享文件

共享文件实验

有了共享资源的基础,再来看共享文件,就容易的多。与共享资源相比,就是多了一步,需要先获取文件句柄(例如创建或者打开一个文件),接下来的步骤与共享资源是一样的。这里直接上代码:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "stdafx.h"
#include "windows.h"

int main(int argc, char* argv[])
{
//这里创建一个文件
HANDLE hFile = CreateFile("C:\\abc.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
//后面的步骤与共享资源一样,注意CreateFileMapping的第一个参数
HANDLE hMapFile = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, BUFSIZ, NULL);
LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);

printf("%x", lpBuff);

getchar();
return 0;
}

这里就不分P了,直接看结果。

可以看到当前进程通过映射文件的方式共享了一个刚刚创建的txt文件。共享文件的主要好处是能够分享处理大文件,从而减少大文件的反复加载内存与拉伸,节省不必要的资源开支。需要注意一点,如果有一个进程修改了该共享文件,那么所有使用该共享文件的进程都会被影响。

文件写拷贝

通过前面几个实验,多次观察进程的Vad树,可能会发现一点,就是有几个映射文件,有点与众不同。

可以看到,框出的这几个映射文件,多出一个Exe的属性,并且这几个文件的内存保护属性都是EXECUTE_WRITECOPY。它有什么用呢?来看下面一个实验,编写如下代码:

c
1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include

int main(int argc, char* argv[])
{
//这里我从System32文件夹下复制了一个notepad.exe到磁盘C目录下,任选一个dll也可以
HANDLE hModule = ::LoadLibrary("C:\\notepad.exe");

getchar();
return 0;
}

运行代码后,观察进程的Vad树,会发现notepad.exe也有了与刚刚几个映射文件相同的属性。

结论:

  • LoadLibrary函数底层实现,就是利用了映射文件的机制,实现的共享文件。
  • LoadLibrary加载的映射文件,会具备EXECUTE_WRITECOPY内存保护属性
  • EXECUTE_WRITECOPY用来防止映射文件被其它进程修改,当一个进程试图修改映射文件时,若该文件的内存保护属性是EXECUTE_WRITECOPY,那么操作系统会让该进程指向一个新的物理页,新的物理页存着映射文件的副本,这样进程试图对映射文件修改时就不会影响到真正的映射文件。此等保护机制,可用于防止对系统函数Hook等手段。

关于模块隐藏

这段时间,反复接触了一个结构,就是Vad树。之前,在学习进程结构体的时候,有一个模块隐藏的思路就是通过断链的方式,使得操作系统无法通过进程结构体找到其所加载的模块,但是,有了Vad树后,进程的内存空间,一目了然,加载的模块都逃不掉。所以说,断链只是一个表面上的模块隐藏,仅能够骗骗3环API。

如果考虑删除Vad树上的节点,实现模块隐藏,则是更加不现实的,这样很容易造成程序出问题,因为操作系统是根据Vad树判断当前进程是否占用了这块内存,如果删除了Vad树上的节点,当别的进程使用VirtualAlloc申请内存时,就有可能申请到你原来隐藏模块的内存,这样程序运行就会出错了。

有一种极为困难的办法,就是自己申请内存,拉伸文件,添加PE头。将模块融入代码中,这样的话,就很难检测出来,仅有通过内存搜索方式,才能找到。

参考资料

参考教程:

  • 海哥逆向中级预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/01/Mapped-Memory/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶