Skip to content

Commit 9ea7518

Browse files
authored
Compressed plist support and better regex
Added support for when encryption context is compressed. Also, broader regex for greater matching.
1 parent c7465ee commit 9ea7518

1 file changed

Lines changed: 41 additions & 15 deletions

File tree

run/fvde2john.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sys
1313
import re
1414
import base64
15+
import zlib
1516

1617
try:
1718
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -30,7 +31,12 @@
3031
BOOT_DIR_REGEX = re.compile(r'com.apple.boot.(?P<boot_letter>[A-Z])')
3132

3233
# long regex for CryptoUsers dict (removable drives only) because difficult to parse with plistlib
33-
CRYPTO_USERS_REGEX = re.compile(r'<key>CryptoUsers<\/key>.*?<key>PassphraseWrappedKEKStruct<\/key><data.*?>(?P<PassphraseWrappedKEKStruct>.*?)<\/data><key>WrapVersion<\/key><integer.*?>(?P<wrap_version>.*?)<\/integer><key>UserType<\/key><integer.*?>(?P<UserType>.*?)<\/integer><key>UserIdent<\/key><string.*?>(?P<UserIdent>.+?)<\/string><key>UserNamesData<\/key><string.*?>(?P<UserNamesData>.*?)<\/string><key>PassphraseHint<\/key><reference.*?><key>KeyEncryptingKeyIdent<\/key><string.*?>(?P<KeyEncryptingKeyIdent>.*?)<\/string><key>UserFullName<\/key><reference.*?><key>EFILoginGraphics<\/key><data.*?>(?P<EFILoginGraphics>.*?)<\/data>')
34+
CRYPTO_USERS_REGEX_STR = r'<key>CryptoUsers<\/key>.*?'
35+
for EntryName in ["PassphraseWrappedKEKStruct", "WrapVersion", "UserType", "UserIdent",
36+
"UserNamesData", "PassphraseHint", "KeyEncryptingKeyIdent", "UserFullName", "EFILoginGraphics"]:
37+
CRYPTO_USERS_REGEX_STR += r'<key>' + EntryName + r'<\/key><.*?>(?P<' + EntryName + r'>.*?)(<.*?>)*?'
38+
CRYPTO_USERS_REGEX_STR += r'<.*?>'
39+
CRYPTO_USERS_REGEX = re.compile(CRYPTO_USERS_REGEX_STR)
3440

3541
def uint_to_int(b):
3642
return int(b[::-1].hex(), 16)
@@ -236,9 +242,15 @@ def parse_metadata_block_0x11(metadata_block):
236242
return volume_group_xml_offset, volume_group_xml_size, volume_groups_descriptor_offset
237243

238244
def parse_metadata_block_0x19(metadata_block):
245+
compressed_data_size = uint_to_int(metadata_block[40:44])
246+
uncompressed_data_size = uint_to_int(metadata_block[44:48])
239247
xml_plist_data_offset = uint_to_int(metadata_block[48:52])
240248
xml_plist_data_size = uint_to_int(metadata_block[52:56])
241249
xml_plist = metadata_block[xml_plist_data_offset - 64: xml_plist_data_offset + xml_plist_data_size - 64]
250+
251+
if compressed_data_size < uncompressed_data_size:
252+
xml_plist = decompress_xml_plist(xml_plist)
253+
242254
return xml_plist
243255

244256
def parse_volume_group_descriptor(volume_group_descriptor):
@@ -269,10 +281,19 @@ def parse_CryptoUsers_dict(CryptoUsers_dict, EncryptedRoot):
269281
if not EncryptedRoot:
270282
PassphraseWrappedKEKStruct = base64.b64decode(PassphraseWrappedKEKStruct)
271283
fvde_hash = construct_fvde_hash(PassphraseWrappedKEKStruct)
272-
273284
sys.stdout.write(f"{username_info}:{fvde_hash}:::{full_name_info} {passphrase_hint}::\n")
274285
return
275286

287+
def decompress_xml_plist(xml_plist):
288+
# sometimes the xml plist in metadata block type 0x19 is compressed with zlib
289+
try:
290+
decompressed_xml_plist = zlib.decompress(xml_plist)
291+
return decompressed_xml_plist
292+
except zlib.error:
293+
sys.stderr.write("[!] Zlib decompression error, exiting.\n")
294+
sys.exit(1)
295+
296+
276297
def get_CryptoUsers_dict_from_EncryptedRoot(EncryptedRoot_data, aes_key1):
277298
aes_key2 = b'\x00' * 16
278299
tweak = b'\x00' * 16
@@ -288,8 +309,8 @@ def get_CryptoUsers_dict_from_encrypted_metadata(cs_start_pos, offsets, block_si
288309
metadata_block = try_read_fp(fp, 0x200)
289310

290311
volume_group_xml_offset, volume_group_xml_size, volume_groups_descriptor_offset = parse_metadata_block(metadata_block)
291-
# fp.seek(metadata_block_start + volume_group_xml_offset)
292-
# xml = try_read_fp(fp, volume_group_xml_size)
312+
fp.seek(metadata_block_start + volume_group_xml_offset)
313+
xml = try_read_fp(fp, volume_group_xml_size)
293314

294315
fp.seek(metadata_block_start + volume_groups_descriptor_offset)
295316
volume_group_descriptor = try_read_fp(fp, 0x200)
@@ -298,30 +319,35 @@ def get_CryptoUsers_dict_from_encrypted_metadata(cs_start_pos, offsets, block_si
298319
fp.seek(cs_start_pos + primary_enc_metadata_block_no * block_size)
299320
enc_metadata = try_read_fp(fp, enc_metadata_size * block_size)
300321

301-
for block_number in range(0, 64): # change to the number
322+
for block_number in range(0, enc_metadata_size):
302323
aes_key2 = physical_UUID
303-
decrypted_block = AES_XTS_decrypt_metadata_block(aes_key1, aes_key2, block_number, enc_metadata)
304324

325+
decrypted_block = AES_XTS_decrypt_metadata_block(aes_key1, aes_key2, block_number, enc_metadata)
305326
decrypted_metadata_block_header = decrypted_block[:64]
306327
block_type, metadata_block_size, possibly_lvfwiped = parse_metadata_block_header(decrypted_metadata_block_header)
307328

329+
# data block is wiped, skip
330+
if possibly_lvfwiped == b'LVFwiped':
331+
continue
332+
308333
# iterate through blocks until find block_type 0x19 - containing encryption context
309334
if block_type == 0x19:
310-
xml_plist = parse_metadata_block_0x19(decrypted_block[64:metadata_block_size]).decode('latin-1')
335+
xml_plist = parse_metadata_block_0x19(decrypted_block[64:metadata_block_size]).decode()
311336

312337
matches = []
313338
for m in re.finditer(CRYPTO_USERS_REGEX, xml_plist):
314339
matches.append(m.groupdict())
315340

316-
CryptoUsers_dict = {}
317-
for user_index in range(len(matches)):
318-
CryptoUsers_dict[user_index] = matches[user_index]
319-
# if present convert user type from string to hex
320-
user_type = CryptoUsers_dict[user_index].get('UserType')
321-
if user_type:
322-
CryptoUsers_dict[user_index]['UserType'] = int(CryptoUsers_dict[user_index]['UserType'], 16)
341+
if matches:
342+
CryptoUsers_dict = {}
343+
for user_index in range(len(matches)):
344+
CryptoUsers_dict[user_index] = matches[user_index]
345+
# if present convert user type from string to hex
346+
user_type = CryptoUsers_dict[user_index].get('UserType')
347+
if user_type:
348+
CryptoUsers_dict[user_index]['UserType'] = int(CryptoUsers_dict[user_index]['UserType'], 16)
323349

324-
return CryptoUsers_dict
350+
return CryptoUsers_dict
325351

326352
def main():
327353

0 commit comments

Comments
 (0)