From bdf926f6fdec9e6ec9a0f6ffee5c0651b0c96f98 Mon Sep 17 00:00:00 2001 From: Plato Mavropoulos Date: Sun, 26 May 2024 19:24:27 +0300 Subject: [PATCH] Panasonic BIOS Package Extractor v4.0 Added ability to parse nested Panasonic BIOS update executable directly Restructured logic to allow more flexibility on input executable parsing Populated code type hints and applied multiple small improvements --- Panasonic_BIOS_Extract.py | 185 ++++++++++++++++++-------------------- common/pe_ops.py | 26 +++--- 2 files changed, 103 insertions(+), 108 deletions(-) diff --git a/Panasonic_BIOS_Extract.py b/Panasonic_BIOS_Extract.py index 13746fe..324a242 100644 --- a/Panasonic_BIOS_Extract.py +++ b/Panasonic_BIOS_Extract.py @@ -11,13 +11,15 @@ import io import logging import os +from re import Match + import pefile 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 -from common.pe_ops import get_pe_file, get_pe_info, is_pe_file, show_pe_info +from common.pe_ops import get_pe_desc, get_pe_file, is_pe_file, show_pe_info from common.patterns import PAT_MICROSOFT_CAB from common.system import printer from common.templates import BIOSUtility @@ -25,81 +27,73 @@ from common.text_ops import file_to_bytes from AMI_PFAT_Extract import is_ami_pfat, parse_pfat_file -TITLE = 'Panasonic BIOS Package Extractor v3.0' +TITLE = 'Panasonic BIOS Package Extractor v4.0' -def is_panasonic_pkg(in_file): +def is_panasonic_pkg(input_object: str | bytes | bytearray) -> bool: """ Check if input is Panasonic BIOS Package PE """ - in_buffer = file_to_bytes(in_file) - - pe_file = get_pe_file(in_buffer, silent=True) + pe_file: pefile.PE | None = get_pe_file(input_object, silent=True) if not pe_file: return False - 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': - return False - - if not PAT_MICROSOFT_CAB.search(in_buffer): + if get_pe_desc(pe_file, silent=True).decode('utf-8', 'ignore').upper() not in (PAN_PE_DESC_UNP, PAN_PE_DESC_UPD): return False return True -def panasonic_cab_extract(buffer, extract_path, padding=0): +def panasonic_pkg_name(input_object: str | bytes | bytearray) -> str: + """ Get Panasonic BIOS Package file name, when applicable """ + + if isinstance(input_object, str) and os.path.isfile(input_object): + return safe_name(path_stem(input_object)) + + return '' + + +def panasonic_cab_extract(input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> str: """ Search and Extract Panasonic BIOS Package PE CAB archive """ - pe_path, pe_file, pe_info = [None] * 3 + input_data: bytes = file_to_bytes(input_object) - 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_match: Match[bytes] | None = PAT_MICROSOFT_CAB.search(input_data) - cab_bin = buffer[cab_bgn:cab_end] + if cab_match: + cab_bgn: int = cab_match.start() - cab_tag = f'[0x{cab_bgn:06X}-0x{cab_end:06X}]' + cab_end: int = cab_bgn + int.from_bytes(input_data[cab_bgn + 0x8:cab_bgn + 0xC], 'little') - cab_path = os.path.join(extract_path, f'CAB_{cab_tag}.cab') + cab_tag: str = f'[0x{cab_bgn:06X}-0x{cab_end:06X}]' - with open(cab_path, 'wb') as cab_file: - cab_file.write(cab_bin) # Store CAB archive + cab_path: str = os.path.join(extract_path, f'CAB_{cab_tag}.cab') - if is_szip_supported(cab_path, padding, check=True): - printer(f'Panasonic BIOS Package > PE > CAB {cab_tag}', padding) + with open(cab_path, 'wb') as cab_file: + cab_file.write(input_data[cab_bgn:cab_end]) # Store CAB archive - if szip_decompress(cab_path, extract_path, 'CAB', padding + 4, check=True) == 0: - 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 + if is_szip_supported(cab_path, padding, check=True): + printer(f'Panasonic BIOS Package > PE > CAB {cab_tag}', padding) - for file_path in get_path_files(extract_path): - pe_file = get_pe_file(file_path, padding, silent=True) + if szip_decompress(cab_path, extract_path, 'CAB', padding + 4, check=True) == 0: + os.remove(cab_path) # Successful extraction, delete CAB archive - if pe_file: - pe_info = get_pe_info(pe_file, padding, silent=True) + for extracted_file_path in get_path_files(extract_path): + extracted_pe_file: pefile.PE | None = get_pe_file(extracted_file_path, padding, silent=True) - if pe_info.get(b'FileDescription', b'').upper() == b'BIOS UPDATE': - pe_path = file_path + if extracted_pe_file: + extracted_pe_desc: bytes = get_pe_desc(extracted_pe_file, silent=True) - break - else: - return pe_path, pe_file, pe_info + if extracted_pe_desc.decode('utf-8', 'ignore').upper() == PAN_PE_DESC_UPD: + return extracted_file_path - return pe_path, pe_file, pe_info + return '' -def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0): +def panasonic_res_extract(pe_file: pefile.PE, extract_path: str, pe_name: str = '', padding: int = 0) -> bool: """ Extract & Decompress Panasonic BIOS Update PE RCDATA (LZNT1) """ - is_rcdata = False + is_rcdata: bool = 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']]) @@ -110,40 +104,40 @@ def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0): 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_bgn: int = resource.directory.entries[0].data.struct.OffsetToData + res_len: int = resource.directory.entries[0].data.struct.Size + res_end: int = res_bgn + res_len - res_bin = pe_file.get_data(res_bgn, res_len) + res_bin: bytes = pe_file.get_data(res_bgn, res_len) - res_tag = f'{pe_name} [0x{res_bgn:06X}-0x{res_end:06X}]' + res_tag: str = f'{pe_name} [0x{res_bgn:06X}-0x{res_end:06X}]'.strip() - res_out = os.path.join(extract_path, f'{res_tag}') + res_out: str = os.path.join(extract_path, f'{res_tag}') - printer(res_tag, padding + 4) + printer(res_tag, padding) try: - res_raw = lznt1.decompress(res_bin[0x8:]) + res_raw: bytes = lznt1.decompress(res_bin[0x8:]) 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) + printer('Succesfull LZNT1 decompression via Dissect!', padding + 4) 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) + printer('Succesfull PE Resource extraction!', padding + 4) # Detect & Unpack AMI BIOS Guard (PFAT) BIOS image if is_ami_pfat(res_raw): - pfat_dir = os.path.join(extract_path, res_tag) + pfat_dir: str = os.path.join(extract_path, res_tag) - parse_pfat_file(res_raw, pfat_dir, padding + 12) + parse_pfat_file(res_raw, pfat_dir, padding + 8) else: if is_pe_file(res_raw): - res_ext = 'exe' + res_ext: str = 'exe' elif res_raw.startswith(b'[') and res_raw.endswith((b'\x0D\x0A', b'\x0A')): res_ext = 'txt' else: @@ -153,9 +147,9 @@ def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0): printer(new_line=False) for line in io.BytesIO(res_raw).readlines(): - line_text = line.decode('utf-8', 'ignore').rstrip() + line_text: str = line.decode('utf-8', 'ignore').rstrip() - printer(line_text, padding + 12, new_line=False) + printer(line_text, padding + 8, new_line=False) with open(f'{res_out}.{res_ext}', 'wb') as out_file: out_file.write(res_raw) @@ -163,78 +157,73 @@ def panasonic_res_extract(pe_name, pe_file, extract_path, padding=0): return is_rcdata -def panasonic_img_extract(pe_name, pe_path, pe_file, extract_path, padding=0): +def panasonic_img_extract(pe_file: pefile.PE, extract_path: str, pe_name: str = '', padding: int = 0) -> bool: """ Extract Panasonic BIOS Update PE Data when RCDATA is not available """ - pe_data = file_to_bytes(pe_path) + pe_data: bytes = bytes(pe_file.__data__) - sec_bgn = pe_file.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY[ + sec_bgn: int = 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_bgn: int = pe_file.OPTIONAL_HEADER.BaseOfData + pe_file.OPTIONAL_HEADER.SizeOfInitializedData + img_end: int = sec_bgn or len(pe_data) - img_bin = pe_data[img_bgn:img_end] + img_bin: bytes = pe_data[img_bgn:img_end] - img_tag = f'{pe_name} [0x{img_bgn:X}-0x{img_end:X}]' + img_tag: str = f'{pe_name} [0x{img_bgn:X}-0x{img_end:X}]'.strip() - img_out = os.path.join(extract_path, f'{img_tag}.bin') + img_out: str = os.path.join(extract_path, f'{img_tag}.bin') - printer(img_tag, padding + 4) + printer(img_tag, padding) with open(img_out, 'wb') as out_img: out_img.write(img_bin) - printer('Succesfull PE Data extraction!', padding + 8) + printer('Succesfull PE Data extraction!', padding + 4) return bool(img_bin) -def panasonic_pkg_extract(input_file, extract_path, padding=0): +def panasonic_pkg_extract(input_object: str | bytes | bytearray, extract_path: str, padding: int = 0) -> int: """ Parse & Extract Panasonic BIOS Package PE """ - input_buffer = file_to_bytes(input_file) + upd_pe_file: pefile.PE | None = get_pe_file(input_object, padding) + + upd_pe_name: str = panasonic_pkg_name(input_object) + + printer(f'Panasonic BIOS Package > PE ({upd_pe_name})\n'.replace(' ()', ''), padding) + + show_pe_info(upd_pe_file, padding + 4) make_dirs(extract_path, delete=True) - pkg_pe_file = get_pe_file(input_buffer, padding) + upd_pe_path: str = panasonic_cab_extract(input_object, extract_path, padding + 8) - if not pkg_pe_file: - return 2 + upd_padding: int = padding - pkg_pe_info = get_pe_info(pkg_pe_file, padding) + if upd_pe_path: + upd_padding = padding + 16 - if not pkg_pe_info: - return 3 + upd_pe_name = panasonic_pkg_name(upd_pe_path) - pkg_pe_name = path_stem(input_file) + printer(f'Panasonic BIOS Update > PE ({upd_pe_name})\n'.replace(' ()', ''), upd_padding) - printer(f'Panasonic BIOS Package > PE ({pkg_pe_name})\n', padding) + upd_pe_file = get_pe_file(upd_pe_path, upd_padding) - show_pe_info(pkg_pe_info, padding + 4) + show_pe_info(upd_pe_file, upd_padding + 4) - upd_pe_path, upd_pe_file, upd_pe_info = panasonic_cab_extract(input_buffer, extract_path, padding + 4) + os.remove(upd_pe_path) - if not (upd_pe_path and upd_pe_file and upd_pe_info): - return 4 + is_upd_extracted: bool = panasonic_res_extract(upd_pe_file, extract_path, upd_pe_name, upd_padding + 8) - upd_pe_name = safe_name(path_stem(upd_pe_path)) + if not is_upd_extracted: + is_upd_extracted = panasonic_img_extract(upd_pe_file, extract_path, upd_pe_name, upd_padding + 8) - printer(f'Panasonic BIOS Update > PE ({upd_pe_name})\n', padding + 12) + return 0 if is_upd_extracted else 1 - 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 +PAN_PE_DESC_UNP: str = 'UNPACK UTILITY' +PAN_PE_DESC_UPD: str = 'BIOS UPDATE' if __name__ == '__main__': BIOSUtility(title=TITLE, check=is_panasonic_pkg, main=panasonic_pkg_extract).run_utility() diff --git a/common/pe_ops.py b/common/pe_ops.py index ac64436..19cf78f 100644 --- a/common/pe_ops.py +++ b/common/pe_ops.py @@ -20,26 +20,30 @@ def is_pe_file(in_file: str | bytes) -> bool: 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 + pe_file: pefile.PE | None = None try: # Analyze detected MZ > PE image buffer - pe_file = pefile.PE(data=in_buffer, fast_load=fast) + pe_file = pefile.PE(data=file_to_bytes(in_file), fast_load=fast) except Exception as error: # pylint: disable=broad-except if not silent: - _filename = in_file if type(in_file).__name__ == 'string' else 'buffer' + filename: str = in_file if isinstance(in_file, str) else 'buffer' - printer(f'Error: Could not get pefile object from {_filename}: {error}!', padding) + printer(f'Error: Could not get pefile object from {filename}: {error}!', padding) return pe_file +def get_pe_desc(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> bytes: + """ Get PE description from pefile object info """ + + return get_pe_info(pe_file, padding, silent).get(b'FileDescription', b'') + + def get_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> dict: """ Get PE info from pefile object """ - pe_info = {} + pe_info: dict = {} try: # When fast_load is used, IMAGE_DIRECTORY_ENTRY_RESOURCE must be parsed prior to FileInfo > StringTable @@ -54,13 +58,15 @@ def get_pe_info(pe_file: pefile.PE, padding: int = 0, silent: bool = False) -> d return pe_info -def show_pe_info(pe_info: dict, padding: int = 0) -> None: +def show_pe_info(pe_file: pefile.PE, padding: int = 0) -> None: """ Print PE info from pefile StringTable """ + pe_info: dict = get_pe_info(pe_file=pe_file, padding=padding) + 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() + info_title: str = title.decode('utf-8', 'ignore').strip() + info_value: str = value.decode('utf-8', 'ignore').strip() if info_title and info_value: printer(f'{info_title}: {info_value}', padding, new_line=False)