In this article I’ll show step-by-step how to recover credentials even when paloalto’s Cortex XDR is “actively protecting” LSASS. If you want to play along, the dumps can be downloaded from here
PROLOGUE
Today I got a DM on twitter from a user asking me if I can take a look at some dumps, LSASS dumps specifically. According to the user, the new version of Palo Alto’s Cortex XDR solution “defeats” pypykatz’s ability to parse LSASS dumps to get the stored credentials.
They specifically have a module called “anti-mimikatz” according to the user which triggers this so-called protection. To be honest this is not the first time an antivirus/EDR/… company comes with a claim they “defeat” mimikatz, and not the first which turned out to sell snakeoil and fooling their customers.
Let’s solve this issue with the scientific method!
OBSERVATION
The kind twitter user shared two LSASS minidump files from the same computer named lsass_protected.dmp and lsass_clear.dmp. Let’s see if we can parse them!
So we see that the protected version is missing the credentials stored in MSV. “But SkelSec! What does this mean?” — you ask.
Without going into much details and as vaguely as possible (because this article is not about how pypy/mimi katz works) MSV stores the main logon session information of all users currently logged in including the NT/LM/SHA1 hashes for all users. There are other modules which hold user credentials (they were not present in the test dumps) but without MSV one would not be able to match the credentials to the logon sessions. In reality this would not be a problem, as the credential entries in other modules store the username and domain as well, so you could still use it but you’d not be able to tell which logon session used them.
And this brings us to a problem. The way mimikatz is implemented mandates that MSV need to be present otherwise it just throws an error.
What to do now? Let’s dig a little deeper.
Probably a few ppl know about the magical -v
switch in pypykatz despite being clearly mentioned in the extensive help menu. Let’s unveil the wonders it holds.
Well, this gave a bit more info but everything looks good so far yet there is no MSV cred in the result. We need to go deeper with -vv
Okay, so we got an error that reads
[Msv] [decryptor] List entry -KIWI_MSV1_0_LIST_63-: Logging failed for position 0x0
this means some pointer took us to 0x0 while trying to parse MSV, let’s check the code! (it’s easier if you wrote this app, but bare with me here)
The following line throws the aforementioned exception because it tries to resolve the pointer moving the reader to 0x0 which is definitely not a place where structures live. Only groovy things stored on that address that you def don’t want to have in your LSASS.
Summary: The initial pointer which would show us where the linked list holding all user sessions has been destroyed.
HYPOTHESIS
At this point I predict that the protection mechanism doesn’t do anything else besides destroying the initial pointer to the linked list. This means that the actual linked list which stores the sessions is still in memory, but we need to find some other way of finding it.
I’m basing my hypothesis on multiple historical examples of the half-assed methods implemented by the AV industry for which I’m too lazy to provide references.
PREDICTION
If my hypothesis is true then we’d be able to recover all user sessions stored in MSV by searching for specific well-known patterns or values stored in the entries of the linked-list and we could just calculate the offset to the Flink or Blink parameters which will allow us to traverse the entire list thus parsing all credentials.
EXPERIMENT
First, let’s look the definition of an entry in the KIWI_MSV1_0_LIST_63
Lovely! Good thing that Benjamin Delpy already had implemented all the structures otherwise it would have taken ages to develop pypykatz. But let’s get down to business.
Even with the protection mechanism enabled, pypykatz can parse individual credentials which include the LUID of the session. This is an 8 bytes long structure which has a weird definition, but whatever. I choose this one for undefined reasons.
The LUID is stored at offset 0x70 from the top of the structure. This means that if we find the pattern which corresponds to the LocallyUniqueIdentifier parameter of our struct, we need to subtract 0x70 for that position to get to the Flink parameter, which will allow us to traverse the whole linked list with the very same methods which are already implemented in pypykatz.
Let’s see the implementation!
The luid_int and offset needs to be set manually.
luid_int is the luid of any user which has a stored session in the MSV, it can be obtained via parsing the protected dump and checking non-MSV entries in the output.
offset needs to be set manually, because that value depends on the Buildversion of the Windows system you obtained the LSASS dump from. The current offset only works for KIWI_MSV1_0_LIST_63 struct with LUID-based searching.
The function searches the entire memory of the LSASS process to search for the byte pattern of the luid. When it find one, it moves the reader to the start of the list entry and parses the list. There is a checking mechanism to test whether the parser LUID matches the LUID we searched for. This filters out some obviously wrong reads but still lets a lot of garbage to be parsed. No problem in this case, because only the correctly parsed structures will populate the result. The only drawback is that it will take a longer time. (on my system it was 2 seconds)
Let’s see that result:
Yes, this seems to be working, crosschecking it with the clear version we see that the hashes are matching for the user “user”
See, it wasn’t that hard (she et al.)
CONCLUSION
We defeated the “anti mimikatz” “protection” from paloalto’s Cortex XDR.
The whole process of finding/solving the problem took way less time than writing this article.