列举进程对应Module其实是个比较常见的问题,最开始想到的是TlHelp32.h头文件里的Module32First和Module32Next函数,代码很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
BOOL CProcessExplorer::SimpleReadModule(DWORD dwProcessID) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessID); if (INVALID_HANDLE_VALUE == hSnapshot) { return FALSE; } MODULEENTRY32 me32 = { sizeof(MODULEENTRY32) }; if (!Module32First(hSnapshot, &me32)) { CloseHandle(hSnapshot); return FALSE; } do { //这里根据me32结构体做自己的事情 } while (Module32Next(hSnapshot, &me32)); CloseHandle(hSnapshot); return TRUE; } |
但是运行起来会发现,通过这套函数在编译成64位的时候,读32位的程序的Module会出错
在编译成32位的时候读32位程序虽然不会出错
然而在读64位的程序的时候会直接报错
这套函数的兼容性有问题,令人尴尬的是用EnumProcessModules加GetModuleFileNameEx函数依然解决不了这个问题
其实原因很简单,Module32那套函数用了NtMapViewOfSection(为啥看到这函数我第一反应就是注入………),对虚拟内存中的节表(Section)进行读取
但是32位系统和64位系统的地址空间是不一样的,这一点在Windows核心编程中很详细的写了
根据ProcessExplorer读出来的信息可以看出,64位的exe和dll的ImageBase和32位的都不同,微软提供的这套函数无法根据实际判断文件是32位还是64位,也就无法判断NtMapViewOfSection需要读取的空间范围
所以,最基本的要做成像ProcessExplorer那样,64位程序要能完美的读出64位和32位的程序Modules信息,32位的程序要能完美的读出32位程序的Module信息。
简单的方法是选择用VirtualQueryEx加GetMappedFileName的方式遍历对应内存的节表(Section),读出用户态每个节的信息,即可完整的读出整个进程对应的Module信息
用图形化工具看Windows的节表大体就是这样的(这是32位下的)
VirtualQueryEx加GetMappedFileName这两个函数在MSDN上都有很明确的解释
对于VirtualQueryEx来说,在正常情况下可以查询用户内存区域所有节,进入内核区域会失败,其HANDLE传入查找的进程的句柄,LPCVOID参数是在该进程中查询的起始地址(内存中的虚拟地址),lpBuffer参数传递PMEMORY_BASIC_INFORMATION结构体,用来存放查询到的相关信息,dwLength为该结构体的大小。
PMEMORY_BASIC_INFORMATION结构体定义如下
然后使用GetMappedFileName函数将文件的路径读出来,函数原型如下
但是要注意的是,该函数读出来的路径是物理路径像这种\\Device\\HarddiskVolume3\\,需要转换一下,这个我就不多说了,直接贴一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
BOOL CProcessExplorer::ReadModule(DWORD dwProcessID) { HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessID); if (hProcess == NULL) return FALSE; MEMORY_BASIC_INFORMATION mbi; #ifdef _WIN64 ULONG64 l64Address = 0; while (VirtualQueryEx(hProcess, (LPCVOID)l64Address, &mbi, sizeof(MEMORY_BASIC_INFORMATION))==sizeof(MEMORY_BASIC_INFORMATION)) { if ( //mbi.Type == MEM_IMAGE && mbi.AllocationBase==(PVOID)l64Address) { TCHAR lpModuleName[MAX_PATH + 1]; if (GetMappedFileName(hProcess, (LPVOID)l64Address, lpModuleName, MAX_PATH) != NULL) { //这里做自己的事情 } } l64Address += mbi.RegionSize; } #else ULONG lAddress = 0; while (VirtualQueryEx(hProcess, (LPCVOID)lAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == sizeof(MEMORY_BASIC_INFORMATION)) { if ( //mbi.Type == MEM_IMAGE && mbi.AllocationBase == (PVOID)lAddress) { TCHAR lpModuleName[MAX_PATH + 1]; if (GetMappedFileName(hProcess, (LPVOID)lAddress, lpModuleName, MAX_PATH) != NULL) { //这里做自己的事情 } } lAddress += mbi.RegionSize; } #endif CloseHandle(hProcess); return TRUE; } |
代码很简单,并没有什么太多需要解释的,用VirtualQueryEx从0x0的内存位置开始查找,查找到的是从该地址开始的第一个节,如果该内存空间被使用(查找的内存位置和节的基址相同),就读出节对应的目录,每次增加RegionSize(该节的大小,如果该段内存空间未被使用,该值为这段空间的大小),直到查找失败(如果不是特殊原因),这里的结构体大小是固定的,基本不会出现空间不足的情况,所以就没加什么错误判断(其实我懒)。
中间分了两种情况写的,因为64位系统的指针和32位系统的指针长度不同,ULONG64是一个8字节的指针,ULONG是一个4字节指针(在64位系统下长度不够),除此之外没什么别的区别。
从图中可以看出,用这组函数,在编译成64位程序时可以完美读出32位和64位程序的Module信息
然而到了这里,有没有第三种方法呢?答案是肯定的,我们先来分析一下VirtualQueryEx和GetMappedFileName这俩函数
可以看到在VirtualQueryEx中调用了NtQueryVirtualMemory(ZwQueryVirtualMemory 在ring3中这俩函数是一样的)这个函数
在网上可以查到该函数的定义如下,该函数是Native函数,MSDN上并未公开这个函数
可以看出该函数有6个参数
所以这时候先记下这几个参数RCX RDX R8 R9 RSP+0x20 RSP+0x28(64位程序函数传参的基本知识 不多解释)
再继续跟到GetMappedFileName里面看看
巧的是在里面依然用了NtQueryVirtualMemory这个函数
明显不同的是第三个参数
用windbg看看这个第三个参数的MEMORY_INFORMATION_CLASS 结构到底是什么东西
可以很显然看出来为该值为0和2时候的作用
好了到了这个时候,前期的准备工作已经做完了,直接贴一下代码,代码是从我总的代码里抽出来的,有些头文件包含之类的就不贴了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
typedef LONG NTSTATUS; #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) typedef struct { USHORT Length; USHORT MaxLen; USHORT *Buffer; }UNICODE_STRING, *PUNICODE_STRING; typedef enum _MEMORY_INFORMATION_CLASS { MemoryBasicInformation, MemoryWorkingSetList, MemoryMappedFilenameInformation }MEMORY_INFORMATION_CLASS; typedef NTSTATUS (WINAPI *ZWQUERYVIRTUALMEMORY) ( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN MEMORY_INFORMATION_CLASS MemoryInformationClass, OUT PVOID MemoryInformation, IN ULONG MemoryInformationLength, OUT PULONG ReturnLength OPTIONAL ); BOOL CProcessExplorer::ZwReadModule(DWORD dwProcessID) { HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessID); if (hProcess == INVALID_HANDLE_VALUE) return FALSE; ZWQUERYVIRTUALMEMORY ZwQueryVirtualMemory = (ZWQUERYVIRTUALMEMORY)(GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "ZwQueryVirtualMemory")); if (NULL == ZwQueryVirtualMemory) return FALSE; MEMORY_BASIC_INFORMATION MemoryInformation; #ifdef _WIN64 for (ULONG64 dwSectionSize = 0; dwSectionSize < 0x800000000000; dwSectionSize += MemoryInformation.RegionSize) { NTSTATUS ntStatus = ZwQueryVirtualMemory( hProcess, (LPVOID)dwSectionSize, MemoryBasicInformation, &MemoryInformation , sizeof(MEMORY_BASIC_INFORMATION), NULL); if (!NT_SUCCESS(ntStatus)) break; else { if (MemoryInformation.AllocationBase == (PVOID)dwSectionSize) { TCHAR lpModuleName[MAX_PATH]; ntStatus = ZwQueryVirtualMemory(hProcess, (LPVOID)dwSectionSize, MemoryMappedFilenameInformation, lpModuleName, sizeof(lpModuleName), NULL); if (NT_SUCCESS(ntStatus)) { PUNICODE_STRING pModuleName = (PUNICODE_STRING)lpModuleName; //这里做自己的事情 } } } } #else for (ULONG dwSectionSize = 0; dwSectionSize < 0x80000000; dwSectionSize += MemoryInformation.RegionSize) { NTSTATUS ntStatus = ZwQueryVirtualMemory( hProcess, (PULONG)dwSectionSize, MemoryBasicInformation, &MemoryInformation , sizeof(MEMORY_BASIC_INFORMATION), NULL); if (!NT_SUCCESS(ntStatus)) break; else { if (MemoryInformation.AllocationBase == (PVOID)dwSectionSize) { TCHAR lpModuleName[MAX_PATH]; ntStatus=ZwQueryVirtualMemory(hProcess, (PULONG)dwSectionSize, MemoryMappedFilenameInformation, lpModuleName, sizeof(lpModuleName),NULL); if (NT_SUCCESS(ntStatus)) { PUNICODE_STRING pModuleName = (PUNICODE_STRING)lpModuleName; //这里做自己的事情 } } } } #endif CloseHandle(hProcess); return TRUE; } |
代码其实很简单,先是调用Native函数的基本步骤,至于在循环的时候,因为32位系统和64位系统的内存空间大小不同,所以写了两套,在定义MEMORY_INFORMATION_CLASS结构体的时候我也并没有把所有的都枚举到,因为只用了第一个和第三个,用到的话再加。至于还有一点需要注意到的是,在内核中的字符串对象是PUNICODE_STRING类型,在第二次调用ZwQueryVirtualMemory的时候获取信息的Buffer需要做一次类型转换。
运行一波发现效果是一样的
这是我在做进程管理器时候想到的一些小东西,记录下来,当然在Ring3下列举进程Module可以用Hook的方式使其获取不到,很多恶意代码也是这么做的,我写的代码也主要是针对Ring3,所以并没有做内核的东西。
至于最后还有一些想法,其实在64位系统下做调试,VS真的是个好东西,相对windbg操作简单、界面友好,对符号表的支持更好,所以在Ring3下调试我很多时候都直接用了VS。