Exploring Mimikatz - Part 2 - SSP
In the first part of this series, we started our dive into Mimikatz. The idea was simple, to reveal how Mimikatz works its magic, allowing for custom and purpose built payloads to be developed. If you haven’t had a chance to check it out, take a look here. Continuing on, in this post we will review what has become a nice way of subverting security controls added by Microsoft to prevent dumping of credentials (such as Credential Guard), as well as extracting credentials as they are provided by a victim. This is of course Mimikatz’s support for SSP.
A Security Support Provider (SSP) is a DLL which permits developers to expose a number of callbacks to be invoked during certain authentication and authorisation events. As we saw in the last post, WDigest is provided with credentials to cache using this exact interface.
Mimikatz offers a few different techniques to leverage SSP. First is “Mimilib”, which is a DLL sporting various bits of functionality, one of which is implementing the Security Support Provider interface. Secondly there is “memssp”, which is an interesting way of achieving the same goal, but relies on memory patching rather than loading a DLL.
Let’s begin by exploring the traditional way of loading an SSP, Mimilib.
Note: As mentioned in the previous post, this writeup uses Mimikatz source code heavily as well as the countless hours dedicated to it by its developer(s). Thanks to Mimikatz, Benjamin Delpy and Vincent Le Toux for their awesome work.
Mimilib
Mimilib is a bit of a chameleon, supporting ServerLevelPluginDll
for lateral movement over RPC, DHCP Server Callout, and even acting as an extension in WinDBG. For our purposes however we’re going to look at how this library acts as an SSP, providing a way for attackers to retrieve credentials as they are entered by a victim.
Mimilib works by taking advantage of the fact that a Security Support Provider is called with plaintext credentials via the SSP interface. This means that credentials can be exfiltrated in the clear. The entry point for Mimilib’s SSP functionality is found within kssp.c, specifically kssp_SpLsaModeInitialize
. This function is exported from the DLL as SpLsaModeInitialize
via the mimilib.def
definition file, and is used by lsass to initialise a structure containing several callbacks.
In the case of Mimilib, the callbacks registered are:
SpInitialize
- Used to initialise the SSP and provide a list of function pointers.SpShutDown
- Called upon unloading an SSP, giving the opportunity to free resources.SpGetInfoFn
- Provides information on the SSP, including a version, name and description.SpAcceptCredentials
- Receives plaintext credentials passed by LSA to be cached by the SSP.
Of course if you read the last post, you will have seen that SpAcceptCredentials
is used by WDigest to cache credentials, leading to the weakness we have all been enjoying for years.
With each callback populated, and knowing that SpAcceptCredentials
will be called with a copy of clear-text credentials, all that is left for Mimilib is to store credentials as they are provided, which is exactly what kssp_SpAcceptCredentials
does:
NTSTATUS NTAPI kssp_SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials)
{
FILE *kssp_logfile;
#pragma warning(push)
#pragma warning(disable:4996)
if(kssp_logfile = _wfopen(L"kiwissp.log", L"a"))
#pragma warning(pop)
{
klog(kssp_logfile, L"[%08x:%08x] [%08x] %wZ\\%wZ (%wZ)\t", PrimaryCredentials->LogonId.HighPart, PrimaryCredentials->LogonId.LowPart, LogonType, &PrimaryCredentials->DomainName, &PrimaryCredentials->DownlevelName, AccountName);
klog_password(kssp_logfile, &PrimaryCredentials->Password);
klog(kssp_logfile, L"\n");
fclose(kssp_logfile);
}
return STATUS_SUCCESS;
}
Now, I don’t believe that there is support provided directly by mimikatz.exe to load Mimilib, but we know from Microsoft’s documentation that an SSP is added via the addition of a registry key and a reboot.
After some searching however, I found this tweet:
Technically, you can use AddSecurityPackage & DeleteSecurityPackage to avoid some reboots ;) (but not < W7)
— 🥝 Benjamin Delpy (@gentilkiwi) April 5, 2018
This is of course reference to the API AddSecurityPackage, which is actually used within @mattifestation‘s Install-SSP.ps1 script to load an SSP, meaning that Mimilib can in-fact be added without a reboot. And when loaded, we find that each auth attempt causes credentials to be written into a file of kiwissp.log:
Now, one disadvantage of working with an SSP within a mature environment is the fact that the SSP must be registered within lsass. This gives defenders a number of artifacts to work from when attempting to track down your malicious activity, whether that is the registry keys created to reference the SSP, or simply an unusual DLL sat within the lsass process. We can also see that SSP’s expose both a name and comment which can be enumerated using the function EnumerateSecurityPackages
as follows:
#define SECURITY_WIN32
#include <stdio.h>
#include <Windows.h>
#include <Security.h>
int main(int argc, char **argv) {
ULONG packageCount = 0;
PSecPkgInfoA packages;
if (EnumerateSecurityPackagesA(&packageCount, &packages) == SEC_E_OK) {
for (int i = 0; i < packageCount; i++) {
printf("Name: %s\nComment: %s\n\n", packages[i].Name, packages[i].Comment);
}
}
}
As we can see below, the output shows information regarding each loaded SSP, meaning that Mimilib may stand out a little:
So what can we do to avoid standing out? Well the obvious thing to do would be to just modify the description returned by Mimilib’s SpGetInfo
callback, which is hardcoded to:
NTSTATUS NTAPI kssp_SpGetInfo(PSecPkgInfoW PackageInfo)
{
PackageInfo->fCapabilities = SECPKG_FLAG_ACCEPT_WIN32_NAME | SECPKG_FLAG_CONNECTION;
PackageInfo->wVersion = 1;
PackageInfo->wRPCID = SECPKG_ID_NONE;
PackageInfo->cbMaxToken = 0;
PackageInfo->Name = L"KiwiSSP";
PackageInfo->Comment = L"Kiwi Security Support Provider";
return STATUS_SUCCESS;
}
So we change up the Name
and Comment
fields, and tada:
OK, obviously this still isn’t great (even with our l33t name and comment fields). And remember that without stripping out and recompiling, Mimilib contains a bunch of functionality other than just acting as an SSP.
So how do we work around this? Well, thankfully Mimikatz also supports misc::memssp
, which offers a nice alternative.
MemSSP
MemSSP goes back to the process of fiddling with lsass memory, this time by identifying and patching functions to redirect execution.
Let’s take a look at the function where this all starts, kuhl_m_misc_memssp. Here we see that the lsass process is opened, and a search begins for a DLL of msv1_0.dll
, which is an authentication package supporting interactive authentication:
NTSTATUS kuhl_m_misc_memssp(int argc, wchar_t * argv[])
{
...
if(kull_m_process_getProcessIdForName(L"lsass.exe", &processId))
{
if(hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, FALSE, processId))
{
if(kull_m_memory_open(KULL_M_MEMORY_TYPE_PROCESS, hProcess, &aLsass.hMemory))
{ if(kull_m_process_getVeryBasicModuleInformationsForName(aLsass.hMemory, L"msv1_0.dll", &iMSV))
{
...
Next up is a search for a pattern in memory, again, similar to what we saw with WDigest:
...
sSearch.kull_m_memoryRange.kull_m_memoryAdress = iMSV.DllBase;
sSearch.kull_m_memoryRange.size = iMSV.SizeOfImage;
if(pGeneric = kull_m_patch_getGenericFromBuild(MSV1_0AcceptReferences, ARRAYSIZE(MSV1_0AcceptReferences), MIMIKATZ_NT_BUILD_NUMBER))
{
aLocal.address = pGeneric->Search.Pattern;
if(kull_m_memory_search(&aLocal, pGeneric->Search.Length, &sSearch, TRUE))
{
...
If we pause our review of the code and head into Ghidra, we can search for the pattern being used, which lands us here:
Here we reveal what is actually happening… memssp is being used to hook the msv1_0.dll``SpAcceptCredentials
function to recover credentials. Let’s jump into our debugger and see how the hook looks once added.
First we confirm that SpAcceptCredentials
contains a hook:
Next, as we step through execution, we find that we are trampolined into a stub responsible for creating a log file by building a filename on the stack and passing this to fopen
:
)
Once open, the credentials passed to SpAcceptCredentials
are written into this file:
Finally execution is directed back to msv1_0.dll
:
If you would like to see the code for this hook, the actual source can be found within the function misc_msv1_0_SpAcceptCredentials of kuhl_m_misc.c
.
So, what are our risks of using this technique? Well, we can see that the above hook is copied into lsass via kull_m_memory_copy
, which actually uses WriteProcessMemory
. Depending on the environment, a WriteProcessMemory
call into another process may be detected or flagged as suspicious, more-so when being targeted towards lsass.
Now, one of the benefits of exploring Mimikatz techniques is to allow us to change up the profile of interacting with lsass, making things a bit more difficult for BlueTeam to point to their detection wizardry and say “ah, I’ve seen that chain of events before, that’s Mimikatz!!”. So let’s see what we can do to mix things up a bit.
Recreating memssp without WriteProcessMemory
We know that after reviewing the provided techniques above, there are advantages and disadvantages to each.
The first method (Mimilib) relies on registering an SSP can be revealed by returning a list of registered providers via EnumerateSecurityPackages
. Also, if the Mimilib library isn’t modified, there is a bunch of additional functionality bundled with the DLL. Further, when loaded with AddSecurityProvider
, registry values will be modified to persist the SSP between reboots. That being said, one big advantage to this technique is that it does not require a potentially risky WriteProcessMemory
API call to achieve its goal.
The second method (memssp) relies heavily on monitored API calls such as WriteProcessMemory
, which is used to load a hook into lsass. A big advantage to this technique however, is that it does not appear within the list of registered SSP’s or as a loaded DLL.
So, what can we do to change things up a bit? Well we can potentially combine these two methods to load our code using AddSecurityProvider
while avoiding appearing as a registered SSP. And how about we find a way to avoid calling the AddSecurityProvider
API directly, which should help to work around any pesky AV or EDR which decides to hook that function.
Let’s start with taking a look at how AddSecurityPackage
works to register a SSP, which means we will need to do some reversing. We will start with the DLL exposing this API, Secur32.dll
.
Opening this in Ghidra, we quickly see that this is actually just a wrapper around a call to sspcli.dll
:
Disassembling AddSecurityPackage
within sspcli.dll
, specifically the outgoing API calls used by this function, we see references to NdrClientCall3
, meaning that this function is leveraging RPC. This makes sense as somehow this call needs to signal to lsass that a new SSP should be loaded:
As we follow the call to NdrClientCall3
, we find the following parameters passed:
This gives us a nProcNum
parameter value of 3
, and if we dig into the sspirpc_ProxyInfo
structure, we reveal the RPC interface UUID as 4f32adc8-6052-4a04-8701-293ccf2096f0
:
Now we have enough information to drop into RpcView and reveal the RPC call as SspirCallRpc
, exposed via sspisrv.dll
:
To use this call, we will need to know the parameters passed, which of course can be recovered from RpcView as:
long Proc3_SspirCallRpc(
[in][context_handle] void* arg_0,
[in]long arg_1,
[in][size_is(arg_1)]/*[range(0,0)]*/ char* arg_2,
[out]long* arg_3,
[out][ref][size_is(, *arg_3)]/*[range(0,0)]*/ char** arg_4,
[out]struct Struct_144_t* arg_5);
Before we can implement this call however, we will need to know the value to pass as parameter arg_2
(arg_1
is marked as the size of arg_2
, and arg_3
, arg_4
and arg_5
are all marked as “out”). The easiest way that I’ve found to do this is to fire up a debugger and add a breakpoint just before AddSecurityPackage
makes its NdrClientCall3
call:
Once we have paused execution, we can then dump the values passed within each parameter. Let’s grab the size of the buffer being passed within that arg_1
parameter using dq rsp+0x20 L1
So we know that in this case, the buffer being passed is 0xEC
bytes long. Now we can dump arg_2
with:
After a bit of digging, I was able to correlate most of these values. Let’s reformat the outputted request as QWORD’s and mark things up so we can see what we are dealing with:
Now that we have mapped out most of the data being passed, we can try and issue an RPC call without having to invoke the AddSecurityPackage
API call directly. The code that I crafted to do this is available via Gist here.
With our ability to load a package without directly calling AddSecurityPackage
, let’s see if we can mix things up a bit further.
Let’s throw sspisrv.dll
into Ghidra and see how the RPC call is handled on the server side. The immediate thing that we come across upon disassembling SspirCallRpc
is that execution is passed via gLsapSspiExtension
:
This is actually a pointer to an array of functions, populated via lsasrv.dll
and pointing to LsapSspiExtensionFunctions
:
We are interested in SspiExCallRpc
which closely resembles what we found in RPCView. This function validates parameters and passes execution onto LpcHandler
:
LpcHandler
is responsible for checking the provided parameters further before ultimately passing execution onto DispatchApi
:
Again, another array of function pointers is used to dispatch the call, pointed to by LpcDispatchTable
:
Now this is an array that should interest us, as we are likely looking for s_AddPackage
based on its name, and the index also matches the 0xb
“Function ID” index we found within the request.
Heading further down the rabbit hole, we land at WLsaAddPackage
which first checks that we are privileged enough to call the RPC method by impersonating the connecting client and then attempting to open a registry key of HKLM\System\CurrentControlSet\Control\Lsa
with Read/Write permission:
If this succeeds (note this as a potentially novel backdoor for privesc ;), then execution moves onto SpmpLoadDll
, which is used to load our provided SSP into lsass via LoadLibraryExW
:
If the SSP is loaded successfully, the DLL is then added to the registry for autoloading:
So we likely want to skip that last bit, as we won’t be using this for persistence and it would be nice to avoid touching the registry where we can help it. We also ideally want to avoid having our DLL listed as present within lsass by something like ProcessExplorer if suspicion is raised. So what we can do is pass our DLL using the RPC call, and force our SSP to fail on load by returning FALSE
from its DllMain
. This will result in the registry modification being skipped, and will also mean that our DLL is unloaded from the process.
Using Mimikatz memssp as our template, I’ve crafted a DLL to be loaded via our RPC call, which will patch SpAddCredentials
with the same hook as used by Mimikatz. This is available via Gist here.
Let’s see the loading of our DLL using our AddSecurityPackage
raw RPC call:
You are also not restricted to loading DLL’s from the local system with this, as UNC paths work fine if passed via the RPC call (although you should ensure that the EDR you are up against doesn’t flag this as suspicious).
Of course you are also not limited to loading this DLL with AddSecurityPackage
. As we have crafted a standalone DLL to perform the memssp patching, let’s take our SAMR RPC script from the previous blog post and have it load our DLL via LoadLibrary
, writing back logon attempts as they occur via a SMB share:
Of course there are a number of ways to improve the effectiveness of these examples, but as with Part 1, I hope that this post has provided you with an idea of how to go about crafting your own SSP to take with you on engagements. While this post only goes into a few possible way to mix things up when loading an SSP into lsass, by understanding just how Mimikatz is able to provide this functionality, you hopefully have the ability to tailor your payload to an environment when attempting to bypass AV or EDR, or simply to test BlueTeam’s detection capability outside of flagging Mimilib and memssp.