-
-
[转帖]How can I close a handle in another process?
-
发表于: 2020-3-18 08:38 3351
-
Original link: https://scorpiosoftware.net/2020/03/15/how-can-i-close-a-handle-in-another-process/
Many of you are probably familiar with Process Explorer‘s ability to close a handle in any process. How can this be accomplished programmatically?
The standard CloseHandle
function can close a handle in the current process only, and most of the time that’s a good thing. But what if you need, for whatever reason, to close a handle in another process?
There are two routes than can be taken here. The first one is using a kernel driver. If this is a viable option, then nothing can prevent you from doing the deed. Process Explorer uses that option, since it has a kernel driver (if launched with admin priveleges at least once). In this post, I will focus on user mode options, some of which are applicable to kernel mode as well.
The first issue to consider is how to locate the handle in question, since its value is unknown in advance. There must be some criteria for which you know how to identify the handle once you stumble upon it. The easiest (and probably most common) case is a handle to a named object.
Let take a concrete example, which I believe is now a classic, Windows Media Player. Regardless of what opnions you may have regarding WMP, it still works. One of it quirks, is that it only allows a single instance of itself to run. This is accomplished by the classic technique of creating a named mutex when WMP comes up, and if it turns out the named mutex already exists (presumabley created by an already existing instance of itself), send a message to its other instance and then exits.
The following screenshot shows the handle in question in a running WMP instance.
This provides an opportunity to close that mutex’ handle “behind WMP’s back” and then being able to launch another instance. You can try this by manually closing the handle with Process Explorer and then launch another WMP instance successfully.
If we want to achieve this programmatically, we have to locate the handle first. Unfortunately, the documented Windows API does not provide a way to enumerate handles, not even in the current process. We have to go with the (officially undocumented) Native API if we want to enumerate handles. There two routes we can use:
- Enumerate all handles in the system with NtQuerySystemInformation, search for the handle in the PID of WMP.
- Enumerate all handles in the WMP process only, searching for the handle yet again.
- Inject code into the WMP process to query handles one by one, until found.
Option 3 requires code injection, which can be done by using the CreateRemoteThreadEx
function, but requires a DLL that we inject. This technique is very well-known, so I won’t repeat it here. It has the advantage of not requring some of the native APIs we’ll be using shortly.
Options 1 and 2 look very similar, and for our purposes, they are. Option 1 retrieves too much information, so it’s probably better to go with option 2.
Let’s start at the beginning: we need to locate the WMP process. Here is a function to do that, using the Toolhelp API for process enumeration:
#include <windows.h> #include <TlHelp32.h> #include <stdio.h> DWORD FindMediaPlayer() { HANDLE hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) return 0; PROCESSENTRY32 pe; pe.dwSize = sizeof(pe); // skip the idle process ::Process32First(hSnapshot, &pe); DWORD pid = 0; while (::Process32Next(hSnapshot, &pe)) { if (::_wcsicmp(pe.szExeFile, L"wmplayer.exe") == 0) { // found it! pid = pe.th32ProcessID; break; } } ::CloseHandle(hSnapshot); return pid; } int main() { DWORD pid = FindMediaPlayer(); if (pid == 0) { printf("Failed to locate media player\n"); return 1; } printf("Located media player: PID=%u\n", pid); return 0; }
Now that we have located WMP, let’s get all handles in that process. The first step is opening a handle to the process with PROCESS_QUERY_INFORMATION
and PROCESS_DUP_HANDLE
(we’ll see why that’s needed in a little bit):
HANDLE hProcess = ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE, FALSE, pid); if (!hProcess) { printf("Failed to open WMP process handle (error=%u)\n", ::GetLastError()); return 1; }
If we can’t open a proper handle, then something is terribly wrong. Maybe WMP closed in the meantime?
Now we need to work with the native API to query the handles in the WMP process. We’ll have to bring in some definitions, which you can find in the excellent phnt project on Github (I added extern "C"
declaration because we use a C++ file).
#include <memory> #pragma comment(lib, "ntdll") #define NT_SUCCESS(status) (status >= 0) #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) enum PROCESSINFOCLASS { ProcessHandleInformation = 51 }; typedef struct _PROCESS_HANDLE_TABLE_ENTRY_INFO { HANDLE HandleValue; ULONG_PTR HandleCount; ULONG_PTR PointerCount; ULONG GrantedAccess; ULONG ObjectTypeIndex; ULONG HandleAttributes; ULONG Reserved; } PROCESS_HANDLE_TABLE_ENTRY_INFO, * PPROCESS_HANDLE_TABLE_ENTRY_INFO; // private typedef struct _PROCESS_HANDLE_SNAPSHOT_INFORMATION { ULONG_PTR NumberOfHandles; ULONG_PTR Reserved; PROCESS_HANDLE_TABLE_ENTRY_INFO Handles[1]; } PROCESS_HANDLE_SNAPSHOT_INFORMATION, * PPROCESS_HANDLE_SNAPSHOT_INFORMATION; extern "C" NTSTATUS NTAPI NtQueryInformationProcess( _In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, _Out_writes_bytes_(ProcessInformationLength) PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength);
The #include
is for using unique_ptr<>
as we’ll do soon enough. The #parma links the NTDLL import library so that we don’t get an “unresolved external” when calling NtQueryInformationProcess
. Some people prefer getting the functions address with GetProcAddress
so that linking with the import library is not necessary. I think using GetProcAddress
is important when using a function that may not exist on the system it’s running on, otherwise the process will crash at startup, when the loader (code inside NTDLL.dll) tries to locate a function. It does not care if we check dynamically whether to use the function or not – it will crash. Using GetProcAddress
will just fail and the code can handle it. In our case, NtQueryInformationProcess
existed since the first Windows NT version, so I chose to go with the simplest route.
Our next step is to enumerate the handles with the process information class I plucked from the full list in the phnt project (ntpsapi.h file):
ULONG size = 1 << 10; std::unique_ptr<BYTE[]> buffer; for (;;) { buffer = std::make_unique<BYTE[]>(size); auto status = ::NtQueryInformationProcess(hProcess, ProcessHandleInformation, buffer.get(), size, &size); if (NT_SUCCESS(status)) break; if (status == STATUS_INFO_LENGTH_MISMATCH) { size += 1 << 10; continue; } printf("Error enumerating handles\n"); return 1; }
The Query* style functions in the native API request a buffer and return STATUS_INFO_LENGTH_MISMATCH
if it’s not large enough or not of the correct size. The code allocates a buffer with make_unique
and tries its luck. If the buffer is not large enough, it receives back the required size and then reallocates the buffer before making another call.
Now we need to step through the handles, looking for our mutex. The information returned from each handle does not include the object’s name, which means we have to make yet another native API call, this time to NtQyeryObject
along with some extra required definitions:
typedef enum _OBJECT_INFORMATION_CLASS { ObjectNameInformation = 1 } OBJECT_INFORMATION_CLASS; typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING; typedef struct _OBJECT_NAME_INFORMATION { UNICODE_STRING Name; } OBJECT_NAME_INFORMATION, * POBJECT_NAME_INFORMATION; extern "C" NTSTATUS NTAPI NtQueryObject( _In_opt_ HANDLE Handle, _In_ OBJECT_INFORMATION_CLASS ObjectInformationClass, _Out_writes_bytes_opt_(ObjectInformationLength) PVOID ObjectInformation, _In_ ULONG ObjectInformationLength, _Out_opt_ PULONG ReturnLength);
NtQueryObject
has several information classes, but we only need the name. But what handle do we provide NtQueryObject
? If we were going with option 3 above and inject code into WMP’s process, we could loop with handle values starting from 4 (the first legal handle) and incrementing the loop handle by four.
Here we are in an external process, so handing out the handles provided by NtQueryInformationProcess
does not make sense. What we have to do is duplicate each handle into our own process, and then make the call. First, we set up a loop for all handles and duplicate each one:
auto info = reinterpret_cast<PROCESS_HANDLE_SNAPSHOT_INFORMATION*>(buffer.get()); for (ULONG i = 0; i < info->NumberOfHandles; i++) { HANDLE h = info->Handles[i].HandleValue; HANDLE hTarget; if (!::DuplicateHandle(hProcess, h, ::GetCurrentProcess(), &hTarget, 0, FALSE, DUPLICATE_SAME_ACCESS)) continue; // move to next handle }
We duplicate the handle from WMP’s process (hProcess
) to our own process. This function requires the handle to the process opened with PROCESS_DUP_HANDLE
.
Now for the name: we need to call NtQueryObject
with our duplicated handle and buffer that should be filled with UNICODE_STRING
and whatever characters make up the name.
BYTE nameBuffer[1 << 10]; auto status = ::NtQueryObject(hTarget, ObjectNameInformation, nameBuffer, sizeof(nameBuffer), nullptr); ::CloseHandle(hTarget); if (!NT_SUCCESS(status)) continue;
Once we query for the name, the handle is not needed and can be closed, so we don’t leak handles in our own process. Next, we need to locate the name and compare it with our target name. But what is the target name? We see in Process Explorer how the name looks. It contains the prefix used by any process (except UWP processes): “\Sessions\<session>\BasedNameObjects\<thename>”. We need the session ID and the “real” name to build our target name:
WCHAR targetName[256]; DWORD sessionId; ::ProcessIdToSessionId(pid, &sessionId); ::swprintf_s(targetName, L"\\Sessions\\%u\\BaseNamedObjects\\Microsoft_WMP_70_CheckForOtherInstanceMutex", sessionId); auto len = ::wcslen(targetName);
This code should come before the loop begins, as we only need to build it once.
Not for the real comparison of names:
auto name = reinterpret_cast<UNICODE_STRING*>(nameBuffer); if (name->Buffer && ::_wcsnicmp(name->Buffer, targetName, len) == 0) { // found it! }
The name buffer is cast to a UNICODE_STRING
, which is the standard string type in the native API (and the kernel). It has a Length
member which is in bytes (not characters) and does not have to be NULL
-terminated. This is why the function used is _wcsnicmp
, which can be limited in its search for a match.
Assuming we find our handle, what do we do with it? Fortunately, there is a trick we can use that allows closing a handle in another process: call DuplicateHandle
again, but add the DUPLICATE_CLOSE_SOURCE
to close the source handle. Then close our own copy, and that’s it! The mutex is gone. Let’s do it:
// found it! ::DuplicateHandle(hProcess, h, ::GetCurrentProcess(), &hTarget, 0, FALSE, DUPLICATE_CLOSE_SOURCE); ::CloseHandle(hTarget); printf("Found it! and closed it!\n"); return 0;
This is it. If we get out of the loop, it means we failed to locate the handle with that name. The general technique of duplicating a handle and closing the source is applicable to kernel mode as well. It does require a process handle with PROCESS_DUP_HANDLE
to make it work, which is not always possible to get from user mode. For example, protected and PPL (protected processes light) processes cannot be opened with this access mask, even by administrators. In kernel mode, on the other hand, any process can be opened with full access.
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课