4

I'm trying to enumerate 32bit process modules names from 64bit application using the following code:

if (EnumProcessModulesEx(hProcess, hMods, sizeof(hMods), &cbNeeded, LIST_MODULES_ALL))
{
    for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++)
    {
       TCHAR szModName[MAX_PATH] = { 0 };

        if (GetModuleFileNameEx(hProcess, hMods[i], szModName,
            sizeof(szModName) / sizeof(TCHAR)))
        {
            printf("module name is: %S", szModName);
        }
    }
}

The code works as expected in Windows 7, as part of the results are:

...

C:\Windows\**SysWOW64**\ntdll.dll

...

In Windows 10 the above code returns the full path but with System32 instead of SysWOW64. e.g,

...

C:\Windows\**System32**\ntdll.dll

...

Looking deeper for the cause, I notice that GetModuleFileNameEx reads the remote process PEB and LDR_TABLE_ENTRY, and starting from Windows 10 the LDR_TABLE_ENTRY contains the full path with System32 and not SysWOW64 - also for 32bit applications.

I also tried to use GetMappedFileName but it isn't straight forward and efficient to translate the path from dos path (\device\harddiskvolume) to standard (c:\) path.

I wonder if there are any other easy way to extract the full syswow64 path.

AK87
  • 613
  • 6
  • 24
  • This might be a super lame suggestion, but have you checked that the process you target/inspect is actually a 32 bit process in Windows 10? – Christian.K Sep 25 '17 at 11:10
  • haha, sure..... – AK87 Sep 25 '17 at 11:11
  • 1
    i check - this is really system mistake. when you use `LIST_MODULES_ALL` - system really work like you set `LIST_MODULES_32BIT`. you need call `EnumProcessModulesEx` twice : once with `LIST_MODULES_64BIT` (you got 4 64-bit modules - ntdll, wow64,wow64win,wow64cpu) and once with `LIST_MODULES_32BIT` – RbMm Sep 25 '17 at 11:17
  • 1
    you can use `ZwQueryVirtualMemory` with `MemoryMappedFilenameInformation` for get canonical, nt form path for any module in target process. or you want only win32 form path ? – RbMm Sep 25 '17 at 11:28
  • also use `cbNeeded / sizeof(HMODULE)` - this is error. the `cbNeeded` - this how many bytes need to store modules, but no how many bytes returned. you need use `min(sizeof(hMods), cbNeeded)/ sizeof(HMODULE)`. and why you permanent calc this in loop, instead once call before loop ? – RbMm Sep 25 '17 at 11:33
  • 1
    RbMm, this is just an example, thanks. I will check the ZwQueryVirtualMemory and MemoryMappedFilenameInformation now. Thanks. – AK87 Sep 25 '17 at 11:39
  • the `ZwQueryVirtualMemory` of course worked well. are you need only win32 form path or nt path is ok for you ? – RbMm Sep 25 '17 at 11:45
  • The win32 standard form only (non UNC) – AK87 Sep 25 '17 at 11:46
  • the simplest solution for make valid win32 path from nt path - add `L"\\\\?\\globalroot"` prefix for name. so your path will be look like `L"\\\\?\\globalroot\\Device\\HarddiskVolumeX\\windows\\syswow64\\ntdll.dll" - this path will be work well for `CreateFileW` but some shell functions not accept this form. for convert in dos-device path (this is subset of valid win32 paths) need use mount manager – RbMm Sep 25 '17 at 11:56
  • 1
    So using ZwQueryVirtualMemory and MemoryMappedFilenameInformation are actually the same as using GetMappedFileName. It returns the dos path form (\\Device\\HarddiskVolumeX). I didn't found any efficient way to transform it to standard path. I tried https://msdn.microsoft.com/en-us/library/windows/desktop/aa366789(v=vs.85).aspx but it is not efficient way. – AK87 Sep 25 '17 at 12:04
  • You are wrong. GetMappedFileName gets handle to the process. See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683195(v=vs.85).aspx – AK87 Sep 25 '17 at 12:10
  • yes, my mistake. forget this – RbMm Sep 25 '17 at 12:12
  • you need `IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH` for convert `"\\Device\\HarddiskVolumeX"` to dos path – RbMm Sep 25 '17 at 12:21

1 Answers1

3

for get valid win32 file path from file nt-path - simplest way - add L"\\\\?\\globalroot" (\\?\globalroot) prefix. this is because CreateFileW looked from \??\ directory and globalroot is symbolic link in \??\ which let as to jump to root of nt namespace.

for example - \Device\HarddiskVolume9\Windows\SysWOW64\ntdll.dll is nt absolute path. and \\?\globalroot\Device\HarddiskVolume9\Windows\SysWOW64\ntdll.dll is valid win32 path for CreateFileW - this api convert well known prefix \\?\ to nt prefix \??\ and pass name \??\globalroot\Device\HarddiskVolume9\Windows\SysWOW64\ntdll.dll to kernel. when parsing this name - after process symbolic link globalroot which point to root of namespace - we again got \Device\HarddiskVolume9\Windows\SysWOW64\ntdll.dll - correct nt path.

so if we need valid win32 path for use in CreateFileW - simply append this prefix to nt path. however some shell32 api not accept this form path. also it not nice looked in UI. if we want got DOS drive letter form path (this is subset of valid win32 paths) - we can use IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH which convert device name to drive letter. this ioctl take as input MOUNTDEV_NAME (declared in mountmgr.h) and output buffer is MOUNTMGR_VOLUME_PATHS. in MOUNTDEV_NAME buffer must be exactly device name, without file path. so we need break returned nt path to 2 components. for example in \Device\HarddiskVolume9\Windows\SysWOW64\ntdll.dll :

  • \Device\HarddiskVolume9 - device path
  • \Windows\SysWOW64\ntdll.dll - file system path

correct way here first open file and call GetFileInformationByHandleEx with FileNameInfo - we got file system path in output. with this we can use wcsstr for separate device path. also if we open file handle - we can use it in call GetFinalPathNameByHandleW with VOLUME_NAME_DOS. this api do exactly which we will be do - query file path, separate device path and call IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH. + open/close mount manager.

but usual nt file path begin from \Device\HarddiskVolumeX. this allow first try fast way - avoid open file and query it path.

so first we need open mount manager:

#include <mountmgr.h>
HANDLE hMountManager = CreateFile(MOUNTMGR_DOS_DEVICE_NAME, 
    0, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, 0, 0);

then we can run next code:

void dumpModules(HANDLE hMountManager, HANDLE hProcess)
{
    ULONG cb = 0, cbNeeded = 16;

    volatile static UCHAR guz;
    PVOID stack = alloca(guz);
    HMODULE *hMods, hmod;

__continue:

    // cumulative allocate memory in stack, not need free it
    cb = RtlPointerToOffset(hMods = (HMODULE*)alloca(cbNeeded - cb), stack);

    if (EnumProcessModulesEx(hProcess, hMods, cb, &cbNeeded, LIST_MODULES_32BIT))
    {
        if (cb < cbNeeded)
        {
            goto __continue;
        }

        if (cbNeeded /= sizeof(HMODULE))
        {
            //i use hard coded size buffers, for reduce code and show main idea
#define FILE_NAME_INFO_buffer_size  FIELD_OFFSET(FILE_NAME_INFO, FileName[MAX_PATH])
#define MOUNTDEV_NAME_buffer_size  FIELD_OFFSET(MOUNTDEV_NAME, Name[MAX_PATH])
#define MOUNTMGR_VOLUME_PATHS_buffer_size  FIELD_OFFSET(MOUNTMGR_VOLUME_PATHS, MultiSz[64])

            // + space for 0 at the end
            PFILE_NAME_INFO pfni = (PFILE_NAME_INFO)alloca(FILE_NAME_INFO_buffer_size + sizeof(WCHAR));

            PMOUNTMGR_VOLUME_PATHS pmvp = (PMOUNTMGR_VOLUME_PATHS)alloca(MOUNTMGR_VOLUME_PATHS_buffer_size);
            PMOUNTDEV_NAME pmdn = (PMOUNTDEV_NAME)alloca(MOUNTDEV_NAME_buffer_size);

            static WCHAR globalroot[] = L"\\\\.\\globalroot";

            alloca(sizeof(globalroot));
            PWSTR win32Path = pmdn->Name - RTL_NUMBER_OF(globalroot) + 1;

            memcpy(win32Path, globalroot, sizeof(globalroot));
            USHORT NameLength = pmdn->NameLength;

            do 
            {
                hmod = *hMods++;

                if (GetMappedFileNameW(hProcess, hmod, pmdn->Name, MAX_PATH))
                {
                    DbgPrint("%p %S\n",hmod, pmdn->Name);

                    PWSTR c = 0;

                    static const WCHAR HarddiskVolume[] = L"\\Device\\HarddiskVolume";

                    // fast way
                    if (!memcmp(pmdn->Name, HarddiskVolume, sizeof(HarddiskVolume) - sizeof(WCHAR)))
                    {
                        c = wcschr(pmdn->Name + RTL_NUMBER_OF(HarddiskVolume) - 1, '\\');
                    }
                    // else - for demo
                    {
                        pmdn->NameLength = NameLength;

                        HANDLE hFile = CreateFile(win32Path, 0, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, 0, 0);

                        if (hFile != INVALID_HANDLE_VALUE)
                        {
                            //++ just for demo
                            WCHAR DosPath[MAX_PATH];
                            if (GetFinalPathNameByHandleW(hFile, DosPath, RTL_NUMBER_OF(DosPath), VOLUME_NAME_DOS))
                            {
                                DbgPrint("%S\n", DosPath);
                            }
                            RtlGetLastNtStatus();
                            //-- just for demo

                            BOOL fOk = GetFileInformationByHandleEx(hFile, FileNameInfo, pfni, FILE_NAME_INFO_buffer_size);

                            CloseHandle(hFile);

                            if (fOk)
                            {
                                // FileName not 0 terminated
                                pfni->FileName[pfni->FileNameLength/sizeof(WCHAR)] = 0;

                                c = wcsstr(pmdn->Name, pfni->FileName);
                            }
                        }

                    }

                    if (c)
                    {
                        pmdn->NameLength = (USHORT)RtlPointerToOffset(pmdn->Name, c);

                        if (DeviceIoControl(hMountManager, IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH,
                            pmdn, MOUNTDEV_NAME_buffer_size, 
                            pmvp, MOUNTMGR_VOLUME_PATHS_buffer_size, &cb, NULL))
                        {
                            DbgPrint("%S%S\n", pmvp->MultiSz, c);
                        }
                    }
                }

            } while (--cbNeeded);
        }
    }
}

and demo output for notepad:

0000000000170000 \Device\HarddiskVolume9\Windows\SysWOW64\notepad.exe
\\?\C:\Windows\SysWOW64\notepad.exe
C:\Windows\SysWOW64\notepad.exe
0000000077A90000 \Device\HarddiskVolume9\Windows\SysWOW64\ntdll.dll
\\?\C:\Windows\SysWOW64\ntdll.dll
0000000075460000 \Device\HarddiskVolume9\Windows\SysWOW64\kernel32.dll
\\?\C:\Windows\SysWOW64\kernel32.dll
C:\Windows\SysWOW64\kernel32.dll
0000000074A30000 \Device\HarddiskVolume9\Windows\SysWOW64\KernelBase.dll
\\?\C:\Windows\SysWOW64\KernelBase.dll
C:\Windows\SysWOW64\KernelBase.dll
00000000749B0000 \Device\HarddiskVolume9\Windows\SysWOW64\advapi32.dll
\\?\C:\Windows\SysWOW64\advapi32.dll
RbMm
  • 31,280
  • 3
  • 35
  • 56
  • You keep posting code with mysterious variables that are either undefined or uninitialized. What is `guz`? – Jonathan Potter Sep 25 '17 at 19:03
  • @JonathanPotter - forget copy it definition. `volatile static UCHAR guz;` i use it for got stack pointer. if write `stack = alloca(0)` with optimization compiler drop this instruction. but when i use `alloca(guz)` instead - this is work ok, because i declare `guz` as `volatile`. of course we can and use `alloca(1)`. but i prefer `alloca(guz)` when `guz==0` – RbMm Sep 25 '17 at 19:33
  • 1
    This should just work with a Tool Help `TH32CS_SNAPMODULE32` snapshot. It calls `RtlQueryProcessModuleInformation`, which uses functions such as `RtlGetNtSystemRoot`and `RtlReplaceSystemDirectoryInPath` to rewrite the paths to use SysWOW64 instead of System32. – Eryk Sun Sep 26 '17 at 09:11
  • 1
    Actually this looks like WinAPI `GetModuleFileNameEx` is denying sound design principles. `RtlQueryProcessModuleInformation` properly relegates the task to the loader via `LdrQueryProcessModuleInformationEx`, which calls `LdrQueryModuleInfoFromLdrEntry32` and `LdrpGetModuleName`. OTOH, `GetModuleFileNameEx` thinks it's too good to play by the rules and completely tangles the hierarchy. It just barges in with assumptions that are no longer valid. – Eryk Sun Sep 26 '17 at 09:24
  • @eryksun yes. you right. i know all this. however `RtlQueryProcessModuleInformation` was different in different os versions. i remember times when it create remote working thread in target process to `LdrQueryProcessModuleInformation` for collect modules from here (and using event pair for sync). however i not use toolhelp api , but yourself create remote thread in taget process (for wow64 with some trics). [`LdrpGetModuleName`](https://prnt.sc/gptkxx) – RbMm Sep 26 '17 at 09:38