From 3253c7e6383c515b64956b2a89d05f093d9fd9c5 Mon Sep 17 00:00:00 2001 From: Skelsec Date: Sun, 30 Apr 2023 00:11:39 +0200 Subject: [PATCH 1/3] adding another dec template for win11 --- pypykatz/_version.py | 2 +- pypykatz/alsadecryptor/lsa_template_nt6.py | 15 ++++++++++++++- pypykatz/lsadecryptor/lsa_template_nt6.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/pypykatz/_version.py b/pypykatz/_version.py index 2fda288..8338551 100644 --- a/pypykatz/_version.py +++ b/pypykatz/_version.py @@ -1,5 +1,5 @@ -__version__ = "0.6.6" +__version__ = "0.6.7" __banner__ = \ """ # pypyKatz %s diff --git a/pypykatz/alsadecryptor/lsa_template_nt6.py b/pypykatz/alsadecryptor/lsa_template_nt6.py index 2cccbca..e793573 100644 --- a/pypykatz/alsadecryptor/lsa_template_nt6.py +++ b/pypykatz/alsadecryptor/lsa_template_nt6.py @@ -408,7 +408,6 @@ def __init__(self): self.key_pattern = LSADecyptorKeyPattern() self.key_pattern.signature = b'\x83\x64\x24\x30\x00\x48\x8d\x45\xe0\x44\x8b\x4d\xd8\x48\x8d\x15' self.key_pattern.IV_length = 16 - #self.key_pattern.offset_to_IV_ptr = 71 self.key_pattern.offset_to_IV_ptr = 58 self.key_pattern.offset_to_DES_key_ptr = -89 self.key_pattern.offset_to_AES_key_ptr = 16 @@ -416,6 +415,19 @@ def __init__(self): self.key_struct = KIWI_BCRYPT_KEY81 self.key_handle_struct = KIWI_BCRYPT_HANDLE_KEY +class LSA_x64_9(LsaTemplate_NT6): + def __init__(self): + LsaTemplate_NT6.__init__(self) + self.key_pattern = LSADecyptorKeyPattern() + self.key_pattern.signature = b'\x83\x64\x24\x30\x00\x48\x8d\x45\xe0\x44\x8b\x4d\xd8\x48\x8d\x15' + self.key_pattern.IV_length = 16 + self.key_pattern.offset_to_IV_ptr = 71 + self.key_pattern.offset_to_DES_key_ptr = -89 + self.key_pattern.offset_to_AES_key_ptr = 16 + + self.key_struct = KIWI_BCRYPT_KEY81 + self.key_handle_struct = KIWI_BCRYPT_HANDLE_KEY + class LSA_x86_1(LsaTemplate_NT6): def __init__(self): LsaTemplate_NT6.__init__(self) @@ -520,6 +532,7 @@ def __init__(self): '6' : LSA_x64_6(), '7' : LSA_x64_7(), '8' : LSA_x64_8(), + '9' : LSA_x64_9(), }, 'x86': { '1' : LSA_x86_1(), diff --git a/pypykatz/lsadecryptor/lsa_template_nt6.py b/pypykatz/lsadecryptor/lsa_template_nt6.py index 8df0e38..2260687 100644 --- a/pypykatz/lsadecryptor/lsa_template_nt6.py +++ b/pypykatz/lsadecryptor/lsa_template_nt6.py @@ -320,7 +320,6 @@ def __init__(self): self.key_pattern = LSADecyptorKeyPattern() self.key_pattern.signature = b'\x83\x64\x24\x30\x00\x48\x8d\x45\xe0\x44\x8b\x4d\xd8\x48\x8d\x15' self.key_pattern.IV_length = 16 - #self.key_pattern.offset_to_IV_ptr = 71 self.key_pattern.offset_to_IV_ptr = 58 self.key_pattern.offset_to_DES_key_ptr = -89 self.key_pattern.offset_to_AES_key_ptr = 16 @@ -328,6 +327,20 @@ def __init__(self): self.key_struct = KIWI_BCRYPT_KEY81 self.key_handle_struct = KIWI_BCRYPT_HANDLE_KEY +class LSA_x64_9(LsaTemplate_NT6): + """Same as LSA_x64_8 but with a different IV offset""" + def __init__(self): + LsaTemplate_NT6.__init__(self) + self.key_pattern = LSADecyptorKeyPattern() + self.key_pattern.signature = b'\x83\x64\x24\x30\x00\x48\x8d\x45\xe0\x44\x8b\x4d\xd8\x48\x8d\x15' + self.key_pattern.IV_length = 16 + self.key_pattern.offset_to_IV_ptr = 71 + self.key_pattern.offset_to_DES_key_ptr = -89 + self.key_pattern.offset_to_AES_key_ptr = 16 + + self.key_struct = KIWI_BCRYPT_KEY81 + self.key_handle_struct = KIWI_BCRYPT_HANDLE_KEY + class LSA_x86_1(LsaTemplate_NT6): def __init__(self): LsaTemplate_NT6.__init__(self) @@ -433,6 +446,7 @@ def __init__(self): '6' : LSA_x64_6(), '7' : LSA_x64_7(), '8' : LSA_x64_8(), + '9' : LSA_x64_9(), #same as 8 but fidderent IV offset }, 'x86': { '1' : LSA_x86_1(), From 478d973a2bda90b8c55dae31e2a25065534e226b Mon Sep 17 00:00:00 2001 From: Skelsec Date: Wed, 3 May 2023 21:54:17 +0200 Subject: [PATCH 2/3] cloudap and cloudapkd added --- pypykatz/_version.py | 2 +- pypykatz/alsadecryptor/cmdhelper.py | 36 ++++++------ pypykatz/alsadecryptor/lsa_decryptor_nt5.py | 2 +- pypykatz/alsadecryptor/lsa_decryptor_nt6.py | 4 +- pypykatz/alsadecryptor/lsa_template_nt6.py | 2 +- pypykatz/alsadecryptor/package_commons.py | 4 +- .../packages/cloudap/decryptor.py | 19 ++++--- .../packages/cloudap/templates.py | 47 +++++++++++++++- .../alsadecryptor/packages/msv/decryptor.py | 15 ++++- pypykatz/commons/common.py | 11 +++- pypykatz/dpapi/cmdhelper.py | 8 +++ pypykatz/dpapi/dpapi.py | 17 +++++- pypykatz/lsadecryptor/cmdhelper.py | 56 ++++++++++--------- pypykatz/lsadecryptor/lsa_decryptor_nt5.py | 2 +- pypykatz/lsadecryptor/lsa_decryptor_nt6.py | 8 +-- pypykatz/lsadecryptor/lsa_template_nt6.py | 16 +++++- pypykatz/lsadecryptor/package_commons.py | 4 +- .../packages/cloudap/decryptor.py | 11 +++- .../packages/cloudap/templates.py | 37 ++++++------ .../lsadecryptor/packages/msv/decryptor.py | 14 ++++- 20 files changed, 217 insertions(+), 98 deletions(-) diff --git a/pypykatz/_version.py b/pypykatz/_version.py index 8338551..d246450 100644 --- a/pypykatz/_version.py +++ b/pypykatz/_version.py @@ -1,5 +1,5 @@ -__version__ = "0.6.7" +__version__ = "0.6.8" __banner__ = \ """ # pypyKatz %s diff --git a/pypykatz/alsadecryptor/cmdhelper.py b/pypykatz/alsadecryptor/cmdhelper.py index 900ddc9..4359200 100644 --- a/pypykatz/alsadecryptor/cmdhelper.py +++ b/pypykatz/alsadecryptor/cmdhelper.py @@ -169,25 +169,25 @@ async def run(self, args): if args.cmd == 'minidump': if args.directory: dir_fullpath = os.path.abspath(args.memoryfile) - file_pattern = '*.dmp' - if args.recursive == True: - globdata = os.path.join(dir_fullpath, '**', file_pattern) - else: - globdata = os.path.join(dir_fullpath, file_pattern) + for file_pattern in ['*.dmp', '*.DMP']: + if args.recursive == True: + globdata = os.path.join(dir_fullpath, '**', file_pattern) + else: + globdata = os.path.join(dir_fullpath, file_pattern) - logger.info('Parsing folder %s' % dir_fullpath) - for filename in glob.glob(globdata, recursive=args.recursive): - logger.info('Parsing file %s' % filename) - try: - mimi = await apypykatz.parse_minidump_file(filename, packages = args.packages) - results[filename] = mimi - except Exception as e: - files_with_error.append(filename) - logger.exception('Error parsing file %s ' % filename) - if args.halt_on_error == True: - raise e - else: - pass + logger.info('Parsing folder %s' % dir_fullpath) + for filename in glob.glob(globdata, recursive=args.recursive): + logger.info('Parsing file %s' % filename) + try: + mimi = await apypykatz.parse_minidump_file(filename, packages = args.packages) + results[filename] = mimi + except Exception as e: + files_with_error.append(filename) + logger.exception('Error parsing file %s ' % filename) + if args.halt_on_error == True: + raise e + else: + pass else: logger.info('Parsing file %s' % args.memoryfile) diff --git a/pypykatz/alsadecryptor/lsa_decryptor_nt5.py b/pypykatz/alsadecryptor/lsa_decryptor_nt5.py index 7071cf9..c14c92c 100644 --- a/pypykatz/alsadecryptor/lsa_decryptor_nt5.py +++ b/pypykatz/alsadecryptor/lsa_decryptor_nt5.py @@ -93,7 +93,7 @@ async def find_signature(self): self.log('Selecting first one @ 0x%08x' % fl[0]) return fl[0] - def decrypt(self, encrypted): + def decrypt(self, encrypted, segment_size=128): # TODO: NT version specific, move from here in subclasses. cleartext = b'' size = len(encrypted) diff --git a/pypykatz/alsadecryptor/lsa_decryptor_nt6.py b/pypykatz/alsadecryptor/lsa_decryptor_nt6.py index 9790184..ab6a82e 100644 --- a/pypykatz/alsadecryptor/lsa_decryptor_nt6.py +++ b/pypykatz/alsadecryptor/lsa_decryptor_nt6.py @@ -77,7 +77,7 @@ async def get_key(self, pos, key_offset): self.log('HARD_KEY data:\n%s' % hexdump(kbk.hardkey.data)) return kbk.hardkey.data - def decrypt(self, encrypted): + def decrypt(self, encrypted, segment_size=128): # TODO: NT version specific, move from here in subclasses. cleartext = b'' size = len(encrypted) @@ -85,7 +85,7 @@ def decrypt(self, encrypted): if size % 8: if not self.aes_key or not self.iv: return cleartext - cipher = AES(self.aes_key, MODE_CFB, self.iv) + cipher = AES(self.aes_key, MODE_CFB, self.iv, segment_size=segment_size) cleartext = cipher.decrypt(encrypted) else: if not self.des_key or not self.iv: diff --git a/pypykatz/alsadecryptor/lsa_template_nt6.py b/pypykatz/alsadecryptor/lsa_template_nt6.py index e793573..da13a9d 100644 --- a/pypykatz/alsadecryptor/lsa_template_nt6.py +++ b/pypykatz/alsadecryptor/lsa_template_nt6.py @@ -100,7 +100,7 @@ def get_template(sysinfo): elif sysinfo.buildnumber < WindowsBuild.WIN_10_1809.value: template = templates['nt6']['x64']['5'] - elif WindowsBuild.WIN_10_1809.value >= sysinfo.buildnumber < WindowsMinBuild.WIN_11.value: + elif WindowsBuild.WIN_10_1809.value <= sysinfo.buildnumber < WindowsMinBuild.WIN_11.value: template = templates['nt6']['x64']['6'] else: template = templates['nt6']['x64']['8'] diff --git a/pypykatz/alsadecryptor/package_commons.py b/pypykatz/alsadecryptor/package_commons.py index 77d9362..570d322 100644 --- a/pypykatz/alsadecryptor/package_commons.py +++ b/pypykatz/alsadecryptor/package_commons.py @@ -102,7 +102,7 @@ async def log_ptr(self, ptr, name, datasize = None): except Exception as e: self.log('%s: Logging failed for position %s' % (name, hex(ptr))) - def decrypt_password(self, enc_password, bytes_expected = False, trim_zeroes = True): + def decrypt_password(self, enc_password, bytes_expected = False, trim_zeroes = True, segment_size = 128): """ Common decryption method for LSA encrypted passwords. Result be string or hex encoded bytes (for machine accounts). Also supports bad data, as orphaned credentials may contain actual password OR garbage @@ -113,7 +113,7 @@ def decrypt_password(self, enc_password, bytes_expected = False, trim_zeroes = T """ dec_password = None - temp = self.lsa_decryptor.decrypt(enc_password) + temp = self.lsa_decryptor.decrypt(enc_password, segment_size=segment_size) if temp and len(temp) > 0: if bytes_expected == False: try: # normal password diff --git a/pypykatz/alsadecryptor/packages/cloudap/decryptor.py b/pypykatz/alsadecryptor/packages/cloudap/decryptor.py index 9863c99..46f595a 100644 --- a/pypykatz/alsadecryptor/packages/cloudap/decryptor.py +++ b/pypykatz/alsadecryptor/packages/cloudap/decryptor.py @@ -22,6 +22,9 @@ def to_dict(self): t['dpapi_key'] = self.dpapi_key t['dpapi_key_sha1'] = self.dpapi_key_sha1 return t + + def get_masterkey_hex(self): + return self.dpapi_key.hex() if isinstance(self.dpapi_key, bytes) else self.dpapi_key def to_json(self): return json.dumps(self.to_dict()) @@ -31,7 +34,7 @@ def __str__(self): t += '\t\tcachedir %s\n' % self.cachedir t += '\t\tPRT %s\n' % self.PRT t += '\t\tkey_guid %s\n' % self.key_guid - t += '\t\tdpapi_key %s\n' % self.dpapi_key + t += '\t\tdpapi_key %s\n' % self.get_masterkey_hex() t += '\t\tdpapi_key_sha1 %s\n' % self.dpapi_key_sha1 return t @@ -58,20 +61,20 @@ async def add_entry(self, cloudap_entry): cred.cachedir = cache.toname.decode('utf-16-le').replace('\x00','') if cache.cbPRT != 0 and cache.PRT.value != 0: ptr_enc = await cache.PRT.read_raw(self.reader, cache.cbPRT) - temp, raw_dec = self.decrypt_password(ptr_enc, bytes_expected=True) + temp, raw_dec = self.decrypt_password(ptr_enc, bytes_expected=True, segment_size=8) try: temp = temp.decode() except: pass - cred.PRT = temp + cred.PRT = str(temp) if cache.toDetermine != 0: unk = await cache.toDetermine.read(self.reader) if unk is not None: - cred.key_guid = unk.guid.value - cred.dpapi_key, raw_dec = self.decrypt_password(unk.unk) - cred.dpapi_key_sha1 = hashlib.sha1(bytes.fromhex(cred.dpapi_key)).hexdigest() + cred.key_guid = unk.guid + cred.dpapi_key, raw_dec = self.decrypt_password(unk.unk, bytes_expected = True) + cred.dpapi_key_sha1 = hashlib.sha1(cred.dpapi_key).hexdigest() if cred.PRT is None and cred.key_guid is None: return @@ -84,9 +87,9 @@ async def start(self): try: entry_ptr_value, entry_ptr_loc = await self.find_first_entry() except Exception as e: - self.log('Failed to find structs! Reason: %s' % e) + self.log('Failed to find list entry! Reason: %s' % e) return await self.reader.move(entry_ptr_loc) - entry_ptr = await self.decryptor_template.list_entry(self.reader) + entry_ptr = await self.decryptor_template.list_entry.load(self.reader) await self.walk_list(entry_ptr, self.add_entry) \ No newline at end of file diff --git a/pypykatz/alsadecryptor/packages/cloudap/templates.py b/pypykatz/alsadecryptor/packages/cloudap/templates.py index ef255e0..6de9ef3 100644 --- a/pypykatz/alsadecryptor/packages/cloudap/templates.py +++ b/pypykatz/alsadecryptor/packages/cloudap/templates.py @@ -16,9 +16,11 @@ def get_template(sysinfo): return None if sysinfo.architecture == KatzSystemArchitecture.X64: - template.signature = b'\x44\x8b\x01\x44\x39\x42\x18\x75' + template.signature = b'\x44\x8b\x01\x44\x39\x42' template.first_entry_offset = -9 template.list_entry = PKIWI_CLOUDAP_LOGON_LIST_ENTRY + if sysinfo.buildnumber > WindowsBuild.WIN_10_1903.value: + template.list_entry = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2 elif sysinfo.architecture == KatzSystemArchitecture.X86: template.signature = b'\x8b\x31\x39\x72\x10\x75' @@ -132,7 +134,7 @@ async def load(reader): res.unk13 = await DWORD.load(reader) res.toDetermine = await PKIWI_CLOUDAP_CACHE_UNK.load(reader) res.unk14 = await PVOID.load(reader) - res.cbPRT = await DWORD.load(reader) + res.cbPRT = await DWORD.loadvalue(reader) await reader.align() res.PRT = await PVOID.load(reader) #PBYTE(reader) return res @@ -171,4 +173,45 @@ async def load(reader): res.unk2 = await DWORD64.load(reader) res.unk3 = await DWORD64.load(reader) res.cacheEntry = await PKIWI_CLOUDAP_CACHE_LIST_ENTRY.load(reader) + return res + +class PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2(POINTER): + def __init__(self): + super().__init__() + + @staticmethod + async def load(reader): + p = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2() + p.location = reader.tell() + p.value = await reader.read_uint() + p.finaltype = KIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2 + return p + +class KIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2: + def __init__(self): + self.Flink = None + self.Blink = None + self.unk0 = None + self.unk1 = None + self.unk2 = None + self.LocallyUniqueIdentifier = None + self.unk3 = None + self.unk4 = None + self.unk5 = None + self.cacheEntry = None + + @staticmethod + async def load(reader): + res = KIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2() + res.Flink = await PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2.load(reader) + res.Blink = await PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2.load(reader) + res.unk0 = await DWORD.load(reader) + res.unk1 = await DWORD.load(reader) + res.unk2 = await DWORD.load(reader) + res.LocallyUniqueIdentifier = await LUID.loadvalue(reader) + res.unk3 = await DWORD.load(reader) + await reader.align() + res.unk4 = await DWORD64.load(reader) + res.unk5 = await DWORD64.load(reader) + res.cacheEntry = await PKIWI_CLOUDAP_CACHE_LIST_ENTRY.load(reader) return res \ No newline at end of file diff --git a/pypykatz/alsadecryptor/packages/msv/decryptor.py b/pypykatz/alsadecryptor/packages/msv/decryptor.py index f413d82..db86548 100644 --- a/pypykatz/alsadecryptor/packages/msv/decryptor.py +++ b/pypykatz/alsadecryptor/packages/msv/decryptor.py @@ -139,6 +139,7 @@ def to_dict(self): t['kerberos_creds'] = [] t['credman_creds'] = [] t['tspkg_creds'] = [] + t['cloudap_creds'] = [] for cred in self.msv_creds: t['msv_creds'].append(cred.to_dict()) for cred in self.wdigest_creds: @@ -155,6 +156,8 @@ def to_dict(self): t['credman_creds'].append(cred.to_dict()) for cred in self.tspkg_creds: t['tspkg_creds'].append(cred.to_dict()) + for cred in self.cloudap_creds: + t['cloudap_creds'].append(cred.to_dict()) return t def to_json(self): @@ -197,6 +200,9 @@ def __str__(self): if len(self.dpapi_creds) > 0: for cred in self.dpapi_creds: t+= str(cred) + if len(self.cloudap_creds) > 0: + for cred in self.cloudap_creds: + t+= str(cred) return t def to_row(self): @@ -227,6 +233,12 @@ def to_row(self): for cred in self.tspkg_creds: t = cred.to_dict() yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'plaintext', t['password']] + for cred in self.cloudap_creds: + t = cred.to_dict() + yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'masterkey', str(cred.get_masterkey_hex())] + yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'sha1', str(t['dpapi_key_sha1'])] + yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'PRT', str(t['PRT'])] + def to_grep_rows(self): for cred in self.msv_creds: @@ -266,8 +278,7 @@ def to_grep_rows(self): for cred in self.cloudap_creds: t = cred.to_dict() - #print(t) - yield [str(t['credtype']), '', '', '', '', '', str(t['dpapi_key']), str(t['dpapi_key_sha1']), str(t['key_guid']), base64.b64encode(str(t['PRT']).encode()).decode()] + yield [str(t['credtype']), '', '', '', '', '', str(cred.get_masterkey_hex()), str(t['dpapi_key_sha1']), str(t['key_guid']), str(t['PRT'])] diff --git a/pypykatz/commons/common.py b/pypykatz/commons/common.py index 7700bf6..986c0ff 100644 --- a/pypykatz/commons/common.py +++ b/pypykatz/commons/common.py @@ -2,6 +2,7 @@ import enum import json import datetime +import base64 from minidump.streams.SystemInfoStream import PROCESSOR_ARCHITECTURE @@ -499,4 +500,12 @@ def from_rekallreader(rekallreader): sysinfo.msv_dll_timestamp = rekallreader.msv_dll_timestamp return sysinfo - + + +def base64_decode_url(value: str, bytes_expected=False) -> str: + padding = 4 - (len(value) % 4) + value = value + ("=" * padding) + result = base64.urlsafe_b64decode(value) + if bytes_expected is True: + return result + return result.decode() \ No newline at end of file diff --git a/pypykatz/dpapi/cmdhelper.py b/pypykatz/dpapi/cmdhelper.py index 3874465..d144ed9 100644 --- a/pypykatz/dpapi/cmdhelper.py +++ b/pypykatz/dpapi/cmdhelper.py @@ -141,6 +141,10 @@ def add_args(self, parser, live_parser): dpapi_describe_group.add_argument('datatype', choices = ['blob', 'masterkey', 'pvk', 'vpol', 'credential'], help= 'Type of structure') dpapi_describe_group.add_argument('data', help='filepath or hex-encoded data') + dpapi_cloudapkd_group = dpapi_subparsers.add_parser('cloudapkd', help='Decrypt KeyValue structure from CloudAPK') + dpapi_cloudapkd_group.add_argument('mkf', help= 'Keyfile generated by the masterkey -o command.') + dpapi_cloudapkd_group.add_argument('keyvalue', help='KeyValue string obtained from PRT') + def execute(self, args): if len(self.keywords) > 0 and args.command in self.keywords: @@ -218,6 +222,10 @@ def run(self, args): dpapi.dump_masterkeys(args.out_file) + elif args.dapi_module == 'cloudapkd': + dpapi.load_masterkeys(args.mkf) + plain = dpapi.decrypt_cloudap_key(args.keyvalue) + print('Clear key: %s' % plain.hex()) elif args.dapi_module == 'credential': dpapi.load_masterkeys(args.mkf) diff --git a/pypykatz/dpapi/dpapi.py b/pypykatz/dpapi/dpapi.py index 624654c..3d42a6d 100644 --- a/pypykatz/dpapi/dpapi.py +++ b/pypykatz/dpapi/dpapi.py @@ -28,7 +28,7 @@ from unicrypto.hashlib import md4 as MD4 from unicrypto.symmetric import AES, MODE_GCM, MODE_CBC from winacl.dtyp.wcee.pvkfile import PVKFile -from pypykatz.commons.common import UniversalEncoder +from pypykatz.commons.common import UniversalEncoder, base64_decode_url from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 @@ -851,6 +851,21 @@ def cookieformatter(host, name, path, content): "Store raw": "firefox-default", #"firefox-default", "First Party Domain": "", #"" } + + def decrypt_cloudap_key(self, keyvalue_url_b64): + keyvalue = base64_decode_url(keyvalue_url_b64, bytes_expected=True) + keyvalue = keyvalue[8:] # skip the first 8 bytes + key_blob = DPAPI_BLOB.from_bytes(keyvalue) + return self.decrypt_blob(key_blob) + + def decrypt_cloudapkd_prt(self, PRT): + prt_json = json.loads(PRT) + keyvalue = prt_json.get('ProofOfPossesionKey',{}).get('KeyValue') + if keyvalue is None: + raise Exception('KeyValue not found in PRT') + + keyvalue_dec = self.decrypt_cloudap_key(keyvalue) + return keyvalue_dec diff --git a/pypykatz/lsadecryptor/cmdhelper.py b/pypykatz/lsadecryptor/cmdhelper.py index 4eab8cd..c816398 100644 --- a/pypykatz/lsadecryptor/cmdhelper.py +++ b/pypykatz/lsadecryptor/cmdhelper.py @@ -102,8 +102,13 @@ def process_results(self, results, files_with_error, args): print(':'.join(row)) for cred in results[result].orphaned_creds: t = cred.to_dict() - if t['credtype'] != 'dpapi': - if t['password'] is not None: + if t['credtype'] == 'cloudap': + x = [str(t['credtype']), '', '', '', '', '', str(cred.get_masterkey_hex()), str(t['dpapi_key_sha1']), str(t['key_guid']), t['PRT']] + if hasattr(args, 'directory') and args.directory is not None: + x = [result] + x + print(':'.join(x)) + elif t['credtype'] != 'dpapi': + if t.get('password', None) is not None and t['password'] is not None: x = [str(t['credtype']), str(t['domainname']), str(t['username']), '', '', '', '', '', str(t['password'])] if hasattr(args, 'directory') and args.directory is not None: x = [result] + x @@ -228,29 +233,30 @@ def run(self, args): elif args.cmd == 'minidump': if args.directory: dir_fullpath = os.path.abspath(args.memoryfile) - file_pattern = '*.dmp' - if args.recursive == True: - globdata = os.path.join(dir_fullpath, '**', file_pattern) - else: - globdata = os.path.join(dir_fullpath, file_pattern) - - logger.info('Parsing folder %s' % dir_fullpath) - for filename in glob.glob(globdata, recursive=args.recursive): - logger.info('Parsing file %s' % filename) - try: - if args.kerberos_dir is not None and 'all' not in args.packages: - args.packages.append('ktickets') - mimi = pypykatz.parse_minidump_file(filename, packages=args.packages) - results[filename] = mimi - if args.halt_on_error == True and len(mimi.errors) > 0: - raise Exception('Error in modules!') - except Exception as e: - files_with_error.append(filename) - logger.exception('Error parsing file %s ' % filename) - if args.halt_on_error == True: - raise e - else: - pass + for file_pattern in ['*.dmp', '*.DMP']: + if args.recursive == True: + globdata = os.path.join(dir_fullpath, '**', file_pattern) + else: + globdata = os.path.join(dir_fullpath, file_pattern) + + logger.info('Parsing folder %s' % dir_fullpath) + for filename in glob.glob(globdata, recursive=args.recursive): + logger.info('Parsing file %s' % filename) + try: + if args.kerberos_dir is not None and 'all' not in args.packages: + args.packages.append('ktickets') + mimi = pypykatz.parse_minidump_file(filename, packages=args.packages) + results[filename] = mimi + if args.halt_on_error == True and len(mimi.errors) > 0: + print(mimi.errors) + raise Exception('Error in modules!') + except Exception as e: + files_with_error.append(filename) + logger.exception('Error parsing file %s ' % filename) + if args.halt_on_error == True: + raise e + else: + pass else: logger.info('Parsing file %s' % args.memoryfile) diff --git a/pypykatz/lsadecryptor/lsa_decryptor_nt5.py b/pypykatz/lsadecryptor/lsa_decryptor_nt5.py index b2d9483..ea1bc6e 100644 --- a/pypykatz/lsadecryptor/lsa_decryptor_nt5.py +++ b/pypykatz/lsadecryptor/lsa_decryptor_nt5.py @@ -94,7 +94,7 @@ def find_signature(self): self.log('Selecting first one @ 0x%08x' % fl[0]) return fl[0] - def decrypt(self, encrypted): + def decrypt(self, encrypted, segment_size = 128): # TODO: NT version specific, move from here in subclasses. cleartext = b'' size = len(encrypted) diff --git a/pypykatz/lsadecryptor/lsa_decryptor_nt6.py b/pypykatz/lsadecryptor/lsa_decryptor_nt6.py index ddc69a4..47bbbcc 100644 --- a/pypykatz/lsadecryptor/lsa_decryptor_nt6.py +++ b/pypykatz/lsadecryptor/lsa_decryptor_nt6.py @@ -88,19 +88,19 @@ def get_key(self, pos, key_offset): self.log('HARD_KEY data:\n%s' % hexdump(kbk.hardkey.data)) return kbk.hardkey.data - def decrypt(self, encrypted): + def decrypt(self, encrypted, segment_size=128): # TODO: NT version specific, move from here in subclasses. cleartext = b'' size = len(encrypted) if size: if size % 8: - logger.debug('AES-CFB') + logger.debug('AES-CFB - %s' % size) if not self.aes_key or not self.iv: return cleartext - cipher = AES(self.aes_key, MODE_CFB, IV = self.iv, segment_size=128) + cipher = AES(self.aes_key, MODE_CFB, IV = self.iv, segment_size=segment_size) cleartext = cipher.decrypt(encrypted) else: - logger.debug('TDES') + logger.debug('TDES - size %s' % size) if not self.des_key or not self.iv: return cleartext cipher = TDES(self.des_key, MODE_CBC, self.iv[:8]) diff --git a/pypykatz/lsadecryptor/lsa_template_nt6.py b/pypykatz/lsadecryptor/lsa_template_nt6.py index 2260687..a29bb0a 100644 --- a/pypykatz/lsadecryptor/lsa_template_nt6.py +++ b/pypykatz/lsadecryptor/lsa_template_nt6.py @@ -8,6 +8,7 @@ from minidump.win_datatypes import ULONG, PVOID, POINTER from pypykatz.commons.common import KatzSystemArchitecture, WindowsMinBuild, WindowsBuild from pypykatz.lsadecryptor.package_commons import PackageTemplate +from pypykatz import logger class LsaTemplate_NT6(PackageTemplate): def __init__(self): @@ -41,12 +42,14 @@ def get_template_brute(sysinfo): keys = [x for x in templates['nt6']['x64']] keys.sort(reverse = True) for key in keys: + logger.debug('BF: using x64 - %s' % key) yield templates['nt6']['x64'][key] @staticmethod def get_template(sysinfo): template = LsaTemplate_NT6() + logger.debug('Buildnumber: %s' % sysinfo.buildnumber) if sysinfo.architecture == KatzSystemArchitecture.X86: if sysinfo.buildnumber <= WindowsMinBuild.WIN_XP.value: @@ -85,35 +88,42 @@ def get_template(sysinfo): elif sysinfo.buildnumber < WindowsMinBuild.WIN_7.value: #vista + logger.debug('using x64 - 1') template = templates['nt6']['x64']['1'] elif sysinfo.buildnumber < WindowsMinBuild.WIN_8.value: - #win 7 + logger.debug('using x64 - 2') template = templates['nt6']['x64']['2'] elif sysinfo.buildnumber < WindowsMinBuild.WIN_10.value: #win 8 and blue if sysinfo.buildnumber < WindowsMinBuild.WIN_BLUE.value: if sysinfo.msv_dll_timestamp < 0x60000000: + logger.debug('using x64 - 3') template = templates['nt6']['x64']['3'] else: + logger.debug('using x64 - 7') template = templates['nt6']['x64']['7'] else: + logger.debug('using x64 - 4') template = templates['nt6']['x64']['4'] #win blue elif sysinfo.buildnumber < WindowsBuild.WIN_10_1809.value: + logger.debug('using x64 - 5') template = templates['nt6']['x64']['5'] - elif WindowsBuild.WIN_10_1809.value >= sysinfo.buildnumber < WindowsMinBuild.WIN_11.value: + elif WindowsBuild.WIN_10_1809.value <= sysinfo.buildnumber < WindowsMinBuild.WIN_11.value: + logger.debug('using x64 - 6') template = templates['nt6']['x64']['6'] else: + logger.debug('using x64 - 8') template = templates['nt6']['x64']['8'] else: raise Exception('Missing LSA decrpytor template for Architecture: %s , Build number %s' % (sysinfo.architecture, sysinfo.buildnumber)) - + template.log_template('key_handle_struct', template.key_handle_struct) template.log_template('key_struct', template.key_struct) template.log_template('hard_key_struct', template.hard_key_struct) diff --git a/pypykatz/lsadecryptor/package_commons.py b/pypykatz/lsadecryptor/package_commons.py index a66b191..3e7d621 100644 --- a/pypykatz/lsadecryptor/package_commons.py +++ b/pypykatz/lsadecryptor/package_commons.py @@ -104,7 +104,7 @@ def log_ptr(self, ptr, name, datasize = None): except Exception as e: self.log('%s: Logging failed for position %s' % (name, hex(ptr))) - def decrypt_password(self, enc_password, bytes_expected = False, trim_zeroes = True): + def decrypt_password(self, enc_password, bytes_expected = False, trim_zeroes = True, segment_size=128): """ Common decryption method for LSA encrypted passwords. Result be string or hex encoded bytes (for machine accounts). Also supports bad data, as orphaned credentials may contain actual password OR garbage @@ -115,7 +115,7 @@ def decrypt_password(self, enc_password, bytes_expected = False, trim_zeroes = T """ dec_password = None - temp = self.lsa_decryptor.decrypt(enc_password) + temp = self.lsa_decryptor.decrypt(enc_password, segment_size=segment_size) if temp and len(temp) > 0: if bytes_expected == False: try: # normal password diff --git a/pypykatz/lsadecryptor/packages/cloudap/decryptor.py b/pypykatz/lsadecryptor/packages/cloudap/decryptor.py index cd9e014..085f287 100644 --- a/pypykatz/lsadecryptor/packages/cloudap/decryptor.py +++ b/pypykatz/lsadecryptor/packages/cloudap/decryptor.py @@ -1,6 +1,7 @@ import json import hashlib from pypykatz.lsadecryptor.package_commons import PackageDecryptor +from pypykatz.dpapi.dpapi import DPAPI class CloudapCredential: def __init__(self): @@ -25,13 +26,16 @@ def to_dict(self): def to_json(self): return json.dumps(self.to_dict()) + + def get_masterkey_hex(self): + return self.dpapi_key.hex() if isinstance(self.dpapi_key, bytes) else self.dpapi_key def __str__(self): t = '\t== Cloudap [%x]==\n' % self.luid t += '\t\tcachedir %s\n' % self.cachedir t += '\t\tPRT %s\n' % self.PRT t += '\t\tkey_guid %s\n' % self.key_guid - t += '\t\tdpapi_key %s\n' % self.dpapi_key + t += '\t\tdpapi_key %s\n' % self.get_masterkey_hex() t += '\t\tdpapi_key_sha1 %s\n' % self.dpapi_key_sha1 return t @@ -57,13 +61,13 @@ def add_entry(self, cloudap_entry): cache = cloudap_entry.cacheEntry.read(self.reader) cred.cachedir = cache.toname.decode('utf-16-le').replace('\x00','') if cache.cbPRT != 0 and cache.PRT.value != 0: - temp, raw_dec = self.decrypt_password(cache.PRT.read_raw(self.reader, cache.cbPRT), bytes_expected=True) + temp, raw_dec = self.decrypt_password(cache.PRT.read_raw(self.reader, cache.cbPRT), bytes_expected=True, segment_size=8) try: temp = temp.decode() except: pass - cred.PRT = temp + cred.PRT = str(temp) if cache.toDetermine != 0: unk = cache.toDetermine.read(self.reader) @@ -74,6 +78,7 @@ def add_entry(self, cloudap_entry): if cred.PRT is None and cred.key_guid is None: return + self.credentials.append(cred) except Exception as e: self.log('CloudAP entry parsing error! Reason %s' % e) diff --git a/pypykatz/lsadecryptor/packages/cloudap/templates.py b/pypykatz/lsadecryptor/packages/cloudap/templates.py index 8eb4abc..29a51a1 100644 --- a/pypykatz/lsadecryptor/packages/cloudap/templates.py +++ b/pypykatz/lsadecryptor/packages/cloudap/templates.py @@ -19,6 +19,9 @@ def get_template(sysinfo): template.signature = b'\x44\x8b\x01\x44\x39\x42' template.first_entry_offset = -9 template.list_entry = PKIWI_CLOUDAP_LOGON_LIST_ENTRY + if sysinfo.buildnumber > WindowsBuild.WIN_10_1903.value: + template.list_entry = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2 + elif sysinfo.architecture == KatzSystemArchitecture.X86: template.signature = b'\x8b\x31\x39\x72\x10\x75' @@ -95,25 +98,21 @@ def __init__(self, reader): self.unk3 = DWORD64(reader) self.cacheEntry = PKIWI_CLOUDAP_CACHE_LIST_ENTRY(reader) - -#### THIS IS FOR TESTING!!! -class PKIWI_CLOUDAP_LOGON_LIST_ENTRY_11(POINTER): +class PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2(POINTER): def __init__(self, reader): - super().__init__(reader, KIWI_CLOUDAP_LOGON_LIST_ENTRY_11) + super().__init__(reader, KIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2) -class KIWI_CLOUDAP_LOGON_LIST_ENTRY_11: +class KIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2: def __init__(self, reader): - self.Flink = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_11(reader) - self.Blink = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_11(reader) - self.LocallyUniqueIdentifier = 1 - reader.read(8*11) - #self.unk0 = DWORD(reader) - #self.unk1 = DWORD(reader) - #self.unk2 = DWORD(reader) - #reader.align() - #self.LocallyUniqueIdentifier = LUID(reader).value - #self.unk3 = DWORD64(reader) - #self.unk4 = DWORD64(reader) - #self.unk5 = DWORD64(reader) - #self.unk6 = DWORD64(reader) - self.cacheEntry = PKIWI_CLOUDAP_CACHE_LIST_ENTRY(reader) + self.Flink = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2(reader) + self.Blink = PKIWI_CLOUDAP_LOGON_LIST_ENTRY_21H2(reader) + self.unk0 = DWORD(reader) + self.unk1 = DWORD(reader) + self.unk2 = DWORD(reader) + #reader.align() #there should be an aloignment here, but it's not??? + self.LocallyUniqueIdentifier = LUID(reader).value + self.unk3 = DWORD(reader) + reader.align() + self.unk4 = DWORD64(reader) + self.unk5 = DWORD64(reader) + self.cacheEntry = PKIWI_CLOUDAP_CACHE_LIST_ENTRY(reader) \ No newline at end of file diff --git a/pypykatz/lsadecryptor/packages/msv/decryptor.py b/pypykatz/lsadecryptor/packages/msv/decryptor.py index 284ae68..1ee8576 100644 --- a/pypykatz/lsadecryptor/packages/msv/decryptor.py +++ b/pypykatz/lsadecryptor/packages/msv/decryptor.py @@ -137,6 +137,7 @@ def to_dict(self): t['kerberos_creds'] = [] t['credman_creds'] = [] t['tspkg_creds'] = [] + t['cloudap_creds'] = [] for cred in self.msv_creds: t['msv_creds'].append(cred.to_dict()) for cred in self.wdigest_creds: @@ -153,6 +154,8 @@ def to_dict(self): t['credman_creds'].append(cred.to_dict()) for cred in self.tspkg_creds: t['tspkg_creds'].append(cred.to_dict()) + for cred in self.cloudap_creds: + t['cloudap_creds'].append(cred.to_dict()) return t def to_json(self): @@ -195,6 +198,9 @@ def __str__(self): if len(self.dpapi_creds) > 0: for cred in self.dpapi_creds: t+= str(cred) + if len(self.cloudap_creds) > 0: + for cred in self.cloudap_creds: + t+= str(cred) return t def to_row(self): @@ -225,6 +231,11 @@ def to_row(self): for cred in self.tspkg_creds: t = cred.to_dict() yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'plaintext', t['password']] + for cred in self.cloudap_creds: + t = cred.to_dict() + yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'masterkey', str(cred.get_masterkey_hex())] + yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'sha1', str(t['dpapi_key_sha1'])] + yield [self.luid, t['credtype'], self.session_id, self.sid, t['credtype'], '', self.domainname, self.username, 'PRT', str(t['PRT'])] def to_grep_rows(self): for cred in self.msv_creds: @@ -264,8 +275,7 @@ def to_grep_rows(self): for cred in self.cloudap_creds: t = cred.to_dict() - #print(t) - yield [str(t['credtype']), '', '', '', '', '', str(t['dpapi_key']), str(t['dpapi_key_sha1']), str(t['key_guid']), base64.b64encode(str(t['PRT']).encode()).decode()] + yield [str(t['credtype']), '', '', '', '', '', str(cred.get_masterkey_hex()), str(t['dpapi_key_sha1']), str(t['key_guid']), str(t['PRT'])] From aa2caf55dd3358c531256551303aaeea3768a977 Mon Sep 17 00:00:00 2001 From: Skelsec Date: Tue, 30 May 2023 18:08:05 +0200 Subject: [PATCH 3/3] fixing package versions --- setup.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index a905444..1e74bca 100644 --- a/setup.py +++ b/setup.py @@ -51,14 +51,14 @@ "Operating System :: OS Independent", ], install_requires=[ - 'unicrypto>=0.0.10', - 'minidump>=0.0.21', - 'minikerberos>=0.4.0', - 'aiowinreg>=0.0.7', - 'msldap>=0.4.7', - 'winacl>=0.1.6', - 'aiosmb>=0.4.4', - 'aesedb>=0.1.3', + 'unicrypto==0.0.10', + 'minidump==0.0.21', + 'minikerberos==0.4.1', + 'aiowinreg==0.0.10', + 'msldap==0.5.5', + 'winacl==0.1.7', + 'aiosmb==0.4.6', + 'aesedb==0.1.4', 'tqdm', ],