diff --git a/Phoenix_TDK_Extract.py b/Phoenix_TDK_Extract.py index 85ddcff..8133047 100644 --- a/Phoenix_TDK_Extract.py +++ b/Phoenix_TDK_Extract.py @@ -7,18 +7,19 @@ Phoenix TDK Packer Extractor Copyright (C) 2021-2022 Plato Mavropoulos """ -TITLE = 'Phoenix TDK Packer Extractor v2.0_a4' +TITLE = 'Phoenix TDK Packer Extractor v2.0_a5' import os import sys import lzma +import pefile import ctypes # Stop __pycache__ generation sys.dont_write_bytecode = True from common.path_ops import safe_name, make_dirs -from common.patterns import PAT_PHOENIX_TDK +from common.patterns import PAT_PHOENIX_TDK, PAT_MICROSOFT_MZ, PAT_MICROSOFT_PE from common.struct_ops import get_struct, char, uint32_t from common.system import script_init, argparse_init, printer from common.text_ops import file_to_bytes @@ -53,31 +54,98 @@ class PhoenixTdkEntry(ctypes.LittleEndianStructure): 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.Offset:X}'], 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) -# Scan input buffer for Phoenix TDK pattern -def get_phoenix_tdk(in_buffer): - return PAT_PHOENIX_TDK.search(in_buffer) +# Get Phoenix TDK Executable (MZ) Base Offset +def get_tdk_base(in_buffer, pack_off): + 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 + pe_off = mz_off + int.from_bytes(in_buffer[mz_off + 0x3C:mz_off + 0x40], 'little') + + # Check if potential MZ > PE image magic value is valid + if PAT_MICROSOFT_PE.search(in_buffer[pe_off:pe_off + 0x4]): + try: + # Analyze detected MZ > PE image buffer + pe_file = pefile.PE(data=in_buffer[mz_off:]) + + # Attempt to retrieve the PE > "Product Name" version string value + pe_name = pe_file.FileInfo[0][0].StringTable[0].entries[b'ProductName'] + except: + # Any error means no PE > "Product Name" retrieved + 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: + break + else: + # No TDK Base Offset could be found, assume 0x0 + tdk_base_off = 0x0 + + return tdk_base_off -# Check if input is Phoenix TDK image +# Scan input buffer for valid Phoenix TDK image +def get_phoenix_tdk(in_buffer): + # 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): buffer = file_to_bytes(in_file) - return bool(get_phoenix_tdk(buffer)) + return bool(get_phoenix_tdk(buffer)[1]) # Parse & Extract Phoenix Tools Development Kit (TDK) Packer -def phoenix_tdk_extract(input_buffer, output_path, padding=0): +def phoenix_tdk_extract(input_buffer, output_path, pack_off, base_off=0, padding=0): exit_code = 0 extract_path = os.path.join(f'{output_path}_extracted') @@ -86,11 +154,8 @@ def phoenix_tdk_extract(input_buffer, output_path, padding=0): printer('Phoenix Tools Development Kit Packer', padding) - # Search for Phoenix TDK Package pattern - tdk_match = get_phoenix_tdk(input_buffer) - # Parse TDK Header structure - tdk_hdr = get_struct(input_buffer, tdk_match.start(), PhoenixTdkHeader) + tdk_hdr = get_struct(input_buffer, pack_off, PhoenixTdkHeader) # Print TDK Header structure info printer('Phoenix TDK Header:\n', padding + 4) @@ -101,34 +166,46 @@ def phoenix_tdk_extract(input_buffer, output_path, padding=0): printer('Error: Phoenix TDK Header Size & Entry Count mismatch!\n', padding + 8, pause=True) exit_code = 1 - # Store TDK Entries offset after the dummy/placeholder data - entries_off = tdk_match.start() + TDK_HDR_LEN + TDK_DUMMY_LEN + # 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) + 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) - # Store TDK Entry raw data (relative to 0x0, not TDK Header) - mod_data = input_buffer[tdk_mod.Offset:tdk_mod.Offset + tdk_mod.Size] + # 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 = 2 + 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 = 3 + exit_code = 4 # Decompress TDK Entry raw data, when applicable (i.e. LZMA) if tdk_mod.get_compression() == 'LZMA': - mod_data = lzma.LZMADecompressor().decompress(mod_data) + try: + mod_data = lzma.LZMADecompressor().decompress(mod_data) + except: + printer('Error: Phoenix TDK Entry > LZMA decompression failed!\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' @@ -148,8 +225,8 @@ def phoenix_tdk_extract(input_buffer, output_path, padding=0): TDK_HDR_LEN = ctypes.sizeof(PhoenixTdkHeader) TDK_MOD_LEN = ctypes.sizeof(PhoenixTdkEntry) -# Set dummy/placeholder TDK Entries Size -TDK_DUMMY_LEN = 0x200 # Top 2, Names only +# Set placeholder TDK Entries Size +TDK_DUMMY_LEN = 0x200 if __name__ == '__main__': # Set argparse Arguments @@ -166,15 +243,17 @@ if __name__ == '__main__': with open(input_file, 'rb') as in_file: input_buffer = in_file.read() + tdk_base_off,tdk_pack_off = get_phoenix_tdk(input_buffer) + # Check if Phoenix TDK Packer pattern was found on executable - if not is_phoenix_tdk(input_buffer): + if not tdk_pack_off: printer('Error: This is not a Phoenix TDK Packer executable!', padding) continue # Next input file extract_path = os.path.join(output_path, input_name) - if phoenix_tdk_extract(input_buffer, extract_path, padding) == 0: + if phoenix_tdk_extract(input_buffer, extract_path, tdk_pack_off, tdk_base_off, padding) == 0: exit_code -= 1 printer('Done!', pause=True) diff --git a/Portwell_EFI_Extract.py b/Portwell_EFI_Extract.py index c241347..c0abac9 100644 --- a/Portwell_EFI_Extract.py +++ b/Portwell_EFI_Extract.py @@ -7,7 +7,7 @@ Portwell EFI Update Extractor Copyright (C) 2021-2022 Plato Mavropoulos """ -TITLE = 'Portwell EFI Update Extractor v2.0_a5' +TITLE = 'Portwell EFI Update Extractor v2.0_a6' import os import sys @@ -18,13 +18,10 @@ sys.dont_write_bytecode = True from common.efi_comp import efi_decompress, is_efi_compressed from common.path_ops import safe_name, make_dirs +from common.patterns import PAT_PORTWELL_EFI, PAT_MICROSOFT_MZ from common.system import script_init, argparse_init, printer from common.text_ops import file_to_bytes -PEFI_MAGIC = br'MZ' - -FILE_MAGIC = br'' - FILE_NAMES = { 0 : 'Flash.efi', 1 : 'Fparts.txt', @@ -40,8 +37,8 @@ def is_portwell_efi(in_file): try: pe_buffer = get_portwell_pe(in_buffer)[1] except: pe_buffer = b'' - is_mz = in_buffer.startswith(PEFI_MAGIC) # EFI images start with PE Header MZ - is_uu = pe_buffer.startswith(FILE_MAGIC) # Portwell EFI files start with + is_mz = in_buffer.startswith(PAT_MICROSOFT_MZ.pattern) # EFI images start with PE Header MZ + is_uu = pe_buffer.startswith(PAT_PORTWELL_EFI.pattern) # Portwell EFI files start with return is_mz and is_uu @@ -65,7 +62,7 @@ def portwell_efi_extract(input_buffer, output_path, padding=0): printer(efi_title, padding) - efi_files = pe_data.split(FILE_MAGIC) # Split EFI Payload into file chunks + efi_files = pe_data.split(PAT_PORTWELL_EFI.pattern) # Split EFI Payload into file chunks parse_efi_files(extract_path, efi_files[1:], padding) diff --git a/README.md b/README.md index 6bb9277..7f0e12a 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,9 @@ Should work at all Windows, Linux or macOS operating systems which have Python 3 #### **Prerequisites** -No prerequisites needed to run the utility. +To run the utility, you must have the following 3rd party Python module installed: + +* [pefile](https://pypi.org/project/pefile/) #### **Build/Freeze/Compile with PyInstaller** @@ -239,7 +241,11 @@ PyInstaller can build/freeze/compile the utility at all three supported platform > pip3 install pyinstaller -3. Build/Freeze/Compile: +3. Use pip to install pefile: + +> pip3 install pefile + +4. Build/Freeze/Compile: > pyinstaller --noupx --onefile \\/Phoenix_TDK_Extract.py diff --git a/common/patterns.py b/common/patterns.py index 326cdcb..c26f02b 100644 --- a/common/patterns.py +++ b/common/patterns.py @@ -13,4 +13,7 @@ PAT_DELL_FTR = re.compile(br'\xEE\xAA\xEE\x8F\x49\x1B\xE8\xAE\x14\x37\x90') PAT_DELL_HDR = re.compile(br'\xEE\xAA\x76\x1B\xEC\xBB\x20\xF1\xE6\x51.\x78\x9C', re.DOTALL) PAT_DELL_PKG = re.compile(br'\x72\x13\x55\x00.{45}7zXZ', re.DOTALL) PAT_INTEL_ENG = re.compile(br'\x04\x00{3}[\xA1\xE1]\x00{3}.{8}\x86\x80.{9}\x00\$((MN2)|(MAN))', re.DOTALL) +PAT_MICROSOFT_MZ = re.compile(br'MZ') +PAT_MICROSOFT_PE = re.compile(br'PE\x00\x00') PAT_PHOENIX_TDK = re.compile(br'\$PACK\x00{3}..\x00{2}.\x00{3}', re.DOTALL) +PAT_PORTWELL_EFI = re.compile(br'')