从进程句柄获取进程 ID






4.40/5 (9投票s)
在只有进程句柄可用时,
引言
进程 ID 是一个唯一标识正在运行进程的值。这意味着,与可以被复制的句柄不同,进程 ID 在进程生命周期内保持不变,并且在此期间没有其他进程可以拥有相同的 ID 值。通过 `OpenProcess()` 调用来处理 ID 以获取进程句柄是很常见的。在这里,我们将讨论相反的问题,即只有一个进程的句柄,以及如何找到该句柄所代表的进程 ID。特别是在您将代码注入其他应用程序但不知道句柄来源时,可能会出现这种情况。如果您使用的是 Windows XP SP1 或更高版本,您只需使用您拥有的句柄调用以下函数即可
DWORD GetProcessId(HANDLE Process);
对于较低版本,没有简单统一的解决方案。下面我将只考虑 Windows 2000 及以上版本,因为(已废弃的)Windows 9X 根本不提供合适的工具。此外,这里也不考虑驱动程序方法。
将讨论三种技术
1. 通过创建远程线程获取 ID
如果我们想找到自己的进程 ID,我们会调用 `GetCurrentProcessId()`。直观地说,最好让目标进程在其自己的上下文中调用它,并将结果返回给我们。为了实现这一点,可以使用远程线程创建技术。这是 `CreateRemoteThread()` 的原型
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process to create thread in
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
);
通过将 `lpStartAddress` 设置为 `GetCurrentProcessId()` 的地址,我们使该函数在目标进程的上下文中作为线程例程执行。然后,通过使用返回的线程句柄,可以获取线程的退出代码。一件微妙的事情是 `GetCurrentProcessId()` 没有输入参数,而 `LPTHREAD_START_ROUTINE` 假定有一个。事实上,`CreateRemoteThread()` 只是将执行传递给 `lpStartAddress` 指向的代码,而不执行所有必要的验证。在大多数情况下,这会是一个问题。这里我们把它当作一个优势:由于 `GetCurrentProcessId()` 是在不使用堆栈中的任何内容的情况下执行的,线程退出,然后其堆栈被销毁,不会造成问题。以下是执行必要步骤的函数的完整代码
DWORD WINAPI GetProcessIDbyProcessHandle(HANDLE hProcess)
{
// [in] process handle
// [out] process ID, or 0xffffffff in case of failure
if (hProcess == NULL) return 0xffffffff;
PTHREAD_START_ROUTINE lpStartAddress = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "GetCurrentProcessId");
if (lpStartAddress == NULL) return 0xffffffff;
// We do not know, whether process handle already has required access rights;
// thus we have to duplicate it
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;
}
// Create a remote thread; as its starting address
// we specify GetCurrentProcessId() address,
// which is the same for all processes. Note that GetCurrentProcessId() has no input
// parameters, and we don't care about our thread stack cleanup,
// as it will be destroyed right after this call
DWORD dwThreadID;
HANDLE hRemoteThread = CreateRemoteThread(hProcessAccAdj, NULL,
0, lpStartAddress, 0, 0, &dwThreadID);
CloseHandle(hProcessAccAdj);
if (hRemoteThread == NULL) return 0xffffffff;
// Wait until process ID is obtained
// (replace INFINITE value below to a smaller value to avoid deadlocks);
// then get the thread exit code, which is a value returned by GetCurrentProcessId()
// in the context of the remote process
WaitForSingleObject(hRemoteThread, INFINITE);
DWORD dwExitCode;
if (GetExitCodeThread(hRemoteThread, &dwExitCode) == 0) dwExitCode = 0xffffffff;
CloseHandle(hRemoteThread);
return dwExitCode;
}
不幸的是,如果目标进程的安全上下文不允许我们以所需的访问权限打开(复制)该进程的句柄,这将失败。特别是,对于 `csrss.exe`、`winlogon.exe` 以及其他少数进程等低 ID 进程,将发生这种情况。获取调试权限可能或可能不会解决问题,因为我们假设无法控制获取初始(可用)句柄的方式。例如,在以下调用序列中...
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);
...如果 `dwProcessId` 代表上述进程之一,则最后一个调用将因 `ERROR_ACCESS_DENIED` 而失败,无论是否授予调试权限。
2. 进程枚举是否有帮助?
使用 `psapi.dll` 提供的功能,可以枚举操作系统中当前正在运行的进程(的标识符),然后获取它们的句柄并尝试处理它们。显然,将进程句柄与给定的句柄进行比较是无用的(想想句柄复制)。相反,我们可以比较使用进程句柄获得的进程属性。虽然没有一个属性可以唯一地确定一个进程,但 `GetProcessTimes()` 可以进行相当好的过滤,特别是在单处理器机器上。当没有其他进程具有相同的创建时间时,所获得的结果是有效的。为了改进结果,如果存在多个具有相同创建时间的候选进程,我们可以通过进程名称继续比较。下面是一个执行此类枚举的函数的草图。如果根据可用数据可以唯一确定进程,则返回其 ID。如果存在多个候选进程,函数将失败并返回 `0xffffffff`。
#include <Psapi.h>
DWORD WINAPI TryGetProcessIDbyProcessHandle(HANDLE hProcess)
{
// [in] process handle
// [out] process ID, or 0xffffffff in case of failure
if (hProcess == NULL) return 0xffffffff;
FILETIME ftSourceCreationTime, ftOtherCreationTime;
FILETIME ftExitTime;
FILETIME ftKernelTime;
FILETIME ftUserTime;
// Get requested process creation time
GetProcessTimes(hProcess, &ftSourceCreationTime,
&ftExitTime, &ftKernelTime, &ftUserTime);
// Enumerate existing processes and compare creation times
DWORD cbNeeded;
DWORD dwProcessesIDs[1024];
DWORD dwGoodProcessesIDs[1024];
DWORD dwGoodProcessCount = 0;
if ( EnumProcesses( dwProcessesIDs, sizeof(dwProcessesIDs), &cbNeeded ) )
{
cbNeeded /= sizeof(DWORD); // bytes to ID cound
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];
// Now we can try to compare process names
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;
// failed to get process name
UINT i = 0;
do
{
szSourceProcessName[i] = towlower(szSourceProcessName[i]);
}
while (szSourceProcessName[++i]);
USHORT szOtherProcessName[MAX_PATH];
// work with selected processes only
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;
// failed to get process name
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;
// more than one coincidence
}
}
return dwRetProcessID;
}
再次强调,总的来说,这种方法不能保证在所有情况下都能确定进程 ID。在函数失败不是关键的情况下,请自行承担风险使用它。
3. 使用“未公开”的 `ZwQueryInformationProcess()`
如果您不害怕本机 API 调用,解决这个问题的另一种方法是使用 `ZwQueryInformationProcess()`。事实上,微软提供了一些关于 ZwQueryInformationProcess Function 的信息。“未公开”这个词目前只意味着微软不保证该函数将在未来的操作系统版本中存在。由于从 Window XP SP1 开始有一个公开的直接解决方案,而以前发布的版本显然无法更改,所以这不是一个大问题。
`ZwQueryInformationProcess()` 由 `Ntdll.dll` 导出,但没有关联的导入库。因此,要使用它,需要运行时动态链接。
这是该函数原型
NTSTATUS WINAPI ZwQueryInformationProcess(
__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass,
__out PVOID ProcessInformation,
__in ULONG ProcessInformationLength,
__out_opt PULONG ReturnLength
);
这里 `PROCESSINFOCLASS` 是在 `ntddk.h` 中定义的枚举;出于我们的目的,我们将 `ProcessInformationClass` 设置为 `ProcessBasicInformation` 值,即 0。结果将通过 `ProcessInformation` 参数返回,该参数必须指向以下结构
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
`PebBaseAddress` 是进程环境块结构的指针。由于我们不需要它,我们可以将其视为 `PVOID`。
遵循与上面相同的风格,我们可以将所有步骤封装在以下函数中
#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;
}
}
示例项目
您可能需要这些东西的实际项目对于示例项目来说过于复杂,但当您读完这篇文章时,您可能已经看到了它们。我附加的示例项目相当人为。您指定一个进程 ID(嗯,如何让这个程序更容易通信?),然后获取该进程的句柄。然后程序尝试执行反向操作,告知您结果和使用的函数。上面的两个函数位于 `ProcessIdFromHandleConversion.cpp` 文件中。
历史
- 2007 年 12 月 7 日 -- 原始版本发布
- 2008 年 1 月 9 日 — 添加了使用 ZwQueryInformationProcess() 的解决方案