获取进程中加载的PE模块
根据所给PE模块的绝对路径(libPath
),获取目标进程中已加载的PE模块的内存加载基址。
HMODULE FindModuleByPath(DWORD pid, const WCHAR* libPath) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // 根据进程id获取进程句柄
if (hProcess == 0) {
return NULL;
}
HMODULE hModules[1024] = {};
SIZE_T hModulesSize = sizeof(hModules);
DWORD hModulesSizeNeeded = 0;
DWORD moduleNameSize = 0;
SIZE_T hModulesCount = 0;
WCHAR rModuleName[MAX_PATH] = { 0 };
HMODULE rModule = NULL;
EnumProcessModules(hProcess, hModules, hModulesSize, &hModulesSizeNeeded); // 枚举进程模块, 将所有模块的内存加载地址存到hModules数组
hModulesCount = hModulesSizeNeeded / sizeof(HMODULE);
size_t i = 0;
for (; i < hModulesCount; i++) {
rModule = hModules[i]; // 这个是模块的内存加载基址
GetModuleFileNameExW(hProcess, rModule, rModuleName, MAX_PATH); // 获取PE模块的文件路径
if (lstrcmpi(rModuleName, libPath) == 0) { // 比较字符串(忽略大小写)
break;
}
}
if (i == hModulesCount)
return NULL;
return rModule;
}
获取进程中加载的PE模块的节表
int GetPESecsInfo(HANDLE hProcess, LPCVOID ModuleBase) {
DWORD headerBufferSize = 0x1000;
LPVOID peHeader = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, headerBufferSize); // 分配空间, 用于保存读取到的PE头
if (peHeader == NULL) return -1;
ReadProcessMemory(hProcess, ModuleBase, peHeader, headerBufferSize, NULL); // 读取dll的PE头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)peHeader;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)peHeader + dosHeader->e_lfanew);
PIMAGE_SECTION_HEADER curSec = IMAGE_FIRST_SECTION(ntHeader), nextSec = NULL; // curSec为节表第一个节
WORD secNum = ntHeader->FileHeader.NumberOfSections;
WORD i;
for (i = 0; i < secNum; i++) { // 遍历节表, 找到.text节
if (curSec == NULL) return -1;
if (i + 1 < secNum) {
nextSec = curSec + 1; // 得到`.text`节的下一节的信息
}
PVOID secStart = (PBYTE)ModuleBase + (size_t)curSec->VirtualAddress; // 计算节的开始地址
PVOID secEndDataRVA = (PBYTE)curSec->VirtualAddress + curSec->Misc.VirtualSize; // 计算节的内容的结束地址
PVOID secDataEnd = (PBYTE)ModuleBase + (size_t)secEndDataRVA; // 计算节中数据的结束地址
PVOID secEnd;
if (nextSec) {
secEnd = (PBYTE)ModuleBase + (size_t)nextSec->VirtualAddress; // 计算节的结束地址(即下一节的起始地址)
} else {
PVOID secEndDataRVA = (PBYTE)curSec->VirtualAddress + curSec->SizeOfRawData;
secEnd = (PBYTE)ModuleBase + (size_t)secEndDataRVA; // 计算节的结束地址
}
printf("Sec: %s; VirtualAddress: 0x%x; VirtualSize: 0x%x; PointerToRawData: 0x%x; SizeOfRawData: 0x%x; secStart: 0x%p; secDataEnd: 0x%p; secEnd: 0x%p\n", curSec[i].Name, curSec->VirtualAddress, curSec->Misc.VirtualSize, curSec->PointerToRawData, curSec->SizeOfRawData, secStart, secDataEnd, secEnd);
curSec = nextSec;
}
HeapFree(GetProcessHeap(), NULL, peHeader);
return 0;
}
测试
测试中查看PING.exe
进程中的PING.exe
模块。首先使用LordPE
查看在磁盘中的PE文件的节表(图一):
使用Process Hacker
查看在内存中的PE文件的节表(图二):
在Process Hacker
看PING.exe
模块在内存中的分区概况(图三):
在Process Hacker
看PING.exe
模块的.text
节内容(也即0x7ff62d0d1000
起始处的内存区)(图四):
前述代码打印的.text
节的信息(图五):
借此测试,复习一下节表涉及到地址的各字段的含义:
VOffset
:对应IMAGE_SECTION_HEADER::VirtualAddress
字段。该值加上模块的内存基址,得到节的内存基址。VSize
:对应IMAGE_SECTION_HEADER::Misc.VirtualSize
字段。以内存中节表的该值加上节的内存基址,得到节的内容的结束地址。图五中打印.text
节的VirtualSize
是0x2c60
,而在图四中可看到,.text
节的数据正好在偏移0x2c60
前结束。ROffset
:对应IMAGE_SECTION_HEADER::PointerToRawData
字段。这个是节在磁盘文件中的偏移位置。RSize
:对应IMAGE_SECTION_HEADER::SizeOfRawData
字段。看起来这个值在磁盘和内存中各不相同(0x2a00
和0x2e00
),但比较有用的是其在磁盘中的值。比如,.text
节的磁盘偏移0x400
加上其RSize
(0x2a00
),得到下一个节.data
节的磁盘偏移0x2e00
。在内存中,要以内存页的大小0x1000
对齐,所以0x1000
+0x2e00
得到0x3e00
,对齐后得到下一节的地址是0x4000
。