0

I've run into an issue where my application needs to be able to find all active processes that are from a specific file path. The problem is that my application does NOT run as admin, and the process that I'm searching for DOES run as admin. So when I use the usual "EnumProcesses" method, it doesn't seem to pickup the process I'm searching for.

Is there anyway to bypass this so it includes "Elevated" processes, without requiring my application to Run as Admin?

Here is my code

int countProcess(const std::wstring& path_to_exe)
{
   int process_count = 0;

   DWORD aProcesses[1024], cbNeeded, cProcesses;
   if (EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded))
   {
      // Calculate how many process identifiers were returned.
      cProcesses = cbNeeded / sizeof(DWORD);

      // Go through each Process...
      for (DWORD i = 0; i < cProcesses; i++)
      {
         if(aProcesses[i] != 0)
         {
            TCHAR szProcessName[MAX_PATH];

            // Get a handle to the process.
            HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, aProcesses[i]);

            // Get the process name.
            if (hProcess != NULL)
            {
               HMODULE hMod;
               DWORD cbNeeded;

               if (EnumProcessModules(hProcess, &hMod, sizeof(hMod), &cbNeeded))
               {
                  if(GetModuleFileNameEx(hProcess, hMod, szProcessName, sizeof(szProcessName)/sizeof(TCHAR)))
                  {
                     std::wstring file_path(szProcessName);
                     if(file_path == path_to_exe)
                     {
                        process_count++;
                     }
                  }
               }
            }

            // Release the handle to the process.
            CloseHandle(hProcess);
         }
      }
   }

   return process_count;
}
Rick
  • 421
  • 3
  • 15
  • 2
    No because that would be a security risk. – CherryDT Feb 27 '23 at 16:47
  • @CherryDT then why can the process exe name (but not the path) be found using "CreateToolhelp32Snapshot", "Process32First", and "Process32Next"? – Rick Feb 27 '23 at 16:48
  • 1
    @Rick Do you have the same problem if you use `GetProcessImageFileName()` or `QueryFullProcessImageName()` instead of `GetModuleFileNameEx()`? That way, you can use just `PROCESS_QUERY_LIMITED_INFORMATION` instead of `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`. Always strive to use as few permissions as possible. – Remy Lebeau Feb 27 '23 at 20:58

1 Answers1

0

Assumptions

  • The program is running as a normal user; elevation is not possible.
  • You want the full path to the executable image; i.e. only the file name is not acceptable.

Potential solutions

The underlying issue is that by default you won't be able to get a handle to a process that belongs to another user, regardless of access rights. (unless you have the SeDebugPrivilege privilege, which would not be a good idea for non-admin users)

1. Toolhelp32

The simplest method to get the executable image is via CreateToolhelp32Snapshot() with TH32CS_SNAPPROCESS. The resulting PROCESSENTRY32 structure has the executable filename in szExeFile and the pid in th32ProcessID.

Example from msdn

  • Pro: Works for all processes, even those that belong to other users.
  • Con: You'll only get the filename, not the path.
1.1 also query the modules

In addition to that you can call CreateToolhelp32Snapshot() with TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32 for each process - the first MODULEENTRY32 is for the executable image, which contains the path to it in szExePath.

Example from msdn

  • Pro: You get the executable path.
  • Con: Only works for your own processes (excluding protected ones), because it attempts to open a handle to the process.

2. QueryFullProcessImageName

Another option is to use QueryFullProcessImageName() or GetProcessImageFileName(). Those unfortunately require a process handle.

Runnable code example on godbolt

std::wstring getProcessImageName(DWORD pid) {
    HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
    if (hProcess == NULL) {
        DWORD errorCode = GetLastError();
        // TODO: Log error
        return {};
    }

    std::wstring buffer;
    buffer.resize(MAX_PATH);

    while (true) {
        DWORD neededSize = static_cast<DWORD>(buffer.size());
        BOOL result = QueryFullProcessImageNameW(hProcess, 0, &buffer[0], &neededSize);
        if (!result) {
            DWORD errorCode = GetLastError();
            if (errorCode != ERROR_INSUFFICIENT_BUFFER) {
                // TODO: Log error
                CloseHandle(hProcess);
                return {};
            }
        }

        if (neededSize < buffer.size()) {
            buffer.resize(neededSize);
            buffer.shrink_to_fit();
            CloseHandle(hProcess);
            return buffer;
        }

        buffer.resize(buffer.size() * 2);
    }
}
  • Pro: Works for all processes that belong to the same user (including protected ones)
  • Con: Does not work for processes that belong to other users.

3. The unsupported way

At this point you might wonder how CreateToolhelp32Snapshot() (and Task Manager) do manage to get access to the full executable path of all processes.

Note: The following relies on undocumented functionality; there is no guarantee that this will continue work in the future.

The way those two manage to get the full executable image path is via an undocumented information class (SystemProcessIdInformation) for NtQuerySystemInformation() that only requires a pid and returns the full executable image path.

Example:
Runnable Code Example on Godbolt

#include <winternl.h>
#include <ntstatus.h>
#pragma comment(lib, "ntdll.lib")


struct SYSTEM_PROCESS_ID_INFORMATION {
    HANDLE ProcessId;
    UNICODE_STRING ImageName;
};

enum SYSTEM_INFORMATION_CLASS_EXTRA {
    SystemProcessIdInformation = 0x58
};

std::wstring getProcessImageNameUndocumented(DWORD pid) {
    DWORD bufferSize = 1 * sizeof(wchar_t);
    HLOCAL buffer = nullptr;
    
    while (true) {
        if (buffer) {
            LocalFree(buffer);
        }
        buffer = LocalAlloc(LMEM_FIXED, bufferSize);
        if (!buffer) {
            DWORD errorCode = GetLastError();
            // TODO: handle error
            return {};
        }

        SYSTEM_PROCESS_ID_INFORMATION info;
        info.ProcessId = reinterpret_cast<HANDLE>(static_cast<unsigned long long>(pid));
        info.ImageName.Length = 0;
        info.ImageName.MaximumLength = static_cast<USHORT>(bufferSize);
        info.ImageName.Buffer = reinterpret_cast<PWSTR>(buffer);

        NTSTATUS result = NtQuerySystemInformation(
            static_cast<SYSTEM_INFORMATION_CLASS>(SystemProcessIdInformation),
            &info,
            sizeof(info),
            nullptr
        );

        if (NT_ERROR(result) && result != STATUS_INFO_LENGTH_MISMATCH) {
            ULONG errorCode = RtlNtStatusToDosError(result);
            // TODO: handle error
            LocalFree(buffer);
            return {};
        }

        if (NT_SUCCESS(result)) {
            std::wstring result{ info.ImageName.Buffer, info.ImageName.Buffer + (info.ImageName.Length / sizeof(wchar_t)) };
            LocalFree(buffer);
            return result;
        }

        bufferSize = bufferSize * 2;
        if (bufferSize > 0xffff) bufferSize = 0xffff;
    }
}
  • Pro: Works for all processes.
  • Con: Relies on undocumented behaviour.
  • Con: The paths you get will be NT namespace absolute paths.
    (i.e. \Device\HarddiskVolume2\Windows\System32\svchost.exe)

Potential workaround

If all what you want to know is if a given executable file is currently in use(most likely because someone is executing it) and you don't need the process pid then you can use a much simpler approach:
Just check if you can open the executable file with write access.

The loader will map the executable into the process without FILE_SHARE_WRITE, so if you attempt to open the executable for writing you'll get an ERROR_SHARING_VIOLATION.

Example:

HANDLE hFile = CreateFileW(
    executableFilePath,
    GENERIC_WRITE,
    FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
);
if(hFile != INVALID_HANDLE_VALUE) {
    // this file is not currently mapped into any process
    CloseHandle(hFile);
} else if(GetLastError() == ERROR_SHARING_VIOLATION) {
    // another process has mapped this executable file
} else {
    DWORD errorCode = GetLastError();
    // unknown error occured
}
Turtlefight
  • 9,420
  • 2
  • 23
  • 40