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