在Windows
操作系统中,每个进程的虚拟地址空间都被划分为若干内存块,每个内存块都具有一些属性,如内存大小、保护模式、类型等。这些属性可以通过VirtualQueryEx
函数查询得到。
该函数可用于查询进程虚拟地址空间中的内存信息的函数。它的作用类似于Windows
操作系统中的Task Manager
中的进程选项卡,可以显示出一个进程的内存使用情况、模块列表等信息。使用VirtualQueryEx
函数,可以枚举一个进程的所有内存块。该函数需要传入要查询的进程的句柄、基地址和一个MEMORY_BASIC_INFORMATION
结构体指针。它会返回当前内存块的基地址、大小、状态(free/commit/reserve
)、保护模式等信息。
在实现对内存块的枚举之前,我们先通过ReadProcessMemory
函数实现一个内存远程内存读取功能,如下代码所示,首先,通过OpenProcess
函数打开进程句柄,获得当前进程的操作权限。然后,调用EnumMemory
函数,传入进程句柄以及起始地址和终止地址参数,依次读取每一页内存,通过循环打印其内存数据。
#include <iostream> #include <windows.h>
void EnumMemory(HANDLE Process, DWORD BeginAddr, DWORD EndAddr) { const DWORD pageSize = 1024;
BYTE page[pageSize]; DWORD tmpAddr = BeginAddr; while (tmpAddr <= EndAddr) { ReadProcessMemory(Process, (LPCVOID)tmpAddr, &page, pageSize, 0); for (int x = 0; x < pageSize; x++) { if (x % 15 == 0) { printf("| 0x%08X \n", tmpAddr + x); } printf("0x%02X ", page[x]); } tmpAddr += pageSize; } }
int main(int argc, char* argv[]) { HANDLE process;
process = OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessId());
EnumMemory(process, 0x00401000, 0x7FFFFFFF);
system("pause"); return 0; }
|
上述代码简单明了,易于理解,但并没有实现过滤特定内存属性的功能。如果需要对特定类型的内存进行分析,需要结合VirtualQueryEx
函数实现内存属性的查询和过滤。
接着我们进入本章的重点,实现枚举进程内存块,要实现该功能首先读者必须要了解一个结构体_SYSTEM_INFO
该结构体是系统信息结构,可用于存储系统硬件和系统配置信息,而我们所需要的内存块数据同样可以使用该结构进行存储。
根据具体需求,可以通过调用GetSystemInfo
函数来获得_SYSTEM_INFO
结构体的信息。GetSystemInfo
函数可以返回系统的硬件信息,包括有多少个处理器,每个处理器有多少个核心,系统页大小等信息,该结构体的定义如下所示;
typedef struct _SYSTEM_INFO { union { DWORD dwOemId; struct { WORD wProcessorArchitecture; WORD wReserved; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; } SYSTEM_INFO, *LPSYSTEM_INFO;
|
接着就是要查询内存块的状态了,我们可通过VirtualQueryEx
函数实现查询进程虚拟地址空间中的内存信息,其原型定义如下:
SIZE_T VirtualQueryEx( HANDLE hProcess, LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, SIZE_T dwLength );
|
参数说明:
- hProcess:进程句柄。需要查询的进程的句柄
- lpAddress:基地址。需要查询的内存块的基地址
- lpBuffer:内存信息缓冲区。 PMEMORY_BASIC_INFORMATION 结构指针,用于存储查询结果。它包含了取得的内存块信息,如基地址、保护属性、状态、大小等
- dwLength:缓冲区大小。缓冲区的大小,以字节为单位。如果缓冲区太小,则函数将返回指定的内存块信息长度存放到此处,不会写入完整的信息
该函数返回实际填充到缓冲区中的字节数。如果函数失败,则返回0。当我们需要了解特定进程的内存使用情况时,可以使用VirtualQueryEx()
函数枚举进程内存中的所有内存块,并按需查询其中的属性值。
#include <iostream> #include <windows.h> #include <Psapi.h>
#pragma comment(lib,"psapi.lib")
VOID ScanProcessMemory(HANDLE hProc) { SIZE_T stSize = 0; PBYTE pAddress = (PBYTE)0; SYSTEM_INFO sysinfo; MEMORY_BASIC_INFORMATION mbi = { 0 };
ZeroMemory(&sysinfo, sizeof(SYSTEM_INFO)); GetSystemInfo(&sysinfo);
pAddress = (PBYTE)sysinfo.lpMinimumApplicationAddress;
printf("------------------------------------------------------------------------ \n"); printf("开始地址 \t 结束地址 \t 大小 \t 状态 \t 内存类型 \t 页面属性 \n"); printf("------------------------------------------------------------------------ \n"); while (pAddress < (PBYTE)sysinfo.lpMaximumApplicationAddress) { ZeroMemory(&mbi, sizeof(MEMORY_BASIC_INFORMATION));
stSize = VirtualQueryEx(hProc, pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
if (stSize == 0) { pAddress += sysinfo.dwPageSize; continue; }
printf("0x%08X \t 0x%08X \t %8d K \t ", mbi.BaseAddress, ((DWORD)mbi.BaseAddress + (DWORD)mbi.RegionSize), mbi.RegionSize >> 10);
switch (mbi.State) { case MEM_FREE: printf("空闲 \t"); break; case MEM_RESERVE: printf("保留 \t"); break; case MEM_COMMIT: printf("提交 \t"); break; default: printf("未知 \t"); break; }
switch (mbi.Type) { case MEM_PRIVATE: printf("私有 \t"); break; case MEM_MAPPED: printf("映射 \t"); break; case MEM_IMAGE: printf("镜像 \t"); break; default: printf("未知 \t"); break; }
if (mbi.Protect == 0) { printf("---"); } else if (mbi.Protect & PAGE_EXECUTE) { printf("E--"); } else if (mbi.Protect & PAGE_EXECUTE_READ) { printf("ER-"); } else if (mbi.Protect & PAGE_EXECUTE_READWRITE) { printf("ERW"); } else if (mbi.Protect & PAGE_READONLY) { printf("-R-"); } else if (mbi.Protect & PAGE_READWRITE) { printf("-RW"); } else if (mbi.Protect & PAGE_WRITECOPY) { printf("WCOPY"); } else if (mbi.Protect & PAGE_EXECUTE_WRITECOPY) { printf("EWCOPY"); }
printf("\n");
pAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize; } }
int main(int argc, char* argv[]) { HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
ScanProcessMemory(hProc); CloseHandle(hProc);
system("pause"); return 0; }
|
运行上述代码片段则首先通过GetCurrentProcessId()
得到自身进程的PID
号,接着通过调用ScanProcessMemory
函数实现对自身进程内存块的枚举功能,最终输出如下图所示的效果;
当然了虽然上述代码可以实现对内存块的枚举功能,但是在实际的开发场景中我们还是需要将枚举结果存储起来以便后期调用,此时我们可以考虑在全局定义vector
容器,容器的属性为每一个内存块的MEMORY_BASIC_INFORMATION
属性,当需要查询时只需要枚举这个容器并循环输出该容器内的数据即可,改进后的代码如下所示;
#include <Windows.h> #include <vector> #include <iostream> #include <assert.h>
using namespace std;
static bool ScanProcessMemory(HANDLE hProcess, OUT vector<MEMORY_BASIC_INFORMATION>& memories) { assert(hProcess != nullptr);
memories.clear(); memories.reserve(200);
SYSTEM_INFO sysInfo = { 0 }; GetSystemInfo(&sysInfo);
const char* p = (const char*)sysInfo.lpMinimumApplicationAddress; MEMORY_BASIC_INFORMATION memInfo = { 0 };
while (p < sysInfo.lpMaximumApplicationAddress) { size_t size = VirtualQueryEx( hProcess, p, &memInfo, sizeof(MEMORY_BASIC_INFORMATION32) );
if (size != sizeof(MEMORY_BASIC_INFORMATION32)) { break; }
memories.push_back(memInfo);
p += memInfo.RegionSize; }
return memories.size() > 0; }
int main(int argc, char* argv[]) { vector<MEMORY_BASIC_INFORMATION> vec;
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
if (ScanProcessMemory(handle, vec)) { printf("------------------------------------------------------------------------ \n"); printf("开始地址 \t 结束地址 \t 大小 \t 状态 \t 内存类型 \t 页面属性 \n"); printf("------------------------------------------------------------------------ \n"); for (int i = 0; i < vec.size(); i++) { printf("0x%08X \t 0x%08X \t %8d K \t ", vec[i].BaseAddress, ((DWORD)vec[i].BaseAddress + (DWORD)vec[i].RegionSize), vec[i].RegionSize >> 10);
switch (vec[i].State) { case MEM_FREE: printf("空闲 \t"); break; case MEM_RESERVE: printf("保留 \t"); break; case MEM_COMMIT: printf("提交 \t"); break; default: printf("未知 \t"); break; }
switch (vec[i].Type) { case MEM_PRIVATE: printf("私有 \t"); break; case MEM_MAPPED: printf("映射 \t"); break; case MEM_IMAGE: printf("镜像 \t"); break; default: printf("未知 \t"); break; }
if (vec[i].Protect == 0) { printf("---"); } else if (vec[i].Protect & PAGE_EXECUTE) { printf("E--"); } else if (vec[i].Protect & PAGE_EXECUTE_READ) { printf("ER-"); } else if (vec[i].Protect & PAGE_EXECUTE_READWRITE) { printf("ERW"); } else if (vec[i].Protect & PAGE_READONLY) { printf("-R-"); } else if (vec[i].Protect & PAGE_READWRITE) { printf("-RW"); } else if (vec[i].Protect & PAGE_WRITECOPY) { printf("WCOPY"); } else if (vec[i].Protect & PAGE_EXECUTE_WRITECOPY) { printf("EWCOPY"); } printf("\n"); } } system("pause"); return 0; }
|
读者可编译并自行运行上述代码,观察输出效果其与第一个案例中的效果保持一致,此处仅仅只是通过容器中转了参数传递,输出效果图如下所示;
对于内存块中的范围区间同样可实现继续查询,例如在开始地址0x5DF00000-0x5DF01000
这个内存区间内,可能灰灰划分为更多的子块,当Basicinfo.State
内存属性中的子块属性为MEM_COMMIT
时,我们还可以继续调用VirtualQuery
函数对这个大内存块内的子内存块进行更加细致的解析效果,这段代码如下所示;
#include <iostream> #include <Windows.h>
int main(int argc, char* argv[]) { DWORD Addres = 0, Size = 0; MEMORY_BASIC_INFORMATION Basicinfo = {};
while (VirtualQuery((LPCVOID)Addres, &Basicinfo, sizeof(MEMORY_BASIC_INFORMATION))) { Size = Basicinfo.RegionSize;
printf("[+] 开始地址: 0x%08X \t 结束地址: 0x%08X \t 大小: %7d K \t 类型: ", Basicinfo.BaseAddress, ((DWORD)Basicinfo.BaseAddress + (DWORD)Basicinfo.RegionSize), Basicinfo.RegionSize >> 10);
switch (Basicinfo.Type) { case MEM_PRIVATE: printf("私有 \t"); break; case MEM_MAPPED: printf("映射 \t"); break; case MEM_IMAGE: printf("镜像 \t"); break; default: printf("未知 \t"); break; }
printf(" \t 状态: "); switch (Basicinfo.State) { case MEM_FREE: printf("空闲 \n"); break; case MEM_RESERVE: printf("保留 \n"); break; case MEM_COMMIT: printf("提交 \n"); break; default: printf("未知 \n"); break; }
if (Basicinfo.State == MEM_COMMIT) { LPVOID BaseBlockAddress = (LPVOID)Addres; DWORD BlockAddress = Addres; DWORD dwBlockSize = 0;
while (VirtualQuery((LPVOID)BlockAddress, &Basicinfo, sizeof(Basicinfo))) { if (BaseBlockAddress != Basicinfo.AllocationBase) { break; } printf("[*] ---> 块地址: 0x%08X \t ", BlockAddress); switch (Basicinfo.Type) { case MEM_PRIVATE: printf("私有 \t "); break; case MEM_MAPPED: printf("映射 \t "); break; case MEM_IMAGE: printf("镜像 \t "); break; default: printf("未知 \t "); break; }
if (Basicinfo.Protect == 0) printf("---"); else if (Basicinfo.Protect & PAGE_EXECUTE) printf("E--"); else if (Basicinfo.Protect & PAGE_EXECUTE_READ) printf("ER-"); else if (Basicinfo.Protect & PAGE_EXECUTE_READWRITE) printf("ERW"); else if (Basicinfo.Protect & PAGE_READONLY) printf("-R-"); else if (Basicinfo.Protect & PAGE_READWRITE) printf("-RW"); else if (Basicinfo.Protect & PAGE_WRITECOPY) printf("WCOPY"); else if (Basicinfo.Protect & PAGE_EXECUTE_WRITECOPY) printf("EWCOPY"); printf("\n");
dwBlockSize += Basicinfo.RegionSize; BlockAddress += Basicinfo.RegionSize; } Size = dwBlockSize ? dwBlockSize : Basicinfo.RegionSize; } Addres += Size; }
system("pause"); return 0; }
|
当上述代码运行后,我们就可以获取到当前内存中有多少个内存块,以及每一个内存块的属性信息,如下图所示;