Blog: Red Teaming
Dumping LSASS in memory undetected using MirrorDump
As I am sure some of you are aware from the occasional ramblings and screenshots on twitter, I am a big fan of .NET based offensive tooling. Not because it’s trendy or cool, but because of the development speed and ease of testing and debugging in comparison to C/C++.
A month or so ago I developed a .NET BOF for Cobalt Strike that was able to create a memory dump of LSASS directly in memory without touching disk at all. The solution is based on hooking Windows APIs that are involved as part of the file writing process when the MiniDumpWriteDump API is invoked. What I didn’t like about this solution was the dependency on both x86 and x64 native DLL’s that were reflectively loaded when executed. The native DLL’s handled the hooking component using the brilliant MinHook library and the correct architecture was chosen and loaded at runtime.
I set myself the challenge of porting this solution to a pure managed C# solution that did not involve any native code at all. As part of this challenge, I also wanted to find a more covert way to obtain the LSASS handle than using the OpenProcess API directly from the memory dumping tool. Using OpenProcess directly is a sure-fire way to raise red flags with various EDR solutions that we commonly see today when you target the lsass process.
Say Boooo to OpenProcess
Recently I have also fallen in love with Boo for offensive tooling. Boo is a programming language that implements the Microsoft Dynamic Language Runtime (DLR) that will allow the developer yielding it’s power to generate .NET assemblies both in memory and on disk on the fly. The language is not too dissimilar to python with some additional .NET based semantics.
The idea was to load an LSA SSP/AP plugin, masquerading itself as a genuine authentication provider to the host operating system. Once loaded within the LSA ecosystem, grab a handle to it’s own process (lsass.exe) and duplicate into our dumping tool ready to create the minidump. There are examples of previous work that have used LSA authentication providers to capture credentials. One such example is the Intercepting Logon Credentials via Custom Security Support Provider and Authentication Packages blog post by ired.team. But in our case we are looking to duplicate the lsass handle instead, using a Boo script that is compiled to a .NET assembly on the fly.
As you can see from the Boo code above, the LSA plugin is very simple. It has one function called SpLsaModeInitialize and a few imports from kernel32 that facilitate the duplication of the LSASS handle. The OpenProcess API call that you can see is opening a handle to the dumping process that we will be duplicating the lsass handle into.
You will also notice that the third parameter is not a hard coded PID integer but that of a .NET format string parameter. This is due to the fact that we are passing the Boo script string into .NET’s string.Format function where the script is updated at runtime with the PID of the MirrorDump tool. Once the PID is injected into the Boo script, it is then compiled with the Boo compiler:
One other thing to note is the [DllExport] attribute attached to the SpLsaModeInitialize function. The LSA subsystem expects an LSA plugin to expose this function to be eligible for loading. Believe it or not, .NET DLL’s can also export functions and be called from native code too, exactly like the DllImport functionality but in reverse. Adam Chester done a great post on this back in 2018 called RunDLL32 your .NET (AKA DLL exports from .NET).
But this functionality is not natively supported by the DLR compiler directly. As Adam mentioned in his blog post, there is the DllExport project by Denis Kuzmin that has support for this, but unfortunately because we are generating our LSA DLL on the fly, it was not possible to use this. I needed to find a way to do this with DLR assemblies.
Enter dnlib. dnlib is a library that facilitates reading and modifying .NET modules and assemblies. One of it’s capabilities is marking methods within the assembly as exported. Using dnlib, we search the recently compiled LSA plugin assembly from our boo script for functions that are tagged with the DllExport attribute. Once found, the function is marked as exportable.
The final compiled .NET LSA assembly looks something similar to this in dnSpy
With the LSA plugin out the way, it was now time to tackle hooking the file writing API’s involved when a minidump is written. I spent some time porting the MinHook library to .NET and subsequently releasing the library in tandem with the MirrorDump tool. I won’t go into too much detail on the port as it’s very similar to MinHook itself. MinHook.NET uses a slightly modified version of the SharpDisasm project for disassembling the instructions at the target function for hooking.
I have tried to keep the API as similar as possible to the native version of MinHook, but here is a quick example of hooking MessageBoxA API:
You can find the standalone version of MinHook.Net on GitHub!
Under the watchful eye of a debugger, I had observed that during the MiniDumpWriteDump API call, only three Windows API’s relating to file writing were called.
If we could hook these three API’s and trick MiniDumpWriteDump that all went well on the file writing front, we could capture the write buffers and redirect them to memory only.
Lets start with the easy one, GetFileSize. Each hooked function has a check at the top to determine if the fileHandle being worked on is the one of interest. If not, then a call to the original function is made. If it is our handle in question then we simply return our DumpContext structure and return the tracked virtual file size. We use a magic handle value (0x55555555) to spot the difference between a real file handle and our virtual minidump file handle.
The SetFilePointer hook is tasked with tracking the position of our in memory file pointer. Again, nothing too fancy other than calculating where the new position is, based on the current position and the move method and distance requested.
Next, we have the WriteFile function hook. This function is tasked with expanding our in memory buffer when we detect that a file write has passed the end of our virtual file size and to also copy the content of the pending write to our in memory buffer instead of a real file. Because memory dumps can be large, I added the concept of a memory dump size limit. In this scenario if a memory dump goes beyond the limit, we simulate an ERROR_DISK_FULL error to prevent potential memory exhaustion on machines with limited resources.
Finally, we have the NtOpenProcess hook. I must give a specific shout out to @TheRealWover here. After some discussions relating to the MiniDumpWriteDump he pointed out that even though you use a stolen handle on LSASS, the internals of MiniDumpWriteDump will open an additional handle to the LSASS process. This of course will trigger event ID 10 in SysMon. After some digging around myself, I found the same behaviour. Internally, MiniDumpWriteDump eventually triggers a call to RtlQueryProcessDebugInformation which is where the source of the additional handle comes from.
So in our hook function we simulate a call to NtOpenProcess by duplicating the stolen handle that we already have for lsass. This will mean no real NtOpenProcess is called and no event ID 10 is generated from SysMon where MirrorDump is the source process and lsass is the target process.
Once the memory dump had been taken, the original end goal was to unload the LSA plugin DLL and delete it. I was hoping that the DeleteSecurityPackage API call would sort all this out for me.
But every time I called DeleteSecurityPackage I was faced with error 0x80090302 (SEC_E_UNSUPPORTED_FUNCTION). I spent quite some time implementing further mandatory functions that should be implemented as part of a real LSA plugin. Nothing worked, I still got the same error.
I then came across this blog post by CyberNigma. It turns out Microsoft have not implemented the DeleteSecuritPackage function even though it is documented on MSDN. So unfortunately a reboot is required to remove the LSA DLL. There is potential for injecting shellcode into lsass and unloading it now that we have a handle. But I’ll leave that as an exercise for the reader ;)
The current POC application will take the uncompressed in-memory dump and save it to a zip file. But this could equally be exfilled without touching disk by uploading to a server or sending the data back through your C2 implant.
The tool can be run without any arguments which will use sane defaults for the output filename and the DLL name used for generating the LSA plugin. There is no default limit to the size of the in memory dump, so use with caution if you have not specified the limit (in bytes).
Example below which will create and load an LSA plugin DLL called LegitLSAPlugin.dll, the in memory dump of LSASS will end up in a ZIP file called NotLSASS.zip and we will limit the memory used to 100MB
The code for MirrorDump can be found on GitHub, for ease of compilation it includes a copy of the MinHook.NET library.
The POC is not completely undetected due to inline user mode hooks usually injected by EDR vendors. Some vendors will hook the NtReadProcessMemory function and alert when the lsass process is being read. Unhooking and restoring NtReadProcessMemory or utilising a tool such as SharpBlock will likely evade detection from user mode hooks also.
SysMon Event Id 7 and 11
My good friend Andy (@Zephrfish) ran a preview release of MirrorDump in his lab to see what alerts were raised by SysMon and Splunk.
SysMon event id’s 7 and 10 were the obvious ones to watch for. Tune SysMon Event ID 7 (ImageLoad) for the lsass.exe process. Generally, day to day, the same LSA plugins will be used. If the ImageLoaded property contains a DLL name outside of the usual list, then this is something worth investigating. Look at Event ID 11 (FileCreate) at around the same time, if a DLL is dropped with the same path as the ImageLoaded property from event ID 11, the source process is worth investigating.
Thanks for the help testing Andy!
Additional LSA Protection
Enable the additional LSA protection option. This will prevent LSA plugins that are not signed by Microsoft from loading into the lsass process. See the Configuring Additional LSA Protection article from Microsoft for further details.
References and Special Thanks