Skip to content

Commit

Permalink
Create fvde2john.py
Browse files Browse the repository at this point in the history
Added fvde2john.py for extracting FileVault hashes. Easier and more reliable hash extraction. Also will give user info such as username/password hint
  • Loading branch information
holly-o authored Dec 20, 2024
1 parent 07110d8 commit 97719c3
Showing 1 changed file with 220 additions and 0 deletions.
220 changes: 220 additions & 0 deletions run/fvde2john.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env python3

# Usage: python3 fvde2john.py <image_file>
# 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.
# 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.
# 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).

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import plistlib
import os
import argparse
import pytsk3 # pip install pytsk3

HEX_CORE_STORAGE_TYPE_GUID = '53746F72-6167-11AA-AA11-00306543ECAC'
HEX_APPLE_BOOT_STORAGE_TYPE_GUID = '426F6F74-0000-11AA-AA11-00306543ECAC'
LOCAL_USER_TYPE_ID = 0x10060002

def uint_to_int(b):
return int(b[::-1].hex(), 16)

def guid_to_hex(guid):
guid_parts = guid.split('-')

hex_str = ''.join([guid_parts[0][i:i+2] for i in range(0, len(guid_parts[0]), 2)][::-1])
hex_str += ''.join([guid_parts[1][i:i+2] for i in range(0, len(guid_parts[1]), 2)][::-1])
hex_str += ''.join([guid_parts[2][i:i+2] for i in range(0, len(guid_parts[2]), 2)][::-1])
hex_str += guid_parts[3]
hex_str += guid_parts[4]

return hex_str.lower()

def parse_partition_table(fp):
# determine whether sector size is 0x200 or 0x1000
sector_size = 0x0

# look for EFI PART at start of sector 1
fp.seek(0x200)
signature = fp.read(0x8)
if signature == b'EFI PART':
sector_size = 0x200

else:
fp.seek(0x1000)
signature = fp.read(0x8)
if signature == b'EFI PART':
sector_size = 0x1000

print("[+] Identified sector size:", sector_size)

if not sector_size:
print(f"[!] Invalid sector size {sector_size} (not 512 or 4096 bytes). Exiting.")

fp.seek(2 * sector_size) # go to sector 2
partitions = []
partition_entry = b'1'
while any(partition_entry):
partition_entry = fp.read(128)
if any(partition_entry):
partitions.append(partition_entry)

partition_dict = {}
for p in partitions:
part_GUID, type_GUID, start, partition_name = parse_partition_entry(p)
sp = uint_to_int(start) * sector_size
partition_dict[part_GUID.hex()] = {'start':sp, 'partition_type':type_GUID.hex(), 'partition_name':partition_name.decode('utf-16').strip('\x00')}

return partition_dict

def findall(p, s):
i = s.find(p)
while i != -1:
yield i
i = s.find(p, i+1)

def parse_partition_entry(partition_entry):
type_GUID = partition_entry[0:0x10]
part_GUID = partition_entry[0x10:0x20]
start_LBA = partition_entry[0x20:0x28]
partition_name = partition_entry[0x38:0x80]
return part_GUID, type_GUID, start_LBA, partition_name

def parse_corestorage_header(fp, start_pos):
fp.seek(start_pos + 176)
aes_key = fp.read(0x10)
return aes_key

def AES_XTS_decrypt(aes_key, tweak, ct):
decryptor = Cipher(
algorithms.AES(key=aes_key + b'\x00' * 16),
modes.XTS(tweak=tweak),
).decryptor()
pt = decryptor.update(ct)
return pt

def parse_keybag_entry(uuid, pt):
uuid_iterator = findall(uuid, pt)
for sp in uuid_iterator:
ke_uuid, ke_tag, ke_keylen = pt[sp:sp+16], uint_to_int(pt[sp + 16:sp + 18]), uint_to_int(pt[sp + 18:sp + 20])
padding = pt[sp + 20:sp + 24]
keydata = pt[sp + 24: sp + 24 + ke_keylen]

# only tag 3 is needed for constructing the hash
if ke_tag == 3:
assert padding == b'\x00\x00\x00\x00'
volume_unlock_record = keydata

return volume_unlock_record

return None

def get_all_partitions_of_type(partition_dict, part_type):
return [partition_dict[p]['start'] for p in partition_dict if partition_dict[p]['partition_type'] == guid_to_hex(part_type)]

def load_plist_dict(pt):
# resultant pt has one extra malformed line in the xml, so we remove this.
plist_str = b''.join(pt.split(b'\n')[:-1]).decode()
d = plistlib.loads(plist_str)
return d

# Recursive traversal - Recovery HD partition does not contain a lot of files, and so this approach is fine
def traverse_filesystem(fs_object, target_file, path='/'):
for entry in fs_object.open_dir(path):
try:
if entry.info.name.name in [b'.', b'..']:
continue

file_path = os.path.join(path, entry.info.name.name.decode('utf-8'))

if entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_REG:
if entry.info.name.name == target_file.encode():
print(f"[+] Found file: {file_path}")
file_data = recover_file(fs_object, file_path)

# this returns to previous call i.e. the dir layer
return file_data

# Traverse lower layer if entry is a dir
elif entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
file_data = traverse_filesystem(fs_object, target_file, file_path)

if file_data:
return file_data

except Exception as e:
print(f"[!] Error reading entry: {e}")

def recover_file(fs_object, file_path):
try:
file_obj = fs_object.open(file_path)
size = file_obj.info.meta.size
offset = 0
data = file_obj.read_random(offset, size)

return data

except Exception as e:
print(f"[!] Error recovering file: {e}")

def get_EncryptedRoot_plist_wipekey(image_file, start_pos):
img = pytsk3.Img_Info(image_file)
fs = pytsk3.FS_Info(img, offset=start_pos)
target_file = 'EncryptedRoot.plist.wipekey'
EncryptedRoot_data = traverse_filesystem(fs, target_file)

return EncryptedRoot_data

def main():

p = argparse.ArgumentParser()
p.add_argument('image_file')
args = p.parse_args()
image_file = args.image_file

with open(image_file, 'rb') as fp:
partition_dict = parse_partition_table(fp)

core_storage_volumes = get_all_partitions_of_type(partition_dict, HEX_CORE_STORAGE_TYPE_GUID)
boot_volumes = get_all_partitions_of_type(partition_dict, HEX_APPLE_BOOT_STORAGE_TYPE_GUID)

# Unlikely to have more than one boot volume, but loop anyway
for boot_start_pos in boot_volumes:
EncryptedRoot_data = get_EncryptedRoot_plist_wipekey(image_file, boot_start_pos)

if core_storage_volumes == []:
print("[!] No CoreStorage volumes found, exiting.")
exit()
for cs_start_pos in core_storage_volumes:
aes_key = parse_corestorage_header(fp, cs_start_pos)

tweak = b'\x00' * 16
pt = AES_XTS_decrypt(aes_key, tweak, EncryptedRoot_data)
d = load_plist_dict(pt)

userIndex = 0
for i in range(len(d['CryptoUsers'])):
# We want the local user login details i.e. not iCloud
if d['CryptoUsers'][i].get('UserType') == LOCAL_USER_TYPE_ID:
userIndex = i

print("\n[+] Finding user info that may be useful to crack the password:")
FullName = d['CryptoUsers'][userIndex].get('UserFullName')
PassphraseHint = d['CryptoUsers'][userIndex].get('PassphraseHint')

print("Full name:", FullName)
print("Passphrase hint:", PassphraseHint)

PassphraseWrappedKEKStruct = d['CryptoUsers'][userIndex].get('PassphraseWrappedKEKStruct')
print("[+] Found PassphraseWrappedKEKStruct in decrypted plist")

salt = PassphraseWrappedKEKStruct[8:24]
kek = PassphraseWrappedKEKStruct[32:56]
iterations = uint_to_int(PassphraseWrappedKEKStruct[168:172])

print(f"[+] Successfully extracted hash: $fvde$1${len(salt)}${salt.hex()}${iterations}${kek.hex()}")

return


if __name__ == "__main__":
main()

0 comments on commit 97719c3

Please sign in to comment.