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
89import plistlib
910import os
1011import argparse
1112import sys
1213import re
14+ import base64
1315
1416try :
1517 from cryptography .hazmat .primitives .ciphers import Cipher , algorithms , modes
2426
2527HEX_CORE_STORAGE_TYPE_GUID = '53746F72-6167-11AA-AA11-00306543ECAC'
2628HEX_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
2830BOOT_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+
3035def 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
102107def 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+
115142def 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
145173def 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+
180326def 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
232357if __name__ == "__main__" :
0 commit comments