Created
October 12, 2024 11:25
-
-
Save platomav/eb33cfaaa80e817d6a4ebe8b6d747d5d to your computer and use it in GitHub Desktop.
Apple eficheck EALF parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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