Skip to content

Commit

Permalink
Merge pull request #129 from skelsec/main
Browse files Browse the repository at this point in the history
Main
  • Loading branch information
skelsec authored May 30, 2023
2 parents 1222aa2 + aa2caf5 commit 3ea56c3
Show file tree
Hide file tree
Showing 21 changed files with 254 additions and 108 deletions.
2 changes: 1 addition & 1 deletion pypykatz/_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

__version__ = "0.6.6"
__version__ = "0.6.8"
__banner__ = \
"""
# pypyKatz %s
Expand Down
36 changes: 18 additions & 18 deletions pypykatz/alsadecryptor/cmdhelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pypykatz/alsadecryptor/lsa_decryptor_nt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pypykatz/alsadecryptor/lsa_decryptor_nt6.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ 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)
if size:
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:
Expand Down
17 changes: 15 additions & 2 deletions pypykatz/alsadecryptor/lsa_template_nt6.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -408,14 +408,26 @@ 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

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)
Expand Down Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions pypykatz/alsadecryptor/package_commons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 11 additions & 8 deletions pypykatz/alsadecryptor/packages/cloudap/decryptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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)
47 changes: 45 additions & 2 deletions pypykatz/alsadecryptor/packages/cloudap/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
15 changes: 13 additions & 2 deletions pypykatz/alsadecryptor/packages/msv/decryptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'])]



Expand Down
11 changes: 10 additions & 1 deletion pypykatz/commons/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import enum
import json
import datetime
import base64

from minidump.streams.SystemInfoStream import PROCESSOR_ARCHITECTURE

Expand Down Expand Up @@ -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()
8 changes: 8 additions & 0 deletions pypykatz/dpapi/cmdhelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion pypykatz/dpapi/dpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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



Expand Down
Loading

0 comments on commit 3ea56c3

Please sign in to comment.