Introduction
A process ID is a value uniquely identifying a running process. This means that unlike handles, which can be duplicated, a process ID remains the same during the process life cycle and no other process can have the same ID value during this time. It is common to obtain a process handle by processing an ID using the OpenProcess()
call. Here we shall talk about the opposite problem, i.e. having only a handle to a process and how to find the ID of the process represented by that handle. This may arise, in particular, when you inject your code into other applications and you do not know the origination of a handle. If you work under Windows XP SP1 or higher, you can just call the following with a handle you have:
DWORD GetProcessId(HANDLE Process);
For lower versions, there is no simple and uniform solution. Below I shall consider only Windows 2000 and up, as (obsolete) Windows 9X provides no proper tools. Also, driver approaches are not considered here.
Three techniques will be discussed:
- Getting the ID by creating remote thread.
- Enumerating processes and comparing their attributes.
- Using "undocumented" ZwQueryInformationProcess().
If we want to find our own process ID, we call GetCurrentProcessId()
. Intuitively it would be nice to make a target process to call it in its own context and to return us the result. To implement this, a remote thread creation technique can be used. Here is a prototype of CreateRemoteThread()
:
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwCreationFlags,
LPDWORD lpThreadId
);
By setting lpStartAddress
to the address of GetCurrentProcessId()
, we cause this function to be executed as a thread routine in the context of a target process. Then it will remain, using the returned thread's handle, to get the thread exit code. One delicate thing is that GetCurrentProcessId()
has no input parameters, whereas LPTHREAD_START_ROUTINE
assumes one. Indeed, CreateRemoteThread()
just passes execution to the code pointed by lpStartAddress
not doing all necessary validations. In the majority of cases, it is rather a problem. Here we use it as an advantage: as GetCurrentProcessId()
has been executed not using anything from the stack, the thread exits and then its stack is destroyed, causing no problem. Here is a full code of a function doing necessary steps:
DWORD WINAPI GetProcessIDbyProcessHandle(HANDLE hProcess)
{
if (hProcess == NULL) return 0xffffffff;
PTHREAD_START_ROUTINE lpStartAddress = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "GetCurrentProcessId");
if (lpStartAddress == NULL) return 0xffffffff;
HANDLE hProcessAccAdj;
BOOL bRes = DuplicateHandle(GetCurrentProcess(),
hProcess, GetCurrentProcess(), &hProcessAccAdj,
PROCESS_QUERY_INFORMATION|PROCESS_CREATE_THREAD|
PROCESS_VM_OPERATION|PROCESS_VM_WRITE,
FALSE, 0);
if (!bRes || hProcessAccAdj == NULL)
{
UINT unError = GetLastError();
return 0xffffffff;
}
DWORD dwThreadID;
HANDLE hRemoteThread = CreateRemoteThread(hProcessAccAdj, NULL,
0, lpStartAddress, 0, 0, &dwThreadID);
CloseHandle(hProcessAccAdj);
if (hRemoteThread == NULL) return 0xffffffff;
WaitForSingleObject(hRemoteThread, INFINITE);
DWORD dwExitCode;
if (GetExitCodeThread(hRemoteThread, &dwExitCode) == 0) dwExitCode = 0xffffffff;
CloseHandle(hRemoteThread);
return dwExitCode;
}
Unfortunately, this will fail if the security context of a target process does not allow us to open (duplicate) a handle to that process with required access rights. In particular, this will happen with low ID processes such as csrss.exe, winlogon.exe and a few others. Getting debug privilege may or may not solve a problem, as we, by assumption, cannot control the way of getting an initial (available) handle. For instance, in the following sequence of calls...
hProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, dwProcessId);
HANDLE hProcessAccessAdjusted;
DuplicateHandle(GetCurrentProcess(), hProcess, GetCurrentProcess(),
&hProcessAccessAdjusted,
PROCESS_QUERY_INFORMATION|PROCESS_CREATE_THREAD|
PROCESS_VM_OPERATION|PROCESS_VM_WRITE,
FALSE, 0);
...the last call will fail with ERROR_ACCESS_DENIED
if dwProcessId
represents one of the above processes, whether or not the debug privilege is granted.
Using functionality provided by psapi.dll, one can enumerate (identifiers of) processes currently running in the operating system and then get their handles and attempt to work with them. Obviously, it's useless to compare process handles with a given one (think about handle duplication). Instead, we can compare process properties, which can be obtained using process handles. Although none of them uniquely determines a process, GetProcessTimes()
can do a pretty good filtering, especially on a single processor machine. The obtained result is valid if no other process has the same creation time. To improve the result, if more than one candidate has the same creation time, we can continue comparison by process name. Below you can see a sketch of a function that does such enumeration. If, by available data, the process can be uniquely identified, its ID is returned. The function fails, returning 0xffffffff
, if more than one candidate exists.
#include <Psapi.h>
DWORD WINAPI TryGetProcessIDbyProcessHandle(HANDLE hProcess)
{
if (hProcess == NULL) return 0xffffffff;
FILETIME ftSourceCreationTime, ftOtherCreationTime;
FILETIME ftExitTime;
FILETIME ftKernelTime;
FILETIME ftUserTime;
GetProcessTimes(hProcess, &ftSourceCreationTime,
&ftExitTime, &ftKernelTime, &ftUserTime);
DWORD cbNeeded;
DWORD dwProcessesIDs[1024];
DWORD dwGoodProcessesIDs[1024];
DWORD dwGoodProcessCount = 0;
if ( EnumProcesses( dwProcessesIDs, sizeof(dwProcessesIDs), &cbNeeded ) )
{
cbNeeded /= sizeof(DWORD);
for (UINT i=0; i < cbNeeded; i++)
{
HANDLE hOtherProcess = OpenProcess(PROCESS_QUERY_INFORMATION,
FALSE, dwProcessesIDs[i]);
GetProcessTimes(hOtherProcess, &ftOtherCreationTime, &ftExitTime,
&ftKernelTime, &ftUserTime);
CloseHandle(hOtherProcess);
if (*(__int64*)(&ftSourceCreationTime) == *(__int64*)(&ftOtherCreationTime))
{
dwGoodProcessesIDs[dwGoodProcessCount] = dwProcessesIDs[i];
dwGoodProcessCount ++;
}
}
}
if (dwGoodProcessCount == 0) return 0xffffffff;
if (dwGoodProcessCount == 1) return dwGoodProcessesIDs[0];
DWORD dwRetProcessID = -1;
USHORT szSourceProcessName[MAX_PATH];
szSourceProcessName[0] = 0;
HMODULE hMod[1024];
if ( EnumProcessModules( hProcess, hMod, sizeof(hMod), &cbNeeded) )
GetModuleBaseNameW( hProcess, hMod[0], szSourceProcessName,
sizeof(szSourceProcessName) );
if (szSourceProcessName[0] == 0) return 0xffffffff;
UINT i = 0;
do
{
szSourceProcessName[i] = towlower(szSourceProcessName[i]);
}
while (szSourceProcessName[++i]);
USHORT szOtherProcessName[MAX_PATH];
for (i=0; i < dwGoodProcessCount; i++)
{
HANDLE hOtherProcess = OpenProcess(PROCESS_QUERY_INFORMATION,
FALSE, dwGoodProcessesIDs[i]);
if ( EnumProcessModules( hProcess, hMod, sizeof(hMod), &cbNeeded) )
GetModuleBaseNameW( hProcess, hMod[0], szOtherProcessName,
sizeof(szOtherProcessName) );
CloseHandle(hOtherProcess);
if (szOtherProcessName[0] == 0) return 0xffffffff;
UINT j = 0;
do
{
szOtherProcessName[j] = towlower(szOtherProcessName[j]);
}
while (szOtherProcessName[++j]);
if (wcscmp(szSourceProcessName, szOtherProcessName) == 0)
{
if (dwRetProcessID == -1) dwRetProcessID = dwGoodProcessesIDs[i];
else return 0xffffffff;
}
}
return dwRetProcessID;
}
Once again, in general, this way cannot guarantee process ID determination in all cases. Use it at your own risk where function failure is not critical.
If you are not afraid of native API calls, one more way to solve this problem is to use ZwQueryInformationProcess()
. Indeed, Microsoft provides some information about ZwQueryInformationProcess Function. The word "undocumented" currently means only that Microsoft does not guarantee that the function will be present in future OS releases. As starting from Window XP SP1 there is a documented straightforward solution, and previously released versions obviously cannot be changed, it is not a big problem.
ZwQueryInformationProcess()
is exported by Ntdll.dll, but it has no associated import library. Thus, to use it, run-time dynamic linking is required.
Here is this function prototype:
NTSTATUS WINAPI ZwQueryInformationProcess(
__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass,
__out PVOID ProcessInformation,
__in ULONG ProcessInformationLength,
__out_opt PULONG ReturnLength
);
Here PROCESSINFOCLASS
is an enumeration defined in ntddk.h; for our purposes we will set ProcessInformationClass
to ProcessBasicInformation
value, which is 0. The result will be returned through ProcessInformation
parameter, which must point to the following structure:
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
PebBaseAddress
is a pointer to process environment block structure. As we don't need it, we may treat it as PVOID
.
Following the same style as above, we can wrap all steps in the following function:
#if !defined NTSTATUS
typedef LONG NTSTATUS;
#endif
#if !defined PROCESSINFOCLASS
typedef LONG PROCESSINFOCLASS;
#endif
#if !defined PPEB
typedef struct _PEB *PPEB;
#endif
#if !defined PROCESS_BASIC_INFORMATION
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
#endif;
typedef NTSTATUS (WINAPI * PFN_ZWQUERYINFORMATIONPROCESS)(HANDLE, PROCESSINFOCLASS,
PVOID, ULONG, PULONG);
DWORD GetProcessIDbyProcessHandleZw(HANDLE hProcess)
{
HINSTANCE hNtDll = LoadLibraryW(L"ntdll.dll");
if (hNtDll == NULL) return 0xffffffff;
PFN_ZWQUERYINFORMATIONPROCESS fnProcInfo = PFN_ZWQUERYINFORMATIONPROCESS(
::GetProcAddress(hNtDll, "ZwQueryInformationProcess"));
if (fnProcInfo == NULL)
{
CloseHandle(hNtDll);
return 0xffffffff;
}
PROCESS_BASIC_INFORMATION pbi;
ZeroMemory(&pbi, sizeof(PROCESS_BASIC_INFORMATION));
if (fnProcInfo(hProcess, 0, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL) == 0)
{
CloseHandle(hNtDll);
return pbi.UniqueProcessId;
}
else
{
CloseHandle(hNtDll);
return 0xffffffff;
}
}
Sample Project
Real projects where you may need this stuff are too complex for a sample project, but as you're finishing reading this article, then you have probably already seen them. The sample project I've attached is pretty artificial. You specify a process ID (well, how to make it easier to communicate with this program?) and a handle to that process is obtained. Then the program attempts to do the reverse procedure, informing you about the results and functions used. The two above functions are located in the ProcessIdFromHandleConversion.cpp file.
History
- 7 December, 2007 -- Original version posted
- 9 January, 2008 -- A solution using ZwQueryInformationProcess() added