The hidden side of Seclogon part 2: Abusing leaked handles to dump LSASS memory
by splinter_code - 7 December 2021
Credential dumping is one of the most common techniques leveraged by attackers to compromise an infrastructure. It allows to steal sensitive credential information and enables attackers to further move laterally on the target environment.
On Windows systems there is a SSO (Single Sign-On) mechanism in which the user types the password only once and is automatically logged on every time it is needed as long as the user session on the operating system is alive. The main advantage of it is improving the usability of the system by not requesting credentials multiple times.
The main drawback of this approach is that those credentials must be stored somewhere. If the system has to authenticate the user automatically, the system must hold the user credentials in some form, that's a fact.
In particular, for Windows systems the process in charge for this is lsass.exe (Local Security Authority Subsystem Service).
Lsass is a critical process and contains a pile of treasures from an attacker perspective. For this reason dumping the memory of lsass process is something often performed when attackers carry out their malicious operations.
With that in mind most EDR/AV try to protect the process memory from unauthorized/malicious access. Like every evasion/detection method is always a game of cat-n-mouse.
New detection methods could be created only if attacking methods are known, that's another fact.
Considering that, in this blog post i'm going to release a new undocumented/unknown way to perform a memory dump of LSASS in a stealthy way. Giving detailed internals on how it works, it should ease (making known what is unknown) the development of effective detection.
On the edges of detection
The most simple way of dumping lsass memory usually involves two main operations:
- Opening a process handle to the lsass PID through an OpenProcess call with the access PROCESS_QUERY_INFORMATION and PROCESS_VM_READ;
- Using MiniDumpWriteDump to read all the process address space of lsass and save it into a file on the disk. Note that MiniDumpWriteDump heavily relies on the usage of the NtReadVirtualMemory system call that allows it to read memory of remote processes.
Now, where does the detection of lsass memory dumping usually occur? On both operations!
The first detection spot usually occurs on the usage of OpenProcess/NtOpenProcess. The Windows kernel allows your driver to register a list of callback routines for thread, process, and desktop handle operations. This can be achieved through ObRegisterCallbacks.
Two structs are required to register a new callback: OB_CALLBACK_REGISTRATION and OB_OPERATION_REGISTRATION.
In particular, the OB_OPERATION_REGISTRATION struct allows to specify a combination of parameters to monitor any newly created/duplicate process handle directly from the kernel.
To mention an example, Sysmon event id 10 is based on this mechanism.
The second detection spot usually occurs on the usage of NtReadVirtualMemory, used internally also by ReadProcessMemory.
In this case the implementation may vary.
The most used approach is the Inline Hooking to intercept NtReadVirtualMemory calls that target the lsass process. The problem with this approach is that the monitoring occurs at the same ring level of the process itself, so techniques like direct system calls or unhooking would easily bypass this kind of detection.
A better approach is using the Threat Intelligence ETW to receive notifications directly from the kernel on specific functions invocation. E.g. whenever a NtReadVirtualMemory is called, the kernel function EtwTiLogReadWriteVm will be used to track the usage and send the event to the registered consumers.
Most modern and effective EDR go for this way.
Known dumping methods
It's important to highlight how some of the known (and most stealthy) dumping methods have inspired me in one way or another:
- Evading WinDefender ATP credential-theft: a hit after a hit-and-miss start by @matteomalvica and @b4rtik:
- Create a snapshot of the process in order to perform indirect memory reads by using the snapshot handle. The snapshot handle is then used in the MiniDumpWriteDump call instead of using the target process handle directly.
- Duping AV with handles by @SkelSec:
- Reuses already opened handles to the lsass process thus avoiding a direct OpenProcess call on lsass.
- Dumping LSASS in memory undetected using MirrorDump by @_EthicalChaos_:
- Load an arbitrary LSA plugin that performs a duplication of the lsass process handle from the lsass process into the dumping process. So the dumping process has a ready to use process handle to lsass without invoking OpenProcess.
The above descriptions are just a brief, I strongly recommend you to read the blog posts in order to have more insights.
A sharp eye could catch that every mentioned method above tries to play on the edges of detection to stay under the radar.
I want to mention a specific thing about the technique of reusing already opened lsass handles. While it's a very valid technique, it has the clear disadvantage that on most systems you won't easily find a handle holder that's not lsass itself. You can verify it with a simple handles enumerator tool:
So it means that if you want to get one of those process handles in your process you still need to open a handle to lsass to duplicate it.
Wouldn't it be nice to coerce a Windows System Service (not lsass clearly) to open a handle for you? Let's find out...
The bug: a bad assumption in SecLogon
Now you are wondering: why are you going to talk about the Secondary Logon Service (seclogon) in a blog post about credential dumping?Long story short: I have spent a lot of time reversing the seclogon service while developing my RunasCs tool and I found many interesting weirdnesses in it.
If you notice, this blog post is a 2nd part of a never written part 1. I hope one day I can find enough time to document all the internals involved in RunasCs, I swear it contains some crazy stuff.
Back to the point... The seclogon service is a RPC server that basically exposes 1 function: SeclCreateProcessWithLogonW.
Whenever you use CreateProcessWithTokenW or CreateProcessWithLogonW in your program you will land in the seclogon service.
So it basically impersonates the caller and then tries to open the calling process with the PID psli->dwProcessId. This part of the code is very important because the hCaller process handle is then used for a series of operations to create the newly requested process.
To change the parent PID of the new process, the process attributes are updated in order to match it with the caller (hCaller):
But where does the psli->dwProcessId value come from?
Luckily enough the seclogon service provides the so-called LOGON_NETCREDENTIALS_ONLY feature that allows to get (almost) a copy of the caller process token valid locally on the machine without providing any valid credentials.
When a series of faults stack nicely together
At this point we have a nice PPID spoofing feature offered by the seclogon service, but still far away from having a lsass handle somewhere.
The first thing that came to my mind was this vulnerability discovered by @tiraniddo, and described here --> Exploiting a Leaked Thread Handle.
It's one of my favourite logic bugs I have ever seen, I strongly recommend you to go through that blog post.
This vulnerability allows you to escalate your privileges from a normal user to SYSTEM. What he had found out is that you can trick the seclogon service into leaking a thread handle of a thread running as SYSTEM. In particular, the leakage occurs through the usage of the pseudo handle -2 (GetCurrentThread) provided as the standard stream handle value in the startup information structure and uses CreateProcessWithLogonW to trigger the seclogon service.
While the vulnerability has been fixed (and you cannot specify a pseudo handle anymore), it's still possible to leak handles from other processes if you combine it with the PPID spoofing primitive. You still need to have the rights to open the target process, so we are not crossing any security boundaries. But... to dump lsass memory you need admin privileges anyway so that's enough for our purpose :D
The scenario is a bit articulated, let's proceed step-by-step to understand the whole picture...
Either CreateProcessWithLogonW and CreateProcessWithTokenW allows to specify a LPSTARTUPINFOW struct for the parameters of the process. One of the things you can specify are the Standard Streams for the process that allows to redirect input and output of console processes on a different stream, e.g. a named pipe.
While these handles are inherited normally while performing a normal process creation, this won't happen in the case of the seclogon service. This occurs because the seclogon is not the real parent of the process so handle inheritance won't work by design.
For this reason the seclogon service has to "emulate" the same behavior.
Let's reverse how this is implemented...
When you specify the flag STARTF_USESTDHANDLES in the startup information a new flag is set to true, reversed code from SlrCreateProcessWithLogon below:
Then the new process with the alternate credentials is created suspended:
Shortly after the flag signaling that the standard stream handles have been specified is checked and SlpSetStdHandles is invoked:
So the function SlpSetStdHandles is the one in charge to duplicate the standard stream handles from the caller to the new process. For the bravest: here you can find the whole reversed function --> https://gist.github.com/antonioCoco/706760df95749974b89546fb8d9fa445
In particular, this is the relevant snippet that allows to leak handles:
The magic happens of course in the DuplicateHandle function. The standard streams handles are duplicated from the hCaller process to the newly created process. We can control the hCaller with the PID spoofing trick. We can also control the standard stream value by using the @tiraniddo's discovery. So the two faults stack very nicely together and could evict lsass handles from lsass itself without interacting directly.
What's the plan to leak lsass process handles then?
- Use NtQuerySystemInformation and get all the process handle values that resides in lsass, some of them are for lsass itself;
- Patch the pid value in the current process TEB and specify the lsass PID;
- Prepare the CreateProcessWithLogonW calls:
- Specify the flag LOGON_NETCREDENTIALS_ONLY as the dwLogonFlags parameter;
- Specify the flag STARTF_USESTDHANDLES in lpStartupInfo->dwFlags;
- Specify the process handle values to leak from lsass in lpStartupInfo->hStdInput, lpStartupInfo->hStdOutput and lpStartupInfo->hStdError. So three at a time.
- Iterate the CreateProcessWithLogonW calls until a leaked lsass process handle is found in the new process;
- Enjoy the leaked lsass handle in the new process :D
Demo time!
Note: some breakpoints on the seclogon service has been added on windbg to demonstrate the whole process better
As shown in the above Demo, a dump of lsass is performed through a leaked handle and saved to the disk. In this case I have used the MiniDumpWriteDump function.
One thing we need to consider while using this function is that it tries to open a new handle to lsass and we definitely want to avoid that. The reason for this behavior seems to be inside RtlQueryProcessDebugInformation called internally by MiniDumpWriteDump, it opens a new handle to lsass instead of using the one provided in the call.
One smarter thing to do is to use SetHandleInformation and protect the leaked handle from any closing attempts through the flag HANDLE_FLAG_PROTECT_FROM_CLOSE.
However the MiniDumpWriteDump call is still reading the lsass process memory directly and this is a very noisy operation.
However, while they could be effective in most cases, they are not very effective against moderns EDR. These kind of techniques are flagged as malicious and the EtwTi monitoring would still catch the remote memory reads.
Leveraging process address space cloning for indirect memory reads
Poking around in ntoskrnl.exe i have found a function that caught my attention for its name: MiCloneProcessAddressSpace. It has the following definition:
What it does, briefly, is to create a copy of the specified "ProcessToClone" address space in the "ProcessToInitialize".
This is done by iterating through every PTE of the source process and clone them into the new process. All the pages in the new process are mapped as shared copy-on-write.
Cool! It seems it is what we need. It would be a nice idea to have a new "bridge" process that has the same memory address space of the real process and could allow us to read it indirectly without interacting with it.
But, how do we get there? Let's use a bottom-up approach.
The first cross reference returned by IDA is the function MmInitializeProcessAddressSpace:
This function checks if the "ProcessToClone" parameter is provided and if that's the case it will invoke the function to clone the address space MiCloneProcessAddressSpace. We need to go to the upper caller...
There are 3 invocations of MmInitializeProcessAddressSpace from PspAllocateProcess. One of the invocations provides a "ProcessToClone" parameter different from 0 to MmInitializeProcessAddressSpace. The value provided as the "ProcessToClone" in this specific invocation is the "ParentObject" for the newly created process. Below a snippet of reversed code from PspAllocateProcess:
The else branch code is executed if a NULL SectionObject is provided as a parameter to the function PspAllocateProcess. Then it assigns the parent process section base address to the new process section base address, so what we need. It starts to look interesting...
Going to the upper caller again we land into PspCreateProcess:
One of the parameter provided to the function is the SectionHandle and this one is of our interest because it sets the SectionObject value:
If a SectionHandle is passed as a parameter to the function, the kernel tries to get the SectionObject from the object manager. If not it sets the SectionObject to 0 (what triggers the chain for the process cloning).
Then it checks if the handle to the parent process provided holds the PROCESS_CREATE_PROCESS access and gets the parent process object (ParentObject) from the object manager:
We are almost there :)
Looking at the cross references of PspCreateProcess we can find the NtCreateProcessEx function:
NtCreateProcessEx is a system call exposed by the kernel and can be invoked from a userland process. It turns out that the parameter "SectionHandle" from NtCreateProcessEx is passed directly to PspCreateProcess.
Cool!
All it requires to create a clone of a process is to invoke NtCreateProcessEx from ntdll.dll and provide the process handle (with PROCESS_CREATE_PROCESS access) into the ParentProcess parameter and 0 to the SectionHandle value.
Using a trick by @tirannido described here, it's possible to get a full access process handle to lsass starting from the leaked handle as long as it holds the duplicate access. Explanation below:
DuplicateHandle((HANDLE)leakedHandle, (HANDLE)-1, GetCurrentProcess(), &hLeakedHandleFullAccess, 0, FALSE, DUPLICATE_SAME_ACCESS);
POC
Conclusion
References
- https://www.matteomalvica.com/blog/2019/12/02/win-defender-atp-cred-bypass/
- https://skelsec.medium.com/duping-av-with-handles-537ef985eb03
- https://www.pentestpartners.com/security-blog/dumping-lsass-in-memory-undetected-using-mirrordump/
- https://github.com/antonioCoco/RunasCs
- https://undev.ninja/introduction-to-threat-intelligence-etw/
- https://bugs.chromium.org/p/project-zero/issues/detail?id=687
- https://googleprojectzero.blogspot.com/2016/03/exploiting-leaked-thread-handle.html
- https://twitter.com/diversenok_zero/status/1463844989612568581
- https://billdemirkapi.me/abusing-windows-implementation-of-fork-for-stealthy-memory-operations/
- https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
- https://0x00sec.org/t/defeating-userland-hooks-ft-bitdefender/12496
- https://www.tiraniddo.dev/2017/10/bypassing-sacl-auditing-on-lsass.html