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
This commit is contained in:
Plato Mavropoulos 2024-05-26 19:24:27 +03:00
parent fdfdab011d
commit bdf926f6fd
2 changed files with 103 additions and 108 deletions

View file

@ -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()

View file

@ -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)