Skip to content

Commit 6531a99

Browse files
holly-osolardiz
authored andcommitted
Update fvde2john.py for removable drive support
Added metadata block decryption and parsing to support FileVault on removable drives
1 parent 3fd5fe9 commit 6531a99

File tree

1 file changed

+165
-40
lines changed

1 file changed

+165
-40
lines changed

run/fvde2john.py

Lines changed: 165 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
# The partition table is parsed to find the boot volume, often named 'Recovery HD'. The boot volume can be identified by its type GUID: 426F6F74-0000-11AA-AA11-00306543ECAC.
55
# The boot volume contains a file called `EncryptedRoot.plist.wipekey`. This is stored on the volume at `/com.apple.boot.X/System/Library/Caches/com.apple.corestorage/EncryptedRoot.plist.wipekey`, where `X` is variable but is often `P` or `R`. This plist file is encrypted with AES-XTS; the key is found in the CoreStorage volume header, and the tweak is b'\x00' * 16.
66
# The decrypted plist contains information relating to the user(s). This includes the salt, kek and iterations required to construct the hash as well as information such as username and password hints (if present).
7+
# For non-system drives, the plist file is found in the encrypted metadata, in block type 0x19.
78

89
import plistlib
910
import os
1011
import argparse
1112
import sys
1213
import re
14+
import base64
1315

1416
try:
1517
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -24,9 +26,12 @@
2426

2527
HEX_CORE_STORAGE_TYPE_GUID = '53746F72-6167-11AA-AA11-00306543ECAC'
2628
HEX_APPLE_BOOT_STORAGE_TYPE_GUID = '426F6F74-0000-11AA-AA11-00306543ECAC'
27-
LOCAL_USER_TYPE_ID = 0x10060002
29+
LOCAL_USER_TYPE_ID = [0x10060002, 0x10000001] # added 0x10000001 for removable drives
2830
BOOT_DIR_REGEX = re.compile(r'com.apple.boot.(?P<boot_letter>[A-Z])')
2931

32+
# 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+
3035
def uint_to_int(b):
3136
return int(b[::-1].hex(), 16)
3237

@@ -100,18 +105,40 @@ def parse_partition_entry(partition_entry):
100105
return part_GUID, type_GUID, start_LBA, partition_name
101106

102107
def parse_corestorage_header(fp, start_pos):
103-
fp.seek(start_pos + 176)
104-
aes_key = try_read_fp(fp, 0x10)
105-
return aes_key
106-
107-
def AES_XTS_decrypt(aes_key, tweak, ct):
108+
fp.seek(start_pos)
109+
cs_header = try_read_fp(fp, 0x200) # Physical volume header is 512 bytes
110+
physical_volume_size = cs_header[64:72]
111+
cs_signature = cs_header[88:90]
112+
assert cs_signature == b'CS'
113+
114+
block_size = uint_to_int(cs_header[96:100])
115+
metadata_block_numbers = cs_header[104:136] # array of 4x8 metadata block numbers
116+
offsets = [uint_to_int(metadata_block_numbers[start: start+8]) for start in range(0, 32, 8)]
117+
118+
aes_key = cs_header[176:192]
119+
physical_UUID = cs_header[304:320]
120+
logical_UUID = cs_header[320:336]
121+
return aes_key, offsets, block_size, physical_UUID
122+
123+
def AES_XTS_decrypt(aes_key1, aes_key2, tweak, ct):
108124
decryptor = Cipher(
109-
algorithms.AES(key=aes_key + b'\x00' * 16),
125+
algorithms.AES(key=aes_key1 + aes_key2),
110126
modes.XTS(tweak=tweak),
111127
).decryptor()
112128
pt = decryptor.update(ct)
113129
return pt
114130

131+
def AES_XTS_decrypt_metadata_block(aes_key1, aes_key2, block_number, enc_metadata):
132+
block_start = block_number * 8192
133+
block_end = block_start + 8192
134+
135+
# tweak = block number
136+
tweak = hex(block_number)[2:].zfill(32)
137+
tweak = bytearray.fromhex(tweak)[::-1]
138+
139+
decrypted_block = AES_XTS_decrypt(aes_key1, aes_key2, tweak, enc_metadata[block_start:block_end])
140+
return decrypted_block
141+
115142
def parse_keybag_entry(uuid, pt):
116143
uuid_iterator = findall(uuid, pt)
117144
for sp in uuid_iterator:
@@ -141,6 +168,7 @@ def get_boot_dir(fs_object):
141168
entry_name = entry.info.name.name.decode()
142169
if re.match(BOOT_DIR_REGEX, entry_name):
143170
return entry_name
171+
return None
144172

145173
def recover_file(fs_object, file_path):
146174
file_obj = fs_object.open(file_path)
@@ -154,12 +182,13 @@ def get_EncryptedRoot_plist_wipekey(image_file, start_pos):
154182
fs = pytsk3.FS_Info(img, offset=start_pos)
155183
boot_dir = get_boot_dir(fs)
156184

157-
file_path = os.path.join(f"{boot_dir}/System/Library/Caches/com.apple.corestorage/EncryptedRoot.plist.wipekey")
158-
EncryptedRoot_data = recover_file(fs, file_path)
185+
if boot_dir:
186+
file_path = os.path.join(f"{boot_dir}/System/Library/Caches/com.apple.corestorage/EncryptedRoot.plist.wipekey")
187+
EncryptedRoot_data = recover_file(fs, file_path)
159188

160-
if not EncryptedRoot_data:
161-
sys.stderr.write("EncryptedRoot.plist.wipekey not found in image file, exiting.")
162-
sys.exit(1)
189+
else:
190+
# EncryptedRoot.plist.wipekey not found in image file, will search metadata blocks for encryption context plist
191+
return None
163192

164193
return EncryptedRoot_data
165194

@@ -177,6 +206,123 @@ def format_hash_str(user_part):
177206
# remove colons so that hash format is consistent and strip newlines
178207
return user_part.replace("\n","").replace("\r","").replace(":","")
179208

209+
def parse_metadata_block(metadata_block):
210+
metadata_block_header = metadata_block[:64] # metadata block header is 64 bytes
211+
metadata_block_data = metadata_block[64:]
212+
213+
block_type, block_size, possibly_lvfwiped = parse_metadata_block_header(metadata_block_header)
214+
215+
if block_type == 0x11:
216+
volume_group_xml_offset, volume_group_xml_size, volume_groups_descriptor_offset = parse_metadata_block_0x11(metadata_block_data)
217+
218+
return volume_group_xml_offset, volume_group_xml_size, volume_groups_descriptor_offset
219+
220+
# return block_type so we know what to parse
221+
def parse_metadata_block_header(metadata_block_header):
222+
crc32 = metadata_block_header[0:4]
223+
possibly_lvfwiped = metadata_block_header[0:8] # have seen lvfwiped here, the rest of the block is zero
224+
version = uint_to_int(metadata_block_header[8:10])
225+
block_type = uint_to_int(metadata_block_header[10:12])
226+
block_size = uint_to_int(metadata_block_header[48:52])
227+
228+
return block_type, block_size, possibly_lvfwiped
229+
230+
def parse_metadata_block_0x11(metadata_block):
231+
metadata_size = uint_to_int(metadata_block[0:4])
232+
volume_groups_descriptor_offset = uint_to_int(metadata_block[156:160])
233+
volume_group_xml_offset = uint_to_int(metadata_block[160:164])
234+
volume_group_xml_size = uint_to_int(metadata_block[164:168])
235+
236+
return volume_group_xml_offset, volume_group_xml_size, volume_groups_descriptor_offset
237+
238+
def parse_metadata_block_0x19(metadata_block):
239+
xml_plist_data_offset = uint_to_int(metadata_block[48:52])
240+
xml_plist_data_size = uint_to_int(metadata_block[52:56])
241+
xml_plist = metadata_block[xml_plist_data_offset - 64: xml_plist_data_offset + xml_plist_data_size - 64]
242+
return xml_plist
243+
244+
def parse_volume_group_descriptor(volume_group_descriptor):
245+
enc_metadata_size = uint_to_int(volume_group_descriptor[8:16]) # in no. of blocks
246+
primary_enc_metadata_block_no = uint_to_int(volume_group_descriptor[32:38])
247+
plist_data = volume_group_descriptor[48:]
248+
249+
return primary_enc_metadata_block_no, enc_metadata_size
250+
251+
def parse_CryptoUsers_dict(CryptoUsers_dict, EncryptedRoot):
252+
for user_index in range(len(CryptoUsers_dict)):
253+
# We want the local user login details i.e. not iCloud
254+
if CryptoUsers_dict[user_index].get('UserType') in LOCAL_USER_TYPE_ID:
255+
passphrase_hint = CryptoUsers_dict[user_index].get('PassphraseHint')
256+
257+
name_info = CryptoUsers_dict[user_index].get('UserNamesData')
258+
full_name_info = ''
259+
username_info = ''
260+
if len(name_info) == 2:
261+
full_name_info, username_info = name_info[0].decode(), name_info[1].decode()
262+
263+
full_name_info = format_hash_str(full_name_info)
264+
passphrase_hint = format_hash_str(passphrase_hint)
265+
266+
# Hash info stored in the PassphraseWrappedKEKStruct in decrypted plist
267+
# Stored in base64 in metadata block plist, but already decoded in EncryptedRoot plist
268+
PassphraseWrappedKEKStruct = CryptoUsers_dict[user_index].get('PassphraseWrappedKEKStruct')
269+
if not EncryptedRoot:
270+
PassphraseWrappedKEKStruct = base64.b64decode(PassphraseWrappedKEKStruct)
271+
fvde_hash = construct_fvde_hash(PassphraseWrappedKEKStruct)
272+
273+
sys.stdout.write(f"{username_info}:{fvde_hash}:::{full_name_info} {passphrase_hint}::\n")
274+
return
275+
276+
def get_CryptoUsers_dict_from_EncryptedRoot(EncryptedRoot_data, aes_key1):
277+
aes_key2 = b'\x00' * 16
278+
tweak = b'\x00' * 16
279+
pt = AES_XTS_decrypt(aes_key1, aes_key2, tweak, EncryptedRoot_data)
280+
CryptoUsers_dict = load_plist_dict(pt)['CryptoUsers']
281+
282+
return CryptoUsers_dict
283+
284+
def get_CryptoUsers_dict_from_encrypted_metadata(cs_start_pos, offsets, block_size, fp, physical_UUID, aes_key1):
285+
o = offsets[0] # all metadata blocks equal so just use first
286+
metadata_block_start = cs_start_pos + o * block_size
287+
fp.seek(metadata_block_start)
288+
metadata_block = try_read_fp(fp, 0x200)
289+
290+
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)
293+
294+
fp.seek(metadata_block_start + volume_groups_descriptor_offset)
295+
volume_group_descriptor = try_read_fp(fp, 0x200)
296+
primary_enc_metadata_block_no, enc_metadata_size = parse_volume_group_descriptor(volume_group_descriptor)
297+
298+
fp.seek(cs_start_pos + primary_enc_metadata_block_no * block_size)
299+
enc_metadata = try_read_fp(fp, enc_metadata_size * block_size)
300+
301+
for block_number in range(0, 64): # change to the number
302+
aes_key2 = physical_UUID
303+
decrypted_block = AES_XTS_decrypt_metadata_block(aes_key1, aes_key2, block_number, enc_metadata)
304+
305+
decrypted_metadata_block_header = decrypted_block[:64]
306+
block_type, metadata_block_size, possibly_lvfwiped = parse_metadata_block_header(decrypted_metadata_block_header)
307+
308+
# iterate through blocks until find block_type 0x19 - containing encryption context
309+
if block_type == 0x19:
310+
xml_plist = parse_metadata_block_0x19(decrypted_block[64:metadata_block_size]).decode('latin-1')
311+
312+
matches = []
313+
for m in re.finditer(CRYPTO_USERS_REGEX, xml_plist):
314+
matches.append(m.groupdict())
315+
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)
323+
324+
return CryptoUsers_dict
325+
180326
def main():
181327

182328
p = argparse.ArgumentParser()
@@ -196,37 +342,16 @@ def main():
196342
# Unlikely to have more than one boot volume, but loop anyway
197343
for boot_start_pos in boot_volumes:
198344
EncryptedRoot_data = get_EncryptedRoot_plist_wipekey(image_file, boot_start_pos)
199-
200345
for cs_start_pos in core_storage_volumes:
201-
aes_key = parse_corestorage_header(fp, cs_start_pos)
202-
203-
tweak = b'\x00' * 16
204-
pt = AES_XTS_decrypt(aes_key, tweak, EncryptedRoot_data)
205-
d = load_plist_dict(pt)
346+
aes_key1, offsets, block_size, physical_UUID = parse_corestorage_header(fp, cs_start_pos)
347+
if EncryptedRoot_data:
348+
CryptoUsers_dict = get_CryptoUsers_dict_from_EncryptedRoot(EncryptedRoot_data, aes_key1)
349+
else:
350+
CryptoUsers_dict = get_CryptoUsers_dict_from_encrypted_metadata(cs_start_pos, offsets, block_size, fp, physical_UUID, aes_key1)
206351

207-
user_index = 0
208-
for i in range(len(d['CryptoUsers'])):
209-
# We want the local user login details i.e. not iCloud
210-
if d['CryptoUsers'][i].get('UserType') == LOCAL_USER_TYPE_ID:
211-
user_index = i
212-
passphrase_hint = d['CryptoUsers'][user_index].get('PassphraseHint')
352+
parse_CryptoUsers_dict(CryptoUsers_dict, EncryptedRoot_data)
213353

214-
name_info = d['CryptoUsers'][user_index].get('UserNamesData')
215-
full_name_info = ''
216-
username_info = ''
217-
if len(name_info) == 2:
218-
full_name_info, username_info = name_info[0].decode(), name_info[1].decode()
219-
220-
full_name_info = format_hash_str(full_name_info)
221-
passphrase_hint = format_hash_str(passphrase_hint)
222-
223-
# Hash info stored in the PassphraseWrappedKEKStruct in decrypted plist
224-
PassphraseWrappedKEKStruct = d['CryptoUsers'][user_index].get('PassphraseWrappedKEKStruct')
225-
fvde_hash = construct_fvde_hash(PassphraseWrappedKEKStruct)
226-
227-
sys.stdout.write(f"{username_info}:{fvde_hash}:::{full_name_info} {passphrase_hint}::\n")
228-
229-
return
354+
return
230355

231356

232357
if __name__ == "__main__":

0 commit comments

Comments
 (0)