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
}