本篇文章主要目的是对进程的用户地址空间进行一个分析。
先来了解一些概念:
区域,块,页面。
用户地址空间划分为不同的区域,然后区域划分为不同的块,块由页面组成。(如果区域是MEM_FREE,则不存在块的概念)
区域中的块是该区域内连续的页面,并且具有相同的保护属性以及以相同类型的物理存储器作为后备存储器。
分析用户地址空间区域,主要要用到VirtualQueryEx函数,该函数声明如下:
DWORD VirtualQueryEx
(
HANDLE hProcess,
LPCVOID pvAddress,
PMEMORY_BASIC_INFORMATION pmbi,
DOWRD dwLength
);
要想正确使用该函数,对MEM_BASIC_INFORMATION的准确理解不可少。
typedef struct _MEMORY_BASIC_INFORMATION
{
/*
将VirtualQueryEx中的参数pvAddres向下用页面大小取整得到的地址(所在页面的起始地址)
*/
PVOID BaseAddress;
/*
标识出pvAddres所在区域的基地址,如果该区域state为MEM_FREE,该值无意义(一般是0)
*/
PVOID AllocationBase;
/*
标识出区域最开始预定的时候指定的保护属性
*/
DWORD AllocationProtect;
/*
标识出以BaseAddress起始,然后连续的拥有相同保护属性,状态和类型的页面的大小总和。
(我觉得这个变量名取得不好,RegionSize容易让人理解为区域的大小.
当BaseAddress为一个块的起始地址的时候,该大小为块的大小。
当state为MEM_FREE时,为整个区域的大小
这个字段要重点理解,理解好了后面的就简单了)
*/
SIZE_T RegionSize;
/*
BaseAddress所在页面的状态,值为下列三个值之一:MEM_FREE,MEM_RESERVE,MEM_COMMIT。
*/
DWORD State;
/*
BaseAddress所在页面的保护属性(PAGE_*)
*/
DWORD Protect;
/*
BaseAddress所在页面的类型(MEM_IMAGE,MEM_MAPPED,MEM_PRIVATE)
*/
DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
有了这个函数,我们只需要从0开始调用这个函数,然后累加地址重复调用这个函数即可。
本来用户地址空间可以看做,区域,块,页面三级去理解。但是这里面MEM_FREE状态的区域是一个异类,没有块的概念。因此我这做一些特殊处理,将MEM_FREE也看做有块的概念,里面只有一个块。这样能保证区域,块和页面的层级关系。
为了便于调试,我们为页面的状态(State),保护属性(protect),类型(type)定义三个枚举:
enum class PageState
{
INVALID = 0,
FREE = MEM_FREE,
RESERVE = MEM_RESERVE,
COMMIT = MEM_COMMIT,
};
enum class PageType
{
INVALID = 0,
IMAGE = MEM_IMAGE,
MAPPED = MEM_MAPPED,
PRIVATE = MEM_PRIVATE,
};
enum class PageProtect
{
INVALID = 0,
NOACCESS = PAGE_NOACCESS,
READONLY = PAGE_READONLY,
READWRITE = PAGE_READWRITE,
EXECUTE = PAGE_EXECUTE,
EXECUTE_READ = PAGE_EXECUTE_READ,
EXECUTE_READWRITE = PAGE_EXECUTE_READWRITE,
WRITECOPY = PAGE_WRITECOPY,
EXECUTE_WRITECOPY = PAGE_EXECUTE_WRITECOPY,
};
然后我们需要定义个结构体,表示块信息:
struct MemBlkInfo
{
PVOID pReginAllocationBase; /* 块所在区域的基地址 */
PageProtect allocationProtect; /* 最开始预定区域时指定的保护属性 */
SIZE_T dwSize; /* 块的大小 */
PageState state; /* 块内页面的状态 */
PageProtect protect; /* 块内页面的保护属性 */
PageType blkType; /* 块内页面的类型 */
};
然后我们实现一个函数,给定块的基地址,获取块的信息:
BOOL VMQueryBlks
(
HANDLE hProcess, /* i: 进程句柄 */
LPCVOID pvBlkBaseAddress, /* i: 块起始地址(需要调用者保证为块起始地址,该函数没有做校验,如果不是块起始地址,信息可能会错误)*/
MemBlkInfo& zBlkInfo /* o: 块信息 */
)
{
zBlkInfo = {};
MEMORY_BASIC_INFORMATION mbi{};
if (!(VirtualQueryEx(hProcess, pvBlkBaseAddress, &mbi, sizeof(mbi)) == sizeof(mbi)))
return FALSE;
zBlkInfo.pReginAllocationBase = mbi.AllocationBase;
zBlkInfo.allocationProtect = static_cast<PageProtect> (mbi.AllocationProtect);
zBlkInfo.dwSize = mbi.RegionSize;
zBlkInfo.state = static_cast<PageState>(mbi.State);
zBlkInfo.protect = static_cast<PageProtect>(mbi.Protect);
zBlkInfo.blkType = static_cast<PageType>(mbi.Type);
//当当前页状态为FREE时候需要特殊处理
if (zBlkInfo.state == PageState::FREE)
{
zBlkInfo.pReginAllocationBase = mbi.BaseAddress; /*对MEM_FREE区域而言,里面只有一个块,因此块起始地址就是区域起始地址*/
}
return TRUE;
}
基于该函数,我们可以实现一个给定区域起始地址获取该区域所有块的信息的函数:
BOOL VMQueryReginBlkInfos
(
HANDLE hProcess, /* i: 进程句柄 */
LPCVOID pvRegionAddr, /* i: 区域基地址 */
std::vector<MemBlkInfo>& vecBlkInfo /* o: 块信息 */
)
{
vecBlkInfo.clear();
LPCVOID pvBlkBaseAddr = pvRegionAddr;
for (;;)
{
MemBlkInfo blkInfo{};
if (!(VMQueryBlks(hProcess, pvBlkBaseAddr, blkInfo)))
return FALSE;
//如果当前块的基地址与区域的基地址不相同,表明当前块不属于当前区域
if (blkInfo.pReginAllocationBase != pvRegionAddr)
break;
pvBlkBaseAddr = (PVOID)((PBYTE)pvBlkBaseAddr + blkInfo.dwSize);
vecBlkInfo.push_back(blkInfo);
}
return (TRUE);
}
然后我们获取区域信息,我们首先定义一个表示区域的结构体:
struct VMRegion
{
LPCVOID pvRgnBaseAddress; /* 区域起始地址 */
PageProtect allocationProtect; /* 最开始预定区域时候指定的保护属性 */
std::wstring wstrDisp; /* 描述 */
SIZE_T dwReginSize; /* 区域大小 */
std::vector<MemBlkInfo> vecMemBlkInfo; /* 区域中块的信息*/
};
然后我们写一个函数完成获取区域的信息
BOOL VMQueryRegion
(
HANDLE hProcess,
LPCVOID pvAddress, /* i: 区域基地址 */
VMRegion* pVMRegion
)
{
if(!pVMRegion)
return FALSE;
pVMRegion->allocationProtect = PageProtect::INVALID;
pVMRegion->dwReginSize = 0;
pVMRegion->pvRgnBaseAddress = NULL;
pVMRegion->wstrDisp.clear();
pVMRegion->vecMemBlkInfo.clear();
pVMRegion->pvRgnBaseAddress = pvAddress;
//获取区域内块信息
if (!VMQueryReginBlkInfos(hProcess, pvAddress, pVMRegion->vecMemBlkInfo))
return FALSE;
if(pVMRegion->vecMemBlkInfo.size() == 0)
return FALSE;
pVMRegion->allocationProtect = pVMRegion->vecMemBlkInfo[0].allocationProtect;
//计算区域的总大小
for (auto& blkInfo : pVMRegion->vecMemBlkInfo)
{
pVMRegion->dwReginSize += blkInfo.dwSize;
}
//尝试获取区域关联的文件信息
if (pVMRegion->vecMemBlkInfo[0].blkType == PageType::MAPPED || pVMRegion->vecMemBlkInfo[0].blkType == PageType::IMAGE)
{
WCHAR pszFilename[MAX_PATH]{};
if (GetMappedFileName(hProcess, (LPVOID)pVMRegion->pvRgnBaseAddress, pszFilename, MAX_PATH) <= 0)
return TRUE;
pVMRegion->wstrDisp = pszFilename;
static BOOL bLoadLogicDosDeviceMap = FALSE;
static std::map<std::wstring, std::wstring> mapLogicDosDeviceMap;
if (!bLoadLogicDosDeviceMap)
{
bLoadLogicDosDeviceMap = TRUE;
if (!GetLogicDosDeviceMap(mapLogicDosDeviceMap))
return FALSE;
}
for (auto& pair : mapLogicDosDeviceMap)
{
if (pVMRegion->wstrDisp.compare(0, pair.first.length(), pair.first) == 0)
{
pVMRegion->wstrDisp = pVMRegin->wstrDisp.replace(0, pair.first.length(), pair.second);
}
}
}
return TRUE;
}
这里面有个函数GetLogicDosDeviceMap,该函数是获取设备卷名和盘符名的对应关系:
BOOL GetLogicDosDeviceMap
(
std::map<std::wstring, std::wstring>& mapDosDeviceLogicDrive
)
{
#define BUFSIZE 1024
TCHAR szLogicalDriveName[BUFSIZE];
szLogicalDriveName[0] = '\0';
if (GetLogicalDriveStrings(BUFSIZE - 1, szLogicalDriveName))
{
TCHAR szDosDevice[MAX_PATH];
TCHAR szDrive[3] = TEXT(" :");
TCHAR* p = szLogicalDriveName;
do
{
// Copy the drive letter to the template string
*szDrive = *p;
/*
Look up each device name
将各个盘符以'\0'隔开连在一起,最后是两个终止符
*/
if (QueryDosDevice(szDrive, szDosDevice, MAX_PATH))
{
mapDosDeviceLogicDrive[szDosDevice] = szDrive;
}
// Go to the next NULL character.
while (*p++);
} while (*p); // end of string
}
return TRUE;
}
最后,写一个函数从NULL地址开始遍历整个用户地址空间:
BOOL GetRegionInfos(DWORD processID, std::vector<VMRegion>& vecRegion)
{
BOOL bOk = TRUE;
PVOID pvAddress = NULL;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ,
FALSE, processID);
if (NULL == hProcess)
return FALSE;
PVOID pvAddressTmp = NULL;
while (bOk)
{
VMRegion zVMRegion{};
bOk = VMQueryRegion(hProcess, pvAddress, &zVMRegion);
pvAddress = ((PBYTE)zVMRegion.pvRgnBaseAddress + zVMRegion.dwReginSize);
vecRegion.push_back(zVMRegion);
}
return TRUE;
}
当type为MEM_MAPPED或者MEM_IMAGE时,调用了GetMappedFileName来获取该区域关联的文件。这点我不确定这样子对不对,如果有懂的,可以留言。