On my quest of writing a library to wrap common winapi functionality using native apis I came across the need to enumerate process modules. One may think that there is a simple undocumented function to achieve such functionality, however that is not the case.
Existing options
First of all lets see what are the existing APIs that are provided by microsoft because surely they’d know their own code and the operating systems limitations best.
Process Status API has a rather understandable and simple interface to get an array of module handles and then acquire the information of module using other functions. If you have written windows specific code before you might feel rather comfortable but for a new developer dealing with extensive error handling due to the fact that you may need to reallocate memory might not be very fun.
BOOL EnumProcessModules(HANDLE process, HMODULE* modules, DWORD byteSize, DWORD* reqByteSize); BOOL GetModuleInformation(HANDLE process, HMODULE module, MODULEINFO* info, DWORD infoSize); DWORD GetModuleFileNameEx(HANDLE process, HMODULE module, TCHAR* filename, DWORD filenameMaxSize);
Next up Tool Help is at the same time simpler and more complex than PSAPI. It does not require the user to allocate memory by himself and returns fully processed structures but requires quite a few extra steps. Toolhelp also leaves some opportunities for the user to shoot himself in the foot such as returning
INVALID_HANDLE_VALUE
instead of nullptr on failure and requires to clean up the resources by closing the handle.
HANDLE CreateToolhelp32Snapshot(DWORD flags, DWORD processId); BOOL Module32First(HANDLE snapshot, MODULEENTRY32* entry); BOOL Module32Next(HANDLE snapshot, MODULEENTRY32* entry);
Performance
An important aspect of these functions is how fast they are. Below are some rough benchmarks of these apis to get full module information including its full path on a machine running Windows 10 with an i7-8550u processor compared to implementation written by ourselves.
TLHELP | PSAPI | native |
---|---|---|
220ms | 2200 ms | 50ms |
Turns out that while EnumProcessModules
is really fast by itself it only saves the dll base address instead of something like LDR_DATA_TABLE_ENTRY
address where all of the information is saved. Due to this every single time you want to for example get the module name it repeats all the work iterating modules list once again.
The implementation
So what exactly do we need to do to get the modules of another process?
In essence the answer is rather simple:
- Get its Process Environment Block (
PEB
) address. - Walk the LDR table whose address we acquired from PEB.
The first problem is that a process may have 2 PEBs - one 32bit and one 64bit. The most intuitive behaviour would be to get the modules of the same architecture as the target process. However NtQueryInformationProcess
with the ProcessBasicInformation
flag gives us access only to the PEB of the same architecture as ours. This problem was solved in the previous post so it will not be discussed further again.
The next problem is that PEB is not the only thing that depends on architecture. All structures and functions we plan to use will also need to be apropriatelly selected. To overcome this hurdle we will create 2 structures containing the correct functions and the type to use for representing pointers. We want to use different memory reading functions because if we are a 32 bit process NtReadVirtualMemory
will also expect a 32 bit address which might not be sufficient when the target process is 64bit.
template<class TargetPointer> struct native_subsystem { using target_pointer = TargetPointer; template<class T> NTSTATUS static read(void* handle, uintptr_t address, T& buffer, size_t size = sizeof(T)) noexcept { return NtReadVirtualMemory( handle, reinterpret_cast<void*>(address), &buffer, size, nullptr); } }; struct wow64_subsystem { using target_pointer = uint64_t; template<class T> NTSTATUS static read(void* handle, uint64_t address, T& buffer, uint64_t size = sizeof(T)) noexcept { return NtWow64ReadVirtualMemory64(handle, address, &buffer, size, nullptr); } };
For convenience sake and to keep our code DRY we will also need to write some sort of helper function which picks one of the aforementioned structures to use. For that we will use some templates. If this doesn’t fit you, macros or just functions pointers are an option, both of which have a set of their own pros and cons.
template<class Fn, class... As> NTSTATUS subsystem_call(bool same_arch, Fn fn, As&&... args) { if(same_arch) return fn.template operator()<native_subsystem<uintptr_t>>(forward<As>(args)...); #ifdef _WIN64 return fn.template operator()<native_subsystem<uint32_t>>(forward<As>(args)...); #else return fn.template operator()<wow64_subsystem>(forward<As>(args)...); #endif }
While I agree this function looks rather ugly, operator() is really the most sane option because you don’t have to worry about naming things. Its usage is rather simple too. All you have to do is pass it whether the architecture is the same and a structure whose operator() is overloaded and accepts the subsystem type. We can take a look at exactly how the usage looks like by finally taking use of the functions we wrote earlier and completing the first step of getting the correct PEB
pointer.
template<class Callback> NTSTATUS enum_modules(void* handle, Callback cb) noexcept { PROCESS_EXTENDED_BASIC_INFORMATION info; info.Size = sizeof(info); ret_on_err(NtQueryInformationProcess( handle, ProcessBasicInformation, &info, sizeof(info), nullptr)); const bool same_arch = is_process_same_arch(info); std::uint64_t peb_address; if(same_arch) peb_address = reinterpret_cast<std::uintptr_t>(info.BasicInfo.PebBaseAddress); else ret_on_err(remote_peb_address(handle, false, peb_address))); return subsystem_call(same_arch, enum_modules_t{}, handle, std::move(cb), peb_address); }
Now all that is left is to implement the actual modules enumeration about which I won’t be talking as much because it is rather straightforward and information about it is easily found on the internet. For it to work you’ll also need architecture agnostic versions of windows structures that you can be easily made by templating their member types and some find+replace.
struct enum_modules_t { template<class Subsystem, class Callback> NTSTATUS operator()(void* handle, Callback cb, std::uint64_t peb_address) const { using ptr_t = typename Subsystem::target_pointer; // we read the Ldr member of peb ptr_t Ldr; ret_on_err(Subsystem::read(handle, static_cast<ptr_t>(peb_address) + offsetof(peb_t<ptr_t>, Ldr), Ldr)); const auto list_head = Ldr + offsetof(peb_ldr_data_t<ptr_t>, InLoadOrderModuleList); // read InLoadOrderModulesList.Flink ptr_t load_order_modules_list_flink; ret_on_err(Subsystem::read(handle, list_head, load_order_modules_list_flink)); ldr_data_table_entry_t<ptr_t> entry; // iterate over the modules list for(auto list_curr = load_order_modules_list_flink; list_curr != list_head;) { // read the entry ret_on_err(Subsystem::read(handle, list_curr, entry)); // update the pointer to entry list_curr = entry.InLoadOrderLinks.Flink; // call the callback with the info. // to get the path we would need another read. cb(info); } return STATUS_SUCCESS; } };BACK TO HOME