From d85a7f82dcf3baf832aa1297dd8c18ab04c44cb3 Mon Sep 17 00:00:00 2001 From: Plato Mavropoulos Date: Wed, 24 Apr 2024 01:22:53 +0300 Subject: [PATCH] Added AMI PFAT RSA 3K signed blocks support Added AMI PFAT nested detection at each file Added Award BIOS payload naming at each file Switched Panasonic BIOS LZNT1 external library Improved Panasonic LZNT1 detection and length Improved Dell PFS code structure and fixed bugs Improved code exception handling (raise, catch) Improved code definitions (PEP8, docs, types) Fixed some arguments missing from help screens --- .gitignore | 9 +- .mypy.ini | 4 + .pylintrc | 19 + AMI_PFAT_Extract.py | 567 ++++++++++------- AMI_UCP_Extract.py | 685 +++++++++++---------- Apple_EFI_ID.py | 230 +++---- Apple_EFI_IM4P.py | 104 ++-- Apple_EFI_PBZX.py | 110 ++-- Apple_EFI_PKG.py | 105 ++-- Award_BIOS_Extract.py | 72 ++- Dell_PFS_Extract.py | 1207 +++++++++++++++++++++---------------- Fujitsu_SFX_Extract.py | 80 +-- Fujitsu_UPC_Extract.py | 39 +- Insyde_IFD_Extract.py | 281 +++++---- LICENSE | 2 +- Panasonic_BIOS_Extract.py | 191 +++--- Phoenix_TDK_Extract.py | 233 ++++--- Portwell_EFI_Extract.py | 154 ++--- README.md | 34 +- Toshiba_COM_Extract.py | 54 +- VAIO_Package_Extract.py | 129 ++-- __init__.py | 6 + common/__init__.py | 6 + common/checksums.py | 22 +- common/comp_efi.py | 57 +- common/comp_szip.py | 66 +- common/externals.py | 74 ++- common/num_ops.py | 23 +- common/path_ops.py | 167 +++-- common/patterns.py | 6 +- common/pe_ops.py | 65 +- common/struct_ops.py | 30 +- common/system.py | 78 ++- common/templates.py | 119 ++-- common/text_ops.py | 39 +- external/requirements.txt | 2 - requirements.txt | 2 + 37 files changed, 2897 insertions(+), 2174 deletions(-) create mode 100644 .mypy.ini create mode 100644 .pylintrc create mode 100644 __init__.py create mode 100644 common/__init__.py delete mode 100644 external/requirements.txt create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 238b83e..a1b95bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -# Skip all external files -external/* - -# Keep external > requirements file -!external/requirements.txt +/.idea/ +/.mypy_cache/ +/external/ +/venv/ diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..4c8becb --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,4 @@ +[mypy] + +explicit_package_bases = True +mypy_path = $MYPY_CONFIG_FILE_DIR/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..45f4bc5 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,19 @@ +[MAIN] + +init-hook="import sys; sys.path.append('./')" + +[MESSAGES CONTROL] + +disable= + duplicate-code, + invalid-name, + line-too-long, + too-few-public-methods, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-nested-blocks, + too-many-return-statements, + too-many-statements \ No newline at end of file diff --git a/AMI_PFAT_Extract.py b/AMI_PFAT_Extract.py index 026b74a..9f15c18 100644 --- a/AMI_PFAT_Extract.py +++ b/AMI_PFAT_Extract.py @@ -1,319 +1,434 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ AMI PFAT Extract AMI BIOS Guard Extractor -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'AMI BIOS Guard Extractor v4.0_a12' - +import ctypes import os import re -import sys -import ctypes - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.externals import get_bgs_tool from common.num_ops import get_ordinal -from common.path_ops import make_dirs, safe_name, get_extract_path, extract_suffix +from common.path_ops import extract_suffix, get_extract_path, make_dirs, path_name, safe_name from common.patterns import PAT_AMI_PFAT -from common.struct_ops import char, get_struct, uint8_t, uint16_t, uint32_t +from common.struct_ops import Char, get_struct, UInt8, UInt16, UInt32 from common.system import printer from common.templates import BIOSUtility -from common.text_ops import file_to_bytes +from common.text_ops import bytes_to_hex, file_to_bytes + +TITLE = 'AMI BIOS Guard Extractor v5.0' + class AmiBiosGuardHeader(ctypes.LittleEndianStructure): + """ AMI BIOS Guard Header """ + _pack_ = 1 + + # noinspection PyTypeChecker _fields_ = [ - ('Size', uint32_t), # 0x00 Header + Entries - ('Checksum', uint32_t), # 0x04 ? - ('Tag', char*8), # 0x04 _AMIPFAT - ('Flags', uint8_t), # 0x10 ? + ('Size', UInt32), # 0x00 Header + Entries + ('Checksum', UInt32), # 0x04 ? + ('Tag', Char * 8), # 0x04 _AMIPFAT + ('Flags', UInt8), # 0x10 ? # 0x11 ] - - def struct_print(self, p): - printer(['Size :', f'0x{self.Size:X}'], p, False) - printer(['Checksum:', f'0x{self.Checksum:04X}'], p, False) - printer(['Tag :', self.Tag.decode('utf-8')], p, False) - printer(['Flags :', f'0x{self.Flags:02X}'], p, False) + + def struct_print(self, padd: int) -> None: + """ Display structure information """ + + printer(['Size :', f'0x{self.Size:X}'], padd, False) + printer(['Checksum:', f'0x{self.Checksum:04X}'], padd, False) + printer(['Tag :', self.Tag.decode('utf-8')], padd, False) + printer(['Flags :', f'0x{self.Flags:02X}'], padd, False) + class IntelBiosGuardHeader(ctypes.LittleEndianStructure): + """ Intel BIOS Guard Header """ + _pack_ = 1 + + # noinspection PyTypeChecker _fields_ = [ - ('BGVerMajor', uint16_t), # 0x00 - ('BGVerMinor', uint16_t), # 0x02 - ('PlatformID', uint8_t*16), # 0x04 - ('Attributes', uint32_t), # 0x14 - ('ScriptVerMajor', uint16_t), # 0x16 - ('ScriptVerMinor', uint16_t), # 0x18 - ('ScriptSize', uint32_t), # 0x1C - ('DataSize', uint32_t), # 0x20 - ('BIOSSVN', uint32_t), # 0x24 - ('ECSVN', uint32_t), # 0x28 - ('VendorInfo', uint32_t), # 0x2C + ('BGVerMajor', UInt16), # 0x00 + ('BGVerMinor', UInt16), # 0x02 + ('PlatformID', UInt8 * 16), # 0x04 + ('Attributes', UInt32), # 0x14 + ('ScriptVerMajor', UInt16), # 0x16 + ('ScriptVerMinor', UInt16), # 0x18 + ('ScriptSize', UInt32), # 0x1C + ('DataSize', UInt32), # 0x20 + ('BIOSSVN', UInt32), # 0x24 + ('ECSVN', UInt32), # 0x28 + ('VendorInfo', UInt32), # 0x2C # 0x30 ] - - def get_platform_id(self): - id_byte = bytes(self.PlatformID) - - id_text = re.sub(r'[\n\t\r\x00 ]', '', id_byte.decode('utf-8','ignore')) - - id_hexs = f'{int.from_bytes(id_byte, "big"):0{0x10 * 2}X}' - id_guid = f'{{{id_hexs[:8]}-{id_hexs[8:12]}-{id_hexs[12:16]}-{id_hexs[16:20]}-{id_hexs[20:]}}}' - + + def get_platform_id(self) -> str: + """ Get Intel BIOS Guard Platform ID """ + + id_byte: bytes = bytes(self.PlatformID) + + id_text: str = re.sub(r'[\n\t\r\x00 ]', '', id_byte.decode('utf-8', 'ignore')) + + id_hexs: str = f'{int.from_bytes(id_byte, "big"):0{0x10 * 2}X}' + id_guid: str = f'{{{id_hexs[:8]}-{id_hexs[8:12]}-{id_hexs[12:16]}-{id_hexs[16:20]}-{id_hexs[20:]}}}' + return f'{id_text} {id_guid}' - - def get_flags(self): + + def get_flags(self) -> tuple: + """ Get Intel BIOS Guard Header Attributes """ + attr = IntelBiosGuardHeaderGetAttributes() - attr.asbytes = self.Attributes - + + attr.asbytes = self.Attributes # pylint: disable=W0201 + return attr.b.SFAM, attr.b.ProtectEC, attr.b.GFXMitDis, attr.b.FTU, attr.b.Reserved - - def struct_print(self, p): - no_yes = ['No','Yes'] - f1,f2,f3,f4,f5 = self.get_flags() - - printer(['BIOS Guard Version :', f'{self.BGVerMajor}.{self.BGVerMinor}'], p, False) - printer(['Platform Identity :', self.get_platform_id()], p, False) - printer(['Signed Flash Address Map :', no_yes[f1]], p, False) - printer(['Protected EC OpCodes :', no_yes[f2]], p, False) - printer(['Graphics Security Disable :', no_yes[f3]], p, False) - printer(['Fault Tolerant Update :', no_yes[f4]], p, False) - printer(['Attributes Reserved :', f'0x{f5:X}'], p, False) - printer(['Script Version :', f'{self.ScriptVerMajor}.{self.ScriptVerMinor}'], p, False) - printer(['Script Size :', f'0x{self.ScriptSize:X}'], p, False) - printer(['Data Size :', f'0x{self.DataSize:X}'], p, False) - printer(['BIOS Security Version Number:', f'0x{self.BIOSSVN:X}'], p, False) - printer(['EC Security Version Number :', f'0x{self.ECSVN:X}'], p, False) - printer(['Vendor Information :', f'0x{self.VendorInfo:X}'], p, False) - + + def struct_print(self, padd: int) -> None: + """ Display structure information """ + + no_yes: dict[int, str] = {0: 'No', 1: 'Yes'} + + sfam, ec_opc, gfx_dis, ft_upd, attr_res = self.get_flags() + + printer(['BIOS Guard Version :', f'{self.BGVerMajor}.{self.BGVerMinor}'], padd, False) + printer(['Platform Identity :', self.get_platform_id()], padd, False) + printer(['Signed Flash Address Map :', no_yes[sfam]], padd, False) + printer(['Protected EC OpCodes :', no_yes[ec_opc]], padd, False) + printer(['Graphics Security Disable :', no_yes[gfx_dis]], padd, False) + printer(['Fault Tolerant Update :', no_yes[ft_upd]], padd, False) + printer(['Attributes Reserved :', f'0x{attr_res:X}'], padd, False) + printer(['Script Version :', f'{self.ScriptVerMajor}.{self.ScriptVerMinor}'], padd, False) + printer(['Script Size :', f'0x{self.ScriptSize:X}'], padd, False) + printer(['Data Size :', f'0x{self.DataSize:X}'], padd, False) + printer(['BIOS Security Version Number:', f'0x{self.BIOSSVN:X}'], padd, False) + printer(['EC Security Version Number :', f'0x{self.ECSVN:X}'], padd, False) + printer(['Vendor Information :', f'0x{self.VendorInfo:X}'], padd, False) + + class IntelBiosGuardHeaderAttributes(ctypes.LittleEndianStructure): + """ Intel BIOS Guard Header Attributes """ + + _pack_ = 1 + _fields_ = [ - ('SFAM', uint32_t, 1), # Signed Flash Address Map - ('ProtectEC', uint32_t, 1), # Protected EC OpCodes - ('GFXMitDis', uint32_t, 1), # GFX Security Disable - ('FTU', uint32_t, 1), # Fault Tolerant Update - ('Reserved', uint32_t, 28) # Reserved/Unknown + ('SFAM', UInt32, 1), # Signed Flash Address Map + ('ProtectEC', UInt32, 1), # Protected EC OpCodes + ('GFXMitDis', UInt32, 1), # GFX Security Disable + ('FTU', UInt32, 1), # Fault Tolerant Update + ('Reserved', UInt32, 28) # Reserved/Unknown ] + class IntelBiosGuardHeaderGetAttributes(ctypes.Union): + """ Intel BIOS Guard Header Attributes Getter """ + + _pack_ = 1 + _fields_ = [ ('b', IntelBiosGuardHeaderAttributes), - ('asbytes', uint32_t) + ('asbytes', UInt32) ] -class IntelBiosGuardSignature2k(ctypes.LittleEndianStructure): + +class IntelBiosGuardSignatureHeader(ctypes.LittleEndianStructure): + """ Intel BIOS Guard Signature Header """ + _pack_ = 1 - _fields_ = [ - ('Unknown0', uint32_t), # 0x000 - ('Unknown1', uint32_t), # 0x004 - ('Modulus', uint32_t*64), # 0x008 - ('Exponent', uint32_t), # 0x108 - ('Signature', uint32_t*64), # 0x10C - # 0x20C - ] - - def struct_print(self, p): - Modulus = f'{int.from_bytes(self.Modulus, "little"):0{0x100 * 2}X}' - Signature = f'{int.from_bytes(self.Signature, "little"):0{0x100 * 2}X}' - - printer(['Unknown 0:', f'0x{self.Unknown0:X}'], p, False) - printer(['Unknown 1:', f'0x{self.Unknown1:X}'], p, False) - printer(['Modulus :', f'{Modulus[:32]} [...]'], p, False) - printer(['Exponent :', f'0x{self.Exponent:X}'], p, False) - printer(['Signature:', f'{Signature[:32]} [...]'], p, False) -def is_ami_pfat(input_file): - input_buffer = file_to_bytes(input_file) - + _fields_ = [ + ('Unknown0', UInt32), # 0x000 + ('Unknown1', UInt32), # 0x004 + # 0x8 + ] + + def struct_print(self, padd: int) -> None: + """ Display structure information """ + + printer(['Unknown 0:', f'0x{self.Unknown0:X}'], padd, False) + printer(['Unknown 1:', f'0x{self.Unknown1:X}'], padd, False) + + +class IntelBiosGuardSignatureRsa2k(ctypes.LittleEndianStructure): + """ Intel BIOS Guard Signature Block 2048-bit """ + + _pack_ = 1 + + # noinspection PyTypeChecker + _fields_ = [ + ('Modulus', UInt8 * 256), # 0x000 + ('Exponent', UInt32), # 0x100 + ('Signature', UInt8 * 256), # 0x104 + # 0x204 + ] + + def struct_print(self, padd: int) -> None: + """ Display structure information """ + + printer(['Modulus :', f'{bytes_to_hex(self.Modulus, "little", 0x100, 32)} [...]'], padd, False) + printer(['Exponent :', f'0x{self.Exponent:X}'], padd, False) + printer(['Signature:', f'{bytes_to_hex(self.Signature, "little", 0x100, 32)} [...]'], padd, False) + + +class IntelBiosGuardSignatureRsa3k(ctypes.LittleEndianStructure): + """ Intel BIOS Guard Signature Block 3072-bit """ + + _pack_ = 1 + + # noinspection PyTypeChecker + _fields_ = [ + ('Modulus', UInt8 * 384), # 0x000 + ('Exponent', UInt32), # 0x180 + ('Signature', UInt8 * 384), # 0x184 + # 0x304 + ] + + def struct_print(self, padd: int) -> None: + """ Display structure information """ + + printer(['Modulus :', f'{int.from_bytes(self.Modulus, "little"):0{0x180 * 2}X}'[:64]], padd, False) + printer(['Exponent :', f'0x{self.Exponent:X}'], padd, False) + printer(['Signature:', f'{int.from_bytes(self.Signature, "little"):0{0x180 * 2}X}'[:64]], padd, False) + + +def is_ami_pfat(input_object: str | bytes | bytearray) -> bool: + """ Check if input is AMI BIOS Guard """ + + input_buffer: bytes = file_to_bytes(input_object) + return bool(get_ami_pfat(input_buffer)) -def get_ami_pfat(input_file): - input_buffer = file_to_bytes(input_file) - + +def get_ami_pfat(input_object: str | bytes | bytearray) -> bytes: + """ Get actual AMI BIOS Guard buffer """ + + input_buffer: bytes = file_to_bytes(input_object) + match = PAT_AMI_PFAT.search(input_buffer) - + return input_buffer[match.start() - 0x8:] if match else b'' -def get_file_name(index, name): + +def get_file_name(index: int, name: str) -> str: + """ Create AMI BIOS Guard output filename """ + return safe_name(f'{index:02d} -- {name}') -def parse_bg_script(script_data, padding=0): - is_opcode_div = len(script_data) % 8 == 0 - + +def parse_bg_script(script_data: bytes, padding: int = 0) -> int: + """ Process Intel BIOS Guard Script """ + + is_opcode_div: bool = len(script_data) % 8 == 0 + if not is_opcode_div: - printer('Error: Script is not divisible by OpCode length!', padding, False) - + printer('Error: BIOS Guard script is not divisible by OpCode length!', padding, False) + return 1 - - is_begin_end = script_data[:8] + script_data[-8:] == b'\x01' + b'\x00' * 7 + b'\xFF' + b'\x00' * 7 - + + is_begin_end: bool = script_data[:8] + script_data[-8:] == b'\x01' + b'\x00' * 7 + b'\xFF' + b'\x00' * 7 + if not is_begin_end: - printer('Error: Script lacks Begin and/or End OpCodes!', padding, False) - + printer('Error: BIOS Guard script lacks Begin and/or End OpCodes!', padding, False) + return 2 - - BigScript = get_bgs_tool() - - if not BigScript: + + big_script = get_bgs_tool() + + if not big_script: printer('Note: BIOS Guard Script Tool optional dependency is missing!', padding, False) - + return 3 - - script = BigScript(code_bytes=script_data).to_string().replace('\t',' ').split('\n') - + + script = big_script(code_bytes=script_data).to_string().replace('\t', ' ').split('\n') + for opcode in script: - if opcode.endswith(('begin','end')): spacing = padding - elif opcode.endswith(':'): spacing = padding + 4 - else: spacing = padding + 12 - + if opcode.endswith(('begin', 'end')): + spacing: int = padding + elif opcode.endswith(':'): + spacing = padding + 4 + else: + spacing = padding + 12 + operands = [operand for operand in opcode.split(' ') if operand] - printer(('{:<12s}' + '{:<11s}' * (len(operands) - 1)).format(*operands), spacing, False) - + + # Largest opcode length is 11 (erase64kblk) and largest operand length is 10 (0xAABBCCDD). + printer(f'{operands[0]:11s}{"".join((f" {o:10s}" for o in operands[1:]))}', spacing, False) + return 0 -def parse_pfat_hdr(buffer, padding=0): - block_all = [] - + +def parse_bg_sign(input_data: bytes, sign_offset: int, print_info: bool = False, padding: int = 0) -> int: + """ Process Intel BIOS Guard Signature """ + + bg_sig_hdr = get_struct(input_data, sign_offset, IntelBiosGuardSignatureHeader) + + if bg_sig_hdr.Unknown0 == 1: + # Unknown0 = 1, Unknown1 = 1 + bg_sig_rsa_struct = IntelBiosGuardSignatureRsa2k + else: + # Unknown0 = 2, Unknown1 = 3 + bg_sig_rsa_struct = IntelBiosGuardSignatureRsa3k + + bg_sig_rsa = get_struct(input_data, sign_offset + PFAT_BLK_SIG_LEN, bg_sig_rsa_struct) + + if print_info: + bg_sig_hdr.struct_print(padding) + + bg_sig_rsa.struct_print(padding) + + # Total size of Signature Header and RSA Structure + return PFAT_BLK_SIG_LEN + ctypes.sizeof(bg_sig_rsa_struct) + + +def parse_pfat_hdr(buffer: bytes | bytearray, padding: int = 0) -> tuple: + """ Parse AMI BIOS Guard Header """ + + block_all: list = [] + pfat_hdr = get_struct(buffer, 0x0, AmiBiosGuardHeader) - - hdr_size = pfat_hdr.Size - hdr_data = buffer[PFAT_AMI_HDR_LEN:hdr_size] - hdr_text = hdr_data.decode('utf-8').splitlines() - + + hdr_size: int = pfat_hdr.Size + + hdr_data: bytes = buffer[PFAT_AMI_HDR_LEN:hdr_size] + + hdr_text: list[str] = hdr_data.decode('utf-8').splitlines() + printer('AMI BIOS Guard Header:\n', padding) - + pfat_hdr.struct_print(padding + 4) - - hdr_title,*hdr_files = hdr_text - - files_count = len(hdr_files) - - hdr_tag,*hdr_indexes = hdr_title.split('II') - + + hdr_title, *hdr_files = hdr_text + + files_count: int = len(hdr_files) + + hdr_tag, *hdr_indexes = hdr_title.split('II') + printer(hdr_tag + '\n', padding + 4) - - bgt_indexes = [int(h, 16) for h in re.findall(r'.{1,4}', hdr_indexes[0])] if hdr_indexes else [] - - for index,entry in enumerate(hdr_files): - entry_parts = entry.split(';') - - info = entry_parts[0].split() - name = entry_parts[1] - - flags = int(info[0]) - param = info[1] - count = int(info[2]) - - order = get_ordinal((bgt_indexes[index] if bgt_indexes else index) + 1) - - desc = f'{name} (Index: {index + 1:02d}, Flash: {order}, Parameter: {param}, Flags: 0x{flags:X}, Blocks: {count})' - + + bgt_indexes: list = [int(h, 16) for h in re.findall(r'.{1,4}', hdr_indexes[0])] if hdr_indexes else [] + + for index, entry in enumerate(hdr_files): + entry_parts: list = entry.split(';') + + info: list = entry_parts[0].split() + + name: str = entry_parts[1] + + flags: int = int(info[0]) + + param: str = info[1] + + count: int = int(info[2]) + + order: str = get_ordinal((bgt_indexes[index] if bgt_indexes else index) + 1) + + desc = f'{name} (Index: {index + 1:02d}, Flash: {order}, ' \ + f'Parameter: {param}, Flags: 0x{flags:X}, Blocks: {count})' + block_all += [(desc, name, order, param, flags, index, i, count) for i in range(count)] - + _ = [printer(block[0], padding + 8, False) for block in block_all if block[6] == 0] - + return block_all, hdr_size, files_count -def parse_pfat_file(input_file, extract_path, padding=0): - input_buffer = file_to_bytes(input_file) - - pfat_buffer = get_ami_pfat(input_buffer) - - file_path = '' - all_blocks_dict = {} - - extract_name = os.path.basename(extract_path).rstrip(extract_suffix()) - + +def parse_pfat_file(input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> int: + """ Process and store AMI BIOS Guard output file """ + + input_buffer: bytes = file_to_bytes(input_object) + + pfat_buffer: bytes = get_ami_pfat(input_buffer) + + file_path: str = '' + + all_blocks_dict: dict = {} + + extract_name: str = path_name(extract_path).removesuffix(extract_suffix()) + make_dirs(extract_path, delete=True) - - block_all,block_off,file_count = parse_pfat_hdr(pfat_buffer, padding) + + block_all, block_off, file_count = parse_pfat_hdr(pfat_buffer, padding) for block in block_all: - file_desc,file_name,_,_,_,file_index,block_index,block_count = block - + file_desc, file_name, _, _, _, file_index, block_index, block_count = block + if block_index == 0: printer(file_desc, padding + 4) - + file_path = os.path.join(extract_path, get_file_name(file_index + 1, file_name)) - + all_blocks_dict[file_index] = b'' - - block_status = f'{block_index + 1}/{block_count}' - + + block_status: str = f'{block_index + 1}/{block_count}' + bg_hdr = get_struct(pfat_buffer, block_off, IntelBiosGuardHeader) - + printer(f'Intel BIOS Guard {block_status} Header:\n', padding + 8) - + bg_hdr.struct_print(padding + 12) - - bg_script_bgn = block_off + PFAT_BLK_HDR_LEN - bg_script_end = bg_script_bgn + bg_hdr.ScriptSize - bg_script_bin = pfat_buffer[bg_script_bgn:bg_script_end] - - bg_data_bgn = bg_script_end - bg_data_end = bg_data_bgn + bg_hdr.DataSize - bg_data_bin = pfat_buffer[bg_data_bgn:bg_data_end] - block_off = bg_data_end # Assume next block starts at data end + bg_script_bgn: int = block_off + PFAT_BLK_HDR_LEN + bg_script_end: int = bg_script_bgn + bg_hdr.ScriptSize + + bg_data_bgn: int = bg_script_end + bg_data_end: int = bg_data_bgn + bg_hdr.DataSize + + bg_data_bin: bytes = pfat_buffer[bg_data_bgn:bg_data_end] + + block_off: int = bg_data_end # Assume next block starts at data end + + is_sfam, _, _, _, _ = bg_hdr.get_flags() # SFAM, ProtectEC, GFXMitDis, FTU, Reserved - is_sfam,_,_,_,_ = bg_hdr.get_flags() # SFAM, ProtectEC, GFXMitDis, FTU, Reserved - if is_sfam: - bg_sig_bgn = bg_data_end - bg_sig_end = bg_sig_bgn + PFAT_BLK_S2K_LEN - bg_sig_bin = pfat_buffer[bg_sig_bgn:bg_sig_end] - - if len(bg_sig_bin) == PFAT_BLK_S2K_LEN: - bg_sig = get_struct(bg_sig_bin, 0x0, IntelBiosGuardSignature2k) - - printer(f'Intel BIOS Guard {block_status} Signature:\n', padding + 8) - - bg_sig.struct_print(padding + 12) + printer(f'Intel BIOS Guard {block_status} Signature:\n', padding + 8) + + # Adjust next block to start after current block Data + Signature + block_off += parse_bg_sign(pfat_buffer, bg_data_end, True, padding + 12) - block_off = bg_sig_end # Adjust next block to start at data + signature end - printer(f'Intel BIOS Guard {block_status} Script:\n', padding + 8) - - _ = parse_bg_script(bg_script_bin, padding + 12) - + + _ = parse_bg_script(pfat_buffer[bg_script_bgn:bg_script_end], padding + 12) + with open(file_path, 'ab') as out_dat: out_dat.write(bg_data_bin) - + all_blocks_dict[file_index] += bg_data_bin - - pfat_oob_data = pfat_buffer[block_off:] # Store out-of-bounds data after the end of PFAT files - - pfat_oob_name = get_file_name(file_count + 1, f'{extract_name}_OOB.bin') - - pfat_oob_path = os.path.join(extract_path, pfat_oob_name) - + + if block_index + 1 == block_count: + if is_ami_pfat(all_blocks_dict[file_index]): + parse_pfat_file(all_blocks_dict[file_index], get_extract_path(file_path), padding + 8) + + pfat_oob_data: bytes = pfat_buffer[block_off:] # Store out-of-bounds data after the end of PFAT files + + pfat_oob_name: str = get_file_name(file_count + 1, f'{extract_name}_OOB.bin') + + pfat_oob_path: str = os.path.join(extract_path, pfat_oob_name) + with open(pfat_oob_path, 'wb') as out_oob: out_oob.write(pfat_oob_data) - + if is_ami_pfat(pfat_oob_data): parse_pfat_file(pfat_oob_data, get_extract_path(pfat_oob_path), padding) - - in_all_data = b''.join([block[1] for block in sorted(all_blocks_dict.items())]) - - in_all_name = get_file_name(0, f'{extract_name}_ALL.bin') - - in_all_path = os.path.join(extract_path, in_all_name) - + + in_all_data: bytes = b''.join([block[1] for block in sorted(all_blocks_dict.items())]) + + in_all_name: str = get_file_name(0, f'{extract_name}_ALL.bin') + + in_all_path: str = os.path.join(extract_path, in_all_name) + with open(in_all_path, 'wb') as out_all: out_all.write(in_all_data + pfat_oob_data) - + return 0 -PFAT_AMI_HDR_LEN = ctypes.sizeof(AmiBiosGuardHeader) -PFAT_BLK_HDR_LEN = ctypes.sizeof(IntelBiosGuardHeader) -PFAT_BLK_S2K_LEN = ctypes.sizeof(IntelBiosGuardSignature2k) + +PFAT_AMI_HDR_LEN: int = ctypes.sizeof(AmiBiosGuardHeader) +PFAT_BLK_HDR_LEN: int = ctypes.sizeof(IntelBiosGuardHeader) +PFAT_BLK_SIG_LEN: int = ctypes.sizeof(IntelBiosGuardSignatureHeader) if __name__ == '__main__': - BIOSUtility(TITLE, is_ami_pfat, parse_pfat_file).run_utility() + BIOSUtility(title=TITLE, check=is_ami_pfat, main=parse_pfat_file).run_utility() diff --git a/AMI_UCP_Extract.py b/AMI_UCP_Extract.py index 2f59e6f..3c9639c 100644 --- a/AMI_UCP_Extract.py +++ b/AMI_UCP_Extract.py @@ -1,29 +1,23 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ AMI UCP Extract AMI UCP Update Extractor -Copyright (C) 2021-2022 Plato Mavropoulos +Copyright (C) 2021-2024 Plato Mavropoulos """ -TITLE = 'AMI UCP Update Extractor v2.0_a20' - +import contextlib +import ctypes import os import re -import sys import struct -import ctypes -import contextlib - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.checksums import get_chk_16 from common.comp_efi import efi_decompress, is_efi_compressed -from common.path_ops import agnostic_path, make_dirs, safe_name, safe_path, get_extract_path +from common.path_ops import agnostic_path, get_extract_path, make_dirs, safe_name, safe_path from common.patterns import PAT_AMI_UCP, PAT_INTEL_ENG -from common.struct_ops import char, get_struct, uint8_t, uint16_t, uint32_t +from common.struct_ops import Char, get_struct, UInt8, UInt16, UInt32 from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes, to_string @@ -31,419 +25,477 @@ from common.text_ops import file_to_bytes, to_string from AMI_PFAT_Extract import is_ami_pfat, parse_pfat_file from Insyde_IFD_Extract import insyde_ifd_extract, is_insyde_ifd +TITLE = 'AMI UCP Update Extractor v3.0' + + class UafHeader(ctypes.LittleEndianStructure): + """ UAF Header """ + _pack_ = 1 _fields_ = [ - ('ModuleTag', char*4), # 0x00 - ('ModuleSize', uint32_t), # 0x04 - ('Checksum', uint16_t), # 0x08 - ('Unknown0', uint8_t), # 0x0A - ('Unknown1', uint8_t), # 0x0A - ('Reserved', uint8_t*4), # 0x0C + ('ModuleTag', Char * 4), # 0x00 + ('ModuleSize', UInt32), # 0x04 + ('Checksum', UInt16), # 0x08 + ('Unknown0', UInt8), # 0x0A + ('Unknown1', UInt8), # 0x0A + ('Reserved', UInt8 * 4), # 0x0C # 0x10 ] - + def _get_reserved(self): res_bytes = bytes(self.Reserved) - + res_hex = f'0x{int.from_bytes(res_bytes, "big"):0{0x4 * 2}X}' - - res_str = re.sub(r'[\n\t\r\x00 ]', '', res_bytes.decode('utf-8','ignore')) - + + res_str = re.sub(r'[\n\t\r\x00 ]', '', res_bytes.decode('utf-8', 'ignore')) + res_txt = f' ({res_str})' if len(res_str) else '' - + return f'{res_hex}{res_txt}' - - def struct_print(self, p): - printer(['Tag :', self.ModuleTag.decode('utf-8')], p, False) - printer(['Size :', f'0x{self.ModuleSize:X}'], p, False) - printer(['Checksum :', f'0x{self.Checksum:04X}'], p, False) - printer(['Unknown 0 :', f'0x{self.Unknown0:02X}'], p, False) - printer(['Unknown 1 :', f'0x{self.Unknown1:02X}'], p, False) - printer(['Reserved :', self._get_reserved()], p, False) + + def struct_print(self, padd): + """ Display structure information """ + + printer(['Tag :', self.ModuleTag.decode('utf-8')], padd, False) + printer(['Size :', f'0x{self.ModuleSize:X}'], padd, False) + printer(['Checksum :', f'0x{self.Checksum:04X}'], padd, False) + printer(['Unknown 0 :', f'0x{self.Unknown0:02X}'], padd, False) + printer(['Unknown 1 :', f'0x{self.Unknown1:02X}'], padd, False) + printer(['Reserved :', self._get_reserved()], padd, False) + class UafModule(ctypes.LittleEndianStructure): + """ UAF Module """ + _pack_ = 1 _fields_ = [ - ('CompressSize', uint32_t), # 0x00 - ('OriginalSize', uint32_t), # 0x04 + ('CompressSize', UInt32), # 0x00 + ('OriginalSize', UInt32), # 0x04 # 0x08 ] - - def struct_print(self, p, filename, description): - printer(['Compress Size:', f'0x{self.CompressSize:X}'], p, False) - printer(['Original Size:', f'0x{self.OriginalSize:X}'], p, False) - printer(['Filename :', filename], p, False) - printer(['Description :', description], p, False) + + def struct_print(self, padd, filename, description): + """ Display structure information """ + + printer(['Compress Size:', f'0x{self.CompressSize:X}'], padd, False) + printer(['Original Size:', f'0x{self.OriginalSize:X}'], padd, False) + printer(['Filename :', filename], padd, False) + printer(['Description :', description], padd, False) + class UiiHeader(ctypes.LittleEndianStructure): + """ UII Header """ + _pack_ = 1 _fields_ = [ - ('UIISize', uint16_t), # 0x00 - ('Checksum', uint16_t), # 0x02 - ('UtilityVersion', uint32_t), # 0x04 AFU|BGT (Unknown, Signed) - ('InfoSize', uint16_t), # 0x08 - ('SupportBIOS', uint8_t), # 0x0A - ('SupportOS', uint8_t), # 0x0B - ('DataBusWidth', uint8_t), # 0x0C - ('ProgramType', uint8_t), # 0x0D - ('ProgramMode', uint8_t), # 0x0E - ('SourceSafeRel', uint8_t), # 0x0F + ('UIISize', UInt16), # 0x00 + ('Checksum', UInt16), # 0x02 + ('UtilityVersion', UInt32), # 0x04 AFU|BGT (Unknown, Signed) + ('InfoSize', UInt16), # 0x08 + ('SupportBIOS', UInt8), # 0x0A + ('SupportOS', UInt8), # 0x0B + ('DataBusWidth', UInt8), # 0x0C + ('ProgramType', UInt8), # 0x0D + ('ProgramMode', UInt8), # 0x0E + ('SourceSafeRel', UInt8), # 0x0F # 0x10 ] - + SBI = {1: 'ALL', 2: 'AMIBIOS8', 3: 'UEFI', 4: 'AMIBIOS8/UEFI'} SOS = {1: 'DOS', 2: 'EFI', 3: 'Windows', 4: 'Linux', 5: 'FreeBSD', 6: 'MacOS', 128: 'Multi-Platform'} DBW = {1: '16b', 2: '16/32b', 3: '32b', 4: '64b'} PTP = {1: 'Executable', 2: 'Library', 3: 'Driver'} PMD = {1: 'API', 2: 'Console', 3: 'GUI', 4: 'Console/GUI'} - - def struct_print(self, p, description): - SupportBIOS = self.SBI.get(self.SupportBIOS, f'Unknown ({self.SupportBIOS})') - SupportOS = self.SOS.get(self.SupportOS, f'Unknown ({self.SupportOS})') - DataBusWidth = self.DBW.get(self.DataBusWidth, f'Unknown ({self.DataBusWidth})') - ProgramType = self.PTP.get(self.ProgramType, f'Unknown ({self.ProgramType})') - ProgramMode = self.PMD.get(self.ProgramMode, f'Unknown ({self.ProgramMode})') - - printer(['UII Size :', f'0x{self.UIISize:X}'], p, False) - printer(['Checksum :', f'0x{self.Checksum:04X}'], p, False) - printer(['Tool Version :', f'0x{self.UtilityVersion:08X}'], p, False) - printer(['Info Size :', f'0x{self.InfoSize:X}'], p, False) - printer(['Supported BIOS:', SupportBIOS], p, False) - printer(['Supported OS :', SupportOS], p, False) - printer(['Data Bus Width:', DataBusWidth], p, False) - printer(['Program Type :', ProgramType], p, False) - printer(['Program Mode :', ProgramMode], p, False) - printer(['SourceSafe Tag:', f'{self.SourceSafeRel:02d}'], p, False) - printer(['Description :', description], p, False) + + def struct_print(self, padd, description): + """ Display structure information """ + + support_bios = self.SBI.get(self.SupportBIOS, f'Unknown ({self.SupportBIOS})') + support_os = self.SOS.get(self.SupportOS, f'Unknown ({self.SupportOS})') + data_bus_width = self.DBW.get(self.DataBusWidth, f'Unknown ({self.DataBusWidth})') + program_type = self.PTP.get(self.ProgramType, f'Unknown ({self.ProgramType})') + program_mode = self.PMD.get(self.ProgramMode, f'Unknown ({self.ProgramMode})') + + printer(['UII Size :', f'0x{self.UIISize:X}'], padd, False) + printer(['Checksum :', f'0x{self.Checksum:04X}'], padd, False) + printer(['Tool Version :', f'0x{self.UtilityVersion:08X}'], padd, False) + printer(['Info Size :', f'0x{self.InfoSize:X}'], padd, False) + printer(['Supported BIOS:', support_bios], padd, False) + printer(['Supported OS :', support_os], padd, False) + printer(['Data Bus Width:', data_bus_width], padd, False) + printer(['Program Type :', program_type], padd, False) + printer(['Program Mode :', program_mode], padd, False) + printer(['SourceSafe Tag:', f'{self.SourceSafeRel:02d}'], padd, False) + printer(['Description :', description], padd, False) + class DisHeader(ctypes.LittleEndianStructure): + """ DIS Header """ + _pack_ = 1 _fields_ = [ - ('PasswordSize', uint16_t), # 0x00 - ('EntryCount', uint16_t), # 0x02 - ('Password', char*12), # 0x04 + ('PasswordSize', UInt16), # 0x00 + ('EntryCount', UInt16), # 0x02 + ('Password', Char * 12), # 0x04 # 0x10 ] - - def struct_print(self, p): - printer(['Password Size:', f'0x{self.PasswordSize:X}'], p, False) - printer(['Entry Count :', self.EntryCount], p, False) - printer(['Password :', self.Password.decode('utf-8')], p, False) + + def struct_print(self, padd): + """ Display structure information """ + + printer(['Password Size:', f'0x{self.PasswordSize:X}'], padd, False) + printer(['Entry Count :', self.EntryCount], padd, False) + printer(['Password :', self.Password.decode('utf-8')], padd, False) + class DisModule(ctypes.LittleEndianStructure): + """ DIS Module """ + _pack_ = 1 _fields_ = [ - ('EnabledDisabled', uint8_t), # 0x00 - ('ShownHidden', uint8_t), # 0x01 - ('Command', char*32), # 0x02 - ('Description', char*256), # 0x22 + ('EnabledDisabled', UInt8), # 0x00 + ('ShownHidden', UInt8), # 0x01 + ('Command', Char * 32), # 0x02 + ('Description', Char * 256), # 0x22 # 0x122 ] - + ENDIS = {0: 'Disabled', 1: 'Enabled'} SHOWN = {0: 'Hidden', 1: 'Shown', 2: 'Shown Only'} - - def struct_print(self, p): - EnabledDisabled = self.ENDIS.get(self.EnabledDisabled, f'Unknown ({self.EnabledDisabled})') - ShownHidden = self.SHOWN.get(self.ShownHidden, f'Unknown ({self.ShownHidden})') - - printer(['State :', EnabledDisabled], p, False) - printer(['Display :', ShownHidden], p, False) - printer(['Command :', self.Command.decode('utf-8').strip()], p, False) - printer(['Description:', self.Description.decode('utf-8').strip()], p, False) -# Validate UCP Module Checksum-16 + def struct_print(self, padd): + """ Display structure information """ + + enabled_disabled = self.ENDIS.get(self.EnabledDisabled, f'Unknown ({self.EnabledDisabled})') + shown_hidden = self.SHOWN.get(self.ShownHidden, f'Unknown ({self.ShownHidden})') + + printer(['State :', enabled_disabled], padd, False) + printer(['Display :', shown_hidden], padd, False) + printer(['Command :', self.Command.decode('utf-8').strip()], padd, False) + printer(['Description:', self.Description.decode('utf-8').strip()], padd, False) + + def chk16_validate(data, tag, padd=0): + """ Validate UCP Module Checksum-16 """ + if get_chk_16(data) != 0: printer(f'Error: Invalid UCP Module {tag} Checksum!', padd, pause=True) else: printer(f'Checksum of UCP Module {tag} is valid!', padd) -# Check if input is AMI UCP image + def is_ami_ucp(in_file): + """ Check if input is AMI UCP image """ + buffer = file_to_bytes(in_file) - + return bool(get_ami_ucp(buffer)[0] is not None) -# Get all input file AMI UCP patterns + def get_ami_ucp(in_file): + """ Get all input file AMI UCP patterns """ + buffer = file_to_bytes(in_file) - - uaf_len_max = 0x0 # Length of largest detected @UAF|@HPU - uaf_buf_bin = None # Buffer of largest detected @UAF|@HPU - uaf_buf_tag = '@UAF' # Tag of largest detected @UAF|@HPU - + + uaf_len_max = 0x0 # Length of largest detected @UAF|@HPU + uaf_buf_bin = None # Buffer of largest detected @UAF|@HPU + uaf_buf_tag = '@UAF' # Tag of largest detected @UAF|@HPU + for uaf in PAT_AMI_UCP.finditer(buffer): uaf_len_cur = int.from_bytes(buffer[uaf.start() + 0x4:uaf.start() + 0x8], 'little') - + if uaf_len_cur > uaf_len_max: uaf_len_max = uaf_len_cur + uaf_hdr_off = uaf.start() + uaf_buf_bin = buffer[uaf_hdr_off:uaf_hdr_off + uaf_len_max] - uaf_buf_tag = uaf.group(0)[:4].decode('utf-8','ignore') - + + uaf_buf_tag = uaf.group(0)[:4].decode('utf-8', 'ignore') + return uaf_buf_bin, uaf_buf_tag -# Get list of @UAF|@HPU Modules + def get_uaf_mod(buffer, uaf_off=0x0): - uaf_all = [] # Initialize list of all @UAF|@HPU Modules - - while buffer[uaf_off] == 0x40: # ASCII of @ is 0x40 - uaf_hdr = get_struct(buffer, uaf_off, UafHeader) # Parse @UAF|@HPU Module Structure - - uaf_tag = uaf_hdr.ModuleTag.decode('utf-8') # Get unique @UAF|@HPU Module Tag - - uaf_all.append([uaf_tag, uaf_off, uaf_hdr]) # Store @UAF|@HPU Module Info - - uaf_off += uaf_hdr.ModuleSize # Adjust to next @UAF|@HPU Module offset - + """ Get list of @UAF|@HPU Modules """ + + uaf_all = [] # Initialize list of all @UAF|@HPU Modules + + while buffer[uaf_off] == 0x40: # ASCII of @ is 0x40 + uaf_hdr = get_struct(buffer, uaf_off, UafHeader) # Parse @UAF|@HPU Module Structure + + uaf_tag = uaf_hdr.ModuleTag.decode('utf-8') # Get unique @UAF|@HPU Module Tag + + uaf_all.append([uaf_tag, uaf_off, uaf_hdr]) # Store @UAF|@HPU Module Info + + uaf_off += uaf_hdr.ModuleSize # Adjust to next @UAF|@HPU Module offset + if uaf_off >= len(buffer): - break # Stop parsing at EOF - + break # Stop parsing at EOF + # Check if @UAF|@HPU Module @NAL exists and place it first # Parsing @NAL first allows naming all @UAF|@HPU Modules - for mod_idx,mod_val in enumerate(uaf_all): + for mod_idx, mod_val in enumerate(uaf_all): if mod_val[0] == '@NAL': - uaf_all.insert(1, uaf_all.pop(mod_idx)) # After UII for visual purposes - - break # @NAL found, skip the rest - + uaf_all.insert(1, uaf_all.pop(mod_idx)) # After UII for visual purposes + + break # @NAL found, skip the rest + return uaf_all -# Parse & Extract AMI UCP structures + def ucp_extract(in_file, extract_path, padding=0, checksum=False): + """ Parse & Extract AMI UCP structures """ + input_buffer = file_to_bytes(in_file) - - nal_dict = {} # Initialize @NAL Dictionary per UCP - + + nal_dict = {} # Initialize @NAL Dictionary per UCP + printer('Utility Configuration Program', padding) - + make_dirs(extract_path, delete=True) - + # Get best AMI UCP Pattern match based on @UAF|@HPU Size - ucp_buffer,ucp_tag = get_ami_ucp(input_buffer) - - uaf_hdr = get_struct(ucp_buffer, 0, UafHeader) # Parse @UAF|@HPU Header Structure - + ucp_buffer, ucp_tag = get_ami_ucp(input_buffer) + + uaf_hdr = get_struct(ucp_buffer, 0, UafHeader) # Parse @UAF|@HPU Header Structure + printer(f'Utility Auxiliary File > {ucp_tag}:\n', padding + 4) - + uaf_hdr.struct_print(padding + 8) - - fake = struct.pack(' @UAF|@HPU Module/Section + def uaf_extract(buffer, extract_path, mod_info, padding=0, checksum=False, nal_dict=None): + """ Parse & Extract AMI UCP > @UAF|@HPU Module/Section """ + if nal_dict is None: nal_dict = {} - - uaf_tag,uaf_off,uaf_hdr = mod_info - - uaf_data_all = buffer[uaf_off:uaf_off + uaf_hdr.ModuleSize] # @UAF|@HPU Module Entire Data - - uaf_data_mod = uaf_data_all[UAF_HDR_LEN:] # @UAF|@HPU Module EFI Data - - uaf_data_raw = uaf_data_mod[UAF_MOD_LEN:] # @UAF|@HPU Module Raw Data - + + uaf_tag, uaf_off, uaf_hdr = mod_info + + uaf_data_all = buffer[uaf_off:uaf_off + uaf_hdr.ModuleSize] # @UAF|@HPU Module Entire Data + + uaf_data_mod = uaf_data_all[UAF_HDR_LEN:] # @UAF|@HPU Module EFI Data + + uaf_data_raw = uaf_data_mod[UAF_MOD_LEN:] # @UAF|@HPU Module Raw Data + printer(f'Utility Auxiliary File > {uaf_tag}:\n', padding) - - uaf_hdr.struct_print(padding + 4) # Print @UAF|@HPU Module Info - - uaf_mod = get_struct(buffer, uaf_off + UAF_HDR_LEN, UafModule) # Parse UAF Module EFI Structure - - is_comp = uaf_mod.CompressSize != uaf_mod.OriginalSize # Detect @UAF|@HPU Module EFI Compression - + + uaf_hdr.struct_print(padding + 4) # Print @UAF|@HPU Module Info + + uaf_mod = get_struct(buffer, uaf_off + UAF_HDR_LEN, UafModule) # Parse UAF Module EFI Structure + + is_comp = uaf_mod.CompressSize != uaf_mod.OriginalSize # Detect @UAF|@HPU Module EFI Compression + if uaf_tag in nal_dict: - uaf_name = nal_dict[uaf_tag][1] # Always prefer @NAL naming first + uaf_name = nal_dict[uaf_tag][1] # Always prefer @NAL naming first elif uaf_tag in UAF_TAG_DICT: - uaf_name = UAF_TAG_DICT[uaf_tag][0] # Otherwise use built-in naming + uaf_name = UAF_TAG_DICT[uaf_tag][0] # Otherwise use built-in naming elif uaf_tag == '@ROM': - uaf_name = 'BIOS.bin' # BIOS/PFAT Firmware (w/o Signature) + uaf_name = 'BIOS.bin' # BIOS/PFAT Firmware (w/o Signature) elif uaf_tag.startswith('@R0'): - uaf_name = f'BIOS_0{uaf_tag[3:]}.bin' # BIOS/PFAT Firmware + uaf_name = f'BIOS_0{uaf_tag[3:]}.bin' # BIOS/PFAT Firmware elif uaf_tag.startswith('@S0'): - uaf_name = f'BIOS_0{uaf_tag[3:]}.sig' # BIOS/PFAT Signature + uaf_name = f'BIOS_0{uaf_tag[3:]}.sig' # BIOS/PFAT Signature elif uaf_tag.startswith('@DR'): - uaf_name = f'DROM_0{uaf_tag[3:]}.bin' # Thunderbolt Retimer Firmware + uaf_name = f'DROM_0{uaf_tag[3:]}.bin' # Thunderbolt Retimer Firmware elif uaf_tag.startswith('@DS'): - uaf_name = f'DROM_0{uaf_tag[3:]}.sig' # Thunderbolt Retimer Signature + uaf_name = f'DROM_0{uaf_tag[3:]}.sig' # Thunderbolt Retimer Signature elif uaf_tag.startswith('@EC'): - uaf_name = f'EC_0{uaf_tag[3:]}.bin' # Embedded Controller Firmware + uaf_name = f'EC_0{uaf_tag[3:]}.bin' # Embedded Controller Firmware elif uaf_tag.startswith('@ME'): - uaf_name = f'ME_0{uaf_tag[3:]}.bin' # Management Engine Firmware + uaf_name = f'ME_0{uaf_tag[3:]}.bin' # Management Engine Firmware else: - uaf_name = uaf_tag # Could not name the @UAF|@HPU Module, use Tag instead - + uaf_name = uaf_tag # Could not name the @UAF|@HPU Module, use Tag instead + uaf_fext = '' if uaf_name != uaf_tag else '.bin' - + uaf_fdesc = UAF_TAG_DICT[uaf_tag][1] if uaf_tag in UAF_TAG_DICT else uaf_name - - uaf_mod.struct_print(padding + 4, uaf_name + uaf_fext, uaf_fdesc) # Print @UAF|@HPU Module EFI Info - + + uaf_mod.struct_print(padding + 4, uaf_name + uaf_fext, uaf_fdesc) # Print @UAF|@HPU Module EFI Info + # Check if unknown @UAF|@HPU Module Tag is present in @NAL but not in built-in dictionary - if uaf_tag in nal_dict and uaf_tag not in UAF_TAG_DICT and not uaf_tag.startswith(('@ROM','@R0','@S0','@DR','@DS')): - printer(f'Note: Detected new AMI UCP Module {uaf_tag} ({nal_dict[uaf_tag][1]}) in @NAL!', padding + 4, pause=True) - + if uaf_tag in nal_dict and uaf_tag not in UAF_TAG_DICT and \ + not uaf_tag.startswith(('@ROM', '@R0', '@S0', '@DR', '@DS')): + + printer(f'Note: Detected new AMI UCP Module {uaf_tag} ({nal_dict[uaf_tag][1]}) in @NAL!', + padding + 4, pause=True) + # Generate @UAF|@HPU Module File name, depending on whether decompression will be required uaf_sname = safe_name(uaf_name + ('.temp' if is_comp else uaf_fext)) + if uaf_tag in nal_dict: uaf_npath = safe_path(extract_path, nal_dict[uaf_tag][0]) + make_dirs(uaf_npath, exist_ok=True) + uaf_fname = safe_path(uaf_npath, uaf_sname) else: uaf_fname = safe_path(extract_path, uaf_sname) - + if checksum: chk16_validate(uaf_data_all, uaf_tag, padding + 4) - + # Parse Utility Identification Information @UAF|@HPU Module (@UII) if uaf_tag == '@UII': - info_hdr = get_struct(uaf_data_raw, 0, UiiHeader) # Parse @UII Module Raw Structure - - info_data = uaf_data_raw[max(UII_HDR_LEN,info_hdr.InfoSize):info_hdr.UIISize] # @UII Module Info Data - + info_hdr = get_struct(uaf_data_raw, 0, UiiHeader) # Parse @UII Module Raw Structure + + info_data = uaf_data_raw[max(UII_HDR_LEN, info_hdr.InfoSize):info_hdr.UIISize] # @UII Module Info Data + # Get @UII Module Info/Description text field - info_desc = info_data.decode('utf-8','ignore').strip('\x00 ') - + info_desc = info_data.decode('utf-8', 'ignore').strip('\x00 ') + printer('Utility Identification Information:\n', padding + 4) - - info_hdr.struct_print(padding + 8, info_desc) # Print @UII Module Info - + + info_hdr.struct_print(padding + 8, info_desc) # Print @UII Module Info + if checksum: chk16_validate(uaf_data_raw, '@UII > Info', padding + 8) - + # Store/Save @UII Module Info in file with open(uaf_fname[:-4] + '.txt', 'a', encoding='utf-8') as uii_out: with contextlib.redirect_stdout(uii_out): - info_hdr.struct_print(0, info_desc) # Store @UII Module Info - + info_hdr.struct_print(0, info_desc) # Store @UII Module Info + # Adjust @UAF|@HPU Module Raw Data for extraction if is_comp: # Some Compressed @UAF|@HPU Module EFI data lack necessary EOF padding if uaf_mod.CompressSize > len(uaf_data_raw): comp_padd = b'\x00' * (uaf_mod.CompressSize - len(uaf_data_raw)) - uaf_data_raw = uaf_data_mod[:UAF_MOD_LEN] + uaf_data_raw + comp_padd # Add missing padding for decompression + + # Add missing padding for decompression + uaf_data_raw = uaf_data_mod[:UAF_MOD_LEN] + uaf_data_raw + comp_padd else: - uaf_data_raw = uaf_data_mod[:UAF_MOD_LEN] + uaf_data_raw # Add the EFI/Tiano Compression info before Raw Data + # Add the EFI/Tiano Compression info before Raw Data + uaf_data_raw = uaf_data_mod[:UAF_MOD_LEN] + uaf_data_raw else: - uaf_data_raw = uaf_data_raw[:uaf_mod.OriginalSize] # No compression, extend to end of Original @UAF|@HPU Module size - + # No compression, extend to end of Original @UAF|@HPU Module size + uaf_data_raw = uaf_data_raw[:uaf_mod.OriginalSize] + # Store/Save @UAF|@HPU Module file - if uaf_tag != '@UII': # Skip @UII binary, already parsed + if uaf_tag != '@UII': # Skip @UII binary, already parsed with open(uaf_fname, 'wb') as uaf_out: uaf_out.write(uaf_data_raw) - + # @UAF|@HPU Module EFI/Tiano Decompression if is_comp and is_efi_compressed(uaf_data_raw, False): - dec_fname = uaf_fname.replace('.temp', uaf_fext) # Decompressed @UAF|@HPU Module file path - + dec_fname = uaf_fname.replace('.temp', uaf_fext) # Decompressed @UAF|@HPU Module file path + if efi_decompress(uaf_fname, dec_fname, padding + 4) == 0: with open(dec_fname, 'rb') as dec: - uaf_data_raw = dec.read() # Read back the @UAF|@HPU Module decompressed Raw data - - os.remove(uaf_fname) # Successful decompression, delete compressed @UAF|@HPU Module file - - uaf_fname = dec_fname # Adjust @UAF|@HPU Module file path to the decompressed one - + uaf_data_raw = dec.read() # Read back the @UAF|@HPU Module decompressed Raw data + + os.remove(uaf_fname) # Successful decompression, delete compressed @UAF|@HPU Module file + + uaf_fname = dec_fname # Adjust @UAF|@HPU Module file path to the decompressed one + # Process and Print known text only @UAF|@HPU Modules (after EFI/Tiano Decompression) if uaf_tag in UAF_TAG_DICT and UAF_TAG_DICT[uaf_tag][2] == 'Text': printer(f'{UAF_TAG_DICT[uaf_tag][1]}:', padding + 4) - printer(uaf_data_raw.decode('utf-8','ignore'), padding + 8) - + + printer(uaf_data_raw.decode('utf-8', 'ignore'), padding + 8) + # Parse Default Command Status @UAF|@HPU Module (@DIS) if len(uaf_data_raw) and uaf_tag == '@DIS': - dis_hdr = get_struct(uaf_data_raw, 0x0, DisHeader) # Parse @DIS Module Raw Header Structure - + dis_hdr = get_struct(uaf_data_raw, 0x0, DisHeader) # Parse @DIS Module Raw Header Structure + printer('Default Command Status Header:\n', padding + 4) - - dis_hdr.struct_print(padding + 8) # Print @DIS Module Raw Header Info - + + dis_hdr.struct_print(padding + 8) # Print @DIS Module Raw Header Info + # Store/Save @DIS Module Header Info in file with open(uaf_fname[:-3] + 'txt', 'a', encoding='utf-8') as dis: with contextlib.redirect_stdout(dis): - dis_hdr.struct_print(0) # Store @DIS Module Header Info - - dis_data = uaf_data_raw[DIS_HDR_LEN:] # @DIS Module Entries Data - + dis_hdr.struct_print(0) # Store @DIS Module Header Info + + dis_data = uaf_data_raw[DIS_HDR_LEN:] # @DIS Module Entries Data + # Parse all @DIS Module Entries for mod_idx in range(dis_hdr.EntryCount): - dis_mod = get_struct(dis_data, mod_idx * DIS_MOD_LEN, DisModule) # Parse @DIS Module Raw Entry Structure - + dis_mod = get_struct(dis_data, mod_idx * DIS_MOD_LEN, DisModule) # Parse @DIS Module Raw Entry Structure + printer(f'Default Command Status Entry {mod_idx + 1:02d}/{dis_hdr.EntryCount:02d}:\n', padding + 8) - - dis_mod.struct_print(padding + 12) # Print @DIS Module Raw Entry Info - + + dis_mod.struct_print(padding + 12) # Print @DIS Module Raw Entry Info + # Store/Save @DIS Module Entry Info in file with open(uaf_fname[:-3] + 'txt', 'a', encoding='utf-8') as dis: with contextlib.redirect_stdout(dis): printer() - dis_mod.struct_print(4) # Store @DIS Module Entry Info - - os.remove(uaf_fname) # Delete @DIS Module binary, info exported as text - + + dis_mod.struct_print(4) # Store @DIS Module Entry Info + + os.remove(uaf_fname) # Delete @DIS Module binary, info exported as text + # Parse Name List @UAF|@HPU Module (@NAL) - if len(uaf_data_raw) >= 5 and (uaf_tag,uaf_data_raw[0],uaf_data_raw[4]) == ('@NAL',0x40,0x3A): - nal_info = uaf_data_raw.decode('utf-8','ignore').replace('\r','').strip().split('\n') - + if len(uaf_data_raw) >= 5 and (uaf_tag, uaf_data_raw[0], uaf_data_raw[4]) == ('@NAL', 0x40, 0x3A): + nal_info = uaf_data_raw.decode('utf-8', 'ignore').replace('\r', '').strip().split('\n') + printer('AMI UCP Module Name List:\n', padding + 4) - + # Parse all @NAL Module Entries for info in nal_info: - info_tag,info_value = info.split(':',1) - - printer(f'{info_tag} : {info_value}', padding + 8, False) # Print @NAL Module Tag-Path Info - - info_part = agnostic_path(info_value).parts # Split OS agnostic path in parts - info_path = to_string(info_part[1:-1], os.sep) # Get path without drive/root or file - info_name = info_part[-1] # Get file from last path part - - nal_dict[info_tag] = (info_path,info_name) # Assign a file path & name to each Tag - + info_tag, info_value = info.split(':', 1) + + printer(f'{info_tag} : {info_value}', padding + 8, False) # Print @NAL Module Tag-Path Info + + info_part = agnostic_path(info_value).parts # Split OS agnostic path in parts + + info_path = to_string(info_part[1:-1], os.sep) # Get path without drive/root or file + + info_name = info_part[-1] # Get file from last path part + + nal_dict[info_tag] = (info_path, info_name) # Assign a file path & name to each Tag + # Parse Insyde BIOS @UAF|@HPU Module (@INS) if uaf_tag == '@INS' and is_insyde_ifd(uaf_fname): - ins_dir = os.path.join(extract_path, safe_name(f'{uaf_tag}_nested-IFD')) # Generate extraction directory - + ins_dir = os.path.join(extract_path, safe_name(f'{uaf_tag}_nested-IFD')) # Generate extraction directory + if insyde_ifd_extract(uaf_fname, get_extract_path(ins_dir), padding + 4) == 0: - os.remove(uaf_fname) # Delete raw nested Insyde IFD image after successful extraction - + os.remove(uaf_fname) # Delete raw nested Insyde IFD image after successful extraction + # Detect & Unpack AMI BIOS Guard (PFAT) BIOS image if is_ami_pfat(uaf_data_raw): pfat_dir = os.path.join(extract_path, safe_name(uaf_name)) - + parse_pfat_file(uaf_data_raw, get_extract_path(pfat_dir), padding + 4) - - os.remove(uaf_fname) # Delete raw PFAT BIOS image after successful extraction - + + os.remove(uaf_fname) # Delete raw PFAT BIOS image after successful extraction + # Detect Intel Engine firmware image and show ME Analyzer advice if uaf_tag.startswith('@ME') and PAT_INTEL_ENG.search(uaf_data_raw): printer('Intel Management Engine (ME) Firmware:\n', padding + 4) printer('Use "ME Analyzer" from https://github.com/platomav/MEAnalyzer', padding + 8, False) - + # Parse Nested AMI UCP image if is_ami_ucp(uaf_data_raw): - uaf_dir = os.path.join(extract_path, safe_name(f'{uaf_tag}_nested-UCP')) # Generate extraction directory - - ucp_extract(uaf_data_raw, get_extract_path(uaf_dir), padding + 4, checksum) # Call recursively - - os.remove(uaf_fname) # Delete raw nested AMI UCP image after successful extraction - + uaf_dir = os.path.join(extract_path, safe_name(f'{uaf_tag}_nested-UCP')) # Generate extraction directory + + ucp_extract(uaf_data_raw, get_extract_path(uaf_dir), padding + 4, checksum) # Call recursively + + os.remove(uaf_fname) # Delete raw nested AMI UCP image after successful extraction + return nal_dict + # Get common ctypes Structure Sizes UAF_HDR_LEN = ctypes.sizeof(UafHeader) UAF_MOD_LEN = ctypes.sizeof(UafModule) @@ -453,63 +505,66 @@ UII_HDR_LEN = ctypes.sizeof(UiiHeader) # AMI UCP Tag Dictionary UAF_TAG_DICT = { - '@3FI' : ['HpBiosUpdate32.efi', 'HpBiosUpdate32.efi', ''], - '@3S2' : ['HpBiosUpdate32.s12', 'HpBiosUpdate32.s12', ''], - '@3S4' : ['HpBiosUpdate32.s14', 'HpBiosUpdate32.s14', ''], - '@3S9' : ['HpBiosUpdate32.s09', 'HpBiosUpdate32.s09', ''], - '@3SG' : ['HpBiosUpdate32.sig', 'HpBiosUpdate32.sig', ''], - '@AMI' : ['UCP_Nested.bin', 'Nested AMI UCP', ''], - '@B12' : ['BiosMgmt.s12', 'BiosMgmt.s12', ''], - '@B14' : ['BiosMgmt.s14', 'BiosMgmt.s14', ''], - '@B32' : ['BiosMgmt32.s12', 'BiosMgmt32.s12', ''], - '@B34' : ['BiosMgmt32.s14', 'BiosMgmt32.s14', ''], - '@B39' : ['BiosMgmt32.s09', 'BiosMgmt32.s09', ''], - '@B3E' : ['BiosMgmt32.efi', 'BiosMgmt32.efi', ''], - '@BM9' : ['BiosMgmt.s09', 'BiosMgmt.s09', ''], - '@BME' : ['BiosMgmt.efi', 'BiosMgmt.efi', ''], - '@CKV' : ['Check_Version.txt', 'Check Version', 'Text'], - '@CMD' : ['AFU_Command.txt', 'AMI AFU Command', 'Text'], - '@CML' : ['CMOSD4.txt', 'CMOS Item Number-Value (MSI)', 'Text'], - '@CMS' : ['CMOSD4.exe', 'Get or Set CMOS Item (MSI)', ''], - '@CPM' : ['AC_Message.txt', 'Confirm Power Message', ''], - '@DCT' : ['DevCon32.exe', 'Device Console WIN32', ''], - '@DCX' : ['DevCon64.exe', 'Device Console WIN64', ''], - '@DFE' : ['HpDevFwUpdate.efi', 'HpDevFwUpdate.efi', ''], - '@DFS' : ['HpDevFwUpdate.s12', 'HpDevFwUpdate.s12', ''], - '@DIS' : ['Command_Status.bin', 'Default Command Status', ''], - '@ENB' : ['ENBG64.exe', 'ENBG64.exe', ''], - '@HPU' : ['UCP_Main.bin', 'Utility Auxiliary File (HP)', ''], - '@INS' : ['Insyde_Nested.bin', 'Nested Insyde SFX', ''], - '@M32' : ['HpBiosMgmt32.s12', 'HpBiosMgmt32.s12', ''], - '@M34' : ['HpBiosMgmt32.s14', 'HpBiosMgmt32.s14', ''], - '@M39' : ['HpBiosMgmt32.s09', 'HpBiosMgmt32.s09', ''], - '@M3I' : ['HpBiosMgmt32.efi', 'HpBiosMgmt32.efi', ''], - '@MEC' : ['FWUpdLcl.txt', 'Intel FWUpdLcl Command', 'Text'], - '@MED' : ['FWUpdLcl_DOS.exe', 'Intel FWUpdLcl DOS', ''], - '@MET' : ['FWUpdLcl_WIN32.exe', 'Intel FWUpdLcl WIN32', ''], - '@MFI' : ['HpBiosMgmt.efi', 'HpBiosMgmt.efi', ''], - '@MS2' : ['HpBiosMgmt.s12', 'HpBiosMgmt.s12', ''], - '@MS4' : ['HpBiosMgmt.s14', 'HpBiosMgmt.s14', ''], - '@MS9' : ['HpBiosMgmt.s09', 'HpBiosMgmt.s09', ''], - '@NAL' : ['UCP_List.txt', 'AMI UCP Module Name List', ''], - '@OKM' : ['OK_Message.txt', 'OK Message', ''], - '@PFC' : ['BGT_Command.txt', 'AMI BGT Command', 'Text'], - '@R3I' : ['CryptRSA32.efi', 'CryptRSA32.efi', ''], - '@RFI' : ['CryptRSA.efi', 'CryptRSA.efi', ''], - '@UAF' : ['UCP_Main.bin', 'Utility Auxiliary File (AMI)', ''], - '@UFI' : ['HpBiosUpdate.efi', 'HpBiosUpdate.efi', ''], - '@UII' : ['UCP_Info.txt', 'Utility Identification Information', ''], - '@US2' : ['HpBiosUpdate.s12', 'HpBiosUpdate.s12', ''], - '@US4' : ['HpBiosUpdate.s14', 'HpBiosUpdate.s14', ''], - '@US9' : ['HpBiosUpdate.s09', 'HpBiosUpdate.s09', ''], - '@USG' : ['HpBiosUpdate.sig', 'HpBiosUpdate.sig', ''], - '@VER' : ['OEM_Version.txt', 'OEM Version', 'Text'], - '@VXD' : ['amifldrv.vxd', 'amifldrv.vxd', ''], - '@W32' : ['amifldrv32.sys', 'amifldrv32.sys', ''], - '@W64' : ['amifldrv64.sys', 'amifldrv64.sys', ''], + '@3FI': ['HpBiosUpdate32.efi', 'HpBiosUpdate32.efi', ''], + '@3S2': ['HpBiosUpdate32.s12', 'HpBiosUpdate32.s12', ''], + '@3S4': ['HpBiosUpdate32.s14', 'HpBiosUpdate32.s14', ''], + '@3S9': ['HpBiosUpdate32.s09', 'HpBiosUpdate32.s09', ''], + '@3SG': ['HpBiosUpdate32.sig', 'HpBiosUpdate32.sig', ''], + '@AMI': ['UCP_Nested.bin', 'Nested AMI UCP', ''], + '@B12': ['BiosMgmt.s12', 'BiosMgmt.s12', ''], + '@B14': ['BiosMgmt.s14', 'BiosMgmt.s14', ''], + '@B32': ['BiosMgmt32.s12', 'BiosMgmt32.s12', ''], + '@B34': ['BiosMgmt32.s14', 'BiosMgmt32.s14', ''], + '@B39': ['BiosMgmt32.s09', 'BiosMgmt32.s09', ''], + '@B3E': ['BiosMgmt32.efi', 'BiosMgmt32.efi', ''], + '@BM9': ['BiosMgmt.s09', 'BiosMgmt.s09', ''], + '@BME': ['BiosMgmt.efi', 'BiosMgmt.efi', ''], + '@CKV': ['Check_Version.txt', 'Check Version', 'Text'], + '@CMD': ['AFU_Command.txt', 'AMI AFU Command', 'Text'], + '@CML': ['CMOSD4.txt', 'CMOS Item Number-Value (MSI)', 'Text'], + '@CMS': ['CMOSD4.exe', 'Get or Set CMOS Item (MSI)', ''], + '@CPM': ['AC_Message.txt', 'Confirm Power Message', ''], + '@DCT': ['DevCon32.exe', 'Device Console WIN32', ''], + '@DCX': ['DevCon64.exe', 'Device Console WIN64', ''], + '@DFE': ['HpDevFwUpdate.efi', 'HpDevFwUpdate.efi', ''], + '@DFS': ['HpDevFwUpdate.s12', 'HpDevFwUpdate.s12', ''], + '@DIS': ['Command_Status.bin', 'Default Command Status', ''], + '@ENB': ['ENBG64.exe', 'ENBG64.exe', ''], + '@HPU': ['UCP_Main.bin', 'Utility Auxiliary File (HP)', ''], + '@INS': ['Insyde_Nested.bin', 'Nested Insyde SFX', ''], + '@M32': ['HpBiosMgmt32.s12', 'HpBiosMgmt32.s12', ''], + '@M34': ['HpBiosMgmt32.s14', 'HpBiosMgmt32.s14', ''], + '@M39': ['HpBiosMgmt32.s09', 'HpBiosMgmt32.s09', ''], + '@M3I': ['HpBiosMgmt32.efi', 'HpBiosMgmt32.efi', ''], + '@MEC': ['FWUpdLcl.txt', 'Intel FWUpdLcl Command', 'Text'], + '@MED': ['FWUpdLcl_DOS.exe', 'Intel FWUpdLcl DOS', ''], + '@MET': ['FWUpdLcl_WIN32.exe', 'Intel FWUpdLcl WIN32', ''], + '@MFI': ['HpBiosMgmt.efi', 'HpBiosMgmt.efi', ''], + '@MS2': ['HpBiosMgmt.s12', 'HpBiosMgmt.s12', ''], + '@MS4': ['HpBiosMgmt.s14', 'HpBiosMgmt.s14', ''], + '@MS9': ['HpBiosMgmt.s09', 'HpBiosMgmt.s09', ''], + '@NAL': ['UCP_List.txt', 'AMI UCP Module Name List', ''], + '@OKM': ['OK_Message.txt', 'OK Message', ''], + '@PFC': ['BGT_Command.txt', 'AMI BGT Command', 'Text'], + '@R3I': ['CryptRSA32.efi', 'CryptRSA32.efi', ''], + '@RFI': ['CryptRSA.efi', 'CryptRSA.efi', ''], + '@UAF': ['UCP_Main.bin', 'Utility Auxiliary File (AMI)', ''], + '@UFI': ['HpBiosUpdate.efi', 'HpBiosUpdate.efi', ''], + '@UII': ['UCP_Info.txt', 'Utility Identification Information', ''], + '@US2': ['HpBiosUpdate.s12', 'HpBiosUpdate.s12', ''], + '@US4': ['HpBiosUpdate.s14', 'HpBiosUpdate.s14', ''], + '@US9': ['HpBiosUpdate.s09', 'HpBiosUpdate.s09', ''], + '@USG': ['HpBiosUpdate.sig', 'HpBiosUpdate.sig', ''], + '@VER': ['OEM_Version.txt', 'OEM Version', 'Text'], + '@VXD': ['amifldrv.vxd', 'amifldrv.vxd', ''], + '@W32': ['amifldrv32.sys', 'amifldrv32.sys', ''], + '@W64': ['amifldrv64.sys', 'amifldrv64.sys', ''], + '@D64': ['amifldrv64.sys', 'amifldrv64.sys', ''], } if __name__ == '__main__': - utility = BIOSUtility(TITLE, is_ami_ucp, ucp_extract) - utility.parse_argument('-c', '--checksum', help='verify AMI UCP Checksums (slow)', action='store_true') + utility_args = [(['-c', '--checksum'], {'help': 'verify AMI UCP Checksums (slow)', 'action': 'store_true'})] + + utility = BIOSUtility(title=TITLE, check=is_ami_ucp, main=ucp_extract, args=utility_args) + utility.run_utility() diff --git a/Apple_EFI_ID.py b/Apple_EFI_ID.py index 1003b67..657f7bb 100644 --- a/Apple_EFI_ID.py +++ b/Apple_EFI_ID.py @@ -1,167 +1,181 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Apple EFI ID Apple EFI Image Identifier -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'Apple EFI Image Identifier v2.0_a5' - -import os -import sys -import zlib -import struct import ctypes +import logging +import os +import struct import subprocess +import zlib -# Stop __pycache__ generation -sys.dont_write_bytecode = True - -from common.externals import get_uefifind_path, get_uefiextract_path +from common.externals import get_uefiextract_path, get_uefifind_path from common.path_ops import del_dirs, path_parent, path_suffixes from common.patterns import PAT_APPLE_EFI -from common.struct_ops import char, get_struct, uint8_t +from common.struct_ops import Char, get_struct, UInt8 from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes +TITLE = 'Apple EFI Image Identifier v3.0' + + class IntelBiosId(ctypes.LittleEndianStructure): + """ Intel BIOS ID Structure """ + _pack_ = 1 _fields_ = [ - ('Signature', char*8), # 0x00 - ('BoardID', uint8_t*16), # 0x08 - ('Dot1', uint8_t*2), # 0x18 - ('BoardExt', uint8_t*6), # 0x1A - ('Dot2', uint8_t*2), # 0x20 - ('VersionMajor', uint8_t*8), # 0x22 - ('Dot3', uint8_t*2), # 0x2A - ('BuildType', uint8_t*2), # 0x2C - ('VersionMinor', uint8_t*4), # 0x2E - ('Dot4', uint8_t*2), # 0x32 - ('Year', uint8_t*4), # 0x34 - ('Month', uint8_t*4), # 0x38 - ('Day', uint8_t*4), # 0x3C - ('Hour', uint8_t*4), # 0x40 - ('Minute', uint8_t*4), # 0x44 - ('NullTerminator', uint8_t*2), # 0x48 + ('Signature', Char * 8), # 0x00 + ('BoardID', UInt8 * 16), # 0x08 + ('Dot1', UInt8 * 2), # 0x18 + ('BoardExt', UInt8 * 6), # 0x1A + ('Dot2', UInt8 * 2), # 0x20 + ('VersionMajor', UInt8 * 8), # 0x22 + ('Dot3', UInt8 * 2), # 0x2A + ('BuildType', UInt8 * 2), # 0x2C + ('VersionMinor', UInt8 * 4), # 0x2E + ('Dot4', UInt8 * 2), # 0x32 + ('Year', UInt8 * 4), # 0x34 + ('Month', UInt8 * 4), # 0x38 + ('Day', UInt8 * 4), # 0x3C + ('Hour', UInt8 * 4), # 0x40 + ('Minute', UInt8 * 4), # 0x44 + ('NullTerminator', UInt8 * 2), # 0x48 # 0x4A ] - - # https://github.com/tianocore/edk2-platforms/blob/master/Platform/Intel/BoardModulePkg/Include/Guid/BiosId.h - - @staticmethod - def decode(field): - return struct.pack('B' * len(field), *field).decode('utf-16','ignore').strip('\x00 ') - - def get_bios_id(self): - BoardID = self.decode(self.BoardID) - BoardExt = self.decode(self.BoardExt) - VersionMajor = self.decode(self.VersionMajor) - BuildType = self.decode(self.BuildType) - VersionMinor = self.decode(self.VersionMinor) - BuildDate = f'20{self.decode(self.Year)}-{self.decode(self.Month)}-{self.decode(self.Day)}' - BuildTime = f'{self.decode(self.Hour)}-{self.decode(self.Minute)}' - - return BoardID, BoardExt, VersionMajor, BuildType, VersionMinor, BuildDate, BuildTime - - def struct_print(self, p): - BoardID,BoardExt,VersionMajor,BuildType,VersionMinor,BuildDate,BuildTime = self.get_bios_id() - - printer(['Intel Signature:', self.Signature.decode('utf-8')], p, False) - printer(['Board Identity: ', BoardID], p, False) - printer(['Apple Identity: ', BoardExt], p, False) - printer(['Major Version: ', VersionMajor], p, False) - printer(['Minor Version: ', VersionMinor], p, False) - printer(['Build Type: ', BuildType], p, False) - printer(['Build Date: ', BuildDate], p, False) - printer(['Build Time: ', BuildTime.replace('-',':')], p, False) -# Check if input is Apple EFI image + # https://github.com/tianocore/edk2-platforms/blob/master/Platform/Intel/BoardModulePkg/Include/Guid/BiosId.h + + @staticmethod + def _decode(field): + return struct.pack('B' * len(field), *field).decode('utf-16', 'ignore').strip('\x00 ') + + def get_bios_id(self): + """ Create Apple EFI BIOS ID """ + + board_id = self._decode(self.BoardID) + board_ext = self._decode(self.BoardExt) + version_major = self._decode(self.VersionMajor) + build_type = self._decode(self.BuildType) + version_minor = self._decode(self.VersionMinor) + build_date = f'20{self._decode(self.Year)}-{self._decode(self.Month)}-{self._decode(self.Day)}' + build_time = f'{self._decode(self.Hour)}-{self._decode(self.Minute)}' + + return board_id, board_ext, version_major, build_type, version_minor, build_date, build_time + + def struct_print(self, padd): + """ Display structure information """ + + board_id, board_ext, version_major, build_type, version_minor, build_date, build_time = self.get_bios_id() + + printer(['Intel Signature:', self.Signature.decode('utf-8')], padd, False) + printer(['Board Identity: ', board_id], padd, False) + printer(['Apple Identity: ', board_ext], padd, False) + printer(['Major Version: ', version_major], padd, False) + printer(['Minor Version: ', version_minor], padd, False) + printer(['Build Type: ', build_type], padd, False) + printer(['Build Date: ', build_date], padd, False) + printer(['Build Time: ', build_time.replace('-', ':')], padd, False) + + def is_apple_efi(input_file): + """ Check if input is Apple EFI image """ + input_buffer = file_to_bytes(input_file) - + if PAT_APPLE_EFI.search(input_buffer): return True - + if not os.path.isfile(input_file): return False - - try: - _ = subprocess.run([get_uefifind_path(), input_file, 'body', 'list', PAT_UEFIFIND], - check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - return True - except Exception: - return False -# Parse & Identify (or Rename) Apple EFI image + try: + _ = subprocess.run([get_uefifind_path(), input_file, 'body', 'list', PAT_UEFIFIND], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + return True + except Exception as error: # pylint: disable=broad-except + logging.debug('Error: Could not check if input is Apple EFI image: %s', error) + + return False + + def apple_efi_identify(input_file, extract_path, padding=0, rename=False): + """ Parse & Identify (or Rename) Apple EFI image """ + if not os.path.isfile(input_file): printer('Error: Could not find input file path!', padding) - + return 1 - + input_buffer = file_to_bytes(input_file) - - bios_id_match = PAT_APPLE_EFI.search(input_buffer) # Detect $IBIOSI$ pattern - + + bios_id_match = PAT_APPLE_EFI.search(input_buffer) # Detect $IBIOSI$ pattern + if bios_id_match: bios_id_res = f'0x{bios_id_match.start():X}' - + bios_id_hdr = get_struct(input_buffer, bios_id_match.start(), IntelBiosId) else: # The $IBIOSI$ pattern is within EFI compressed modules so we need to use UEFIFind and UEFIExtract try: bios_id_res = subprocess.check_output([get_uefifind_path(), input_file, 'body', 'list', PAT_UEFIFIND], - text=True)[:36] - - del_dirs(extract_path) # UEFIExtract must create its output folder itself, make sure it is not present - + text=True)[:36] + + del_dirs(extract_path) # UEFIExtract must create its output folder itself, make sure it is not present + _ = subprocess.run([get_uefiextract_path(), input_file, bios_id_res, '-o', extract_path, '-m', 'body'], - check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + with open(os.path.join(extract_path, 'body.bin'), 'rb') as raw_body: body_buffer = raw_body.read() - - bios_id_match = PAT_APPLE_EFI.search(body_buffer) # Detect decompressed $IBIOSI$ pattern - + + bios_id_match = PAT_APPLE_EFI.search(body_buffer) # Detect decompressed $IBIOSI$ pattern + bios_id_hdr = get_struct(body_buffer, bios_id_match.start(), IntelBiosId) - - del_dirs(extract_path) # Successful UEFIExtract extraction, remove its output (temp) folder - except Exception: - printer('Error: Failed to parse compressed $IBIOSI$ pattern!', padding) - + + del_dirs(extract_path) # Successful UEFIExtract extraction, remove its output (temp) folder + except Exception as error: # pylint: disable=broad-except + printer(f'Error: Failed to parse compressed $IBIOSI$ pattern: {error}!', padding) + return 2 - + printer(f'Detected $IBIOSI$ at {bios_id_res}\n', padding) - + bios_id_hdr.struct_print(padding + 4) - + if rename: input_parent = path_parent(input_file) - + input_suffix = path_suffixes(input_file)[-1] - + input_adler32 = zlib.adler32(input_buffer) - - ID,Ext,Major,Type,Minor,Date,Time = bios_id_hdr.get_bios_id() - - output_name = f'{ID}_{Ext}_{Major}_{Type}{Minor}_{Date}_{Time}_{input_adler32:08X}{input_suffix}' - + + fw_id, fw_ext, fw_major, fw_type, fw_minor, fw_date, fw_time = bios_id_hdr.get_bios_id() + + output_name = f'{fw_id}_{fw_ext}_{fw_major}_{fw_type}{fw_minor}_{fw_date}_{fw_time}_' \ + f'{input_adler32:08X}{input_suffix}' + output_file = os.path.join(input_parent, output_name) - + if not os.path.isfile(output_file): - os.replace(input_file, output_file) # Rename input file based on its EFI tag - + os.replace(input_file, output_file) # Rename input file based on its EFI tag + printer(f'Renamed to {output_name}', padding) - + return 0 -PAT_UEFIFIND = f'244942494F534924{"."*32}2E00{"."*12}2E00{"."*16}2E00{"."*12}2E00{"."*40}0000' + +PAT_UEFIFIND = f'244942494F534924{"." * 32}2E00{"." * 12}2E00{"." * 16}2E00{"." * 12}2E00{"." * 40}0000' if __name__ == '__main__': - utility = BIOSUtility(TITLE, is_apple_efi, apple_efi_identify) - utility.parse_argument('-r', '--rename', help='rename EFI image based on its tag', action='store_true') + utility_args = [(['-r', '--rename'], {'help': 'rename EFI image based on its tag', 'action': 'store_true'})] + + utility = BIOSUtility(title=TITLE, check=is_apple_efi, main=apple_efi_identify, args=utility_args) + utility.run_utility() diff --git a/Apple_EFI_IM4P.py b/Apple_EFI_IM4P.py index 5dceefa..4faf66f 100644 --- a/Apple_EFI_IM4P.py +++ b/Apple_EFI_IM4P.py @@ -1,19 +1,13 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Apple EFI IM4P Apple EFI IM4P Splitter -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'Apple EFI IM4P Splitter v3.0_a5' - import os -import sys - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.path_ops import make_dirs, path_stem from common.patterns import PAT_APPLE_IM4P, PAT_INTEL_IFD @@ -21,125 +15,133 @@ from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes -# Check if input is Apple EFI IM4P image +TITLE = 'Apple EFI IM4P Splitter v4.0' + + def is_apple_im4p(input_file): + """ Check if input is Apple EFI IM4P image """ + input_buffer = file_to_bytes(input_file) - + is_im4p = PAT_APPLE_IM4P.search(input_buffer) - + is_ifd = PAT_INTEL_IFD.search(input_buffer) - + return bool(is_im4p and is_ifd) -# Parse & Split Apple EFI IM4P image -def apple_im4p_split(input_file, extract_path, padding=0): + +def apple_im4p_split(input_file, extract_path, padding=0): + """ Parse & Split Apple EFI IM4P image """ + exit_codes = [] - + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - + # Detect IM4P EFI pattern im4p_match = PAT_APPLE_IM4P.search(input_buffer) - + # After IM4P mefi (0x15), multi EFI payloads have _MEFIBIN (0x100) but is difficult to RE w/o varying samples. # However, _MEFIBIN is not required for splitting SPI images due to Intel Flash Descriptor Components Density. - + # IM4P mefi payload start offset mefi_data_bgn = im4p_match.start() + input_buffer[im4p_match.start() - 0x1] - + # IM4P mefi payload size mefi_data_len = int.from_bytes(input_buffer[im4p_match.end() + 0x5:im4p_match.end() + 0x9], 'big') - + # Check if mefi is followed by _MEFIBIN mefibin_exist = input_buffer[mefi_data_bgn:mefi_data_bgn + 0x8] == b'_MEFIBIN' - + # Actual multi EFI payloads start after _MEFIBIN efi_data_bgn = mefi_data_bgn + 0x100 if mefibin_exist else mefi_data_bgn - + # Actual multi EFI payloads size without _MEFIBIN efi_data_len = mefi_data_len - 0x100 if mefibin_exist else mefi_data_len - + # Adjust input file buffer to actual multi EFI payloads data input_buffer = input_buffer[efi_data_bgn:efi_data_bgn + efi_data_len] - + # Parse Intel Flash Descriptor pattern matches for ifd in PAT_INTEL_IFD.finditer(input_buffer): # Component Base Address from FD start (ICH8-ICH10 = 1, IBX = 2, CPT+ = 3) ifd_flmap0_fcba = input_buffer[ifd.start() + 0x4] * 0x10 - + # I/O Controller Hub (ICH) if ifd_flmap0_fcba == 0x10: # At ICH, Flash Descriptor starts at 0x0 ifd_bgn_substruct = 0x0 - + # 0xBC for [0xAC] + 0xFF * 16 sanity check ifd_end_substruct = 0xBC - + # Platform Controller Hub (PCH) else: # At PCH, Flash Descriptor starts at 0x10 ifd_bgn_substruct = 0x10 - + # 0xBC for [0xAC] + 0xFF * 16 sanity check ifd_end_substruct = 0xBC - + # Actual Flash Descriptor Start Offset ifd_match_start = ifd.start() - ifd_bgn_substruct - + # Actual Flash Descriptor End Offset ifd_match_end = ifd.end() - ifd_end_substruct - + # Calculate Intel Flash Descriptor Flash Component Total Size - + # Component Count (00 = 1, 01 = 2) ifd_flmap0_nc = ((int.from_bytes(input_buffer[ifd_match_end:ifd_match_end + 0x4], 'little') >> 8) & 3) + 1 - + # PCH/ICH Strap Length (ME 2-8 & TXE 0-2 & SPS 1-2 <= 0x12, ME 9+ & TXE 3+ & SPS 3+ >= 0x13) ifd_flmap1_isl = input_buffer[ifd_match_end + 0x7] - + # Component Density Byte (ME 2-8 & TXE 0-2 & SPS 1-2 = 0:5, ME 9+ & TXE 3+ & SPS 3+ = 0:7) ifd_comp_den = input_buffer[ifd_match_start + ifd_flmap0_fcba] - + # Component 1 Density Bits (ME 2-8 & TXE 0-2 & SPS 1-2 = 3, ME 9+ & TXE 3+ & SPS 3+ = 4) ifd_comp_1_bitwise = 0xF if ifd_flmap1_isl >= 0x13 else 0x7 - + # Component 2 Density Bits (ME 2-8 & TXE 0-2 & SPS 1-2 = 3, ME 9+ & TXE 3+ & SPS 3+ = 4) ifd_comp_2_bitwise = 0x4 if ifd_flmap1_isl >= 0x13 else 0x3 - + # Component 1 Density (FCBA > C0DEN) ifd_comp_all_size = IFD_COMP_LEN[ifd_comp_den & ifd_comp_1_bitwise] - + # Component 2 Density (FCBA > C1DEN) if ifd_flmap0_nc == 2: ifd_comp_all_size += IFD_COMP_LEN[ifd_comp_den >> ifd_comp_2_bitwise] - + ifd_data_bgn = ifd_match_start ifd_data_end = ifd_data_bgn + ifd_comp_all_size + ifd_data_txt = f'0x{ifd_data_bgn:07X}-0x{ifd_data_end:07X}' - + output_data = input_buffer[ifd_data_bgn:ifd_data_end] - + output_size = len(output_data) - + output_name = path_stem(input_file) if os.path.isfile(input_file) else 'Part' - + output_path = os.path.join(extract_path, f'{output_name}_[{ifd_data_txt}].fd') - + with open(output_path, 'wb') as output_image: output_image.write(output_data) - + printer(f'Split Apple EFI image at {ifd_data_txt}!', padding) - + if output_size != ifd_comp_all_size: printer(f'Error: Bad image size 0x{output_size:07X}, expected 0x{ifd_comp_all_size:07X}!', padding + 4) - + exit_codes.append(1) - + return sum(exit_codes) + # Intel Flash Descriptor Component Sizes (4MB, 8MB, 16MB and 32MB) IFD_COMP_LEN = {3: 0x400000, 4: 0x800000, 5: 0x1000000, 6: 0x2000000} if __name__ == '__main__': - BIOSUtility(TITLE, is_apple_im4p, apple_im4p_split).run_utility() + BIOSUtility(title=TITLE, check=is_apple_im4p, main=apple_im4p_split).run_utility() diff --git a/Apple_EFI_PBZX.py b/Apple_EFI_PBZX.py index 8e4f553..f11af8f 100644 --- a/Apple_EFI_PBZX.py +++ b/Apple_EFI_PBZX.py @@ -1,118 +1,128 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Apple PBZX Extract Apple EFI PBZX Extractor -Copyright (C) 2021-2022 Plato Mavropoulos +Copyright (C) 2021-2024 Plato Mavropoulos """ -TITLE = 'Apple EFI PBZX Extractor v1.0_a5' - -import os -import sys -import lzma import ctypes - -# Stop __pycache__ generation -sys.dont_write_bytecode = True +import logging +import lzma +import os from common.comp_szip import is_szip_supported, szip_decompress from common.path_ops import make_dirs, path_stem from common.patterns import PAT_APPLE_PBZX -from common.struct_ops import get_struct, uint32_t +from common.struct_ops import get_struct, UInt32 from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes +TITLE = 'Apple EFI PBZX Extractor v2.0' + + class PbzxChunk(ctypes.BigEndianStructure): + """ PBZX Chunk Header """ + _pack_ = 1 _fields_ = [ - ('Reserved0', uint32_t), # 0x00 - ('InitSize', uint32_t), # 0x04 - ('Reserved1', uint32_t), # 0x08 - ('CompSize', uint32_t), # 0x0C + ('Reserved0', UInt32), # 0x00 + ('InitSize', UInt32), # 0x04 + ('Reserved1', UInt32), # 0x08 + ('CompSize', UInt32), # 0x0C # 0x10 ] - - def struct_print(self, p): - printer(['Reserved 0 :', f'0x{self.Reserved0:X}'], p, False) - printer(['Initial Size :', f'0x{self.InitSize:X}'], p, False) - printer(['Reserved 1 :', f'0x{self.Reserved1:X}'], p, False) - printer(['Compressed Size:', f'0x{self.CompSize:X}'], p, False) -# Check if input is Apple PBZX image + def struct_print(self, padd): + """ Display structure information """ + + printer(['Reserved 0 :', f'0x{self.Reserved0:X}'], padd, False) + printer(['Initial Size :', f'0x{self.InitSize:X}'], padd, False) + printer(['Reserved 1 :', f'0x{self.Reserved1:X}'], padd, False) + printer(['Compressed Size:', f'0x{self.CompSize:X}'], padd, False) + + def is_apple_pbzx(input_file): + """ Check if input is Apple PBZX image """ + input_buffer = file_to_bytes(input_file) - + return bool(PAT_APPLE_PBZX.search(input_buffer[:0x4])) -# Parse & Extract Apple PBZX image + def apple_pbzx_extract(input_file, extract_path, padding=0): + """ Parse & Extract Apple PBZX image """ + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - - cpio_bin = b'' # Initialize PBZX > CPIO Buffer - cpio_len = 0x0 # Initialize PBZX > CPIO Length - - chunk_off = 0xC # First PBZX Chunk starts at 0xC + + cpio_bin = b'' # Initialize PBZX > CPIO Buffer + + cpio_len = 0x0 # Initialize PBZX > CPIO Length + + chunk_off = 0xC # First PBZX Chunk starts at 0xC while chunk_off < len(input_buffer): chunk_hdr = get_struct(input_buffer, chunk_off, PbzxChunk) - + printer(f'PBZX Chunk at 0x{chunk_off:08X}\n', padding) - + chunk_hdr.struct_print(padding + 4) - + # PBZX Chunk data starts after its Header comp_bgn = chunk_off + PBZX_CHUNK_HDR_LEN - + # To avoid a potential infinite loop, double-check Compressed Size comp_end = comp_bgn + max(chunk_hdr.CompSize, PBZX_CHUNK_HDR_LEN) - + comp_bin = input_buffer[comp_bgn:comp_end] - + try: # Attempt XZ decompression, if applicable to Chunk data cpio_bin += lzma.LZMADecompressor().decompress(comp_bin) - + printer('Successful LZMA decompression!', padding + 8) - except Exception: + except Exception as error: # pylint: disable=broad-except + logging.debug('Error: Failed to LZMA decompress PBZX Chunk 0x%X: %s', chunk_off, error) + # Otherwise, Chunk data is not compressed cpio_bin += comp_bin - + # Final CPIO size should match the sum of all Chunks > Initial Size cpio_len += chunk_hdr.InitSize - + # Next Chunk starts at the end of current Chunk's data chunk_off = comp_end - + # Check that CPIO size is valid based on all Chunks > Initial Size if cpio_len != len(cpio_bin): printer('Error: Unexpected CPIO archive size!', padding) - + return 1 - + cpio_name = path_stem(input_file) if os.path.isfile(input_file) else 'Payload' - + cpio_path = os.path.join(extract_path, f'{cpio_name}.cpio') - + with open(cpio_path, 'wb') as cpio_object: cpio_object.write(cpio_bin) - + # Decompress PBZX > CPIO archive with 7-Zip if is_szip_supported(cpio_path, padding, args=['-tCPIO'], check=True): if szip_decompress(cpio_path, extract_path, 'CPIO', padding, args=['-tCPIO'], check=True) == 0: - os.remove(cpio_path) # Successful extraction, delete PBZX > CPIO archive + os.remove(cpio_path) # Successful extraction, delete PBZX > CPIO archive else: return 3 else: return 2 - + return 0 + # Get common ctypes Structure Sizes PBZX_CHUNK_HDR_LEN = ctypes.sizeof(PbzxChunk) if __name__ == '__main__': - BIOSUtility(TITLE, is_apple_pbzx, apple_pbzx_extract).run_utility() + BIOSUtility(title=TITLE, check=is_apple_pbzx, main=apple_pbzx_extract).run_utility() diff --git a/Apple_EFI_PKG.py b/Apple_EFI_PKG.py index a185547..5f44609 100644 --- a/Apple_EFI_PKG.py +++ b/Apple_EFI_PKG.py @@ -1,22 +1,16 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Apple EFI PKG Apple EFI Package Extractor -Copyright (C) 2019-2022 Plato Mavropoulos +Copyright (C) 2019-2024 Plato Mavropoulos """ -TITLE = 'Apple EFI Package Extractor v2.0_a5' - import os -import sys - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.comp_szip import is_szip_supported, szip_decompress -from common.path_ops import copy_file, del_dirs, get_path_files, make_dirs, path_name, path_parent, get_extract_path +from common.path_ops import copy_file, del_dirs, get_extract_path, get_path_files, make_dirs, path_name, path_parent from common.patterns import PAT_APPLE_PKG from common.system import printer from common.templates import BIOSUtility @@ -26,105 +20,138 @@ from Apple_EFI_ID import apple_efi_identify, is_apple_efi from Apple_EFI_IM4P import apple_im4p_split, is_apple_im4p from Apple_EFI_PBZX import apple_pbzx_extract, is_apple_pbzx -# Check if input is Apple EFI PKG package +TITLE = 'Apple EFI Package Extractor v3.0' + + def is_apple_pkg(input_file): + """ Check if input is Apple EFI PKG package """ + input_buffer = file_to_bytes(input_file) - + return bool(PAT_APPLE_PKG.search(input_buffer[:0x4])) -# Split Apple EFI image (if applicable) and Rename + def efi_split_rename(in_file, out_path, padding=0): + """ Split Apple EFI image (if applicable) and Rename """ + exit_codes = [] - + working_dir = get_extract_path(in_file) - + if is_apple_im4p(in_file): printer(f'Splitting IM4P via {is_apple_im4p.__module__}...', padding) + im4p_exit = apple_im4p_split(in_file, working_dir, padding + 4) + exit_codes.append(im4p_exit) else: make_dirs(working_dir, delete=True) + copy_file(in_file, working_dir, True) - + for efi_file in get_path_files(working_dir): if is_apple_efi(efi_file): printer(f'Renaming EFI via {is_apple_efi.__module__}...', padding) + name_exit = apple_efi_identify(efi_file, efi_file, padding + 4, True) + exit_codes.append(name_exit) - + for named_file in get_path_files(working_dir): copy_file(named_file, out_path, True) - + del_dirs(working_dir) - + return sum(exit_codes) -# Parse & Extract Apple EFI PKG packages + def apple_pkg_extract(input_file, extract_path, padding=0): + """ Parse & Extract Apple EFI PKG packages """ + if not os.path.isfile(input_file): printer('Error: Could not find input file path!', padding) + return 1 - + make_dirs(extract_path, delete=True) - + xar_path = os.path.join(extract_path, 'xar') - + # Decompress PKG > XAR archive with 7-Zip if is_szip_supported(input_file, padding, args=['-tXAR'], check=True): if szip_decompress(input_file, xar_path, 'XAR', padding, args=['-tXAR'], check=True) != 0: return 3 else: return 2 - + for xar_file in get_path_files(xar_path): if path_name(xar_file) == 'Payload': pbzx_module = is_apple_pbzx.__module__ + if is_apple_pbzx(xar_file): printer(f'Extracting PBZX via {pbzx_module}...', padding + 4) + pbzx_path = get_extract_path(xar_file) + if apple_pbzx_extract(xar_file, pbzx_path, padding + 8) == 0: printer(f'Succesfull PBZX extraction via {pbzx_module}!', padding + 4) + for pbzx_file in get_path_files(pbzx_path): if path_name(pbzx_file) == 'UpdateBundle.zip': if is_szip_supported(pbzx_file, padding + 8, args=['-tZIP'], check=True): zip_path = get_extract_path(pbzx_file) - if szip_decompress(pbzx_file, zip_path, 'ZIP', padding + 8, args=['-tZIP'], check=True) == 0: + + if szip_decompress(pbzx_file, zip_path, 'ZIP', padding + 8, args=['-tZIP'], + check=True) == 0: for zip_file in get_path_files(zip_path): if path_name(path_parent(zip_file)) == 'MacEFI': printer(path_name(zip_file), padding + 12) + if efi_split_rename(zip_file, extract_path, padding + 16) != 0: - printer(f'Error: Could not split and rename {path_name(zip_file)}!', padding) + printer(f'Error: Could not split and rename {path_name(zip_file)}!', + padding) + return 10 else: return 9 else: return 8 - break # ZIP found, stop + + break # ZIP found, stop else: printer('Error: Could not find "UpdateBundle.zip" file!', padding) + return 7 else: printer(f'Error: Failed to extract PBZX file via {pbzx_module}!', padding) + return 6 else: printer(f'Error: Failed to detect file as PBZX via {pbzx_module}!', padding) + return 5 - - break # Payload found, stop searching - + + break # Payload found, stop searching + if path_name(xar_file) == 'Scripts': if is_szip_supported(xar_file, padding + 4, args=['-tGZIP'], check=True): gzip_path = get_extract_path(xar_file) + if szip_decompress(xar_file, gzip_path, 'GZIP', padding + 4, args=['-tGZIP'], check=True) == 0: for gzip_file in get_path_files(gzip_path): if is_szip_supported(gzip_file, padding + 8, args=['-tCPIO'], check=True): cpio_path = get_extract_path(gzip_file) - if szip_decompress(gzip_file, cpio_path, 'CPIO', padding + 8, args=['-tCPIO'], check=True) == 0: + + if szip_decompress(gzip_file, cpio_path, 'CPIO', padding + 8, args=['-tCPIO'], + check=True) == 0: for cpio_file in get_path_files(cpio_path): if path_name(path_parent(cpio_file)) == 'EFIPayloads': printer(path_name(cpio_file), padding + 12) + if efi_split_rename(cpio_file, extract_path, padding + 16) != 0: - printer(f'Error: Could not split and rename {path_name(cpio_file)}!', padding) + printer(f'Error: Could not split and rename {path_name(cpio_file)}!', + padding) + return 15 else: return 14 @@ -134,15 +161,17 @@ def apple_pkg_extract(input_file, extract_path, padding=0): return 12 else: return 11 - - break # Scripts found, stop searching + + break # Scripts found, stop searching else: printer('Error: Could not find "Payload" or "Scripts" file!', padding) + return 4 - - del_dirs(xar_path) # Delete temporary/working XAR folder - + + del_dirs(xar_path) # Delete temporary/working XAR folder + return 0 + if __name__ == '__main__': - BIOSUtility(TITLE, is_apple_pkg, apple_pkg_extract).run_utility() + BIOSUtility(title=TITLE, check=is_apple_pkg, main=apple_pkg_extract).run_utility() diff --git a/Award_BIOS_Extract.py b/Award_BIOS_Extract.py index 12d1e96..bb9e23c 100644 --- a/Award_BIOS_Extract.py +++ b/Award_BIOS_Extract.py @@ -1,74 +1,84 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Award BIOS Extract Award BIOS Module Extractor -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'Award BIOS Module Extractor v2.0_a5' - import os -import sys - -# Stop __pycache__ generation -sys.dont_write_bytecode = True +import stat from common.comp_szip import szip_decompress -from common.path_ops import make_dirs, safe_name, get_extract_path +from common.path_ops import get_extract_path, make_dirs, safe_name from common.patterns import PAT_AWARD_LZH from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes -# Check if input is Award BIOS image +TITLE = 'Award BIOS Module Extractor v3.0' + + def is_award_bios(in_file): + """ Check if input is Award BIOS image """ + in_buffer = file_to_bytes(in_file) - + return bool(PAT_AWARD_LZH.search(in_buffer)) -# Parse & Extract Award BIOS image + def award_bios_extract(input_file, extract_path, padding=0): + """ Parse & Extract Award BIOS image """ + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - + for lzh_match in PAT_AWARD_LZH.finditer(input_buffer): lzh_type = lzh_match.group(0).decode('utf-8') + lzh_text = f'LZH-{lzh_type.strip("-").upper()}' - + lzh_bgn = lzh_match.start() - + mod_bgn = lzh_bgn - 0x2 hdr_len = input_buffer[mod_bgn] mod_len = int.from_bytes(input_buffer[mod_bgn + 0x7:mod_bgn + 0xB], 'little') mod_end = lzh_bgn + hdr_len + mod_len + mod_bin = input_buffer[mod_bgn:mod_end] - - tag_bgn = mod_bgn + 0x16 - tag_end = tag_bgn + input_buffer[mod_bgn + 0x15] - tag_txt = input_buffer[tag_bgn:tag_end].decode('utf-8','ignore') - + + if len(mod_bin) != 0x2 + hdr_len + mod_len: + printer(f'Error: Skipped incomplete LZH stream at 0x{mod_bgn:X}!', padding, False) + + continue + + tag_txt = safe_name(mod_bin[0x16:0x16 + mod_bin[0x15]].decode('utf-8', 'ignore').strip()) + printer(f'{lzh_text} > {tag_txt} [0x{mod_bgn:06X}-0x{mod_end:06X}]', padding) - - mod_path = os.path.join(extract_path, safe_name(tag_txt)) + + mod_path = os.path.join(extract_path, tag_txt) + lzh_path = f'{mod_path}.lzh' - + with open(lzh_path, 'wb') as lzh_file: - lzh_file.write(mod_bin) # Store LZH archive - + lzh_file.write(mod_bin) # Store LZH archive + # 7-Zip returns critical exit code (i.e. 2) if LZH CRC is wrong, do not check result szip_decompress(lzh_path, extract_path, lzh_text, padding + 4, check=False) - + # Manually check if 7-Zip extracted LZH due to its CRC check issue if os.path.isfile(mod_path): - os.remove(lzh_path) # Successful extraction, delete LZH archive - + os.chmod(lzh_path, stat.S_IWRITE) + + os.remove(lzh_path) # Successful extraction, delete LZH archive + # Extract any nested LZH archives if is_award_bios(mod_path): # Recursively extract nested Award BIOS modules award_bios_extract(mod_path, get_extract_path(mod_path), padding + 8) + if __name__ == '__main__': - BIOSUtility(TITLE, is_award_bios, award_bios_extract).run_utility() + BIOSUtility(title=TITLE, check=is_award_bios, main=award_bios_extract).run_utility() diff --git a/Dell_PFS_Extract.py b/Dell_PFS_Extract.py index b8cb686..fafdd8b 100644 --- a/Dell_PFS_Extract.py +++ b/Dell_PFS_Extract.py @@ -1,282 +1,331 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Dell PFS Extract Dell PFS Update Extractor -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'Dell PFS Update Extractor v6.0_a16' - -import os -import io -import sys -import lzma -import zlib -import ctypes import contextlib - -# Skip __pycache__ generation -sys.dont_write_bytecode = True +import ctypes +import io +import lzma +import os +import zlib from common.checksums import get_chk_8_xor from common.comp_szip import is_szip_supported, szip_decompress from common.num_ops import get_ordinal from common.path_ops import del_dirs, get_path_files, make_dirs, path_name, path_parent, path_stem, safe_name from common.patterns import PAT_DELL_FTR, PAT_DELL_HDR, PAT_DELL_PKG -from common.struct_ops import char, get_struct, uint8_t, uint16_t, uint32_t, uint64_t +from common.struct_ops import Char, get_struct, UInt8, UInt16, UInt32, UInt64 from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes -from AMI_PFAT_Extract import IntelBiosGuardHeader, IntelBiosGuardSignature2k, parse_bg_script +from AMI_PFAT_Extract import IntelBiosGuardHeader, parse_bg_script, parse_bg_sign + +TITLE = 'Dell PFS Update Extractor v7.0' + -# Dell PFS Header Structure class DellPfsHeader(ctypes.LittleEndianStructure): + """ Dell PFS Header Structure """ + _pack_ = 1 _fields_ = [ - ('Tag', char*8), # 0x00 - ('HeaderVersion', uint32_t), # 0x08 - ('PayloadSize', uint32_t), # 0x0C + ('Tag', Char * 8), # 0x00 + ('HeaderVersion', UInt32), # 0x08 + ('PayloadSize', UInt32), # 0x0C # 0x10 ] - - def struct_print(self, p): - printer(['Header Tag :', self.Tag.decode('utf-8')], p, False) - printer(['Header Version:', self.HeaderVersion], p, False) - printer(['Payload Size :', f'0x{self.PayloadSize:X}'], p, False) -# Dell PFS Footer Structure + def struct_print(self, padd): + """ Display structure information """ + + printer(['Header Tag :', self.Tag.decode('utf-8')], padd, False) + printer(['Header Version:', self.HeaderVersion], padd, False) + printer(['Payload Size :', f'0x{self.PayloadSize:X}'], padd, False) + + class DellPfsFooter(ctypes.LittleEndianStructure): + """ Dell PFS Footer Structure """ + _pack_ = 1 _fields_ = [ - ('PayloadSize', uint32_t), # 0x00 - ('Checksum', uint32_t), # 0x04 ~CRC32 w/ Vector 0 - ('Tag', char*8), # 0x08 + ('PayloadSize', UInt32), # 0x00 + ('Checksum', UInt32), # 0x04 ~CRC32 w/ Vector 0 + ('Tag', Char * 8), # 0x08 # 0x10 ] - - def struct_print(self, p): - printer(['Payload Size :', f'0x{self.PayloadSize:X}'], p, False) - printer(['Payload Checksum:', f'0x{self.Checksum:08X}'], p, False) - printer(['Footer Tag :', self.Tag.decode('utf-8')], p, False) -# Dell PFS Entry Base Structure + def struct_print(self, padd): + """ Display structure information """ + + printer(['Payload Size :', f'0x{self.PayloadSize:X}'], padd, False) + printer(['Payload Checksum:', f'0x{self.Checksum:08X}'], padd, False) + printer(['Footer Tag :', self.Tag.decode('utf-8')], padd, False) + + class DellPfsEntryBase(ctypes.LittleEndianStructure): + """ Dell PFS Entry Base Structure """ + _pack_ = 1 _fields_ = [ - ('GUID', uint32_t*4), # 0x00 Little Endian - ('HeaderVersion', uint32_t), # 0x10 1 or 2 - ('VersionType', uint8_t*4), # 0x14 - ('Version', uint16_t*4), # 0x18 - ('Reserved', uint64_t), # 0x20 - ('DataSize', uint32_t), # 0x28 - ('DataSigSize', uint32_t), # 0x2C - ('DataMetSize', uint32_t), # 0x30 - ('DataMetSigSize', uint32_t), # 0x34 + ('GUID', UInt32 * 4), # 0x00 Little Endian + ('HeaderVersion', UInt32), # 0x10 1 or 2 + ('VersionType', UInt8 * 4), # 0x14 + ('Version', UInt16 * 4), # 0x18 + ('Reserved', UInt64), # 0x20 + ('DataSize', UInt32), # 0x28 + ('DataSigSize', UInt32), # 0x2C + ('DataMetSize', UInt32), # 0x30 + ('DataMetSigSize', UInt32), # 0x34 # 0x38 (parent class, base) ] - - def struct_print(self, p): - GUID = f'{int.from_bytes(self.GUID, "little"):0{0x10 * 2}X}' - Unknown = f'{int.from_bytes(self.Unknown, "little"):0{len(self.Unknown) * 8}X}' - Version = get_entry_ver(self.Version, self.VersionType) - - printer(['Entry GUID :', GUID], p, False) - printer(['Entry Version :', self.HeaderVersion], p, False) - printer(['Payload Version :', Version], p, False) - printer(['Reserved :', f'0x{self.Reserved:X}'], p, False) - printer(['Payload Data Size :', f'0x{self.DataSize:X}'], p, False) - printer(['Payload Signature Size :', f'0x{self.DataSigSize:X}'], p, False) - printer(['Metadata Data Size :', f'0x{self.DataMetSize:X}'], p, False) - printer(['Metadata Signature Size:', f'0x{self.DataMetSigSize:X}'], p, False) - printer(['Unknown :', f'0x{Unknown}'], p, False) -# Dell PFS Entry Revision 1 Structure + def struct_print(self, padd): + """ Display structure information """ + + guid = f'{int.from_bytes(self.GUID, "little"):0{0x10 * 2}X}' + unknown = f'{int.from_bytes(self.Unknown, "little"):0{len(self.Unknown) * 8}X}' + version = get_entry_ver(self.Version, self.VersionType) + + printer(['Entry GUID :', guid], padd, False) + printer(['Entry Version :', self.HeaderVersion], padd, False) + printer(['Payload Version :', version], padd, False) + printer(['Reserved :', f'0x{self.Reserved:X}'], padd, False) + printer(['Payload Data Size :', f'0x{self.DataSize:X}'], padd, False) + printer(['Payload Signature Size :', f'0x{self.DataSigSize:X}'], padd, False) + printer(['Metadata Data Size :', f'0x{self.DataMetSize:X}'], padd, False) + printer(['Metadata Signature Size:', f'0x{self.DataMetSigSize:X}'], padd, False) + printer(['Unknown :', f'0x{unknown}'], padd, False) + + class DellPfsEntryR1(DellPfsEntryBase): + """ Dell PFS Entry Revision 1 Structure """ + _pack_ = 1 _fields_ = [ - ('Unknown', uint32_t*4), # 0x38 + ('Unknown', UInt32 * 4), # 0x38 # 0x48 (child class, R1) ] -# Dell PFS Entry Revision 2 Structure + class DellPfsEntryR2(DellPfsEntryBase): + """ Dell PFS Entry Revision 2 Structure """ + _pack_ = 1 _fields_ = [ - ('Unknown', uint32_t*8), # 0x38 + ('Unknown', UInt32 * 8), # 0x38 # 0x58 (child class, R2) ] -# Dell PFS Information Header Structure + class DellPfsInfo(ctypes.LittleEndianStructure): + """ Dell PFS Information Header Structure """ + _pack_ = 1 _fields_ = [ - ('HeaderVersion', uint32_t), # 0x00 - ('GUID', uint32_t*4), # 0x04 Little Endian + ('HeaderVersion', UInt32), # 0x00 + ('GUID', UInt32 * 4), # 0x04 Little Endian # 0x14 ] - - def struct_print(self, p): - GUID = f'{int.from_bytes(self.GUID, "little"):0{0x10 * 2}X}' - - printer(['Info Version:', self.HeaderVersion], p, False) - printer(['Entry GUID :', GUID], p, False) -# Dell PFS FileName Header Structure + def struct_print(self, padd): + """ Display structure information """ + + guid = f'{int.from_bytes(self.GUID, "little"):0{0x10 * 2}X}' + + printer(['Info Version:', self.HeaderVersion], padd, False) + printer(['Entry GUID :', guid], padd, False) + + class DellPfsName(ctypes.LittleEndianStructure): + """ Dell PFS FileName Header Structure """ + _pack_ = 1 _fields_ = [ - ('Version', uint16_t*4), # 0x00 - ('VersionType', uint8_t*4), # 0x08 - ('CharacterCount', uint16_t), # 0x0C UTF-16 2-byte Characters + ('Version', UInt16 * 4), # 0x00 + ('VersionType', UInt8 * 4), # 0x08 + ('CharacterCount', UInt16), # 0x0C UTF-16 2-byte Characters # 0x0E ] - - def struct_print(self, p, name): - Version = get_entry_ver(self.Version, self.VersionType) - - printer(['Payload Version:', Version], p, False) - printer(['Character Count:', self.CharacterCount], p, False) - printer(['Payload Name :', name], p, False) -# Dell PFS Metadata Header Structure + def struct_print(self, padd, name): + """ Display structure information """ + + version = get_entry_ver(self.Version, self.VersionType) + + printer(['Payload Version:', version], padd, False) + printer(['Character Count:', self.CharacterCount], padd, False) + printer(['Payload Name :', name], padd, False) + + class DellPfsMetadata(ctypes.LittleEndianStructure): + """ Dell PFS Metadata Header Structure """ + _pack_ = 1 _fields_ = [ - ('ModelIDs', char*501), # 0x000 - ('FileName', char*100), # 0x1F5 - ('FileVersion', char*33), # 0x259 - ('Date', char*33), # 0x27A - ('Brand', char*80), # 0x29B - ('ModelFile', char*80), # 0x2EB - ('ModelName', char*100), # 0x33B - ('ModelVersion', char*33), # 0x39F + ('ModelIDs', Char * 501), # 0x000 + ('FileName', Char * 100), # 0x1F5 + ('FileVersion', Char * 33), # 0x259 + ('Date', Char * 33), # 0x27A + ('Brand', Char * 80), # 0x29B + ('ModelFile', Char * 80), # 0x2EB + ('ModelName', Char * 100), # 0x33B + ('ModelVersion', Char * 33), # 0x39F # 0x3C0 ] - - def struct_print(self, p): - printer(['Model IDs :', self.ModelIDs.decode('utf-8').strip(',END')], p, False) - printer(['File Name :', self.FileName.decode('utf-8')], p, False) - printer(['File Version :', self.FileVersion.decode('utf-8')], p, False) - printer(['Date :', self.Date.decode('utf-8')], p, False) - printer(['Brand :', self.Brand.decode('utf-8')], p, False) - printer(['Model File :', self.ModelFile.decode('utf-8')], p, False) - printer(['Model Name :', self.ModelName.decode('utf-8')], p, False) - printer(['Model Version:', self.ModelVersion.decode('utf-8')], p, False) -# Dell PFS BIOS Guard Metadata Structure + def struct_print(self, padd): + """ Display structure information """ + + printer(['Model IDs :', self.ModelIDs.decode('utf-8').removesuffix(',END')], padd, False) + printer(['File Name :', self.FileName.decode('utf-8')], padd, False) + printer(['File Version :', self.FileVersion.decode('utf-8')], padd, False) + printer(['Date :', self.Date.decode('utf-8')], padd, False) + printer(['Brand :', self.Brand.decode('utf-8')], padd, False) + printer(['Model File :', self.ModelFile.decode('utf-8')], padd, False) + printer(['Model Name :', self.ModelName.decode('utf-8')], padd, False) + printer(['Model Version:', self.ModelVersion.decode('utf-8')], padd, False) + + class DellPfsPfatMetadata(ctypes.LittleEndianStructure): + """ Dell PFS BIOS Guard Metadata Structure """ + _pack_ = 1 _fields_ = [ - ('Address', uint32_t), # 0x00 - ('Unknown0', uint32_t), # 0x04 - ('Offset', uint32_t), # 0x08 Matches BG Script > I0 - ('DataSize', uint32_t), # 0x0C Matches BG Script > I2 & Header > Data Size - ('Unknown1', uint32_t), # 0x10 - ('Unknown2', uint32_t), # 0x14 - ('Unknown3', uint8_t), # 0x18 + ('Address', UInt32), # 0x00 + ('Unknown0', UInt32), # 0x04 + ('Offset', UInt32), # 0x08 Matches BG Script > I0 + ('DataSize', UInt32), # 0x0C Matches BG Script > I2 & Header > Data Size + ('Unknown1', UInt32), # 0x10 + ('Unknown2', UInt32), # 0x14 + ('Unknown3', UInt8), # 0x18 # 0x19 ] - - def struct_print(self, p): - printer(['Address :', f'0x{self.Address:X}'], p, False) - printer(['Unknown 0:', f'0x{self.Unknown0:X}'], p, False) - printer(['Offset :', f'0x{self.Offset:X}'], p, False) - printer(['Length :', f'0x{self.DataSize:X}'], p, False) - printer(['Unknown 1:', f'0x{self.Unknown1:X}'], p, False) - printer(['Unknown 2:', f'0x{self.Unknown2:X}'], p, False) - printer(['Unknown 3:', f'0x{self.Unknown3:X}'], p, False) -# The Dell ThinOS PKG update images usually contain multiple sections. -# Each section starts with a 0x30 header, which begins with pattern 72135500. -# The section length is found at 0x10-0x14 and its (optional) MD5 hash at 0x20-0x30. -# Section data can be raw or LZMA2 (7zXZ) compressed. The latter contains the PFS update image. + def struct_print(self, padd): + """ Display structure information """ + + printer(['Address :', f'0x{self.Address:X}'], padd, False) + printer(['Unknown 0:', f'0x{self.Unknown0:X}'], padd, False) + printer(['Offset :', f'0x{self.Offset:X}'], padd, False) + printer(['Length :', f'0x{self.DataSize:X}'], padd, False) + printer(['Unknown 1:', f'0x{self.Unknown1:X}'], padd, False) + printer(['Unknown 2:', f'0x{self.Unknown2:X}'], padd, False) + printer(['Unknown 3:', f'0x{self.Unknown3:X}'], padd, False) + + def is_pfs_pkg(input_file): + """ + The Dell ThinOS PKG update images usually contain multiple sections. + Each section starts with a 0x30 header, which begins with pattern 72135500. + The section length is found at 0x10-0x14 and its (optional) MD5 hash at 0x20-0x30. + Section data can be raw or LZMA2 (7zXZ) compressed. The latter contains the PFS update image. + """ + input_buffer = file_to_bytes(input_file) - + return PAT_DELL_PKG.search(input_buffer) -# The Dell PFS update images usually contain multiple sections. -# Each section is zlib-compressed with header pattern ********++EEAA761BECBB20F1E651--789C, -# where ******** is the zlib stream size, ++ is the section type and -- the header Checksum XOR 8. -# The "Firmware" section has type AA and its files are stored in PFS format. -# The "Utility" section has type BB and its files are stored in PFS, BIN or 7z formats. + def is_pfs_hdr(input_file): + """ + The Dell PFS update images usually contain multiple sections. + Each section is zlib-compressed with header pattern ********++EEAA761BECBB20F1E651--789C, + where ******** is the zlib stream size, ++ is the section type and -- the header Checksum XOR 8. + The "Firmware" section has type AA and its files are stored in PFS format. + The "Utility" section has type BB and its files are stored in PFS, BIN or 7z formats. + """ + input_buffer = file_to_bytes(input_file) - + return bool(PAT_DELL_HDR.search(input_buffer)) -# Each section is followed by the footer pattern ********EEAAEE8F491BE8AE143790--, -# where ******** is the zlib stream size and ++ the footer Checksum XOR 8. + def is_pfs_ftr(input_file): + """ + Each section is followed by the footer pattern ********EEAAEE8F491BE8AE143790--, + where ******** is the zlib stream size and ++ the footer Checksum XOR 8. + """ + input_buffer = file_to_bytes(input_file) - + return bool(PAT_DELL_FTR.search(input_buffer)) -# Check if input is Dell PFS/PKG image + def is_dell_pfs(input_file): + """ Check if input is Dell PFS/PKG image """ + input_buffer = file_to_bytes(input_file) - + is_pkg = is_pfs_pkg(input_buffer) - + is_hdr = is_pfs_hdr(input_buffer) - + is_ftr = is_pfs_ftr(input_buffer) - + return bool(is_pkg or is_hdr and is_ftr) -# Parse & Extract Dell PFS Update image + def pfs_pkg_parse(input_file, extract_path, padding=0, structure=True, advanced=True): + """ Parse & Extract Dell PFS Update image """ + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - + is_dell_pkg = is_pfs_pkg(input_buffer) - + if is_dell_pkg: pfs_results = thinos_pkg_extract(input_buffer, extract_path) else: pfs_results = {path_stem(input_file) if os.path.isfile(input_file) else 'Image': input_buffer} - + # Parse each Dell PFS image contained in the input file - for pfs_index,(pfs_name,pfs_buffer) in enumerate(pfs_results.items(), start=1): + for pfs_index, (pfs_name, pfs_buffer) in enumerate(pfs_results.items(), start=1): # At ThinOS PKG packages, multiple PFS images may be included in separate model-named folders pfs_path = os.path.join(extract_path, f'{pfs_index} {pfs_name}') if is_dell_pkg else extract_path + # Parse each PFS ZLIB section for zlib_offset in get_section_offsets(pfs_buffer): # Call the PFS ZLIB section parser function - pfs_section_parse(pfs_buffer, zlib_offset, pfs_path, pfs_name, pfs_index, 1, False, padding, structure, advanced) + pfs_section_parse(pfs_buffer, zlib_offset, pfs_path, pfs_name, pfs_index, 1, False, + padding, structure, advanced) + -# Extract Dell ThinOS PKG 7zXZ def thinos_pkg_extract(input_file, extract_path): + """ Extract Dell ThinOS PKG 7zXZ """ + input_buffer = file_to_bytes(input_file) - + # Initialize PFS results (Name: Buffer) pfs_results = {} - + # Search input image for ThinOS PKG 7zXZ header thinos_pkg_match = PAT_DELL_PKG.search(input_buffer) - + lzma_len_off = thinos_pkg_match.start() + 0x10 lzma_len_int = int.from_bytes(input_buffer[lzma_len_off:lzma_len_off + 0x4], 'little') lzma_bin_off = thinos_pkg_match.end() - 0x5 + lzma_bin_dat = input_buffer[lzma_bin_off:lzma_bin_off + lzma_len_int] - + # Check if the compressed 7zXZ stream is complete if len(lzma_bin_dat) != lzma_len_int: return pfs_results - + working_path = os.path.join(extract_path, 'THINOS_PKG_TEMP') - + make_dirs(working_path, delete=True) - + pkg_tar_path = os.path.join(working_path, 'THINOS_PKG.TAR') - + with open(pkg_tar_path, 'wb') as pkg_payload: pkg_payload.write(lzma.decompress(lzma_bin_dat)) - + if is_szip_supported(pkg_tar_path, 0, args=['-tTAR'], check=True, silent=True): if szip_decompress(pkg_tar_path, working_path, 'TAR', 0, args=['-tTAR'], check=True, silent=True) == 0: os.remove(pkg_tar_path) @@ -284,333 +333,381 @@ def thinos_pkg_extract(input_file, extract_path): return pfs_results else: return pfs_results - + for pkg_file in get_path_files(working_path): if is_pfs_hdr(pkg_file): pfs_name = path_name(path_parent(pkg_file)) + pfs_results.update({pfs_name: file_to_bytes(pkg_file)}) - + del_dirs(working_path) - + return pfs_results -# Get PFS ZLIB Section Offsets + def get_section_offsets(buffer): - pfs_zlib_list = [] # Initialize PFS ZLIB offset list - + """ Get PFS ZLIB Section Offsets """ + + pfs_zlib_list = [] # Initialize PFS ZLIB offset list + pfs_zlib_init = list(PAT_DELL_HDR.finditer(buffer)) - + if not pfs_zlib_init: - return pfs_zlib_list # No PFS ZLIB detected - + return pfs_zlib_list # No PFS ZLIB detected + # Remove duplicate/nested PFS ZLIB offsets for zlib_c in pfs_zlib_init: - is_duplicate = False # Initialize duplicate/nested PFS ZLIB offset - + is_duplicate = False # Initialize duplicate/nested PFS ZLIB offset + for zlib_o in pfs_zlib_init: zlib_o_size = int.from_bytes(buffer[zlib_o.start() - 0x5:zlib_o.start() - 0x1], 'little') - + # If current PFS ZLIB offset is within another PFS ZLIB range (start-end), set as duplicate if zlib_o.start() < zlib_c.start() < zlib_o.start() + zlib_o_size: is_duplicate = True - + if not is_duplicate: pfs_zlib_list.append(zlib_c.start()) - + return pfs_zlib_list -# Dell PFS ZLIB Section Parser -def pfs_section_parse(zlib_data, zlib_start, extract_path, pfs_name, pfs_index, pfs_count, is_rec, padding=0, structure=True, advanced=True): - is_zlib_error = False # Initialize PFS ZLIB-related error state - - section_type = zlib_data[zlib_start - 0x1] # Byte before PFS ZLIB Section pattern is Section Type (e.g. AA, BB) - section_name = {0xAA:'Firmware', 0xBB:'Utilities'}.get(section_type, f'Unknown ({section_type:02X})') - + +def pfs_section_parse(zlib_data, zlib_start, extract_path, pfs_name, pfs_index, pfs_count, is_rec, + padding=0, structure=True, advanced=True): + """ Dell PFS ZLIB Section Parser """ + + is_zlib_error = False # Initialize PFS ZLIB-related error state + + section_type = zlib_data[zlib_start - 0x1] # Byte before PFS ZLIB Section pattern is Section Type (e.g. AA, BB) + + section_name = {0xAA: 'Firmware', 0xBB: 'Utilities'}.get(section_type, f'Unknown ({section_type:02X})') + # Show extraction complete message for each main PFS ZLIB Section printer(f'Extracting Dell PFS {pfs_index} > {pfs_name} > {section_name}', padding) - + # Set PFS ZLIB Section extraction sub-directory path section_path = os.path.join(extract_path, safe_name(section_name)) - + # Create extraction sub-directory and delete old (if present, not in recursions) make_dirs(section_path, delete=(not is_rec), parents=True, exist_ok=True) - + # Store the compressed zlib stream start offset compressed_start = zlib_start + 0xB - + # Store the PFS ZLIB section header start offset header_start = zlib_start - 0x5 - + # Store the PFS ZLIB section header contents (16 bytes) header_data = zlib_data[header_start:compressed_start] - + # Check if the PFS ZLIB section header Checksum XOR 8 is valid if get_chk_8_xor(header_data[:0xF]) != header_data[0xF]: printer('Error: Invalid Dell PFS ZLIB section Header Checksum!', padding) + is_zlib_error = True - + # Store the compressed zlib stream size from the header contents compressed_size_hdr = int.from_bytes(header_data[:0x4], 'little') - + # Store the compressed zlib stream end offset compressed_end = compressed_start + compressed_size_hdr - + # Store the compressed zlib stream contents compressed_data = zlib_data[compressed_start:compressed_end] - + # Check if the compressed zlib stream is complete, based on header if len(compressed_data) != compressed_size_hdr: printer('Error: Incomplete Dell PFS ZLIB section data (Header)!', padding) + is_zlib_error = True - + # Store the PFS ZLIB section footer contents (16 bytes) footer_data = zlib_data[compressed_end:compressed_end + 0x10] - + # Check if PFS ZLIB section footer was found in the section if not is_pfs_ftr(footer_data): printer('Error: This Dell PFS ZLIB section is corrupted!', padding) + is_zlib_error = True - + # Check if the PFS ZLIB section footer Checksum XOR 8 is valid if get_chk_8_xor(footer_data[:0xF]) != footer_data[0xF]: printer('Error: Invalid Dell PFS ZLIB section Footer Checksum!', padding) + is_zlib_error = True - + # Store the compressed zlib stream size from the footer contents compressed_size_ftr = int.from_bytes(footer_data[:0x4], 'little') - + # Check if the compressed zlib stream is complete, based on footer if compressed_size_ftr != compressed_size_hdr: printer('Error: Incomplete Dell PFS ZLIB section data (Footer)!', padding) + is_zlib_error = True - + # Decompress PFS ZLIB section payload try: if is_zlib_error: - raise Exception('ZLIB_ERROR') # ZLIB errors are critical - section_data = zlib.decompress(compressed_data) # ZLIB decompression - except Exception: - section_data = zlib_data # Fallback to raw ZLIB data upon critical error - + raise ValueError('ZLIB_ERROR_OCCURED') # ZLIB errors are critical + + section_data = zlib.decompress(compressed_data) # ZLIB decompression + except Exception as error: # pylint: disable=broad-except + printer(f'Error: Failed to decompress PFS ZLIB section: {error}!', padding) + + section_data = zlib_data # Fallback to raw ZLIB data upon critical error + # Call the PFS Extract function on the decompressed PFS ZLIB Section pfs_extract(section_data, pfs_index, pfs_name, pfs_count, section_path, padding, structure, advanced) -# Parse & Extract Dell PFS Volume -def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, extract_path, padding=0, structure=True, advanced=True): + +def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, extract_path, padding=0, structure=True, advanced=True): + """ Parse & Extract Dell PFS Volume """ + # Show PFS Volume indicator if structure: printer('PFS Volume:', padding) - + # Get PFS Header Structure values pfs_hdr = get_struct(buffer, 0, DellPfsHeader) - + # Validate that a PFS Header was parsed if pfs_hdr.Tag != b'PFS.HDR.': printer('Error: PFS Header could not be found!', padding + 4) - - return # Critical error, abort - + + return # Critical error, abort + # Show PFS Header Structure info if structure: printer('PFS Header:\n', padding + 4) + pfs_hdr.struct_print(padding + 8) - + # Validate that a known PFS Header Version was encountered chk_hdr_ver(pfs_hdr.HeaderVersion, 'PFS', padding + 8) - + # Get PFS Payload Data pfs_payload = buffer[PFS_HEAD_LEN:PFS_HEAD_LEN + pfs_hdr.PayloadSize] - + # Parse all PFS Payload Entries/Components - entry_index = 1 # Index number of each PFS Entry - entry_start = 0 # Increasing PFS Entry starting offset - entries_all = [] # Storage for each PFS Entry details - filename_info = [] # Buffer for FileName Information Entry Data - signature_info = [] # Buffer for Signature Information Entry Data - pfs_entry_struct,pfs_entry_size = get_pfs_entry(pfs_payload, entry_start) # Get PFS Entry Info + entry_index = 1 # Index number of each PFS Entry + entry_start = 0 # Increasing PFS Entry starting offset + entries_all = [] # Storage for each PFS Entry details + filename_info = [] # Buffer for FileName Information Entry Data + signature_info = [] # Buffer for Signature Information Entry Data + + pfs_entry_struct, pfs_entry_size = get_pfs_entry(pfs_payload, entry_start) # Get PFS Entry Info + while len(pfs_payload[entry_start:entry_start + pfs_entry_size]) == pfs_entry_size: # Analyze PFS Entry Structure and get relevant info - _,entry_version,entry_guid,entry_data,entry_data_sig,entry_met,entry_met_sig,next_entry = \ - parse_pfs_entry(pfs_payload, entry_start, pfs_entry_size, pfs_entry_struct, 'PFS Entry', padding, structure) - - entry_type = 'OTHER' # Adjusted later if PFS Entry is Zlib, PFAT, PFS Info, Model Info - - # Get PFS Information from the PFS Entry with GUID E0717CE3A9BB25824B9F0DC8FD041960 or B033CB16EC9B45A14055F80E4D583FD3 - if entry_guid in ['E0717CE3A9BB25824B9F0DC8FD041960','B033CB16EC9B45A14055F80E4D583FD3']: - filename_info = entry_data + _, entry_version, entry_guid, entry_data, entry_data_sig, entry_met, entry_met_sig, next_entry = \ + parse_pfs_entry(pfs_payload, entry_start, pfs_entry_size, pfs_entry_struct, 'PFS Entry', padding, structure) + + entry_type = 'OTHER' # Adjusted later if PFS Entry is Zlib, PFAT, PFS Info, Model Info + + # Get PFS Information from the relevant (hardcoded) PFS Entry GUIDs + if entry_guid in ['E0717CE3A9BB25824B9F0DC8FD041960', 'B033CB16EC9B45A14055F80E4D583FD3']: entry_type = 'NAME_INFO' - - # Get Model Information from the PFS Entry with GUID 6F1D619A22A6CB924FD4DA68233AE3FB + + filename_info = entry_data + + # Get Model Information from the relevant (hardcoded) PFS Entry GUID elif entry_guid == '6F1D619A22A6CB924FD4DA68233AE3FB': entry_type = 'MODEL_INFO' - - # Get Signature Information from the PFS Entry with GUID D086AFEE3ADBAEA94D5CED583C880BB7 + + # Get Signature Information from the relevant (hardcoded) PFS Entry GUID elif entry_guid == 'D086AFEE3ADBAEA94D5CED583C880BB7': - signature_info = entry_data entry_type = 'SIG_INFO' - - # Get Nested PFS from the PFS Entry with GUID 900FAE60437F3AB14055F456AC9FDA84 + + signature_info = entry_data + + # Get Nested PFS from the relevant (hardcoded) PFS Entry GUID elif entry_guid == '900FAE60437F3AB14055F456AC9FDA84': - entry_type = 'NESTED_PFS' # Nested PFS are usually zlib-compressed so it might change to 'ZLIB' later - + entry_type = 'NESTED_PFS' # Nested PFS are usually zlib-compressed so it might change to 'ZLIB' later + # Store all relevant PFS Entry details - entries_all.append([entry_index, entry_guid, entry_version, entry_type, entry_data, entry_data_sig, entry_met, entry_met_sig]) - - entry_index += 1 # Increase PFS Entry Index number for user-friendly output and name duplicates - entry_start = next_entry # Next PFS Entry starts after PFS Entry Metadata Signature - + entries_all.append([entry_index, entry_guid, entry_version, entry_type, + entry_data, entry_data_sig, entry_met, entry_met_sig]) + + entry_index += 1 # Increase PFS Entry Index number for user-friendly output and name duplicates + + entry_start = next_entry # Next PFS Entry starts after PFS Entry Metadata Signature + # Parse all PFS Information Entries/Descriptors - info_start = 0 # Increasing PFS Information Entry starting offset - info_all = [] # Storage for each PFS Information Entry details + info_start = 0 # Increasing PFS Information Entry starting offset + info_all = [] # Storage for each PFS Information Entry details + while len(filename_info[info_start:info_start + PFS_INFO_LEN]) == PFS_INFO_LEN: # Get PFS Information Header Structure info - entry_info_hdr = get_struct(filename_info, info_start, DellPfsInfo) - + filename_info_hdr = get_struct(filename_info, info_start, DellPfsInfo) + # Show PFS Information Header Structure info if structure: - printer('PFS Information Header:\n', padding + 4) - entry_info_hdr.struct_print(padding + 8) - + printer('PFS Filename Information Header:\n', padding + 4) + + filename_info_hdr.struct_print(padding + 8) + # Validate that a known PFS Information Header Version was encountered - if entry_info_hdr.HeaderVersion != 1: - printer(f'Error: Unknown PFS Information Header Version {entry_info_hdr.HeaderVersion}!', padding + 8) - break # Skip PFS Information Entries/Descriptors in case of unknown PFS Information Header Version - - # Get PFS Information Header GUID in Big Endian format to match each Info to the equivalent stored PFS Entry details - entry_guid = f'{int.from_bytes(entry_info_hdr.GUID, "little"):0{0x10 * 2}X}' - + if filename_info_hdr.HeaderVersion != 1: + printer(f'Error: Unknown PFS Filename Information Header Version {filename_info_hdr.HeaderVersion}!', + padding + 8) + + break # Skip PFS Information Entries/Descriptors in case of unknown PFS Information Header Version + + # Get PFS Information Header GUID in Big Endian format, in order + # to match each Info to the equivalent stored PFS Entry details. + entry_guid = f'{int.from_bytes(filename_info_hdr.GUID, "little"):0{0x10 * 2}X}' + # Get PFS FileName Structure values entry_info_mod = get_struct(filename_info, info_start + PFS_INFO_LEN, DellPfsName) - - # The PFS FileName Structure is not complete by itself. The size of the last field (Entry Name) is determined from - # CharacterCount multiplied by 2 due to usage of UTF-16 2-byte Characters. Any Entry Name leading and/or trailing - # space/null characters are stripped and common Windows reserved/illegal filename characters are replaced - name_start = info_start + PFS_INFO_LEN + PFS_NAME_LEN # PFS Entry's FileName start offset - name_size = entry_info_mod.CharacterCount * 2 # PFS Entry's FileName buffer total size - name_data = filename_info[name_start:name_start + name_size] # PFS Entry's FileName buffer - entry_name = safe_name(name_data.decode('utf-16').strip()) # PFS Entry's FileName value - + + # The PFS FileName Structure is not complete by itself. The size of the last field (Entry Name) is determined + # from CharacterCount multiplied by 2 due to usage of UTF-16 2-byte Characters. Any Entry Name leading and/or + # trailing space/null characters are stripped and Windows reserved/illegal filename characters are replaced. + name_start = info_start + PFS_INFO_LEN + PFS_NAME_LEN # PFS Entry's FileName start offset + + name_size = entry_info_mod.CharacterCount * 2 # PFS Entry's FileName buffer total size + + name_data = filename_info[name_start:name_start + name_size] # PFS Entry's FileName buffer + + entry_name = safe_name(name_data.decode('utf-16').strip()) # PFS Entry's FileName value + # Show PFS FileName Structure info if structure: printer('PFS FileName Entry:\n', padding + 8) + entry_info_mod.struct_print(padding + 12, entry_name) - + # Get PFS FileName Version string via "Version" and "VersionType" fields # PFS FileName Version string must be preferred over PFS Entry's Version entry_version = get_entry_ver(entry_info_mod.Version, entry_info_mod.VersionType) - + # Store all relevant PFS FileName details info_all.append([entry_guid, entry_name, entry_version]) - + # The next PFS Information Header starts after the calculated FileName size # Two space/null characters seem to always exist after each FileName value info_start += (PFS_INFO_LEN + PFS_NAME_LEN + name_size + 0x2) - + # Parse Nested PFS Metadata when its PFS Information Entry is missing - for index in range(len(entries_all)): - if entries_all[index][3] == 'NESTED_PFS' and not filename_info: - entry_guid = entries_all[index][1] # Nested PFS Entry GUID in Big Endian format - entry_metadata = entries_all[index][6] # Use Metadata as PFS Information Entry - + for entry in entries_all: + _, entry_guid, _, entry_type, _, _, entry_metadata, _ = entry + + if entry_type == 'NESTED_PFS' and not filename_info: # When PFS Information Entry exists, Nested PFS Metadata contains only Model IDs # When it's missing, the Metadata structure is large and contains equivalent info if len(entry_metadata) >= PFS_META_LEN: # Get Nested PFS Metadata Structure values entry_info = get_struct(entry_metadata, 0, DellPfsMetadata) - + # Show Nested PFS Metadata Structure info if structure: printer('PFS Metadata Information:\n', padding + 4) + entry_info.struct_print(padding + 8) - + # As Nested PFS Entry Name, we'll use the actual PFS File Name # Replace common Windows reserved/illegal filename characters - entry_name = safe_name(entry_info.FileName.decode('utf-8').strip('.exe')) - + entry_name = safe_name(entry_info.FileName.decode('utf-8').removesuffix('.exe').removesuffix('.bin')) + # As Nested PFS Entry Version, we'll use the actual PFS File Version entry_version = entry_info.FileVersion.decode('utf-8') - + # Store all relevant Nested PFS Metadata/Information details info_all.append([entry_guid, entry_name, entry_version]) - + # Re-set Nested PFS Entry Version from Metadata - entries_all[index][2] = entry_version - + entry[2] = entry_version + # Parse all PFS Signature Entries/Descriptors - sign_start = 0 # Increasing PFS Signature Entry starting offset + sign_start = 0 # Increasing PFS Signature Entry starting offset + while len(signature_info[sign_start:sign_start + PFS_INFO_LEN]) == PFS_INFO_LEN: # Get PFS Information Header Structure info - entry_info_hdr = get_struct(signature_info, sign_start, DellPfsInfo) - + signature_info_hdr = get_struct(signature_info, sign_start, DellPfsInfo) + # Show PFS Information Header Structure info if structure: - printer('PFS Information Header:\n', padding + 4) - entry_info_hdr.struct_print(padding + 8) - + printer('PFS Signature Information Header:\n', padding + 4) + + signature_info_hdr.struct_print(padding + 8) + # Validate that a known PFS Information Header Version was encountered - if entry_info_hdr.HeaderVersion != 1: - printer(f'Error: Unknown PFS Information Header Version {entry_info_hdr.HeaderVersion}!', padding + 8) - break # Skip PFS Signature Entries/Descriptors in case of unknown Header Version - + if signature_info_hdr.HeaderVersion != 1: + printer(f'Error: Unknown PFS Signature Information Header Version {signature_info_hdr.HeaderVersion}!', + padding + 8) + + break # Skip PFS Signature Entries/Descriptors in case of unknown Header Version + # PFS Signature Entries/Descriptors have DellPfsInfo + DellPfsEntryR* + Sign Size [0x2] + Sign Data [Sig Size] - pfs_entry_struct, pfs_entry_size = get_pfs_entry(signature_info, sign_start + PFS_INFO_LEN) # Get PFS Entry Info - + pfs_entry_struct, pfs_entry_size = get_pfs_entry(signature_info, sign_start + PFS_INFO_LEN) # PFS Entry Info + # Get PFS Entry Header Structure info entry_hdr = get_struct(signature_info, sign_start + PFS_INFO_LEN, pfs_entry_struct) - + # Show PFS Information Header Structure info if structure: printer('PFS Information Entry:\n', padding + 8) + entry_hdr.struct_print(padding + 12) - + # Show PFS Signature Size & Data (after DellPfsEntryR*) sign_info_start = sign_start + PFS_INFO_LEN + pfs_entry_size + sign_size = int.from_bytes(signature_info[sign_info_start:sign_info_start + 0x2], 'little') + sign_data_raw = signature_info[sign_info_start + 0x2:sign_info_start + 0x2 + sign_size] + sign_data_txt = f'{int.from_bytes(sign_data_raw, "little"):0{sign_size * 2}X}' - + if structure: printer('Signature Information:\n', padding + 8) + printer(f'Signature Size: 0x{sign_size:X}', padding + 12, False) + printer(f'Signature Data: {sign_data_txt[:32]} [...]', padding + 12, False) - + # The next PFS Signature Entry/Descriptor starts after the previous Signature Data sign_start += (PFS_INFO_LEN + pfs_entry_size + 0x2 + sign_size) - + # Parse each PFS Entry Data for special types (zlib or PFAT) - for index in range(len(entries_all)): - entry_data = entries_all[index][4] # Get PFS Entry Data - entry_type = entries_all[index][3] # Get PFS Entry Type - + for entry in entries_all: + _, _, _, entry_type, entry_data, _, _, _ = entry + # Very small PFS Entry Data cannot be of special type if len(entry_data) < PFS_HEAD_LEN: continue - + # Check if PFS Entry contains zlib-compressed sub-PFS Volume pfs_zlib_offsets = get_section_offsets(entry_data) - + # Check if PFS Entry contains sub-PFS Volume with PFAT Payload - is_pfat = False # Initial PFAT state for sub-PFS Entry - _, pfat_entry_size = get_pfs_entry(entry_data, PFS_HEAD_LEN) # Get possible PFS PFAT Entry Size - pfat_hdr_off = PFS_HEAD_LEN + pfat_entry_size # Possible PFAT Header starts after PFS Header & Entry - pfat_entry_hdr = get_struct(entry_data, 0, DellPfsHeader) # Possible PFS PFAT Entry + is_pfat = False # Initial PFAT state for sub-PFS Entry + + _, pfat_entry_size = get_pfs_entry(entry_data, PFS_HEAD_LEN) # Get possible PFS PFAT Entry Size + + pfat_hdr_off = PFS_HEAD_LEN + pfat_entry_size # Possible PFAT Header starts after PFS Header & Entry + + pfat_entry_hdr = get_struct(entry_data, 0, DellPfsHeader) # Possible PFS PFAT Entry + if len(entry_data) - pfat_hdr_off >= PFAT_HDR_LEN: pfat_hdr = get_struct(entry_data, pfat_hdr_off, IntelBiosGuardHeader) + is_pfat = pfat_hdr.get_platform_id().upper().startswith('DELL') - + # Parse PFS Entry which contains sub-PFS Volume with PFAT Payload if pfat_entry_hdr.Tag == b'PFS.HDR.' and is_pfat: - entry_type = 'PFAT' # Re-set PFS Entry Type from OTHER to PFAT, to use such info afterwards - - entry_data = parse_pfat_pfs(pfat_entry_hdr, entry_data, padding, structure) # Parse sub-PFS PFAT Volume - + entry_type = 'PFAT' # Re-set PFS Entry Type from OTHER to PFAT, to use such info afterwards + + entry_data = parse_pfat_pfs(pfat_entry_hdr, entry_data, padding, structure) # Parse sub-PFS PFAT Volume + # Parse PFS Entry which contains zlib-compressed sub-PFS Volume elif pfs_zlib_offsets: - entry_type = 'ZLIB' # Re-set PFS Entry Type from OTHER to ZLIB, to use such info afterwards - pfs_count += 1 # Increase the count/index of parsed main PFS structures by one - + entry_type = 'ZLIB' # Re-set PFS Entry Type from OTHER to ZLIB, to use such info afterwards + + pfs_count += 1 # Increase the count/index of parsed main PFS structures by one + # Parse each sub-PFS ZLIB Section - for offset in pfs_zlib_offsets: + for offset in pfs_zlib_offsets: # Get the Name of the zlib-compressed full PFS structure via the already stored PFS Information # The zlib-compressed full PFS structure(s) are used to contain multiple FW (CombineBiosNameX) # When zlib-compressed full PFS structure(s) exist within the main/first full PFS structure, @@ -618,438 +715,505 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, extract_path, padding=0, # full PFS structure has count/index 1, the rest start at 2+ and thus, their PFS Information # names can be retrieved in order by subtracting 2 from the main/first PFS Information values sub_pfs_name = f'{info_all[pfs_count - 2][1]} v{info_all[pfs_count - 2][2]}' if info_all else ' UNKNOWN' - + # Set the sub-PFS output path (create sub-folders for each sub-PFS and its ZLIB sections) sub_pfs_path = os.path.join(extract_path, f'{pfs_count} {safe_name(sub_pfs_name)}') - + # Recursively call the PFS ZLIB Section Parser function for the sub-PFS Volume (pfs_index = pfs_count) - pfs_section_parse(entry_data, offset, sub_pfs_path, sub_pfs_name, pfs_count, pfs_count, True, padding + 4, structure, advanced) - - entries_all[index][4] = entry_data # Adjust PFS Entry Data after parsing PFAT (same ZLIB raw data, not stored afterwards) - entries_all[index][3] = entry_type # Adjust PFS Entry Type from OTHER to PFAT or ZLIB (ZLIB is ignored at file extraction) - + pfs_section_parse(entry_data, offset, sub_pfs_path, sub_pfs_name, pfs_count, pfs_count, + True, padding + 4, structure, advanced) + + # Adjust PFS Entry Data after parsing PFAT (same ZLIB raw data, not stored afterwards) + entry[4] = entry_data + + # Adjust PFS Entry Type from OTHER to PFAT or ZLIB (ZLIB is ignored at file extraction) + entry[3] = entry_type + # Name & Store each PFS Entry/Component Data, Data Signature, Metadata, Metadata Signature - for entry_index in range(len(entries_all)): - file_index = entries_all[entry_index][0] - file_guid = entries_all[entry_index][1] - file_version = entries_all[entry_index][2] - file_type = entries_all[entry_index][3] - file_data = entries_all[entry_index][4] - file_data_sig = entries_all[entry_index][5] - file_meta = entries_all[entry_index][6] - file_meta_sig = entries_all[entry_index][7] - + for entry in entries_all: + file_index, file_guid, file_version, file_type, file_data, file_data_sig, file_meta, file_meta_sig = entry + # Give Names to special PFS Entries, not covered by PFS Information if file_type == 'MODEL_INFO': file_name = 'Model Information' elif file_type == 'NAME_INFO': file_name = 'Filename Information' + if not advanced: - continue # Don't store Filename Information in non-advanced user mode + continue # Don't store Filename Information in non-advanced user mode elif file_type == 'SIG_INFO': file_name = 'Signature Information' + if not advanced: - continue # Don't store Signature Information in non-advanced user mode + continue # Don't store Signature Information in non-advanced user mode else: file_name = '' - + # Most PFS Entry Names & Versions are found at PFS Information via their GUID # Version can be found at DellPfsEntryR* but prefer PFS Information when possible - for info_index in range(len(info_all)): - info_guid = info_all[info_index][0] - info_name = info_all[info_index][1] - info_version = info_all[info_index][2] - + for info in info_all: + info_guid, info_name, info_version = info + # Give proper Name & Version info if Entry/Information GUIDs match if info_guid == file_guid: file_name = info_name + file_version = info_version - - info_all[info_index][0] = 'USED' # PFS with zlib-compressed sub-PFS use the same GUID - - break # Break at 1st Name match to not rename again from next zlib-compressed sub-PFS with the same GUID - + + # PFS with zlib-compressed sub-PFS use the same GUID + info[0] = 'USED' + + # Break at 1st Name match to not rename again from + # next zlib-compressed sub-PFS with the same GUID. + break + # For both advanced & non-advanced users, the goal is to store final/usable files only # so empty or intermediate files such as sub-PFS, PFS w/ PFAT or zlib-PFS are skipped # Main/First PFS CombineBiosNameX Metadata files must be kept for accurate Model Information # All users should check these files in order to choose the correct CombineBiosNameX modules - write_files = [] # Initialize list of output PFS Entry files to be written/extracted - - is_zlib = bool(file_type == 'ZLIB') # Determine if PFS Entry Data was zlib-compressed - + write_files = [] # Initialize list of output PFS Entry files to be written/extracted + + is_zlib = bool(file_type == 'ZLIB') # Determine if PFS Entry Data was zlib-compressed + if file_data and not is_zlib: - write_files.append([file_data, 'data']) # PFS Entry Data Payload - + write_files.append([file_data, 'data']) # PFS Entry Data Payload + if file_data_sig and advanced: - write_files.append([file_data_sig, 'sign_data']) # PFS Entry Data Signature - + write_files.append([file_data_sig, 'sign_data']) # PFS Entry Data Signature + if file_meta and (is_zlib or advanced): - write_files.append([file_meta, 'meta']) # PFS Entry Metadata Payload - + write_files.append([file_meta, 'meta']) # PFS Entry Metadata Payload + if file_meta_sig and advanced: - write_files.append([file_meta_sig, 'sign_meta']) # PFS Entry Metadata Signature - + write_files.append([file_meta_sig, 'sign_meta']) # PFS Entry Metadata Signature + # Write/Extract PFS Entry files for file in write_files: - full_name = f'{pfs_index} {pfs_name} -- {file_index} {file_name} v{file_version}' # Full PFS Entry Name + full_name = f'{pfs_index} {pfs_name} -- {file_index} {file_name} v{file_version}' # Full PFS Entry Name + pfs_file_write(file[0], file[1], file_type, full_name, extract_path, padding, structure, advanced) - + # Get PFS Footer Data after PFS Header Payload pfs_footer = buffer[PFS_HEAD_LEN + pfs_hdr.PayloadSize:PFS_HEAD_LEN + pfs_hdr.PayloadSize + PFS_FOOT_LEN] - + # Analyze PFS Footer Structure chk_pfs_ftr(pfs_footer, pfs_payload, pfs_hdr.PayloadSize, 'PFS', padding, structure) -# Analyze Dell PFS Entry Structure -def parse_pfs_entry(entry_buffer, entry_start, entry_size, entry_struct, text, padding=0, structure=True): + +def parse_pfs_entry(entry_buffer, entry_start, entry_size, entry_struct, text, padding=0, structure=True): + """ Analyze Dell PFS Entry Structure """ + # Get PFS Entry Structure values pfs_entry = get_struct(entry_buffer, entry_start, entry_struct) - + # Show PFS Entry Structure info if structure: printer('PFS Entry:\n', padding + 4) + pfs_entry.struct_print(padding + 8) - + # Validate that a known PFS Entry Header Version was encountered chk_hdr_ver(pfs_entry.HeaderVersion, text, padding + 8) - + # Validate that the PFS Entry Reserved field is empty if pfs_entry.Reserved != 0: printer(f'Error: Detected non-empty {text} Reserved field!', padding + 8) - + # Get PFS Entry Version string via "Version" and "VersionType" fields entry_version = get_entry_ver(pfs_entry.Version, pfs_entry.VersionType) - + # Get PFS Entry GUID in Big Endian format entry_guid = f'{int.from_bytes(pfs_entry.GUID, "little"):0{0x10 * 2}X}' - + # PFS Entry Data starts after the PFS Entry Structure entry_data_start = entry_start + entry_size entry_data_end = entry_data_start + pfs_entry.DataSize - + # PFS Entry Data Signature starts after PFS Entry Data entry_data_sig_start = entry_data_end entry_data_sig_end = entry_data_sig_start + pfs_entry.DataSigSize - + # PFS Entry Metadata starts after PFS Entry Data Signature - entry_met_start = entry_data_sig_end + entry_met_start = entry_data_sig_end entry_met_end = entry_met_start + pfs_entry.DataMetSize - + # PFS Entry Metadata Signature starts after PFS Entry Metadata entry_met_sig_start = entry_met_end entry_met_sig_end = entry_met_sig_start + pfs_entry.DataMetSigSize - - entry_data = entry_buffer[entry_data_start:entry_data_end] # Store PFS Entry Data - entry_data_sig = entry_buffer[entry_data_sig_start:entry_data_sig_end] # Store PFS Entry Data Signature - entry_met = entry_buffer[entry_met_start:entry_met_end] # Store PFS Entry Metadata - entry_met_sig = entry_buffer[entry_met_sig_start:entry_met_sig_end] # Store PFS Entry Metadata Signature - + + entry_data = entry_buffer[entry_data_start:entry_data_end] # Store PFS Entry Data + + entry_data_sig = entry_buffer[entry_data_sig_start:entry_data_sig_end] # Store PFS Entry Data Signature + + entry_met = entry_buffer[entry_met_start:entry_met_end] # Store PFS Entry Metadata + + entry_met_sig = entry_buffer[entry_met_sig_start:entry_met_sig_end] # Store PFS Entry Metadata Signature + return pfs_entry, entry_version, entry_guid, entry_data, entry_data_sig, entry_met, entry_met_sig, entry_met_sig_end -# Parse Dell PFS Volume with PFAT Payload + def parse_pfat_pfs(entry_hdr, entry_data, padding=0, structure=True): + """ Parse Dell PFS Volume with PFAT Payload """ + # Show PFS Volume indicator if structure: printer('PFS Volume:', padding + 4) - + # Show sub-PFS Header Structure Info if structure: printer('PFS Header:\n', padding + 8) + entry_hdr.struct_print(padding + 12) - + # Validate that a known sub-PFS Header Version was encountered chk_hdr_ver(entry_hdr.HeaderVersion, 'sub-PFS', padding + 12) - + # Get sub-PFS Payload Data pfat_payload = entry_data[PFS_HEAD_LEN:PFS_HEAD_LEN + entry_hdr.PayloadSize] - - # Get sub-PFS Footer Data after sub-PFS Header Payload (must be retrieved at the initial entry_data, before PFAT parsing) + + # Get sub-PFS Footer Data after sub-PFS Header Payload, which must + # must be retrieved at the initial entry_data, before PFAT parsing. pfat_footer = entry_data[PFS_HEAD_LEN + entry_hdr.PayloadSize:PFS_HEAD_LEN + entry_hdr.PayloadSize + PFS_FOOT_LEN] - + # Parse all sub-PFS Payload PFAT Entries - pfat_entries_all = [] # Storage for all sub-PFS PFAT Entries Order/Offset & Payload/Raw Data - pfat_entry_start = 0 # Increasing sub-PFS PFAT Entry start offset - pfat_entry_index = 1 # Increasing sub-PFS PFAT Entry count index - _, pfs_entry_size = get_pfs_entry(pfat_payload, 0) # Get initial PFS PFAT Entry Size for loop + pfat_entries_all = [] # Storage for all sub-PFS PFAT Entries Order/Offset & Payload/Raw Data + pfat_entry_start = 0 # Increasing sub-PFS PFAT Entry start offset + pfat_entry_index = 1 # Increasing sub-PFS PFAT Entry count index + + _, pfs_entry_size = get_pfs_entry(pfat_payload, 0) # Get initial PFS PFAT Entry Size for loop + while len(pfat_payload[pfat_entry_start:pfat_entry_start + pfs_entry_size]) == pfs_entry_size: # Get sub-PFS PFAT Entry Structure & Size info - pfat_entry_struct,pfat_entry_size = get_pfs_entry(pfat_payload, pfat_entry_start) - + pfat_entry_struct, pfat_entry_size = get_pfs_entry(pfat_payload, pfat_entry_start) + # Analyze sub-PFS PFAT Entry Structure and get relevant info - pfat_entry,_,_,pfat_entry_data,_,pfat_entry_met,_,pfat_next_entry = parse_pfs_entry(pfat_payload, - pfat_entry_start, pfat_entry_size, pfat_entry_struct, 'sub-PFS PFAT Entry', padding + 4, structure) - + pfat_entry, _, _, pfat_entry_data, _, pfat_entry_met, _, pfat_next_entry = \ + parse_pfs_entry(pfat_payload, pfat_entry_start, pfat_entry_size, pfat_entry_struct, + 'sub-PFS PFAT Entry', padding + 4, structure) + # Each sub-PFS PFAT Entry includes an AMI BIOS Guard (a.k.a. PFAT) block at the beginning # We need to parse the PFAT block and remove its contents from the final Payload/Raw Data - pfat_hdr_off = pfat_entry_start + pfat_entry_size # PFAT block starts after PFS Entry - + pfat_hdr_off = pfat_entry_start + pfat_entry_size # PFAT block starts after PFS Entry + # Get sub-PFS PFAT Header Structure values pfat_hdr = get_struct(pfat_payload, pfat_hdr_off, IntelBiosGuardHeader) - + # Get ordinal value of the sub-PFS PFAT Entry Index pfat_entry_idx_ord = get_ordinal(pfat_entry_index) - + # Show sub-PFS PFAT Header Structure info if structure: printer(f'PFAT Block {pfat_entry_idx_ord} - Header:\n', padding + 12) + pfat_hdr.struct_print(padding + 16) - - pfat_script_start = pfat_hdr_off + PFAT_HDR_LEN # PFAT Block Script Start - pfat_script_end = pfat_script_start + pfat_hdr.ScriptSize # PFAT Block Script End - pfat_script_data = pfat_payload[pfat_script_start:pfat_script_end] # PFAT Block Script Data - pfat_payload_start = pfat_script_end # PFAT Block Payload Start (at Script end) - pfat_payload_end = pfat_script_end + pfat_hdr.DataSize # PFAT Block Data End - pfat_payload_data = pfat_payload[pfat_payload_start:pfat_payload_end] # PFAT Block Raw Data - pfat_hdr_bgs_size = PFAT_HDR_LEN + pfat_hdr.ScriptSize # PFAT Block Header & Script Size - - # The PFAT Script End should match the total Entry Data Size w/o PFAT block + + pfat_script_start = pfat_hdr_off + PFAT_HDR_LEN # PFAT Block Script Start + + pfat_script_end = pfat_script_start + pfat_hdr.ScriptSize # PFAT Block Script End + + pfat_script_data = pfat_payload[pfat_script_start:pfat_script_end] # PFAT Block Script Data + + pfat_payload_start = pfat_script_end # PFAT Block Payload Start (at Script end) + + pfat_payload_end = pfat_script_end + pfat_hdr.DataSize # PFAT Block Data End + + pfat_payload_data = pfat_payload[pfat_payload_start:pfat_payload_end] # PFAT Block Raw Data + + pfat_hdr_bgs_size = PFAT_HDR_LEN + pfat_hdr.ScriptSize # PFAT Block Header & Script Size + + # The PFAT Script End should match the total Entry Data Size w/o PFAT block if pfat_hdr_bgs_size != pfat_entry.DataSize - pfat_hdr.DataSize: - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Header & PFAT Size mismatch!', padding + 16) - + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Header & PFAT Size mismatch!', + padding + 16) + # Get PFAT Header Flags (SFAM, ProtectEC, GFXMitDis, FTU, Reserved) - is_sfam,_,_,_,_ = pfat_hdr.get_flags() - + is_sfam, _, _, _, _ = pfat_hdr.get_flags() + # Parse sub-PFS PFAT Signature, if applicable (only when PFAT Header > SFAM flag is set) - if is_sfam and len(pfat_payload[pfat_payload_end:pfat_payload_end + PFAT_SIG_LEN]) == PFAT_SIG_LEN: - # Get sub-PFS PFAT Signature Structure values - pfat_sig = get_struct(pfat_payload, pfat_payload_end, IntelBiosGuardSignature2k) - - # Show sub-PFS PFAT Signature Structure info + if is_sfam: if structure: printer(f'PFAT Block {pfat_entry_idx_ord} - Signature:\n', padding + 12) - pfat_sig.struct_print(padding + 16) - + + # Get sub-PFS PFAT Signature Structure values + bg_sign_len = parse_bg_sign(pfat_payload, pfat_payload_end, structure, padding + 16) + + if len(pfat_payload[pfat_payload_end:pfat_payload_end + bg_sign_len]) != bg_sign_len: + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Signature Size mismatch!', + padding + 12) + # Show PFAT Script via BIOS Guard Script Tool if structure: printer(f'PFAT Block {pfat_entry_idx_ord} - Script:\n', padding + 12) - + _ = parse_bg_script(pfat_script_data, padding + 16) - + # The payload of sub-PFS PFAT Entries is not in proper order by default # We can get each payload's order from PFAT Script > OpCode #2 (set I0 imm) # PFAT Script OpCode #2 > Operand #3 stores the payload Offset in final image pfat_entry_off = int.from_bytes(pfat_script_data[0xC:0x10], 'little') - + # We can get each payload's length from PFAT Script > OpCode #4 (set I2 imm) # PFAT Script OpCode #4 > Operand #3 stores the payload Length in final image pfat_entry_len = int.from_bytes(pfat_script_data[0x1C:0x20], 'little') - + # Check that the PFAT Entry Length from Header & Script match if pfat_hdr.DataSize != pfat_entry_len: - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Header & Script Length mismatch!', padding + 12) - + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Header & Script Size mismatch!', + padding + 12) + # Initialize sub-PFS PFAT Entry Metadata Address pfat_entry_adr = pfat_entry_off - + # Parse sub-PFS PFAT Entry/Block Metadata if len(pfat_entry_met) >= PFS_PFAT_LEN: # Get sub-PFS PFAT Metadata Structure values pfat_met = get_struct(pfat_entry_met, 0, DellPfsPfatMetadata) - + # Store sub-PFS PFAT Entry Metadata Address pfat_entry_adr = pfat_met.Address - + # Show sub-PFS PFAT Metadata Structure info if structure: printer(f'PFAT Block {pfat_entry_idx_ord} - Metadata:\n', padding + 12) + pfat_met.struct_print(padding + 16) - + # Another way to get each PFAT Entry Offset is from its Metadata, if applicable # Check that the PFAT Entry Offsets from PFAT Script and PFAT Metadata match if pfat_entry_off != pfat_met.Offset: - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Metadata & PFAT Offset mismatch!', padding + 16) - pfat_entry_off = pfat_met.Offset # Prefer Offset from Metadata, in case PFAT Script differs - + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Metadata & PFAT Offset mismatch!', + padding + 16) + + # Prefer Offset from Metadata, in case PFAT Script differs + pfat_entry_off = pfat_met.Offset + # Another way to get each PFAT Entry Length is from its Metadata, if applicable # Check that the PFAT Entry Length from PFAT Script and PFAT Metadata match - if not (pfat_hdr.DataSize == pfat_entry_len == pfat_met.DataSize): - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Metadata & PFAT Length mismatch!', padding + 16) - + if not pfat_hdr.DataSize == pfat_entry_len == pfat_met.DataSize: + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Metadata & PFAT Length mismatch!', + padding + 16) + # Check that the PFAT Entry payload Size from PFAT Header matches the one from PFAT Metadata if pfat_hdr.DataSize != pfat_met.DataSize: - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Metadata & PFAT Block Size mismatch!', padding + 16) - + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} Metadata & PFAT Block Size mismatch!', + padding + 16) + # Get sub-PFS Entry Raw Data by subtracting PFAT Header & Script from PFAT Entry Data pfat_entry_data_raw = pfat_entry_data[pfat_hdr_bgs_size:] - + # The sub-PFS Entry Raw Data (w/o PFAT Header & Script) should match with the PFAT Block payload if pfat_entry_data_raw != pfat_payload_data: - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} w/o PFAT & PFAT Block Data mismatch!', padding + 16) - pfat_entry_data_raw = pfat_payload_data # Prefer Data from PFAT Block, in case PFAT Entry differs - + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} w/o PFAT & PFAT Block Data mismatch!', + padding + 16) + + # Prefer Data from PFAT Block, in case PFAT Entry differs + pfat_entry_data_raw = pfat_payload_data + # Store each sub-PFS PFAT Entry/Block Offset, Address, Ordinal Index and Payload/Raw Data # Goal is to sort these based on Offset first and Address second, in cases of same Offset # For example, Precision 3430 has two PFAT Entries with the same Offset of 0x40000 at both # BG Script and PFAT Metadata but their PFAT Metadata Address is 0xFF040000 and 0xFFA40000 pfat_entries_all.append((pfat_entry_off, pfat_entry_adr, pfat_entry_idx_ord, pfat_entry_data_raw)) - - # Check if next sub-PFS PFAT Entry offset is valid + + # Check if next sub-PFS PFAT Entry offset is valid if pfat_next_entry <= 0: - printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} with invalid next PFAT Block offset!', padding + 16) - pfat_next_entry += pfs_entry_size # Avoid a potential infinite loop if next sub-PFS PFAT Entry offset is bad - - pfat_entry_start = pfat_next_entry # Next sub-PFS PFAT Entry starts after sub-PFS Entry Metadata Signature - + printer(f'Error: Detected sub-PFS PFAT Block {pfat_entry_idx_ord} with invalid next PFAT Block offset!', + padding + 16) + + # Avoid a potential infinite loop if next sub-PFS PFAT Entry offset is bad + pfat_next_entry += pfs_entry_size + + # Next sub-PFS PFAT Entry starts after sub-PFS Entry Metadata Signature + pfat_entry_start = pfat_next_entry + pfat_entry_index += 1 - - pfat_entries_all.sort() # Sort all sub-PFS PFAT Entries based on their Offset/Address - - block_start_exp = 0 # Initialize sub-PFS PFAT Entry expected Offset - total_pfat_data = b'' # Initialize final/ordered sub-PFS Entry Data - + + # Sort all sub-PFS PFAT Entries based on their Offset/Address + pfat_entries_all.sort() + + block_start_exp = 0 # Initialize sub-PFS PFAT Entry expected Offset + total_pfat_data = b'' # Initialize final/ordered sub-PFS Entry Data + # Parse all sorted sub-PFS PFAT Entries and merge their payload/data - for block_start,_,block_index,block_data in pfat_entries_all: + for block_start, _, block_index, block_data in pfat_entries_all: # Fill any data gaps between sorted sub-PFS PFAT Entries with padding # For example, Precision 7960 v0.16.68 has gap at 0x1190000-0x11A0000 block_data_gap = block_start - block_start_exp + if block_data_gap > 0: - printer(f'Warning: Filled sub-PFS PFAT {block_index} data gap 0x{block_data_gap:X} [0x{block_start_exp:X}-0x{block_start:X}]!', padding + 8) - total_pfat_data += b'\xFF' * block_data_gap # Use 0xFF padding to fill in data gaps in PFAT UEFI firmware images - - total_pfat_data += block_data # Append sorted sub-PFS PFAT Entry payload/data - - block_start_exp = len(total_pfat_data) # Set next sub-PFS PFAT Entry expected Start - + printer(f'Warning: Filled sub-PFS PFAT {block_index} data gap 0x{block_data_gap:X} ' + f'[0x{block_start_exp:X}-0x{block_start:X}]!', padding + 8) + + # Use 0xFF padding to fill in data gaps in PFAT UEFI firmware images + total_pfat_data += b'\xFF' * block_data_gap + + total_pfat_data += block_data # Append sorted sub-PFS PFAT Entry payload/data + + block_start_exp = len(total_pfat_data) # Set next sub-PFS PFAT Entry expected Start + # Verify that the end offset of the last PFAT Entry matches the final sub-PFS Entry Data Size if len(total_pfat_data) != pfat_entries_all[-1][0] + len(pfat_entries_all[-1][3]): printer('Error: Detected sub-PFS PFAT total buffer size and last block end mismatch!', padding + 8) - + # Analyze sub-PFS Footer Structure chk_pfs_ftr(pfat_footer, pfat_payload, entry_hdr.PayloadSize, 'Sub-PFS', padding + 4, structure) - + return total_pfat_data -# Get Dell PFS Entry Structure & Size via its Version + def get_pfs_entry(buffer, offset): - pfs_entry_ver = int.from_bytes(buffer[offset + 0x10:offset + 0x14], 'little') # PFS Entry Version - + """ Get Dell PFS Entry Structure & Size via its Version """ + + pfs_entry_ver = int.from_bytes(buffer[offset + 0x10:offset + 0x14], 'little') # PFS Entry Version + if pfs_entry_ver == 1: return DellPfsEntryR1, ctypes.sizeof(DellPfsEntryR1) - + if pfs_entry_ver == 2: return DellPfsEntryR2, ctypes.sizeof(DellPfsEntryR2) return DellPfsEntryR2, ctypes.sizeof(DellPfsEntryR2) -# Determine Dell PFS Entry Version string + def get_entry_ver(version_fields, version_types): - version = '' # Initialize Version string - - # Each Version Type (1 byte) determines the type of each Version Value (2 bytes) + """ Determine Dell PFS Entry Version string """ + + version = '' # Initialize Version string + + # Version Type (1 byte) determines the type of Version Value (2 bytes) # Version Type 'N' is Number, 'A' is Text and ' ' is Empty/Unused - for index,field in enumerate(version_fields): + for index, field in enumerate(version_fields): eol = '' if index == len(version_fields) - 1 else '.' - + if version_types[index] == 65: - version += f'{field:X}{eol}' # 0x41 = ASCII + version += f'{field:X}{eol}' # 0x41 = ASCII elif version_types[index] == 78: - version += f'{field:d}{eol}' # 0x4E = Number + version += f'{field:d}{eol}' # 0x4E = Number elif version_types[index] in (0, 32): - version = version.strip('.') # 0x00 or 0x20 = Unused + version = version.strip('.') # 0x00 or 0x20 = Unused else: - version += f'{field:X}{eol}' # Unknown - + version += f'{field:X}{eol}' # Unknown + return version -# Check if Dell PFS Header Version is known + def chk_hdr_ver(version, text, padding=0): - if version in (1,2): + """ Check if Dell PFS Header Version is known """ + + if version in (1, 2): return - + printer(f'Error: Unknown {text} Header Version {version}!', padding) - + return -# Analyze Dell PFS Footer Structure -def chk_pfs_ftr(footer_buffer, data_buffer, data_size, text, padding=0, structure=True): + +def chk_pfs_ftr(footer_buffer, data_buffer, data_size, text, padding=0, structure=True): + """ Analyze Dell PFS Footer Structure """ + # Get PFS Footer Structure values pfs_ftr = get_struct(footer_buffer, 0, DellPfsFooter) - + # Validate that a PFS Footer was parsed if pfs_ftr.Tag == b'PFS.FTR.': # Show PFS Footer Structure info if structure: printer('PFS Footer:\n', padding + 4) + pfs_ftr.struct_print(padding + 8) else: printer(f'Error: {text} Footer could not be found!', padding + 4) - + # Validate that PFS Header Payload Size matches the one at PFS Footer if data_size != pfs_ftr.PayloadSize: printer(f'Error: {text} Header & Footer Payload Size mismatch!', padding + 4) - + # Calculate the PFS Payload Data CRC-32 w/ Vector 0 pfs_ftr_crc = ~zlib.crc32(data_buffer, 0) & 0xFFFFFFFF - + # Validate PFS Payload Data Checksum via PFS Footer if pfs_ftr.Checksum != pfs_ftr_crc: printer(f'Error: Invalid {text} Footer Payload Checksum!', padding + 4) -# Write/Extract Dell PFS Entry Files (Data, Metadata, Signature) + def pfs_file_write(bin_buff, bin_name, bin_type, full_name, out_path, padding=0, structure=True, advanced=True): + """ Write/Extract Dell PFS Entry Files (Data, Metadata, Signature) """ + # Store Data/Metadata Signature (advanced users only) if bin_name.startswith('sign'): final_name = f'{safe_name(full_name)}.{bin_name.split("_")[1]}.sig' - final_path = os.path.join(out_path, final_name) - - with open(final_path, 'wb') as pfs_out: - pfs_out.write(bin_buff) # Write final Data/Metadata Signature - - return # Skip further processing for Signatures - - # Store Data/Metadata Payload - bin_ext = f'.{bin_name}.bin' if advanced else '.bin' # Simpler Data/Metadata Extension for non-advanced users - - # Some Data may be Text or XML files with useful information for non-advanced users - is_text,final_data,file_ext,write_mode = bin_is_text(bin_buff, bin_type, bin_name == 'meta', padding, structure, advanced) - - final_name = f'{safe_name(full_name)}{bin_ext[:-4] + file_ext if is_text else bin_ext}' - final_path = os.path.join(out_path, final_name) - - with open(final_path, write_mode) as pfs_out: - pfs_out.write(final_data) # Write final Data/Metadata Payload -# Check if Dell PFS Entry file/data is Text/XML and Convert + final_path = os.path.join(out_path, final_name) + + with open(final_path, 'wb') as pfs_out: + pfs_out.write(bin_buff) # Write final Data/Metadata Signature + + return # Skip further processing for Signatures + + # Store Data/Metadata Payload + bin_ext = f'.{bin_name}.bin' if advanced else '.bin' # Simpler Data/Metadata Extension for non-advanced users + + # Some Data may be Text or XML files with useful information for non-advanced users + is_text, final_data, file_ext, write_mode = \ + bin_is_text(bin_buff, bin_type, bin_name == 'meta', padding, structure, advanced) + + final_name = f'{safe_name(full_name)}{bin_ext[:-4] + file_ext if is_text else bin_ext}' + + final_path = os.path.join(out_path, final_name) + + with open(final_path, write_mode) as pfs_out: + pfs_out.write(final_data) # Write final Data/Metadata Payload + + def bin_is_text(buffer, file_type, is_metadata, padding=0, structure=True, advanced=True): + """ Check if Dell PFS Entry file/data is Text/XML and Convert """ + is_text = False write_mode = 'wb' extension = '.bin' buffer_in = buffer - - if b',END' in buffer[-0x8:]: # Text Type 1 + + if b',END' in buffer[-0x8:]: # Text Type 1 is_text = True write_mode = 'w' extension = '.txt' - buffer = buffer.decode('utf-8').split(',END')[0].replace(';','\n') - elif buffer.startswith(b'VendorName=Dell'): # Text Type 2 + + buffer = buffer.decode('utf-8').split(',END')[0].replace(';', '\n') + elif buffer.startswith(b'VendorName=Dell'): # Text Type 2 is_text = True write_mode = 'w' extension = '.txt' - buffer = buffer.split(b'\x00')[0].decode('utf-8').replace(';','\n') - elif b' int: + return self.TotalSize - self.ImageSize + + def get_image_tag(self) -> str: + """ Get Insyde iFlash image tag """ + + return self.ImageTag.decode('utf-8', 'ignore').strip('_') + + def struct_print(self, padd: int) -> None: + """ Display structure information """ + + printer(['Signature :', self.Signature.decode('utf-8')], padd, False) + printer(['Image Name:', self.get_image_tag()], padd, False) + printer(['Image Size:', f'0x{self.ImageSize:X}'], padd, False) + printer(['Total Size:', f'0x{self.TotalSize:X}'], padd, False) + printer(['Padd Size :', f'0x{self._get_padd_len():X}'], padd, False) + + +def is_insyde_ifd(input_object: str | bytes | bytearray) -> bool: + """ Check if input is Insyde iFlash/iFdPacker Update image """ + + input_buffer: bytes = file_to_bytes(input_object) + + is_ifl: bool = bool(insyde_iflash_detect(input_buffer)) + + is_sfx: bool = bool(PAT_INSYDE_SFX.search(input_buffer)) + return is_ifl or is_sfx -# Parse & Extract Insyde iFlash/iFdPacker Update images -def insyde_ifd_extract(input_file, extract_path, padding=0): - input_buffer = file_to_bytes(input_file) - - iflash_code = insyde_iflash_extract(input_buffer, extract_path, padding) - - ifdpack_path = os.path.join(extract_path, 'Insyde iFdPacker SFX') - - ifdpack_code = insyde_packer_extract(input_buffer, ifdpack_path, padding) - + +def insyde_ifd_extract(input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> int: + """ Parse & Extract Insyde iFlash/iFdPacker Update images """ + + input_buffer: bytes = file_to_bytes(input_object) + + iflash_code: int = insyde_iflash_extract(input_buffer, extract_path, padding) + + ifdpack_path: str = os.path.join(extract_path, 'Insyde iFdPacker SFX') + + ifdpack_code: int = insyde_packer_extract(input_buffer, ifdpack_path, padding) + return iflash_code and ifdpack_code -# Detect Insyde iFlash Update image -def insyde_iflash_detect(input_buffer): - iflash_match_all = [] - iflash_match_nan = [0x0,0xFFFFFFFF] - + +def insyde_iflash_detect(input_buffer: bytes) -> list: + """ Detect Insyde iFlash Update image """ + + iflash_match_all: list = [] + iflash_match_nan: list = [0x0, 0xFFFFFFFF] + for iflash_match in PAT_INSYDE_IFL.finditer(input_buffer): - ifl_bgn = iflash_match.start() - + ifl_bgn: int = iflash_match.start() + if len(input_buffer[ifl_bgn:]) <= INS_IFL_LEN: continue - + ifl_hdr = get_struct(input_buffer, ifl_bgn, IflashHeader) - + if ifl_hdr.TotalSize in iflash_match_nan \ - or ifl_hdr.ImageSize in iflash_match_nan \ - or ifl_hdr.TotalSize < ifl_hdr.ImageSize \ - or ifl_bgn + INS_IFL_LEN + ifl_hdr.TotalSize > len(input_buffer): + or ifl_hdr.ImageSize in iflash_match_nan \ + or ifl_hdr.TotalSize < ifl_hdr.ImageSize \ + or ifl_bgn + INS_IFL_LEN + ifl_hdr.TotalSize > len(input_buffer): continue - + iflash_match_all.append([ifl_bgn, ifl_hdr]) - + return iflash_match_all -# Extract Insyde iFlash Update image -def insyde_iflash_extract(input_buffer, extract_path, padding=0): - insyde_iflash_all = insyde_iflash_detect(input_buffer) - + +def insyde_iflash_extract(input_buffer: bytes, extract_path: str, padding: int = 0) -> int: + """ Extract Insyde iFlash Update image """ + + insyde_iflash_all: list = insyde_iflash_detect(input_buffer) + if not insyde_iflash_all: return 127 - + printer('Detected Insyde iFlash Update image!', padding) - + make_dirs(extract_path, delete=True) - - exit_codes = [] - + + exit_codes: list = [] + for insyde_iflash in insyde_iflash_all: - exit_code = 0 - - ifl_bgn,ifl_hdr = insyde_iflash - - img_bgn = ifl_bgn + INS_IFL_LEN - img_end = img_bgn + ifl_hdr.ImageSize - img_bin = input_buffer[img_bgn:img_end] - + exit_code: int = 0 + + ifl_bgn, ifl_hdr = insyde_iflash + + img_bgn: int = ifl_bgn + INS_IFL_LEN + img_end: int = img_bgn + ifl_hdr.ImageSize + img_bin: bytes = input_buffer[img_bgn:img_end] + if len(img_bin) != ifl_hdr.ImageSize: exit_code = 1 - - img_val = [ifl_hdr.get_image_tag(), 'bin'] - img_tag,img_ext = INS_IFL_IMG.get(img_val[0], img_val) - - img_name = f'{img_tag} [0x{img_bgn:08X}-0x{img_end:08X}]' - + + img_val: list = [ifl_hdr.get_image_tag(), 'bin'] + img_tag, img_ext = INS_IFL_IMG.get(img_val[0], img_val) + + img_name: str = f'{img_tag} [0x{img_bgn:08X}-0x{img_end:08X}]' + printer(f'{img_name}\n', padding + 4) - + ifl_hdr.struct_print(padding + 8) - - if img_val == [img_tag,img_ext]: + + if img_val == [img_tag, img_ext]: printer(f'Note: Detected new Insyde iFlash tag {img_tag}!', padding + 12, pause=True) - - out_name = f'{img_name}.{img_ext}' - - out_path = os.path.join(extract_path, safe_name(out_name)) - + + out_name: str = f'{img_name}.{img_ext}' + + out_path: str = os.path.join(extract_path, safe_name(out_name)) + with open(out_path, 'wb') as out_image: out_image.write(img_bin) - + printer(f'Succesfull Insyde iFlash > {img_tag} extraction!', padding + 12) - + exit_codes.append(exit_code) - + return sum(exit_codes) -# Extract Insyde iFdPacker 7-Zip SFX 7z Update image -def insyde_packer_extract(input_buffer, extract_path, padding=0): + +def insyde_packer_extract(input_buffer: bytes, extract_path: str, padding: int = 0) -> int: + """ Extract Insyde iFdPacker 7-Zip SFX 7z Update image """ + match_sfx = PAT_INSYDE_SFX.search(input_buffer) - + if not match_sfx: return 127 - + printer('Detected Insyde iFdPacker Update image!', padding) - + make_dirs(extract_path, delete=True) - - sfx_buffer = bytearray(input_buffer[match_sfx.end() - 0x5:]) - + + sfx_buffer: bytearray = bytearray(input_buffer[match_sfx.end() - 0x5:]) + if sfx_buffer[:0x5] == b'\x6E\xF4\x79\x5F\x4E': printer('Detected Insyde iFdPacker > 7-Zip SFX > Obfuscation!', padding + 4) - - for index,byte in enumerate(sfx_buffer): + + for index, byte in enumerate(sfx_buffer): sfx_buffer[index] = byte // 2 + (128 if byte % 2 else 0) - + printer('Removed Insyde iFdPacker > 7-Zip SFX > Obfuscation!', padding + 8) - + printer('Extracting Insyde iFdPacker > 7-Zip SFX archive...', padding + 4) - + if bytes(INS_SFX_PWD, 'utf-16le') in input_buffer[:match_sfx.start()]: printer('Detected Insyde iFdPacker > 7-Zip SFX > Password!', padding + 8) + printer(INS_SFX_PWD, padding + 12) - - sfx_path = os.path.join(extract_path, 'Insyde_iFdPacker_SFX.7z') - + + sfx_path: str = os.path.join(extract_path, 'Insyde_iFdPacker_SFX.7z') + with open(sfx_path, 'wb') as sfx_file: sfx_file.write(sfx_buffer) - + if is_szip_supported(sfx_path, padding + 8, args=[f'-p{INS_SFX_PWD}'], check=True): if szip_decompress(sfx_path, extract_path, 'Insyde iFdPacker > 7-Zip SFX', - padding + 8, args=[f'-p{INS_SFX_PWD}'], check=True) == 0: + padding + 8, args=[f'-p{INS_SFX_PWD}'], check=True) == 0: os.remove(sfx_path) else: return 125 else: return 126 - + exit_codes = [] - + for sfx_file in get_path_files(extract_path): if is_insyde_ifd(sfx_file): printer(f'{os.path.basename(sfx_file)}', padding + 12) - - ifd_code = insyde_ifd_extract(sfx_file, get_extract_path(sfx_file), padding + 16) - + + ifd_code: int = insyde_ifd_extract(sfx_file, get_extract_path(sfx_file), padding + 16) + exit_codes.append(ifd_code) - + return sum(exit_codes) + # Insyde iFdPacker known 7-Zip SFX Password -INS_SFX_PWD = 'Y`t~i!L@i#t$U%h^s7A*l(f)E-d=y+S_n?i' +INS_SFX_PWD: str = 'Y`t~i!L@i#t$U%h^s7A*l(f)E-d=y+S_n?i' # Insyde iFlash known Image Names -INS_IFL_IMG = { - 'BIOSCER' : ['Certificate', 'bin'], - 'BIOSCR2' : ['Certificate 2nd', 'bin'], - 'BIOSIMG' : ['BIOS-UEFI', 'bin'], - 'DRV_IMG' : ['isflash', 'efi'], - 'EC_IMG' : ['Embedded Controller', 'bin'], - 'INI_IMG' : ['platform', 'ini'], - 'ME_IMG' : ['Management Engine', 'bin'], - 'OEM_ID' : ['OEM Identifier', 'bin'], - } +INS_IFL_IMG: dict = { + 'BIOSCER': ['Certificate', 'bin'], + 'BIOSCR2': ['Certificate 2nd', 'bin'], + 'BIOSIMG': ['BIOS-UEFI', 'bin'], + 'DRV_IMG': ['isflash', 'efi'], + 'EC_IMG': ['Embedded Controller', 'bin'], + 'INI_IMG': ['platform', 'ini'], + 'ME_IMG': ['Management Engine', 'bin'], + 'OEM_ID': ['OEM Identifier', 'bin'], +} # Get common ctypes Structure Sizes -INS_IFL_LEN = ctypes.sizeof(IflashHeader) +INS_IFL_LEN: int = ctypes.sizeof(IflashHeader) if __name__ == '__main__': - BIOSUtility(TITLE, is_insyde_ifd, insyde_ifd_extract).run_utility() + BIOSUtility(title=TITLE, check=is_insyde_ifd, main=insyde_ifd_extract).run_utility() diff --git a/LICENSE b/LICENSE index 06831fb..0265ddc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Plato Mavropoulos +Copyright (c) 2019-2024 Plato Mavropoulos Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Panasonic_BIOS_Extract.py b/Panasonic_BIOS_Extract.py index 096bec3..13746fe 100644 --- a/Panasonic_BIOS_Extract.py +++ b/Panasonic_BIOS_Extract.py @@ -1,22 +1,19 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Panasonic BIOS Extract Panasonic BIOS Package Extractor -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'Panasonic BIOS Package Extractor v2.0_a10' - -import os import io -import sys -import lznt1 +import logging +import os + import pefile -# Stop __pycache__ generation -sys.dont_write_bytecode = True +from dissect.util.compression import lznt1 from common.comp_szip import is_szip_supported, szip_decompress from common.path_ops import get_path_files, make_dirs, path_stem, safe_name @@ -28,182 +25,216 @@ from common.text_ops import file_to_bytes from AMI_PFAT_Extract import is_ami_pfat, parse_pfat_file -# Check if input is Panasonic BIOS Package PE +TITLE = 'Panasonic BIOS Package Extractor v3.0' + + def is_panasonic_pkg(in_file): + """ Check if input is Panasonic BIOS Package PE """ + in_buffer = file_to_bytes(in_file) - - pe_file = get_pe_file(in_buffer, fast=True) - + + pe_file = get_pe_file(in_buffer, silent=True) + if not pe_file: return False - - pe_info = get_pe_info(pe_file) - + + pe_info = get_pe_info(pe_file, silent=True) + if not pe_info: return False - - if pe_info.get(b'FileDescription',b'').upper() != b'UNPACK UTILITY': + + if pe_info.get(b'FileDescription', b'').upper() != b'UNPACK UTILITY': return False - + if not PAT_MICROSOFT_CAB.search(in_buffer): return False - + return True -# Search and Extract Panasonic BIOS Package PE CAB archive + def panasonic_cab_extract(buffer, extract_path, padding=0): - pe_path,pe_file,pe_info = [None] * 3 - + """ Search and Extract Panasonic BIOS Package PE CAB archive """ + + pe_path, pe_file, pe_info = [None] * 3 + cab_bgn = PAT_MICROSOFT_CAB.search(buffer).start() cab_len = int.from_bytes(buffer[cab_bgn + 0x8:cab_bgn + 0xC], 'little') cab_end = cab_bgn + cab_len + cab_bin = buffer[cab_bgn:cab_end] + cab_tag = f'[0x{cab_bgn:06X}-0x{cab_end:06X}]' - + cab_path = os.path.join(extract_path, f'CAB_{cab_tag}.cab') - + with open(cab_path, 'wb') as cab_file: - cab_file.write(cab_bin) # Store CAB archive - + cab_file.write(cab_bin) # Store CAB archive + if is_szip_supported(cab_path, padding, check=True): printer(f'Panasonic BIOS Package > PE > CAB {cab_tag}', padding) - + if szip_decompress(cab_path, extract_path, 'CAB', padding + 4, check=True) == 0: - os.remove(cab_path) # Successful extraction, delete CAB archive + os.remove(cab_path) # Successful extraction, delete CAB archive else: return pe_path, pe_file, pe_info else: return pe_path, pe_file, pe_info - + for file_path in get_path_files(extract_path): - pe_file = get_pe_file(file_path, fast=True) + pe_file = get_pe_file(file_path, padding, silent=True) + if pe_file: - pe_info = get_pe_info(pe_file) - if pe_info.get(b'FileDescription',b'').upper() == b'BIOS UPDATE': + pe_info = get_pe_info(pe_file, padding, silent=True) + + if pe_info.get(b'FileDescription', b'').upper() == b'BIOS UPDATE': pe_path = file_path + break else: return pe_path, pe_file, pe_info - + return pe_path, pe_file, pe_info -# Extract & Decompress Panasonic BIOS Update PE RCDATA (LZNT1) + def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0): + """ Extract & Decompress Panasonic BIOS Update PE RCDATA (LZNT1) """ + is_rcdata = False - + # When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to RCDATA Directories pe_file.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) - + # Parse all Resource Data Directories > RCDATA (ID = 10) for entry in pe_file.DIRECTORY_ENTRY_RESOURCE.entries: if entry.struct.name == 'IMAGE_RESOURCE_DIRECTORY_ENTRY' and entry.struct.Id == 0xA: is_rcdata = True + for resource in entry.directory.entries: res_bgn = resource.directory.entries[0].data.struct.OffsetToData res_len = resource.directory.entries[0].data.struct.Size res_end = res_bgn + res_len + res_bin = pe_file.get_data(res_bgn, res_len) + res_tag = f'{pe_name} [0x{res_bgn:06X}-0x{res_end:06X}]' + res_out = os.path.join(extract_path, f'{res_tag}') - + printer(res_tag, padding + 4) - + try: res_raw = lznt1.decompress(res_bin[0x8:]) - - printer('Succesfull LZNT1 decompression via lznt1!', padding + 8) - except Exception: + + if len(res_raw) != int.from_bytes(res_bin[0x4:0x8], 'little'): + raise ValueError('LZNT1_DECOMPRESS_BAD_SIZE') + + printer('Succesfull LZNT1 decompression via Dissect!', padding + 8) + except Exception as error: # pylint: disable=broad-except + logging.debug('Error: LZNT1 decompression of %s failed: %s', res_tag, error) + res_raw = res_bin - + printer('Succesfull PE Resource extraction!', padding + 8) - + # Detect & Unpack AMI BIOS Guard (PFAT) BIOS image if is_ami_pfat(res_raw): pfat_dir = os.path.join(extract_path, res_tag) - + parse_pfat_file(res_raw, pfat_dir, padding + 12) else: if is_pe_file(res_raw): res_ext = 'exe' - elif res_raw.startswith(b'[') and res_raw.endswith((b'\x0D\x0A',b'\x0A')): + elif res_raw.startswith(b'[') and res_raw.endswith((b'\x0D\x0A', b'\x0A')): res_ext = 'txt' else: res_ext = 'bin' - + if res_ext == 'txt': printer(new_line=False) + for line in io.BytesIO(res_raw).readlines(): - line_text = line.decode('utf-8','ignore').rstrip() + line_text = line.decode('utf-8', 'ignore').rstrip() + printer(line_text, padding + 12, new_line=False) - + with open(f'{res_out}.{res_ext}', 'wb') as out_file: out_file.write(res_raw) - + return is_rcdata -# Extract Panasonic BIOS Update PE Data when RCDATA is not available + def panasonic_img_extract(pe_name, pe_path, pe_file, extract_path, padding=0): + """ Extract Panasonic BIOS Update PE Data when RCDATA is not available """ + pe_data = file_to_bytes(pe_path) - - sec_bgn = pe_file.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']].VirtualAddress + + sec_bgn = pe_file.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY[ + 'IMAGE_DIRECTORY_ENTRY_SECURITY']].VirtualAddress + img_bgn = pe_file.OPTIONAL_HEADER.BaseOfData + pe_file.OPTIONAL_HEADER.SizeOfInitializedData img_end = sec_bgn or len(pe_data) + img_bin = pe_data[img_bgn:img_end] + img_tag = f'{pe_name} [0x{img_bgn:X}-0x{img_end:X}]' + img_out = os.path.join(extract_path, f'{img_tag}.bin') - + printer(img_tag, padding + 4) - + with open(img_out, 'wb') as out_img: out_img.write(img_bin) - + printer('Succesfull PE Data extraction!', padding + 8) - + return bool(img_bin) -# Parse & Extract Panasonic BIOS Package PE + def panasonic_pkg_extract(input_file, extract_path, padding=0): + """ Parse & Extract Panasonic BIOS Package PE """ + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - - pkg_pe_file = get_pe_file(input_buffer, fast=True) - + + pkg_pe_file = get_pe_file(input_buffer, padding) + if not pkg_pe_file: return 2 - - pkg_pe_info = get_pe_info(pkg_pe_file) - + + pkg_pe_info = get_pe_info(pkg_pe_file, padding) + if not pkg_pe_info: return 3 - + pkg_pe_name = path_stem(input_file) - + printer(f'Panasonic BIOS Package > PE ({pkg_pe_name})\n', padding) - + show_pe_info(pkg_pe_info, padding + 4) - - upd_pe_path,upd_pe_file,upd_pe_info = panasonic_cab_extract(input_buffer, extract_path, padding + 4) - + + upd_pe_path, upd_pe_file, upd_pe_info = panasonic_cab_extract(input_buffer, extract_path, padding + 4) + if not (upd_pe_path and upd_pe_file and upd_pe_info): return 4 - + upd_pe_name = safe_name(path_stem(upd_pe_path)) - + printer(f'Panasonic BIOS Update > PE ({upd_pe_name})\n', padding + 12) - + show_pe_info(upd_pe_info, padding + 16) - + is_upd_res, is_upd_img = False, False - + is_upd_res = panasonic_res_extract(upd_pe_name, upd_pe_file, extract_path, padding + 16) - + if not is_upd_res: is_upd_img = panasonic_img_extract(upd_pe_name, upd_pe_path, upd_pe_file, extract_path, padding + 16) os.remove(upd_pe_path) - + return 0 if is_upd_res or is_upd_img else 1 + if __name__ == '__main__': - BIOSUtility(TITLE, is_panasonic_pkg, panasonic_pkg_extract).run_utility() + BIOSUtility(title=TITLE, check=is_panasonic_pkg, main=panasonic_pkg_extract).run_utility() diff --git a/Phoenix_TDK_Extract.py b/Phoenix_TDK_Extract.py index 3328ad4..b469ade 100644 --- a/Phoenix_TDK_Extract.py +++ b/Phoenix_TDK_Extract.py @@ -1,237 +1,270 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Phoenix TDK Extract Phoenix TDK Packer Extractor -Copyright (C) 2021-2022 Plato Mavropoulos +Copyright (C) 2021-2024 Plato Mavropoulos """ -TITLE = 'Phoenix TDK Packer Extractor v2.0_a10' - -import os -import sys -import lzma import ctypes - -# Stop __pycache__ generation -sys.dont_write_bytecode = True +import logging +import lzma +import os from common.path_ops import make_dirs, safe_name from common.pe_ops import get_pe_file, get_pe_info from common.patterns import PAT_MICROSOFT_MZ, PAT_MICROSOFT_PE, PAT_PHOENIX_TDK -from common.struct_ops import char, get_struct, uint32_t +from common.struct_ops import Char, get_struct, UInt32 from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes +TITLE = 'Phoenix TDK Packer Extractor v3.0' + + class PhoenixTdkHeader(ctypes.LittleEndianStructure): + """ Phoenix TDK Header """ + _pack_ = 1 _fields_ = [ - ('Tag', char*8), # 0x00 - ('Size', uint32_t), # 0x08 - ('Count', uint32_t), # 0x0C + ('Tag', Char * 8), # 0x00 + ('Size', UInt32), # 0x08 + ('Count', UInt32), # 0x0C # 0x10 ] - + def _get_tag(self): - return self.Tag.decode('utf-8','ignore').strip() - - def struct_print(self, p): - printer(['Tag :', self._get_tag()], p, False) - printer(['Size :', f'0x{self.Size:X}'], p, False) - printer(['Entries:', self.Count], p, False) + return self.Tag.decode('utf-8', 'ignore').strip() + + def struct_print(self, padd): + """ Display structure information """ + + printer(['Tag :', self._get_tag()], padd, False) + printer(['Size :', f'0x{self.Size:X}'], padd, False) + printer(['Entries:', self.Count], padd, False) + class PhoenixTdkEntry(ctypes.LittleEndianStructure): + """ Phoenix TDK Entry """ + _pack_ = 1 _fields_ = [ - ('Name', char*256), # 0x000 - ('Offset', uint32_t), # 0x100 - ('Size', uint32_t), # 0x104 - ('Compressed', uint32_t), # 0x108 - ('Reserved', uint32_t), # 0x10C + ('Name', Char * 256), # 0x000 + ('Offset', UInt32), # 0x100 + ('Size', UInt32), # 0x104 + ('Compressed', UInt32), # 0x108 + ('Reserved', UInt32), # 0x10C # 0x110 ] - + COMP = {0: 'None', 1: 'LZMA'} - + def __init__(self, mz_base, *args, **kwargs): super().__init__(*args, **kwargs) - self.Base = mz_base - - def get_name(self): - return self.Name.decode('utf-8','replace').strip() - - def get_offset(self): - return self.Base + self.Offset - - def get_compression(self): - return self.COMP.get(self.Compressed, f'Unknown ({self.Compressed})') - - def struct_print(self, p): - printer(['Name :', self.get_name()], p, False) - printer(['Offset :', f'0x{self.get_offset():X}'], p, False) - printer(['Size :', f'0x{self.Size:X}'], p, False) - printer(['Compression:', self.get_compression()], p, False) - printer(['Reserved :', f'0x{self.Reserved:X}'], p, False) -# Get Phoenix TDK Executable (MZ) Base Offset + self.mz_base = mz_base + + def get_name(self): + """ Get TDK Entry decoded name """ + + return self.Name.decode('utf-8', 'replace').strip() + + def get_offset(self): + """ Get TDK Entry absolute offset """ + + return self.mz_base + self.Offset + + def get_compression(self): + """ Get TDK Entry compression type """ + + return self.COMP.get(self.Compressed, f'Unknown ({self.Compressed})') + + def struct_print(self, padd): + """ Display structure information """ + + printer(['Name :', self.get_name()], padd, False) + printer(['Offset :', f'0x{self.get_offset():X}'], padd, False) + printer(['Size :', f'0x{self.Size:X}'], padd, False) + printer(['Compression:', self.get_compression()], padd, False) + printer(['Reserved :', f'0x{self.Reserved:X}'], padd, False) + + def get_tdk_base(in_buffer, pack_off): - tdk_base_off = None # Initialize Phoenix TDK Base MZ Offset - + """ Get Phoenix TDK Executable (MZ) Base Offset """ + + tdk_base_off = None # Initialize Phoenix TDK Base MZ Offset + # Scan input file for all Microsoft executable patterns (MZ) before TDK Header Offset mz_all = [mz for mz in PAT_MICROSOFT_MZ.finditer(in_buffer) if mz.start() < pack_off] - + # Phoenix TDK Header structure is an index table for all TDK files # Each TDK file is referenced from the TDK Packer executable base # The TDK Header is always at the end of the TDK Packer executable # Thus, prefer the TDK Packer executable (MZ) closest to TDK Header # For speed, check MZ closest to (or at) 0x0 first (expected input) mz_ord = [mz_all[0]] + list(reversed(mz_all[1:])) - + # Parse each detected MZ - for mz in mz_ord: - mz_off = mz.start() - - # MZ (DOS) > PE (NT) image Offset is found at offset 0x3C-0x40 relative to MZ base + for mz_match in mz_ord: + mz_off = mz_match.start() + + # MZ (DOS) > PE (NT) image Offset is found at offset 0x3C-0x40 relative to MZ base pe_off = mz_off + int.from_bytes(in_buffer[mz_off + 0x3C:mz_off + 0x40], 'little') - + # Skip MZ (DOS) with bad PE (NT) image Offset if pe_off == mz_off or pe_off >= pack_off: continue - + # Check if potential MZ > PE image magic value is valid if PAT_MICROSOFT_PE.search(in_buffer[pe_off:pe_off + 0x4]): try: # Parse detected MZ > PE > Image, quickly (fast_load) - pe_file = get_pe_file(in_buffer[mz_off:], fast=True) - + pe_file = get_pe_file(in_buffer[mz_off:], silent=True) + # Parse detected MZ > PE > Info - pe_info = get_pe_info(pe_file) - + pe_info = get_pe_info(pe_file, silent=True) + # Parse detected MZ > PE > Info > Product Name - pe_name = pe_info.get(b'ProductName',b'') - except Exception: + pe_name = pe_info.get(b'ProductName', b'') + except Exception as error: # pylint: disable=broad-except # Any error means no MZ > PE > Info > Product Name + logging.debug('Error: Invalid potential MZ > PE match at 0x%X: %s', pe_off, error) + pe_name = b'' - + # Check for valid Phoenix TDK Packer PE > Product Name # Expected value is "TDK Packer (Extractor for Windows)" if pe_name.upper().startswith(b'TDK PACKER'): # Set TDK Base Offset to valid TDK Packer MZ offset tdk_base_off = mz_off - + # Stop parsing detected MZ once TDK Base Offset is found if tdk_base_off is not None: break else: # No TDK Base Offset could be found, assume 0x0 tdk_base_off = 0x0 - + return tdk_base_off -# Scan input buffer for valid Phoenix TDK image + def get_phoenix_tdk(in_buffer): + """ Scan input buffer for valid Phoenix TDK image """ + # Scan input buffer for Phoenix TDK pattern tdk_match = PAT_PHOENIX_TDK.search(in_buffer) - + if not tdk_match: return None, None - + # Set Phoenix TDK Header ($PACK) Offset tdk_pack_off = tdk_match.start() - + # Get Phoenix TDK Executable (MZ) Base Offset tdk_base_off = get_tdk_base(in_buffer, tdk_pack_off) - + return tdk_base_off, tdk_pack_off -# Check if input contains valid Phoenix TDK image + def is_phoenix_tdk(in_file): + """ Check if input contains valid Phoenix TDK image """ + buffer = file_to_bytes(in_file) - + return bool(get_phoenix_tdk(buffer)[1] is not None) -# Parse & Extract Phoenix Tools Development Kit (TDK) Packer + def phoenix_tdk_extract(input_file, extract_path, padding=0): + """ Parse & Extract Phoenix Tools Development Kit (TDK) Packer """ + exit_code = 0 - + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - + printer('Phoenix Tools Development Kit Packer', padding) - - base_off,pack_off = get_phoenix_tdk(input_buffer) - + + base_off, pack_off = get_phoenix_tdk(input_buffer) + # Parse TDK Header structure tdk_hdr = get_struct(input_buffer, pack_off, PhoenixTdkHeader) - + # Print TDK Header structure info printer('Phoenix TDK Header:\n', padding + 4) + tdk_hdr.struct_print(padding + 8) - + # Check if reported TDK Header Size matches manual TDK Entry Count calculation if tdk_hdr.Size != TDK_HDR_LEN + TDK_DUMMY_LEN + tdk_hdr.Count * TDK_MOD_LEN: printer('Error: Phoenix TDK Header Size & Entry Count mismatch!\n', padding + 8, pause=True) + exit_code = 1 - + # Store TDK Entries offset after the placeholder data entries_off = pack_off + TDK_HDR_LEN + TDK_DUMMY_LEN - + # Parse and extract each TDK Header Entry for entry_index in range(tdk_hdr.Count): # Parse TDK Entry structure tdk_mod = get_struct(input_buffer, entries_off + entry_index * TDK_MOD_LEN, PhoenixTdkEntry, [base_off]) - + # Print TDK Entry structure info printer(f'Phoenix TDK Entry ({entry_index + 1}/{tdk_hdr.Count}):\n', padding + 8) + tdk_mod.struct_print(padding + 12) - + # Get TDK Entry raw data Offset (TDK Base + Entry Offset) mod_off = tdk_mod.get_offset() - + # Check if TDK Entry raw data Offset is valid if mod_off >= len(input_buffer): printer('Error: Phoenix TDK Entry > Offset is out of bounds!\n', padding + 12, pause=True) + exit_code = 2 - + # Store TDK Entry raw data (relative to TDK Base, not TDK Header) mod_data = input_buffer[mod_off:mod_off + tdk_mod.Size] - + # Check if TDK Entry raw data is complete if len(mod_data) != tdk_mod.Size: printer('Error: Phoenix TDK Entry > Data is truncated!\n', padding + 12, pause=True) + exit_code = 3 - + # Check if TDK Entry Reserved is present if tdk_mod.Reserved: printer('Error: Phoenix TDK Entry > Reserved is not empty!\n', padding + 12, pause=True) + exit_code = 4 - + # Decompress TDK Entry raw data, when applicable (i.e. LZMA) if tdk_mod.get_compression() == 'LZMA': try: mod_data = lzma.LZMADecompressor().decompress(mod_data) - except Exception: - printer('Error: Phoenix TDK Entry > LZMA decompression failed!\n', padding + 12, pause=True) + except Exception as error: # pylint: disable=broad-except + printer(f'Error: Phoenix TDK Entry > LZMA decompression failed: {error}!\n', padding + 12, pause=True) + exit_code = 5 - + # Generate TDK Entry file name, avoid crash if Entry data is bad mod_name = tdk_mod.get_name() or f'Unknown_{entry_index + 1:02d}.bin' - + # Generate TDK Entry file data output path mod_file = os.path.join(extract_path, safe_name(mod_name)) - + # Account for potential duplicate file names - if os.path.isfile(mod_file): mod_file += f'_{entry_index + 1:02d}' - + if os.path.isfile(mod_file): + mod_file += f'_{entry_index + 1:02d}' + # Save TDK Entry data to output file with open(mod_file, 'wb') as out_file: out_file.write(mod_data) - + return exit_code + # Get ctypes Structure Sizes TDK_HDR_LEN = ctypes.sizeof(PhoenixTdkHeader) TDK_MOD_LEN = ctypes.sizeof(PhoenixTdkEntry) @@ -240,4 +273,4 @@ TDK_MOD_LEN = ctypes.sizeof(PhoenixTdkEntry) TDK_DUMMY_LEN = 0x200 if __name__ == '__main__': - BIOSUtility(TITLE, is_phoenix_tdk, phoenix_tdk_extract).run_utility() + BIOSUtility(title=TITLE, check=is_phoenix_tdk, main=phoenix_tdk_extract).run_utility() diff --git a/Portwell_EFI_Extract.py b/Portwell_EFI_Extract.py index bb40705..7b22550 100644 --- a/Portwell_EFI_Extract.py +++ b/Portwell_EFI_Extract.py @@ -1,19 +1,14 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Portwell EFI Extract Portwell EFI Update Extractor -Copyright (C) 2021-2022 Plato Mavropoulos +Copyright (C) 2021-2024 Plato Mavropoulos """ -TITLE = 'Portwell EFI Update Extractor v2.0_a12' - +import logging import os -import sys - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.comp_efi import efi_decompress, is_efi_compressed from common.path_ops import make_dirs, safe_name @@ -23,114 +18,133 @@ from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes +TITLE = 'Portwell EFI Update Extractor v3.0' + FILE_NAMES = { - 0 : 'Flash.efi', - 1 : 'Fparts.txt', - 2 : 'Update.nsh', - 3 : 'Temp.bin', - 4 : 'SaveDmiData.efi' + 0: 'Flash.efi', + 1: 'Fparts.txt', + 2: 'Update.nsh', + 3: 'Temp.bin', + 4: 'SaveDmiData.efi' } -# Check if input is Portwell EFI executable + def is_portwell_efi(in_file): + """ Check if input is Portwell EFI executable """ + in_buffer = file_to_bytes(in_file) - + try: pe_buffer = get_portwell_pe(in_buffer)[1] - except Exception: + except Exception as error: # pylint: disable=broad-except + logging.debug('Error: Could not check if input is Portwell EFI executable: %s', error) + pe_buffer = b'' - - is_mz = PAT_MICROSOFT_MZ.search(in_buffer[:0x2]) # EFI images start with PE Header MZ - - is_uu = PAT_PORTWELL_EFI.search(pe_buffer[:0x4]) # Portwell EFI files start with - + + is_mz = PAT_MICROSOFT_MZ.search(in_buffer[:0x2]) # EFI images start with PE Header MZ + + is_uu = PAT_PORTWELL_EFI.search(pe_buffer[:0x4]) # Portwell EFI files start with + return bool(is_mz and is_uu) -# Get PE of Portwell EFI executable -def get_portwell_pe(in_buffer): - pe_file = get_pe_file(in_buffer, fast=True) # Analyze EFI Portable Executable (PE) - - pe_data = in_buffer[pe_file.OPTIONAL_HEADER.SizeOfImage:] # Skip EFI executable (pylint: disable=E1101) - + +def get_portwell_pe(in_buffer): + """ Get PE of Portwell EFI executable """ + + pe_file = get_pe_file(in_buffer, silent=True) # Analyze EFI Portable Executable (PE) + + pe_data = in_buffer[pe_file.OPTIONAL_HEADER.SizeOfImage:] # Skip EFI executable (pylint: disable=E1101) + return pe_file, pe_data -# Parse & Extract Portwell UEFI Unpacker + def portwell_efi_extract(input_file, extract_path, padding=0): - efi_files = [] # Initialize EFI Payload file chunks - + """ Parse & Extract Portwell UEFI Unpacker """ + + efi_files = [] # Initialize EFI Payload file chunks + input_buffer = file_to_bytes(input_file) - + make_dirs(extract_path, delete=True) - - pe_file,pe_data = get_portwell_pe(input_buffer) - + + pe_file, pe_data = get_portwell_pe(input_buffer) + efi_title = get_unpacker_tag(input_buffer, pe_file) - + printer(efi_title, padding) - + # Split EFI Payload into file chunks efi_list = list(PAT_PORTWELL_EFI.finditer(pe_data)) - for idx,val in enumerate(efi_list): + + for idx, val in enumerate(efi_list): efi_bgn = val.end() efi_end = len(pe_data) if idx == len(efi_list) - 1 else efi_list[idx + 1].start() + efi_files.append(pe_data[efi_bgn:efi_end]) - + parse_efi_files(extract_path, efi_files, padding) - -# Get Portwell UEFI Unpacker tag + + def get_unpacker_tag(input_buffer, pe_file): + """ Get Portwell UEFI Unpacker tag """ + unpacker_tag_txt = 'UEFI Unpacker' - + for pe_section in pe_file.sections: # Unpacker Tag, Version, Strings etc are found in .data PE section if pe_section.Name.startswith(b'.data'): pe_data_bgn = pe_section.PointerToRawData pe_data_end = pe_data_bgn + pe_section.SizeOfRawData - + # Decode any valid UTF-16 .data PE section info to a parsable text buffer - pe_data_txt = input_buffer[pe_data_bgn:pe_data_end].decode('utf-16','ignore') - + pe_data_txt = input_buffer[pe_data_bgn:pe_data_end].decode('utf-16', 'ignore') + # Search .data for UEFI Unpacker tag unpacker_tag_bgn = pe_data_txt.find(unpacker_tag_txt) + if unpacker_tag_bgn != -1: unpacker_tag_len = pe_data_txt[unpacker_tag_bgn:].find('=') + if unpacker_tag_len != -1: unpacker_tag_end = unpacker_tag_bgn + unpacker_tag_len unpacker_tag_raw = pe_data_txt[unpacker_tag_bgn:unpacker_tag_end] - + # Found full UEFI Unpacker tag, store and slightly beautify the resulting text - unpacker_tag_txt = unpacker_tag_raw.strip().replace(' ',' ').replace('<',' <') - - break # Found PE .data section, skip the rest - + unpacker_tag_txt = unpacker_tag_raw.strip().replace(' ', ' ').replace('<', ' <') + + break # Found PE .data section, skip the rest + return unpacker_tag_txt -# Process Portwell UEFI Unpacker payload files + def parse_efi_files(extract_path, efi_files, padding): - for file_index,file_data in enumerate(efi_files): + """ Process Portwell UEFI Unpacker payload files """ + + for file_index, file_data in enumerate(efi_files): if file_data in (b'', b'NULL'): - continue # Skip empty/unused files - - file_name = FILE_NAMES.get(file_index, f'Unknown_{file_index}.bin') # Assign Name to EFI file - - printer(f'[{file_index}] {file_name}', padding + 4) # Print EFI file name, indicate progress - + continue # Skip empty/unused files + + file_name = FILE_NAMES.get(file_index, f'Unknown_{file_index}.bin') # Assign Name to EFI file + + printer(f'[{file_index}] {file_name}', padding + 4) # Print EFI file name, indicate progress + if file_name.startswith('Unknown_'): - printer(f'Note: Detected new Portwell EFI file ID {file_index}!', padding + 8, pause=True) # Report new EFI files - - file_path = os.path.join(extract_path, safe_name(file_name)) # Store EFI file output path - + printer(f'Note: Detected new Portwell EFI file ID {file_index}!', padding + 8, pause=True) + + file_path = os.path.join(extract_path, safe_name(file_name)) # Store EFI file output path + with open(file_path, 'wb') as out_file: - out_file.write(file_data) # Store EFI file data to drive - + out_file.write(file_data) # Store EFI file data to drive + # Attempt to detect EFI compression & decompress when applicable if is_efi_compressed(file_data): - comp_fname = file_path + '.temp' # Store temporary compressed file name - - os.replace(file_path, comp_fname) # Rename initial/compressed file - + comp_fname = file_path + '.temp' # Store temporary compressed file name + + os.replace(file_path, comp_fname) # Rename initial/compressed file + if efi_decompress(comp_fname, file_path, padding + 8) == 0: - os.remove(comp_fname) # Successful decompression, delete compressed file + os.remove(comp_fname) # Successful decompression, delete compressed file + if __name__ == '__main__': - BIOSUtility(TITLE, is_portwell_efi, portwell_efi_extract).run_utility() + BIOSUtility(title=TITLE, check=is_portwell_efi, main=portwell_efi_extract).run_utility() diff --git a/README.md b/README.md index 0d8c29c..d130e25 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ You can either Drag & Drop or manually enter AMI BIOS Guard (PFAT) image file(s) #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -75,7 +75,7 @@ You can either Drag & Drop or manually enter AMI UCP Update executable file(s). #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -112,7 +112,7 @@ You can either Drag & Drop or manually enter Apple EFI IM4P file(s). Optional ar #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -143,7 +143,7 @@ You can either Drag & Drop or manually enter Apple EFI image file(s). Optional a #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -176,7 +176,7 @@ You can either Drag & Drop or manually enter Apple EFI PKG package file(s). Opti #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -208,7 +208,7 @@ You can either Drag & Drop or manually enter Apple EFI PBZX image file(s). Optio #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -240,7 +240,7 @@ You can either Drag & Drop or manually enter Award BIOS image file(s). Optional #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -274,7 +274,7 @@ You can either Drag & Drop or manually enter Dell PFS Update images(s). Optional #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -306,7 +306,7 @@ You can either Drag & Drop or manually enter Fujitsu SFX BIOS image file(s). Opt #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -338,7 +338,7 @@ You can either Drag & Drop or manually enter Fujitsu UPC BIOS image file(s). Opt #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -370,7 +370,7 @@ You can either Drag & Drop or manually enter Insyde iFlash/iFdPacker Update imag #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -400,14 +400,14 @@ You can either Drag & Drop or manually enter Panasonic BIOS Package executable f #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** To run the utility, you must have the following 3rd party Python modules installed: * [pefile](https://pypi.org/project/pefile/) -* [lznt1](https://pypi.org/project/lznt1/) +* [dissect.util](https://pypi.org/project/dissect.util/) Moreover, you must have the following 3rd party tool at the "external" project directory: @@ -437,7 +437,7 @@ You can either Drag & Drop or manually enter Phoenix Tools Development Kit (TDK) #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -469,7 +469,7 @@ You can either Drag & Drop or manually enter Portwell UEFI Unpacker EFI executab #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -507,7 +507,7 @@ You can either Drag & Drop or manually enter Toshiba BIOS COM image file(s). Opt #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** @@ -539,7 +539,7 @@ You can either Drag & Drop or manually enter VAIO Packaging Manager executable f #### **Compatibility** -Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 or newer support. #### **Prerequisites** diff --git a/Toshiba_COM_Extract.py b/Toshiba_COM_Extract.py index da6ee43..9ff8915 100644 --- a/Toshiba_COM_Extract.py +++ b/Toshiba_COM_Extract.py @@ -1,20 +1,14 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ Toshiba COM Extract Toshiba BIOS COM Extractor -Copyright (C) 2018-2022 Plato Mavropoulos +Copyright (C) 2018-2024 Plato Mavropoulos """ -TITLE = 'Toshiba BIOS COM Extractor v2.0_a4' - import os -import sys import subprocess - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.externals import get_comextract_path from common.path_ops import make_dirs, path_stem, path_suffixes @@ -23,41 +17,49 @@ from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes -# Check if input is Toshiba BIOS COM image +TITLE = 'Toshiba BIOS COM Extractor v3.0' + + def is_toshiba_com(in_file): + """ Check if input is Toshiba BIOS COM image """ + buffer = file_to_bytes(in_file) - + is_ext = path_suffixes(in_file)[-1].upper() == '.COM' if os.path.isfile(in_file) else True - + is_com = PAT_TOSHIBA_COM.search(buffer) - + return is_ext and is_com -# Parse & Extract Toshiba BIOS COM image + def toshiba_com_extract(input_file, extract_path, padding=0): + """ Parse & Extract Toshiba BIOS COM image """ + if not os.path.isfile(input_file): printer('Error: Could not find input file path!', padding) - + return 1 - + make_dirs(extract_path, delete=True) - + output_name = path_stem(input_file) + output_file = os.path.join(extract_path, f'{output_name}.bin') - + try: subprocess.run([get_comextract_path(), input_file, output_file], check=True, stdout=subprocess.DEVNULL) - + if not os.path.isfile(output_file): - raise Exception('EXTRACT_FILE_MISSING') - except Exception: - printer(f'Error: ToshibaComExtractor could not extract file {input_file}!', padding) - + raise ValueError('EXTRACT_FILE_MISSING') + except Exception as error: # pylint: disable=broad-except + printer(f'Error: ToshibaComExtractor could not extract file {input_file}: {error}!', padding) + return 2 - + printer(f'Succesfull {output_name} extraction via ToshibaComExtractor!', padding) - + return 0 + if __name__ == '__main__': - BIOSUtility(TITLE, is_toshiba_com, toshiba_com_extract).run_utility() + BIOSUtility(title=TITLE, check=is_toshiba_com, main=toshiba_com_extract).run_utility() diff --git a/VAIO_Package_Extract.py b/VAIO_Package_Extract.py index 9bb49bf..11ff2a5 100644 --- a/VAIO_Package_Extract.py +++ b/VAIO_Package_Extract.py @@ -1,19 +1,13 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ VAIO Package Extractor VAIO Packaging Manager Extractor -Copyright (C) 2019-2022 Plato Mavropoulos +Copyright (C) 2019-2024 Plato Mavropoulos """ -TITLE = 'VAIO Packaging Manager Extractor v3.0_a8' - import os -import sys - -# Stop __pycache__ generation -sys.dont_write_bytecode = True from common.comp_szip import is_szip_supported, szip_decompress from common.path_ops import make_dirs @@ -22,126 +16,149 @@ from common.system import printer from common.templates import BIOSUtility from common.text_ops import file_to_bytes -# Check if input is VAIO Packaging Manager +TITLE = 'VAIO Packaging Manager Extractor v4.0' + + def is_vaio_pkg(in_file): + """ Check if input is VAIO Packaging Manager """ + buffer = file_to_bytes(in_file) - + return bool(PAT_VAIO_CFG.search(buffer)) -# Extract VAIO Packaging Manager executable + def vaio_cabinet(name, buffer, extract_path, padding=0): - match_cab = PAT_VAIO_CAB.search(buffer) # Microsoft CAB Header XOR 0xFF - + """ Extract VAIO Packaging Manager executable """ + + match_cab = PAT_VAIO_CAB.search(buffer) # Microsoft CAB Header XOR 0xFF + if not match_cab: return 1 - + printer('Detected obfuscated CAB archive!', padding) - + # Determine the Microsoft CAB image size - cab_size = int.from_bytes(buffer[match_cab.start() + 0x8:match_cab.start() + 0xC], 'little') # Get LE XOR-ed CAB size - xor_size = int.from_bytes(b'\xFF' * 0x4, 'little') # Create CAB size XOR value - cab_size ^= xor_size # Perform XOR 0xFF and get actual CAB size + cab_size = int.from_bytes(buffer[match_cab.start() + 0x8:match_cab.start() + 0xC], 'little') # Get LE XOR CAB size + + xor_size = int.from_bytes(b'\xFF' * 0x4, 'little') # Create CAB size XOR value + + cab_size ^= xor_size # Perform XOR 0xFF and get actual CAB size printer('Removing obfuscation...', padding + 4) - + # Determine the Microsoft CAB image Data - cab_data = int.from_bytes(buffer[match_cab.start():match_cab.start() + cab_size], 'big') # Get BE XOR-ed CAB data - xor_data = int.from_bytes(b'\xFF' * cab_size, 'big') # Create CAB data XOR value - cab_data = (cab_data ^ xor_data).to_bytes(cab_size, 'big') # Perform XOR 0xFF and get actual CAB data - + cab_data = int.from_bytes(buffer[match_cab.start():match_cab.start() + cab_size], 'big') # Get BE XOR CAB data + + xor_data = int.from_bytes(b'\xFF' * cab_size, 'big') # Create CAB data XOR value + + cab_data = (cab_data ^ xor_data).to_bytes(cab_size, 'big') # Perform XOR 0xFF and get actual CAB data + printer('Extracting archive...', padding + 4) - + cab_path = os.path.join(extract_path, f'{name}_Temporary.cab') - + with open(cab_path, 'wb') as cab_file: - cab_file.write(cab_data) # Create temporary CAB archive - + cab_file.write(cab_data) # Create temporary CAB archive + if is_szip_supported(cab_path, padding + 8, check=True): - if szip_decompress(cab_path, extract_path, 'CAB', padding + 8, check=True) == 0: - os.remove(cab_path) # Successful extraction, delete temporary CAB archive + if szip_decompress(cab_path, extract_path, 'VAIO CAB', padding + 8, check=True) == 0: + os.remove(cab_path) # Successful extraction, delete temporary CAB archive else: return 3 else: return 2 - + return 0 -# Unlock VAIO Packaging Manager executable + def vaio_unlock(name, buffer, extract_path, padding=0): + """ Unlock VAIO Packaging Manager executable """ + match_cfg = PAT_VAIO_CFG.search(buffer) - + if not match_cfg: return 1 - + printer('Attempting to Unlock executable!', padding) - + # Initialize VAIO Package Configuration file variables (assume overkill size of 0x500) - cfg_bgn,cfg_end,cfg_false,cfg_true = [match_cfg.start(), match_cfg.start() + 0x500, b'', b''] - + cfg_bgn, cfg_end, cfg_false, cfg_true = [match_cfg.start(), match_cfg.start() + 0x500, b'', b''] + # Get VAIO Package Configuration file info, split at new_line and stop at payload DOS header (EOF) - cfg_info = buffer[cfg_bgn:cfg_end].split(b'\x0D\x0A\x4D\x5A')[0].replace(b'\x0D',b'').split(b'\x0A') - + cfg_info = buffer[cfg_bgn:cfg_end].split(b'\x0D\x0A\x4D\x5A')[0].replace(b'\x0D', b'').split(b'\x0A') + printer('Retrieving True/False values...', padding + 4) - + # Determine VAIO Package Configuration file True & False values for info in cfg_info: if info.startswith(b'ExtractPathByUser='): - cfg_false = bytearray(b'0' if info[18:] in (b'0',b'1') else info[18:]) # Should be 0/No/False + cfg_false = bytearray(b'0' if info[18:] in (b'0', b'1') else info[18:]) # Should be 0/No/False + if info.startswith(b'UseCompression='): - cfg_true = bytearray(b'1' if info[15:] in (b'0',b'1') else info[15:]) # Should be 1/Yes/True - + cfg_true = bytearray(b'1' if info[15:] in (b'0', b'1') else info[15:]) # Should be 1/Yes/True + # Check if valid True/False values have been retrieved if cfg_false == cfg_true or not cfg_false or not cfg_true: printer('Error: Could not retrieve True/False values!', padding + 8) + return 2 - + printer('Adjusting UseVAIOCheck entry...', padding + 4) - + # Find and replace UseVAIOCheck entry from 1/Yes/True to 0/No/False vaio_check = PAT_VAIO_CHK.search(buffer[cfg_bgn:]) + if vaio_check: buffer[cfg_bgn + vaio_check.end():cfg_bgn + vaio_check.end() + len(cfg_true)] = cfg_false else: printer('Error: Could not find entry UseVAIOCheck!', padding + 8) + return 3 - + printer('Adjusting ExtractPathByUser entry...', padding + 4) - + # Find and replace ExtractPathByUser entry from 0/No/False to 1/Yes/True user_path = PAT_VAIO_EXT.search(buffer[cfg_bgn:]) + if user_path: buffer[cfg_bgn + user_path.end():cfg_bgn + user_path.end() + len(cfg_false)] = cfg_true else: printer('Error: Could not find entry ExtractPathByUser!', padding + 8) + return 4 - + printer('Storing unlocked executable...', padding + 4) - + # Store Unlocked VAIO Packaging Manager executable if vaio_check and user_path: unlock_path = os.path.join(extract_path, f'{name}_Unlocked.exe') + with open(unlock_path, 'wb') as unl_file: unl_file.write(buffer) - + return 0 -# Parse & Extract or Unlock VAIO Packaging Manager + def vaio_pkg_extract(input_file, extract_path, padding=0): + """ Parse & Extract or Unlock VAIO Packaging Manager """ + input_buffer = file_to_bytes(input_file) - + input_name = os.path.basename(input_file) - + make_dirs(extract_path, delete=True) - + if vaio_cabinet(input_name, input_buffer, extract_path, padding) == 0: printer('Successfully Extracted!', padding) elif vaio_unlock(input_name, bytearray(input_buffer), extract_path, padding) == 0: printer('Successfully Unlocked!', padding) else: printer('Error: Failed to Extract or Unlock executable!', padding) + return 1 - + return 0 + if __name__ == '__main__': - BIOSUtility(TITLE, is_vaio_pkg, vaio_pkg_extract).run_utility() + BIOSUtility(title=TITLE, check=is_vaio_pkg, main=vaio_pkg_extract).run_utility() diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ce12aa1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 -B +# coding=utf-8 + +""" +Copyright (C) 2019-2024 Plato Mavropoulos +""" diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..ce12aa1 --- /dev/null +++ b/common/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 -B +# coding=utf-8 + +""" +Copyright (C) 2019-2024 Plato Mavropoulos +""" diff --git a/common/checksums.py b/common/checksums.py index 3e958ab..e9d150f 100644 --- a/common/checksums.py +++ b/common/checksums.py @@ -1,25 +1,31 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ + # Get Checksum 16-bit def get_chk_16(data, value=0, order='little'): + """ Calculate Checksum-16 of data, controlling IV and Endianess """ + for idx in range(0, len(data), 2): # noinspection PyTypeChecker - value += int.from_bytes(data[idx:idx + 2], order) - + value += int.from_bytes(data[idx:idx + 2], byteorder=order) + value &= 0xFFFF - + return value + # Get Checksum 8-bit XOR def get_chk_8_xor(data, value=0): + """ Calculate Checksum-8 XOR of data, controlling IV """ + for byte in data: value ^= byte - + value ^= 0x0 - + return value diff --git a/common/comp_efi.py b/common/comp_efi.py index 2837898..dbc7121 100644 --- a/common/comp_efi.py +++ b/common/comp_efi.py @@ -1,57 +1,60 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import os import subprocess -from common.path_ops import project_root, safe_path -from common.system import get_os_ver, printer +from common.externals import get_tiano_path +from common.system import printer + + +def get_compress_sizes(data): + """ Get EFI compression sizes """ -def get_compress_sizes(data): size_compress = int.from_bytes(data[0x0:0x4], 'little') size_original = int.from_bytes(data[0x4:0x8], 'little') - + return size_compress, size_original + def is_efi_compressed(data, strict=True): - size_comp,size_orig = get_compress_sizes(data) - + """ Check if data is EFI compressed, controlling EOF padding """ + + size_comp, size_orig = get_compress_sizes(data) + check_diff = size_comp < size_orig - + if strict: check_size = size_comp + 0x8 == len(data) else: check_size = size_comp + 0x8 <= len(data) - + return check_diff and check_size -# Get TianoCompress path -def get_tiano_path(): - exec_name = f'TianoCompress{".exe" if get_os_ver()[1] else ""}' - - return safe_path(project_root(), ['external',exec_name]) -# EFI/Tiano Decompression via TianoCompress def efi_decompress(in_path, out_path, padding=0, silent=False, comp_type='--uefi'): + """ EFI/Tiano Decompression via TianoCompress """ + try: - subprocess.run([get_tiano_path(), '-d', in_path, '-o', out_path, '-q', comp_type], check=True, stdout=subprocess.DEVNULL) - + subprocess.run([get_tiano_path(), '-d', in_path, '-o', out_path, '-q', comp_type], + check=True, stdout=subprocess.DEVNULL) + with open(in_path, 'rb') as file: - _,size_orig = get_compress_sizes(file.read()) - + _, size_orig = get_compress_sizes(file.read()) + if os.path.getsize(out_path) != size_orig: - raise Exception('EFI_DECOMPRESS_ERROR') - except Exception: + raise OSError('EFI decompressed file & header size mismatch!') + except Exception as error: # pylint: disable=broad-except if not silent: - printer(f'Error: TianoCompress could not extract file {in_path}!', padding) - + printer(f'Error: TianoCompress could not extract file {in_path}: {error}!', padding) + return 1 - + if not silent: printer('Succesfull EFI decompression via TianoCompress!', padding) - + return 0 diff --git a/common/comp_szip.py b/common/comp_szip.py index fb6041b..38e3857 100644 --- a/common/comp_szip.py +++ b/common/comp_szip.py @@ -1,72 +1,72 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import os import subprocess -from common.path_ops import project_root, safe_path -from common.system import get_os_ver, printer +from common.externals import get_szip_path +from common.system import printer -# Get 7-Zip path -def get_szip_path(): - exec_name = '7z.exe' if get_os_ver()[1] else '7zzs' - - return safe_path(project_root(), ['external',exec_name]) -# Check 7-Zip bad exit codes (0 OK, 1 Warning) def check_bad_exit_code(exit_code): - if exit_code not in (0,1): - raise Exception(f'BAD_EXIT_CODE_{exit_code}') + """ Check 7-Zip bad exit codes (0 OK, 1 Warning) """ + + if exit_code not in (0, 1): + raise ValueError(f'Bad exit code: {exit_code}') + -# Check if file is 7-Zip supported def is_szip_supported(in_path, padding=0, args=None, check=False, silent=False): + """ Check if file is 7-Zip supported """ + try: if args is None: args = [] - + szip_c = [get_szip_path(), 't', in_path, *args, '-bso0', '-bse0', '-bsp0'] - + szip_t = subprocess.run(szip_c, check=False) - + if check: check_bad_exit_code(szip_t.returncode) - except Exception: + except Exception as error: # pylint: disable=broad-except if not silent: - printer(f'Error: 7-Zip could not check support for file {in_path}!', padding) - + printer(f'Error: 7-Zip could not check support for file {in_path}: {error}!', padding) + return False - + return True -# Archive decompression via 7-Zip + def szip_decompress(in_path, out_path, in_name, padding=0, args=None, check=False, silent=False): + """ Archive decompression via 7-Zip """ + if not in_name: in_name = 'archive' - + try: if args is None: args = [] - + szip_c = [get_szip_path(), 'x', *args, '-aou', '-bso0', '-bse0', '-bsp0', f'-o{out_path}', in_path] - + szip_x = subprocess.run(szip_c, check=False) - + if check: check_bad_exit_code(szip_x.returncode) - + if not os.path.isdir(out_path): - raise Exception('EXTRACT_DIR_MISSING') - except Exception: + raise OSError(f'Extraction directory not found: {out_path}') + except Exception as error: # pylint: disable=broad-except if not silent: - printer(f'Error: 7-Zip could not extract {in_name} file {in_path}!', padding) - + printer(f'Error: 7-Zip could not extract {in_name} file {in_path}: {error}!', padding) + return 1 - + if not silent: printer(f'Succesfull {in_name} decompression via 7-Zip!', padding) - + return 0 diff --git a/common/externals.py b/common/externals.py index f81e3b9..33b546a 100644 --- a/common/externals.py +++ b/common/externals.py @@ -1,38 +1,66 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ from common.path_ops import project_root, safe_path from common.system import get_os_ver -# https://github.com/allowitsme/big-tool by Dmitry Frolov -# https://github.com/platomav/BGScriptTool by Plato Mavropoulos + def get_bgs_tool(): + """ + https://github.com/allowitsme/big-tool by Dmitry Frolov + https://github.com/platomav/BGScriptTool by Plato Mavropoulos + """ + try: # noinspection PyUnresolvedReferences - from external.big_script_tool import BigScript # pylint: disable=E0401,E0611 - except Exception: - BigScript = None - - return BigScript + from external.big_script_tool import BigScript # pylint: disable=C0415 -# Get UEFIFind path -def get_uefifind_path(): - exec_name = f'UEFIFind{".exe" if get_os_ver()[1] else ""}' - - return safe_path(project_root(), ['external', exec_name]) + return BigScript + except ModuleNotFoundError: + pass -# Get UEFIExtract path -def get_uefiextract_path(): - exec_name = f'UEFIExtract{".exe" if get_os_ver()[1] else ""}' - - return safe_path(project_root(), ['external', exec_name]) + return None + + +def get_comextract_path() -> str: + """ Get ToshibaComExtractor path """ -# Get ToshibaComExtractor path -def get_comextract_path(): exec_name = f'comextract{".exe" if get_os_ver()[1] else ""}' - + + return safe_path(project_root(), ['external', exec_name]) + + +def get_szip_path() -> str: + """ Get 7-Zip path """ + + exec_name = '7z.exe' if get_os_ver()[1] else '7zzs' + + return safe_path(project_root(), ['external', exec_name]) + + +def get_tiano_path() -> str: + """ Get TianoCompress path """ + + exec_name = f'TianoCompress{".exe" if get_os_ver()[1] else ""}' + + return safe_path(project_root(), ['external', exec_name]) + + +def get_uefifind_path() -> str: + """ Get UEFIFind path """ + + exec_name = f'UEFIFind{".exe" if get_os_ver()[1] else ""}' + + return safe_path(project_root(), ['external', exec_name]) + + +def get_uefiextract_path() -> str: + """ Get UEFIExtract path """ + + exec_name = f'UEFIExtract{".exe" if get_os_ver()[1] else ""}' + return safe_path(project_root(), ['external', exec_name]) diff --git a/common/num_ops.py b/common/num_ops.py index c37e4d7..3b95aab 100644 --- a/common/num_ops.py +++ b/common/num_ops.py @@ -1,14 +1,19 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ -# https://leancrew.com/all-this/2020/06/ordinals-in-python/ by Dr. Drang + def get_ordinal(number): - s = ('th', 'st', 'nd', 'rd') + ('th',) * 10 - - v = number % 100 - - return f'{number}{s[v % 10]}' if v > 13 else f'{number}{s[v]}' + """ + Get ordinal (textual) representation of input numerical value + https://leancrew.com/all-this/2020/06/ordinals-in-python/ by Dr. Drang + """ + + txt = ('th', 'st', 'nd', 'rd') + ('th',) * 10 + + val = number % 100 + + return f'{number}{txt[val % 10]}' if val > 13 else f'{number}{txt[val]}' diff --git a/common/path_ops.py b/common/path_ops.py index bcff167..47e70bd 100644 --- a/common/path_ops.py +++ b/common/path_ops.py @@ -1,154 +1,213 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import os import re -import sys -import stat import shutil +import stat +import sys + from pathlib import Path, PurePath +from common.system import get_os_ver from common.text_ops import is_encased, to_string -# Fix illegal/reserved Windows characters +MAX_WIN_COMP_LEN = 255 + + def safe_name(in_name): + """ + Fix illegal/reserved Windows characters + Can also be used to nuke dangerous paths + """ + name_repr = repr(in_name).strip("'") return re.sub(r'[\\/:"*?<>|]+', '_', name_repr) -# Check and attempt to fix illegal/unsafe OS path traversals + def safe_path(base_path, user_paths): + """ Check and attempt to fix illegal/unsafe OS path traversals """ + # Convert base path to absolute path base_path = real_path(base_path) - + # Merge user path(s) to string with OS separators user_path = to_string(user_paths, os.sep) - + # Create target path from base + requested user path target_path = norm_path(base_path, user_path) - + # Check if target path is OS illegal/unsafe if is_safe_path(base_path, target_path): return target_path - + # Re-create target path from base + leveled/safe illegal "path" (now file) nuked_path = norm_path(base_path, safe_name(user_path)) - + # Check if illegal path leveling worked if is_safe_path(base_path, nuked_path): return nuked_path - - # Still illegal, raise exception to halt execution - raise Exception(f'ILLEGAL_PATH_TRAVERSAL: {user_path}') -# Check for illegal/unsafe OS path traversal + # Still illegal, raise exception to halt execution + raise OSError(f'Encountered illegal path traversal: {user_path}') + + def is_safe_path(base_path, target_path): + """ Check for illegal/unsafe OS path traversal """ + base_path = real_path(base_path) - + target_path = real_path(target_path) - + common_path = os.path.commonpath((base_path, target_path)) - + return base_path == common_path -# Create normalized base path + OS separator + user path + def norm_path(base_path, user_path): + """ Create normalized base path + OS separator + user path """ + return os.path.normpath(base_path + os.sep + user_path) -# Get absolute path, resolving any symlinks + def real_path(in_path): + """ Get absolute path, resolving any symlinks """ + return os.path.realpath(in_path) -# Get Windows/Posix OS agnostic path + def agnostic_path(in_path): + """ Get Windows/Posix OS agnostic path """ + return PurePath(in_path.replace('\\', os.sep)) -# Get absolute parent of path + def path_parent(in_path): + """ Get absolute parent of path """ + return Path(in_path).parent.absolute() -# Get final path component, with suffix -def path_name(in_path): - return PurePath(in_path).name -# Get final path component, w/o suffix +def path_name(in_path, limit=False): + """ Get final path component, with suffix """ + + comp_name = PurePath(in_path).name + + if limit and get_os_ver()[1]: + comp_name = comp_name[:MAX_WIN_COMP_LEN - len(extract_suffix())] + + return comp_name + + def path_stem(in_path): + """ Get final path component, w/o suffix """ + return PurePath(in_path).stem -# Get list of path file extensions + def path_suffixes(in_path): + """ Get list of path file extensions """ + return PurePath(in_path).suffixes or [''] -# Check if path is absolute + def is_path_absolute(in_path): + """ Check if path is absolute """ + return Path(in_path).is_absolute() -# Create folder(s), controlling parents, existence and prior deletion + def make_dirs(in_path, parents=True, exist_ok=False, delete=False): + """ Create folder(s), controlling parents, existence and prior deletion """ + if delete: del_dirs(in_path) - + Path.mkdir(Path(in_path), parents=parents, exist_ok=exist_ok) -# Delete folder(s), if present -def del_dirs(in_path): - if Path(in_path).is_dir(): - shutil.rmtree(in_path, onerror=clear_readonly) -# Copy file to path with or w/o metadata +def del_dirs(in_path): + """ Delete folder(s), if present """ + + if Path(in_path).is_dir(): + shutil.rmtree(in_path, onerror=clear_readonly_callback) + + def copy_file(in_path, out_path, meta=False): + """ Copy file to path with or w/o metadata """ + if meta: shutil.copy2(in_path, out_path) else: shutil.copy(in_path, out_path) -# Clear read-only file attribute (on shutil.rmtree error) -def clear_readonly(in_func, in_path, _): + +def clear_readonly(in_path): + """ Clear read-only file attribute """ + os.chmod(in_path, stat.S_IWRITE) + + +def clear_readonly_callback(in_func, in_path, _): + """ Clear read-only file attribute (on shutil.rmtree error) """ + + clear_readonly(in_path) + in_func(in_path) -# Walk path to get all files + def get_path_files(in_path): + """ Walk path to get all files """ + path_files = [] - + for root, _, files in os.walk(in_path): for name in files: path_files.append(os.path.join(root, name)) - + return path_files -# Get path without leading/trailing quotes + def get_dequoted_path(in_path): + """ Get path without leading/trailing quotes """ + out_path = to_string(in_path).strip() - - if len(out_path) >= 2 and is_encased(out_path, ("'",'"')): + + if len(out_path) >= 2 and is_encased(out_path, ("'", '"')): out_path = out_path[1:-1] - + return out_path -# Set utility extraction stem + def extract_suffix(): + """ Set utility extraction stem """ + return '_extracted' -# Get utility extraction path + def get_extract_path(in_path, suffix=extract_suffix()): + """ Get utility extraction path """ + return f'{in_path}{suffix}' -# Get project's root directory -def project_root(): - root = Path(__file__).parent.parent - - return real_path(root) -# Get runtime's root directory +def project_root(): + """ Get project's root directory """ + + return real_path(Path(__file__).parent.parent) + + def runtime_root(): + """ Get runtime's root directory """ + if getattr(sys, 'frozen', False): root = Path(sys.executable).parent else: root = project_root() - + return real_path(root) diff --git a/common/patterns.py b/common/patterns.py index ecdde39..66034b2 100644 --- a/common/patterns.py +++ b/common/patterns.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import re diff --git a/common/pe_ops.py b/common/pe_ops.py index ba23828..ac64436 100644 --- a/common/pe_ops.py +++ b/common/pe_ops.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import pefile @@ -10,40 +10,57 @@ import pefile from common.system import printer from common.text_ops import file_to_bytes -# Check if input is a PE file -def is_pe_file(in_file): - return bool(get_pe_file(in_file)) -# Get pefile object from PE file -def get_pe_file(in_file, fast=True): +def is_pe_file(in_file: str | bytes) -> bool: + """ Check if input is a PE file """ + + return bool(get_pe_file(in_file, silent=True)) + + +def get_pe_file(in_file: str | bytes, padding: int = 0, fast: bool = True, silent: bool = False) -> pefile.PE | None: + """ Get pefile object from PE file """ + in_buffer = file_to_bytes(in_file) - + + pe_file = None + try: # Analyze detected MZ > PE image buffer pe_file = pefile.PE(data=in_buffer, fast_load=fast) - except Exception: - pe_file = None - + except Exception as error: # pylint: disable=broad-except + if not silent: + _filename = in_file if type(in_file).__name__ == 'string' else 'buffer' + + printer(f'Error: Could not get pefile object from {_filename}: {error}!', padding) + return pe_file -# Get PE info from pefile object -def get_pe_info(pe_file): + +def get_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> dict: + """ Get PE info from pefile object """ + + pe_info = {} + try: # When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to FileInfo > StringTable pe_file.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) - + # Retrieve MZ > PE > FileInfo > StringTable information pe_info = pe_file.FileInfo[0][0].StringTable[0].entries - except Exception: - pe_info = {} - + except Exception as error: # pylint: disable=broad-except + if not silent: + printer(f'Error: Could not get PE info from pefile object: {error}!', padding) + return pe_info -# Print PE info from pefile StringTable -def show_pe_info(pe_info, padding=0): - if type(pe_info).__name__ == 'dict': - for title,value in pe_info.items(): - info_title = title.decode('utf-8','ignore').strip() - info_value = value.decode('utf-8','ignore').strip() + +def show_pe_info(pe_info: dict, padding: int = 0) -> None: + """ Print PE info from pefile StringTable """ + + if isinstance(pe_info, dict): + for title, value in pe_info.items(): + info_title = title.decode('utf-8', 'ignore').strip() + info_value = value.decode('utf-8', 'ignore').strip() + if info_title and info_value: printer(f'{info_title}: {info_value}', padding, new_line=False) diff --git a/common/struct_ops.py b/common/struct_ops.py index b995ba1..4ec6909 100644 --- a/common/struct_ops.py +++ b/common/struct_ops.py @@ -1,26 +1,32 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import ctypes -char = ctypes.c_char -uint8_t = ctypes.c_ubyte -uint16_t = ctypes.c_ushort -uint32_t = ctypes.c_uint -uint64_t = ctypes.c_uint64 +Char: type[ctypes.c_char] | int = ctypes.c_char +UInt8: type[ctypes.c_ubyte] | int = ctypes.c_ubyte +UInt16: type[ctypes.c_ushort] | int = ctypes.c_ushort +UInt32: type[ctypes.c_uint] | int = ctypes.c_uint +UInt64: type[ctypes.c_uint64] | int = ctypes.c_uint64 + -# https://github.com/skochinsky/me-tools/blob/master/me_unpack.py by Igor Skochinsky def get_struct(buffer, start_offset, class_name, param_list=None): - if param_list is None: - param_list = [] + """ + https://github.com/skochinsky/me-tools/blob/master/me_unpack.py by Igor Skochinsky + """ + + parameters = [] if param_list is None else param_list + + structure = class_name(*parameters) # Unpack parameter list - structure = class_name(*param_list) # Unpack parameter list struct_len = ctypes.sizeof(structure) + struct_data = buffer[start_offset:start_offset + struct_len] + fit_len = min(len(struct_data), struct_len) ctypes.memmove(ctypes.addressof(structure), struct_data, fit_len) diff --git a/common/system.py b/common/system.py index 9598e30..dd1ddaf 100644 --- a/common/system.py +++ b/common/system.py @@ -1,68 +1,84 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ import sys from common.text_ops import padder, to_string -# Get Python Version (tuple) + def get_py_ver(): + """ Get Python Version (tuple) """ + return sys.version_info -# Get OS Platform (string) + def get_os_ver(): + """ Get OS Platform (string) """ + sys_os = sys.platform - + is_win = sys_os == 'win32' + is_lnx = sys_os.startswith('linux') or sys_os == 'darwin' or sys_os.find('bsd') != -1 - + return sys_os, is_win, is_win or is_lnx -# Check for --auto-exit|-e + def is_auto_exit(): + """ Check for --auto-exit|-e """ + return bool('--auto-exit' in sys.argv or '-e' in sys.argv) -# Check Python Version + def check_sys_py(): + """ # Check Python Version """ + sys_py = get_py_ver() - - if sys_py < (3,10): + + if sys_py < (3, 10): sys.stdout.write(f'\nError: Python >= 3.10 required, not {sys_py[0]}.{sys_py[1]}!') - + if not is_auto_exit(): # noinspection PyUnresolvedReferences - (raw_input if sys_py[0] <= 2 else input)('\nPress enter to exit') # pylint: disable=E0602 - + (raw_input if sys_py[0] <= 2 else input)('\nPress enter to exit') # pylint: disable=E0602 + sys.exit(125) -# Check OS Platform + def check_sys_os(): - os_tag,os_win,os_sup = get_os_ver() - + """ Check OS Platform """ + + os_tag, os_win, os_sup = get_os_ver() + if not os_sup: printer(f'Error: Unsupported platform "{os_tag}"!') - + if not is_auto_exit(): input('\nPress enter to exit') - - sys.exit(126) - + + sys.exit(126) + # Fix Windows Unicode console redirection if os_win: + # noinspection PyUnresolvedReferences sys.stdout.reconfigure(encoding='utf-8') -# Show message(s) while controlling padding, newline, pausing & separator -def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char=' '): - message = to_string(in_message, sep_char) - - padding = padder(padd_count) - + +def printer(message=None, padd=0, new_line=True, pause=False, sep_char=' '): + """ Show message(s), controlling padding, newline, pausing & separator """ + + message_input = '' if message is None else message + + string = to_string(message_input, sep_char) + + padding = padder(padd) + newline = '\n' if new_line else '' - - output = newline + padding + message - - (input if pause and not is_auto_exit() else print)(output) + + message_output = newline + padding + string + + (input if pause and not is_auto_exit() else print)(message_output) diff --git a/common/templates.py b/common/templates.py index f69af33..f7e2f53 100644 --- a/common/templates.py +++ b/common/templates.py @@ -1,111 +1,122 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ +import argparse +import ctypes import os import sys -import ctypes -import argparse import traceback from common.num_ops import get_ordinal -from common.path_ops import get_dequoted_path, get_extract_path, get_path_files, is_path_absolute, path_parent, runtime_root, safe_path +from common.path_ops import (get_dequoted_path, get_extract_path, get_path_files, + is_path_absolute, path_name, path_parent, real_path, runtime_root) from common.system import check_sys_os, check_sys_py, get_os_ver, is_auto_exit, printer + class BIOSUtility: - + """ Template utility class for BIOSUtilities """ + MAX_FAT32_ITEMS = 65535 - - def __init__(self, title, check, main, padding=0): + + def __init__(self, title, check, main, args=None, padding=0): self._title = title self._main = main self._check = check + self._arg_defs = args if args is not None else [] self._padding = padding + self._arguments_kw = {} - + self._arguments_kw_dest = [] + # Initialize argparse argument parser self._argparser = argparse.ArgumentParser() - + self._argparser.add_argument('files', type=argparse.FileType('r', encoding='utf-8'), nargs='*') self._argparser.add_argument('-e', '--auto-exit', help='skip all user action prompts', action='store_true') self._argparser.add_argument('-v', '--version', help='show utility name and version', action='store_true') self._argparser.add_argument('-o', '--output-dir', help='extract in given output directory') self._argparser.add_argument('-i', '--input-dir', help='extract from given input directory') - - self._arguments,self._arguments_unk = self._argparser.parse_known_args() - + + for _arg_def in self._arg_defs: + _action = self._argparser.add_argument(*_arg_def[0], **_arg_def[1]) + + self._arguments_kw_dest.append(_action.dest) + + self._arguments, _ = self._argparser.parse_known_args() + + for _arg_dest in self._arguments_kw_dest: + self._arguments_kw.update({_arg_dest: self._arguments.__dict__[_arg_dest]}) + # Managed Python exception handler sys.excepthook = self._exception_handler - + # Check Python Version check_sys_py() - + # Check OS Platform check_sys_os() - + # Show Script Title printer(self._title, new_line=False) - + # Show Utility Version on demand if self._arguments.version: sys.exit(0) - + # Set console/terminal window title (Windows only) if get_os_ver()[1]: ctypes.windll.kernel32.SetConsoleTitleW(self._title) - + # Process input files and generate output path self._process_input_files() - + # Count input files for exit code self._exit_code = len(self._input_files) - - def parse_argument(self, *args, **kwargs): - _dest = self._argparser.add_argument(*args, **kwargs).dest - self._arguments = self._argparser.parse_known_args(self._arguments_unk)[0] - self._arguments_kw.update({_dest: self._arguments.__dict__[_dest]}) - + def run_utility(self): + """ Run utility after checking for supported format """ + for _input_file in self._input_files: - _input_name = os.path.basename(_input_file) - + _input_name = path_name(_input_file, limit=True) + printer(['***', _input_name], self._padding) - + if not self._check(_input_file): printer('Error: This is not a supported input!', self._padding + 4) - - continue # Next input file - + + continue # Next input file + _extract_path = os.path.join(self._output_path, get_extract_path(_input_name)) - + if os.path.isdir(_extract_path): for _suffix in range(2, self.MAX_FAT32_ITEMS): _renamed_path = f'{os.path.normpath(_extract_path)}_{get_ordinal(_suffix)}' - + if not os.path.isdir(_renamed_path): _extract_path = _renamed_path - - break # Extract path is now unique - + + break # Extract path is now unique + if self._main(_input_file, _extract_path, self._padding + 4, **self._arguments_kw) in [0, None]: self._exit_code -= 1 - + printer('Done!', pause=True) - + sys.exit(self._exit_code) # Process input files def _process_input_files(self): self._input_files = [] - + if len(sys.argv) >= 2: # Drag & Drop or CLI if self._arguments.input_dir: _input_path_user = self._arguments.input_dir - _input_path_full = self._get_input_path(_input_path_user) if _input_path_user else '' + _input_path_full = self._get_user_path(_input_path_user) if _input_path_user else '' self._input_files = get_path_files(_input_path_full) else: # Parse list of input files (i.e. argparse FileType objects) @@ -114,25 +125,25 @@ class BIOSUtility: self._input_files.append(_file_object.name) # Close each argparse FileType object (i.e. allow input file changes) _file_object.close() - + # Set output fallback value for missing argparse Output and Input Path _output_fallback = path_parent(self._input_files[0]) if self._input_files else None - + # Set output path via argparse Output path or argparse Input path or first input file path _output_path = self._arguments.output_dir or self._arguments.input_dir or _output_fallback else: # Script w/o parameters _input_path_user = get_dequoted_path(input('\nEnter input directory path: ')) - _input_path_full = self._get_input_path(_input_path_user) if _input_path_user else '' + _input_path_full = self._get_user_path(_input_path_user) if _input_path_user else '' self._input_files = get_path_files(_input_path_full) - + _output_path = get_dequoted_path(input('\nEnter output directory path: ')) - - self._output_path = self._get_input_path(_output_path) - - # Get absolute input file path + + self._output_path = self._get_user_path(_output_path) + + # Get absolute user file path @staticmethod - def _get_input_path(input_path): + def _get_user_path(input_path): if not input_path: # Use runtime directory if no user path is specified absolute_path = runtime_root() @@ -142,8 +153,8 @@ class BIOSUtility: absolute_path = input_path # Otherwise, make it runtime directory relative else: - absolute_path = safe_path(runtime_root(), input_path) - + absolute_path = real_path(input_path) + return absolute_path # https://stackoverflow.com/a/781074 by Torsten Marek @@ -153,7 +164,7 @@ class BIOSUtility: printer('') else: printer('Error: Utility crashed, please report the following:\n') - + traceback.print_exception(exc_type, exc_value, exc_traceback) if not is_auto_exit(): diff --git a/common/text_ops.py b/common/text_ops.py index f007051..318e869 100644 --- a/common/text_ops.py +++ b/common/text_ops.py @@ -1,33 +1,48 @@ -#!/usr/bin/env python3 -#coding=utf-8 +#!/usr/bin/env python3 -B +# coding=utf-8 """ -Copyright (C) 2022 Plato Mavropoulos +Copyright (C) 2022-2024 Plato Mavropoulos """ -# Generate padding (spaces or tabs) + def padder(padd_count, tab=False): + """ Generate padding (spaces or tabs) """ + return ('\t' if tab else ' ') * padd_count -# Get String from given input object + def to_string(in_object, sep_char=''): - if type(in_object).__name__ in ('list','tuple'): + """ Get String from given input object """ + + if type(in_object).__name__ in ('list', 'tuple'): out_string = sep_char.join(map(str, in_object)) else: out_string = str(in_object) - + return out_string -# Get Bytes from given buffer or file path + def file_to_bytes(in_object): + """ Get Bytes from given buffer or file path """ + object_bytes = in_object - - if type(in_object).__name__ not in ('bytes','bytearray'): + + if type(in_object).__name__ not in ('bytes', 'bytearray'): with open(to_string(in_object), 'rb') as object_data: object_bytes = object_data.read() - + return object_bytes -# Check if string starts and ends with given character(s) + +def bytes_to_hex(buffer: bytes, order: str, data_len: int, slice_len: int | None = None) -> str: + """ Converts bytes to hex string, controlling endianess, data size and string slicing """ + + # noinspection PyTypeChecker + return f'{int.from_bytes(buffer, order):0{data_len * 2}X}'[:slice_len] + + def is_encased(in_string, chars): + """ Check if string starts and ends with given character(s) """ + return in_string.startswith(chars) and in_string.endswith(chars) diff --git a/external/requirements.txt b/external/requirements.txt deleted file mode 100644 index 798c16c..0000000 --- a/external/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -lznt1 >= 0.2 -pefile >= 2022.5.30 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3bfae9f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +dissect.util == 3.15 +pefile == 2023.2.7