« Back to home

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:

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.