Before you start reading this post I'd like to point out that this is not a practical technique, no sane person would manually hunt for DPAPI blobs and decryption keys during an assessment - in fact, this is not a "technique" at all, the post is meant to showcase how LSASS handles DPAPI keys under the hood.
With that out of the way, carry on ʕ •ᴥ•ʔ
When it comes to DPAPI master keys, we often think of the %APPDATA%\Microsoft\Protect\{SID} folder for user keys or the %WINDIR%\System32\Microsoft\Protect\S-1-5-18 folder for system keys
but the keys are also cached as encrypted blobs in the lsass process.
These keys can be opened into a hex editor like HxD and we can see that the GUID of the key is placed at the very top
To examine the rest of the parameters we could find how data is organized and extract the rest of the information; during my research, I found this post that conveniently lists all the attributes contained inside a DPAPI master key
dwLocalEncKeySiz: current slot length
dwVersion: data structure version
pSalt: salt
dwPBKDF2IterationCount: iterations in the PBKDF2 encryption key generation function
HMACAlgId: hashing algorithm identifier
CryptAlgId: encryption algorithm used
pKey: encrypted Local Encryption Key, used for decrypting Local Backup Key in Windows 2000
We can also use the tool from the post to view these attributes
After checking out the demo version of the tool I opened x64dbg and attached a debugger to the lsass.exe process to see what DLLs are loaded into it and their symbols and found what seemed to be the library responsible for handling the cached master keys: dpapisrv.dll and its MasterKeyCacheList function.
Attaching a debugger to the LSASS process will probably cause the system to reboot
So we can open the DLL in IDA64 and take a closer look: while I didn't find the MasterKeyCacheList function in the DLLs functions, I found references to it in other functions like FindMasterKeyEntry
The g_MasterKeyCacheList only gets referenced in the following functions
and since I'm focusing on extracting already existing keys, I focused on the FindMasterKeyEntry function and tried to reverse its functionality to see if it does anything interesting: I'm not a master at RE so take this with a grain of salt, also I think IDA might have messed up some of the logic but this is enough to get a general idea of what the function does
HLOCAL*__fastcallFindMasterKeyEntry(struct_LIST_ENTRY*cacheList,constunsigned__int16*keyIndentifier,struct_LUID*userIdentifier,struct_GUID*masterKeyGuid){ HLOCAL *currentEntry; HLOCAL *foundEntry; __int64 guidDifference;constunsigned __int16 *currentKeyId;int currentKeyIdChar;int comparisonResult; // initialize the head of the master key cache list currentEntry = (HLOCAL *)g_MasterKeyCacheList; // set found entry pointer to nullptr foundEntry =0i64; // loop through the cache list while ( currentEntry !=&g_MasterKeyCacheList ) { // check if a GUID is providedif ( masterKeyGuid ) { // compare the GUIDs guidDifference =*(_QWORD *)&masterKeyGuid->Data1 - (_QWORD)currentEntry[3];if ( *(HLOCAL *)&masterKeyGuid->Data1 ==currentEntry[3] ) guidDifference =*(_QWORD *)masterKeyGuid->Data4 - (_QWORD)currentEntry[4]; // if the difference between the GUIDs is not 0 (the GUIDs are not the same) // continue to the next entryif ( guidDifference )goto NEXT_CACHE_ENTRY; } // check if user and key identifiers are providedif ( !userIdentifier ) {if ( !keyIndentifier )goto FOUND_CACHE_ENTRY;// compare key identifiersCOMPARE_KEY_IDENTIFIERS: currentKeyId = keyIndentifier;do { currentKeyIdChar =*(constunsigned __int16 *)((char*)currentKeyId+ (_BYTE *)currentEntry[15]- (_BYTE *)keyIndentifier); comparisonResult =*currentKeyId - currentKeyIdChar;if ( comparisonResult )break;++currentKeyId; }while ( currentKeyIdChar );if ( !comparisonResult ) {FOUND_CACHE_ENTRY: // update the last access time attribute // and return the found entry // (this is only called if a matching entry is found) foundEntry = currentEntry;GetSystemTimeAsFileTime((LPFILETIME)currentEntry +5);return foundEntry; }goto NEXT_CACHE_ENTRY; } // check if the user identifier matches the current entryif ( *((_DWORD *)currentEntry +5) ==userIdentifier->HighPart&&*((_DWORD *)currentEntry +4) ==userIdentifier->LowPart ) {goto FOUND_CACHE_ENTRY; }if ( keyIndentifier )goto COMPARE_KEY_IDENTIFIERS;NEXT_CACHE_ENTRY: // move to the next entry currentEntry = (HLOCAL *)*currentEntry; }return foundEntry;}
In summary, the function searches a list of cached entries for a specific Data Protection API (DPAPI) key. It can use different criteria to find the key:
Master Key GUID: searches for an entry with a matching GUID (unique identifier)
User Identifier: searches for an entry associated with a specific user account
Key Identifier: searches for an entry with a matching key identifier string
Based on the reversed code, one or more of these three attributes might not be present.
If we now switch back to debugging the LSASS process we can go to the address of the FindMasterKeyEntry function and see the values in memory of g_MasterKeyCacheList; as we can see from the image above, the cache list starts with the System Keys as the first 16 bytes are the name of the fist System Key in Little Endian format
With a quick search on the Mimikatz Github repo, we can find this header file which contains the complete structure of the cache entry
and we're able to find the 4 bytes that represent the length of the key; the value is 40 00 00 00 so the encrypted value of the key will be 40 bytes long and it's represented by the section highlighted in light gray.
Now it's time to find out how the key is encrypted and decrypt it: since Windows Vista, the entries for the Master Key cache are encrypted with AES-256 in CFB mode so we should be able to retrieve the IV and key from somewhere in memory.
To find this information I repeated the same steps as before: loaded LSASS in a debugger, looked at the symbols and tried to find functions related to the key's encryption.
Doing so I found the lsasrv.dll library which contained symbols like InitializationVector, aesKey and LspAES256DecryptData so I opened it in IDA.
When it comes to the AES key used for encryption and decryption we can simply look at the hAesKey@@3PEAXEA symbol and look at where the value is referenced to find the original hAESKey value in the LsaInitializeProtectedMemory function
We can also reverse said function to understand better what it's doing and how the memory is initialized
__int64LsaInitializeProtectedMemory(){ NTSTATUS status; UCHAR *allocatedMemory3DES; UCHAR *allocatedMemoryAES; UCHAR *v3; DWORD lastError; ULONG resultSize; UCHAR outputLength3DES[4]; UCHAR outputLengthAES[4]; UCHAR randomBuffer[16]; __int64 temp; // initialize all the needed buffers*(_DWORD *)outputLength3DES =0;*(_DWORD *)outputLengthAES =0; resultSize =0; temp =0i64; // open the 3DES crypto provider*(_OWORD *)randomBuffer =0i64; status =BCryptOpenAlgorithmProvider(&h3DesProvider,L"3DES",0i64,0);if ( status <0 )goto CLEANUP_FUNCTION; // open the AES crypto provider status =BCryptOpenAlgorithmProvider(&hAesProvider,L"AES",0i64,0);if ( status <0 )goto CLEANUP_FUNCTION; // set chaining mode to CBC for 3DES status =BCryptSetProperty(h3DesProvider,L"ChainingMode", (PUCHAR)L"ChainingModeCBC",0x20u,0);if ( status <0 )goto CLEANUP_FUNCTION; // set chaining mode to CFB for AES status =BCryptSetProperty(hAesProvider,L"ChainingMode", (PUCHAR)L"ChainingModeCFB",0x20u,0);if ( status <0 )goto CLEANUP_FUNCTION; resultSize =4; // get the object length for 3DES status =BCryptGetProperty(h3DesProvider,L"ObjectLength", outputLength3DES,4u,&resultSize,0);if ( status <0 )goto CLEANUP_FUNCTION;if ( resultSize ==4 ) { resultSize =4; // get the object length for AES status =BCryptGetProperty(hAesProvider,L"ObjectLength", outputLengthAES,4u,&resultSize,0);if ( status <0 ) {CLEANUP_FUNCTION:LsaCleanupProtectedMemory();return (unsignedint)status; }if ( resultSize ==4 ) { // calculate the total memory size required // for both 3DES and AESLODWORD(CredLockedMemorySize) =*(_DWORD *)outputLength3DES +*(_DWORD *)outputLengthAES; allocatedMemory3DES = (UCHAR *)VirtualAlloc(0i64, (unsignedint)(*(_DWORD *)outputLength3DES +*(_DWORD *)outputLengthAES),0x1000u,4u); // allocate said memory CredLockedMemory = allocatedMemory3DES;if ( allocatedMemory3DES &&VirtualLock(allocatedMemory3DES, (unsignedint)CredLockedMemorySize) ) { allocatedMemoryAES = CredLockedMemory; v3 =&CredLockedMemory[*(unsignedint*)outputLength3DES]; // generate random bytes for AES key status =BCryptGenRandom(0i64, randomBuffer,0x18u,2u);if ( status <0 )goto CLEANUP_FUNCTION; // generate AES key status =BCryptGenerateSymmetricKey( h3DesProvider,&h3DesKey, allocatedMemoryAES,*(ULONG *)outputLength3DES, randomBuffer,0x18u,0);if ( status <0 )goto CLEANUP_FUNCTION; status =BCryptGenRandom(0i64, randomBuffer,0x10u,2u);if ( status <0 )goto CLEANUP_FUNCTION; // generate a random IV status =BCryptGenerateSymmetricKey( hAesProvider,&hAesKey, v3,*(ULONG *)outputLengthAES, randomBuffer,0x10u,0);if ( status <0 )goto CLEANUP_FUNCTION; status =BCryptGenRandom(0i64,&InitializationVector,0x10u,2u);if ( status <0 )goto CLEANUP_FUNCTION; status =0; }else { lastError =GetLastError(); status =I_RpcMapWin32Status(lastError); } } }if ( status <0 )goto CLEANUP_FUNCTION;return (unsignedint)status;}
Now we know where the AES key is stored and how to retrieve it but we'll have to find where the IV is stored.
I tried looking at Mimikatz's source code again to see if I could quickly see where the IV is extracted from but to no avail (I probably missed it).
Opening the file I noticed there is no official PDB file for it
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\symchk.exe" /v C:\Windows\System32\lsasrv.dll
...
SYMCHK: lsasrv.dll FAILED - lsasrv.pdb mismatched or not found
...
so I had to read it from here: I just downloaded the raw HTML contents (just skip the header), saved it to the desktop as lsasrv.pdb and IDA found the symbols as soon as I opened it up.
Next up I looked for some of the symbols I mentioned starting from InitializationVector as it seemed a pretty good place to start looking; that symbol is only referenced by the LsaEncryptMemory and LsaInitializeProtectedMemory functions: this is the reversed code from LsaEncryptMemory
void__fastcallLsaEncryptMemory(PUCHAR pbOutput,ULONG cbInput,int operation){ // handle to the key used for encryption and decryption BCRYPT_KEY_HANDLE keyHandle; // size of the IV ULONG ivSize; // size of the encryption result ULONG resultSize; // buffer for the IV (16 bytes) UCHAR ivBuffer[16]; if ( pbOutput ) { // set the value of the key handle to the // default 3DES key handle (???) keyHandle = h3DesKey; resultSize =0; // default IV size for 3DES ivSize =8; if ( cbInput ) { // copy the initialization vector // to the dedicated buffer*(_OWORD *)ivBuffer =*(_OWORD *)&InitializationVector; // check if the input size if a multiple of 8 // if it is, use AES instead of 3DES if ( (cbInput &7) !=0 ) { // set the value of the key handle // to the AES key keyHandle = hAesKey; // default IV size for AES ivSize =16; }if ( operation ) { // if operation == 1 : perform encryption // else : perform decryptionif ( operation ==1 ) BCryptEncrypt(keyHandle, pbOutput, cbInput,0i64, ivBuffer, ivSize, pbOutput, cbInput,&resultSize,0); }else {BCryptDecrypt(keyHandle, pbOutput, cbInput,0i64, ivBuffer, ivSize, pbOutput, cbInput,&resultSize,0); } } }}
This is a really valuable snippet of code: not only it shows how LSASS decides whether to use 3DES or AES, but it also gives us a direct reference to the InitializationVector that we can now read from memory by using a debugger (light-gray highlighted text)
The same process can be repeated for the 3DES key which is referenced in the LsaEncryptMemory.
Now we have everything we need to decrypt the DPAPI master keys!
It's possible to write a console application that gets a handle to the LSASS process, enumerates the base addresses of the lsasrv.dll and dpapisrv.dll libraries and extracts the needed values from memory to decrypt the key, but in this case I went with something simpler and wrote the following script: its functionality is pretty basic as it just uses the Python Crypto module to AES-CFB decrypt the encrypted key based on the IV and AES key values supplied by the user.
from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadimport binasciidefdecryptMasterKey(encrypted_master_key,aes_key,iv): cipher = AES.new(aes_key, AES.MODE_CFB, iv=iv) decrypted_master_key = cipher.decrypt(encrypted_master_key)return decrypted_master_keyif__name__=="__main__":# replace these with the actual encrypted master key,# AES key, and IV found in memory encrypted_master_key_hex ="<MASTER_KEY_HEX>" aes_key_hex ="<AES_KEY_HEX>" iv_hex ="<IV_HEX>" encrypted_master_key = binascii.unhexlify(encrypted_master_key_hex) aes_key = binascii.unhexlify(aes_key_hex) iv = binascii.unhexlify(iv_hex) decrypted_master_key =decryptMasterKey(encrypted_master_key, aes_key, iv)print("[~] Master Key:", binascii.hexlify(decrypted_master_key).decode())
To test this script, I decrypted the first entry in the master key cache list with GUID 5b31d113-c5ac-441e-bc2d-391de8323a5f (the same one I documented above): this is the output of the Python script