Skip to content

Instantly share code, notes, and snippets.

@platomav
Created October 12, 2024 11:25
Show Gist options
  • Save platomav/eb33cfaaa80e817d6a4ebe8b6d747d5d to your computer and use it in GitHub Desktop.
Save platomav/eb33cfaaa80e817d6a4ebe8b6d747d5d to your computer and use it in GitHub Desktop.
Apple eficheck EALF parser
#!/usr/bin/env python3
# coding=utf-8
"""
Apple eficheck EALF parser
Copyright (C) 2024 Plato Mavropoulos
"""
import ctypes
import os
import struct
from typing import Any, Final
CHAR: Final[type[ctypes.c_char] | int] = ctypes.c_char
UINT8: Final[type[ctypes.c_ubyte] | int] = ctypes.c_ubyte
UINT16: Final[type[ctypes.c_ushort] | int] = ctypes.c_ushort
UINT32: Final[type[ctypes.c_uint] | int] = ctypes.c_uint
def ctypes_struct(buffer: bytes | bytearray, start_offset: int, class_object: Any) -> Any:
"""
https://github.com/skochinsky/me-tools/blob/master/me_unpack.py by Igor Skochinsky
"""
structure: Any = class_object()
struct_len: int = ctypes.sizeof(structure)
struct_data: bytes | bytearray = buffer[start_offset:start_offset + struct_len]
least_len: int = min(len(struct_data), struct_len)
ctypes.memmove(ctypes.addressof(structure), struct_data, least_len)
return structure
def path_files(in_path: str) -> list[str]:
""" Walk path to get all files """
file_paths: list[str] = []
for root_path, _, file_names in os.walk(in_path):
for file_name in file_names:
file_path: str = os.path.abspath(os.path.join(root_path, file_name))
if os.path.isfile(file_path):
file_paths.append(file_path)
return file_paths
class EalfHeader(ctypes.LittleEndianStructure):
""" EALF Header """
_pack_ = 1
_fields_ = [
('HeaderTag', CHAR * 4), # 0x00 EALF
('EntryCount', UINT32), # 0x04
('TotalSize', UINT32), # 0x08 Header + Entries
('HeaderVersion', UINT8), # 0x0C
('IntelTag', UINT8 * 84) # 0x0D UTF-16
# 0x61
]
@staticmethod
def _decode_bytes(field: bytes) -> str:
""" Decode null-prefixed/suffixed bytes """
return struct.pack('B' * len(field), *field).decode(encoding='utf-16', errors='ignore').strip('\x00 ')
def get_ibiosi(self) -> str:
""" Get Intel $IBIOSI$ Tag """
return self._decode_bytes(field=self.IntelTag)
def struct_print(self) -> None:
""" Display structure information """
print('\nEALF Header\n')
print('Header Tag :', self.HeaderTag.decode(encoding='utf-8'))
print('Entry Count :', self.EntryCount)
print('Total Size :', f'0x{self.TotalSize:X}')
print('Header Version:', self.HeaderVersion)
print('EFI IBIOSI :', self.get_ibiosi())
class EalfEntry(ctypes.LittleEndianStructure):
""" EALF Entry """
_pack_ = 1
_fields_ = [
('Type', UINT8), # 0x00 Type (0 Volume, 1 File, 2 OOB/?, 3 FreeSpace, 4 Padding/Non-Empty)
('Unknown0', UINT8), # 0x01 ? (3 NVRAM)
('Unknown1', UINT16), # 0x02 ? increasing
('DataOffset', UINT32), # 0x04
('DataLength', UINT32), # 0x08
('DataGuid', UINT8 * 16), # 0x0C
('DataHash', UINT8 * 32) # 0x1C SHA-256 (zeroes for NVRAM)
# 0x3C
]
TYPE: Final[dict[int, str]] = {0: 'Volume', 1: 'File', 2: 'Non-UEFI', 3: 'FreeSpace', 4: 'Padding'}
@staticmethod
def _reverse_hex(bytes_hex: str) -> str:
""" Reverse hex bytes """
return ''.join(reversed([bytes_hex[i:i + 2] for i in range(0, len(bytes_hex), 2)]))
def get_guid(self) -> str:
""" Get EALF Entry GUID """
guid_hex: str = f'{int.from_bytes(bytes=self.DataGuid, byteorder="big"):0{0x10 * 2}X}'
guid_1: str = self._reverse_hex(bytes_hex=guid_hex[0:8])
guid_2: str = self._reverse_hex(bytes_hex=guid_hex[8:12])
guid_3: str = self._reverse_hex(bytes_hex=guid_hex[12:16])
guid_4: str = guid_hex[16:20]
guid_5: str = guid_hex[20:32]
return f'{guid_1}-{guid_2}-{guid_3}-{guid_4}-{guid_5}'
def get_hash(self) -> str:
""" Get EALF Entry Hash """
return f'{int.from_bytes(bytes=self.DataHash, byteorder="big"):0{0x20 * 2}x}'
def struct_print(self) -> None:
""" Display structure information """
print('\nEALF Entry\n')
print('Type :', self.TYPE.get(self.Type, f'Unknown ({self.Type})'))
print('Unknown 0 :', f'0x{self.Unknown0:02X}')
print('Unknown 1 :', f'0x{self.Unknown1:04X}')
print('Data Offset:', f'0x{self.DataOffset:X}')
print('Data Length:', f'0x{self.DataLength:X}')
print('Data GUID :', self.get_guid())
print('Data Hash :', self.get_hash())
EALF_HEADER_SIZE: Final[int] = ctypes.sizeof(EalfHeader)
EALF_ENTRY_SIZE: Final[int] = ctypes.sizeof(EalfEntry)
for ealf_file in path_files(in_path='./allowlists')[:10]:
if not ealf_file.lower().endswith('.ealf'):
continue
ealf_name: str = os.path.basename(ealf_file)
print(f'\n{ealf_name}')
with open(file=ealf_file, mode='rb') as ealf_object:
ealf_data: bytes = ealf_object.read()
ealf_header: Any = ctypes_struct(buffer=ealf_data, start_offset=0, class_object=EalfHeader)
if ealf_header.HeaderVersion != 1:
print(f'\nError: Unknown Header Version: {ealf_header.HeaderVersion}')
if not (len(ealf_data) == ealf_header.TotalSize == EALF_HEADER_SIZE + EALF_ENTRY_SIZE * ealf_header.EntryCount):
print('\nError: Failed to validate total EALF Header + Entries Size')
ealf_header.struct_print()
for entry_index in range(ealf_header.EntryCount):
entry_offset: int = EALF_HEADER_SIZE + EALF_ENTRY_SIZE * entry_index
ealf_entry: Any = ctypes_struct(buffer=ealf_data, start_offset=entry_offset, class_object=EalfEntry)
ealf_entry.struct_print()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment