在Windows中枚举进程中的模块主要是其中加载的dll,在VC上主要有2种方式,一种是解析PE文件中导入表,从导入表中获取它将要静态加载的dll,一种是利用查询进程地址空间中的模块,根据模块的句柄来得到对应的dll,最后再补充一种利用Windows中的NATIVE API获取进程内核空间中的模块,下面根据给出这些方式的具体的代码片段:
解析PE文件来获取其中的dll
在之前介绍PE文件时说过PE文件中中存在一个导入表,表中记录了程序中加载的导入dll以及这些dll中函数的信息,这个结构的定义如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
我只需要获取这个结构并且根据RVA计算出它在文件中的偏移即可找到对应名称,利用之前PE解析器中的CPeFileInfo类来解析它即可,下面是具体的代码:
void EnumModulesByPe(LPCTSTR pszPath)
{
CPeFileInfo peFile;
peFile.strFilePath = pszPath;
peFile.LoadFile();
if (!peFile.IsPeFile())
{
printf("is not a pe file!\n");
return ;
}
peFile.InitDataDirectoryTable();
PIMAGE_IMPORT_DESCRIPTOR pImportTable = peFile.GetImportDescriptor();
while(!peFile.IsEndOfImportTable(pImportTable))
{
printf("%s\n", peFile.RVA2fOffset(pImportTable->Name, (DWORD)peFile.pImageBase));
pImportTable++;
}
}
利用之前的PE解析的类,首先给类中的文件路径赋值,然后加载到内存,并初始化它的数据目录表信息,从表中取出导入表的结构,根据结构中的Name字段的值来计算它的真实地址,即可解析出它里面的模块,这里我们只能解析出PE文件中自身保存的信息,如果dll是在程序运行之时调用LoadLibrary动态加载的,利用这个方法是找不到的。
解析进程地址空间中的模块
这个方法首先通过OpenProcess函数获取对应进程的句柄,然后调用EnumProcessModules枚举进程地址空间中当前存在的模块,这个函数会返回一个HMODULE句柄的数组,我们遍历这个数组,对其中的每个句柄调用GetModuleFileNameEx(很多模块GetModuleFileName获取不到,具体原因我没有深入研究)获取对应的文件路径。下面是具体的代码:
HMODULE* phMods = NULL;
HANDLE hProcess = NULL;
DWORD dwNeeded = 0;
DWORD i = 0;
TCHAR szModName[MAX_PATH] = {};
hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId );
if (NULL == hProcess)
{
printf("不能打开进程[ID:0x%x]句柄,错误码:0x%08x\n",dwProcessId);
return;
}
EnumProcessModules(hProcess, NULL, 0, &dwNeeded);
phMods = (HMODULE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwProcessId);
if( EnumProcessModules(hProcess, phMods, dwNeeded, &dwNeeded))
{
for ( i = 0; i < (dwNeeded / sizeof(HMODULE)); i++ )
{
ZeroMemory(szModName,MAX_PATH*sizeof(TCHAR));
//在这如果使用GetModuleFileName,有的模块名称获取不到,函数返回无法找到该模块的错误
if ( GetModuleFileNameEx(hProcess, phMods[i], szModName,MAX_PATH))
{
printf("%ws\n", szModName);
}
}
}
HeapFree(GetProcessHeap(), 0, phMods);
CloseHandle( hProcess );
由于静态加载的dll在进程启动之时就已经被加载到内存中,所以利用这个方法自然可以获取静态加载的dll,但是由于它是获取进程地址空间中加载的dll,所以要求进程要正在运行,毕竟进程如果没有运行,那么也就不存在地址空间,也就无法获取其中加载的dll,另外它只能获取当前进程地址空间中的dll,有的dll这个时候还没有被加载的话,它自然也获取不到。所以这个方法也不是能获取所有加载的dll
使用进程快照获取模块
这种方式是先使用 CreateToolhelp32Snapshot
函数来创建一个进程模块的快照,然后通过 Module32First
和 Module32Next
函数来遍历快照中的模块信息
Module32First
和Module32Next
函数会通过参数返回一个 MODULEENTRY32
结构的模块信息,该结构的定义如下:
typedef struct tagMODULEENTRY32
{
DWORD dwSize;
DWORD th32ModuleID;
DWORD th32ProcessID;
DWORD GlblcntUsage;
DWORD ProccntUsage;
BYTE* modBaseAddr;
DWORD modBaseSize;
HMODULE hModule;
TCHAR szModule[MAX_MODULE_NAME32 + 1];
TCHAR szExePath[MAX_PATH];
} MODULEENTRY32, *PMODULEENTRY32;
- dwSize 参数表示该结构体的大小,在调用
Module32First
和Module32Next
之前需要先设置这个成员的值 - th32ProcessID:对应进程的ID
- modBaseAddr:模块的起始地址,也就是模块被加载到内存的基地址
- hModule: 模块的句柄
- szModule:模块名称
- szExePath: 模块对应文件所在的路径
下面是一个使用该方式获取加载模块的例子
BOOL GetModulesTH32(WORD wPID)
{
HANDLE hSnap;
MODULEENTRY32 me;
me.dwSize = sizeof(me);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, wPID);
if (hSnap == INVALID_HANDLE_VALUE)
{
FreeLibrary(hToolhelp);
return FALSE;
}
keepGoing = Module32First(hSnap, &me);
while (keepGoing)
{
printf("%d, %08x, %s, %s\r\n", me.modBaseAddr, me.modBaseSize, me.szModule, me.szExePath);
keepGoing = Module32Next(hSnap, &me);
}
CloseHandle(hSnap);
FreeLibrary(hToolhelp);
return TRUE;
}
获取内核地址空间中的模块
不管是解析PE文件还是调用EnumProcessModules都只能获取用户层地址空间中的模块,但是进程不光有用户空间,还有内核空间,所以在这再提供一种枚举内核地址空间的模块的方法。
枚举内核地址空间主要使用函数ZwQuerySystemInformation(也可以使用NtQuerySystemInformation)在msdn中明确指出,这两个函数未来可能不在使用,不推荐使用,但是至少现在是仍然支持的,并且可以很好的完成任务。
这两个函数主要在ntdll.dll中导出,两个函数的参数用法完全相同,只是一个是比较上层一个比较底层而已。在这主要说明一个,另一个完全一样:
NTSTATUS WINAPI ZwQuerySystemInformation(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
__inout PVOID SystemInformation,
__in ULONG SystemInformationLength,
__out_opt PULONG ReturnLength
);
函数的第一个参数是一个枚举类型,用来表示我们将要调用此函数来获取系统哪方面的信息,第二个参数是一个缓冲区,用来存储该函数输出的值,第三个参数是缓冲区的长度,第四个参数是实际需要缓冲区的长度,说到这应该很快就可以反应过来,我们可以第一次调用这个函数传入一个NULL缓冲,缓冲长度给0,让他返回具体的长度,然后根据这个长度,动态分配一块内存,再次调用传入正确的缓冲和长度,获取数据。
在调用这个函数时需要注意下面几点:
1. 这个函数是未导出的,所以在微软的开发环境中是没有它的定义的,要使用它需要我们自己定义,定义的代码如下:
//这个NTSTATUS结构在应用层有定义,直接使用即可
typedef NTSTATUS(WINAPI *ZWQUERYSYSTEMINFORMATION)(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
__inout PVOID SystemInformation,
__in ULONG SystemInformationLength,
__out_opt PULONG ReturnLength
);
这个函数使用的一些结构是在内核开发环境DDK中定义的,在应用层中可能没有它的定义,所以在这我们也需要对它们进行定义:
#define NT_SUCCESS(status) ((NTSTATUS)(status)>=0)
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation,
SystemProcessorInformation,
SystemPerformanceInformation,
SystemTimeOfDayInformation,
SystemPathInformation,
SystemProcessInformation,
SystemCallCountInformation,
SystemDeviceInformation,
SystemProcessorPerformanceInformation,
SystemFlagsInformation,
SystemCallTimeInformation,
SystemModuleInformation,
SystemLocksInformation,
SystemStackTraceInformation,
SystemPagedPoolInformation,
SystemNonPagedPoolInformation,
SystemHandleInformation,
SystemObjectInformation,
SystemPageFileInformation,
SystemVdmInstemulInformation,
SystemVdmBopInformation,
SystemFileCacheInformation,
SystemPoolTagInformation,
SystemInterruptInformation,
SystemDpcBehaviorInformation,
SystemFullMemoryInformation,
SystemLoadGdiDriverInformation,
SystemUnloadGdiDriverInformation,
SystemTimeAdjustmentInformation,
SystemSummaryMemoryInformation,
SystemNextEventIdInformation,
SystemEventIdsInformation,
SystemCrashDumpInformation,
SystemExceptionInformation,
SystemCrashDumpStateInformation,
SystemKernelDebuggerInformation,
SystemContextSwitchInformation,
SystemRegistryQuotaInformation,
SystemExtendServiceTableInformation,
SystemPrioritySeperation,
SystemPlugPlayBusInformation,
SystemDockInformation,
SystemProcessorSpeedInformation,
SystemCurrentTimeZoneInformation,
SystemLookasideInformation
} SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;
- 缓冲区中存储的数据是一个表示返回数组中元素个数的DWORD类型的数据和一个对应结构体的数组,在MSDN上对这个缓冲进行解释时说这个缓冲区的头4个字节存储了对应数组的元素个数,而后面的存储的是对应结构的数组,所以在获取这个结构的数组时需要向后偏移4个字节。这个结构与我们传入的枚举值有关,比如我们在这获取的是进程内核空间中加载的模块信息,即传入的枚举值是SystemModuleInformation,它对应的结构应该是SYSTEM_MODULE_INFORMATION,它们之间的对应关系可以在MSDN中找到。这个结构也需要自己定义,它的定义如下:
typedef struct _SYSTEM_MODULE_INFORMATION // Information Class 11
{
ULONG Reserved[2];
PVOID pBase;
ULONG Size;
ULONG Flags;
USHORT Index;
USHORT Unknown;
USHORT LoadCount;
USHORT ModuleNameOffset;
CHAR ImageName[256];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
下面就是这个的代码:
void EnumKernelModules()
{
HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
if (INVALID_HANDLE_VALUE == hNtDll)
{
printf("加载ntdll.dll失败\n");
return ;
}
ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDll, "ZwQuerySystemInformation");
if (NULL == ZwQuerySystemInformation)
{
printf("导出函数失败\n");
return;
}
PULONG pBuffInfo = NULL;
DWORD dwSize = 0;
ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, 0, &dwSize);
pBuffInfo = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
NTSTATUS status = ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, dwSize, &dwSize);
if (!NT_SUCCESS(status))
{
return;
}
//在这为了验证之前说的,通过这两句输出发现他们的结果相同
printf("%d\n", *pBuffInfo);
printf("%d\n", dwSize / sizeof(SYSTEM_MODULE_INFORMATION));
PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)((ULONG)pBuffInfo + 4);
for (int i = 0; i < *pBuffInfo; i++)
{
printf("%s\n", pModuleInfo[i].ImageName);
}
}