diff --git a/AMI_UCP_Extract.py b/AMI_UCP_Extract.py index 5e35f97..769048b 100644 --- a/AMI_UCP_Extract.py +++ b/AMI_UCP_Extract.py @@ -159,7 +159,7 @@ def chk16_validate(data, tag, padd=0): else: printer('Checksum of UCP Module %s is valid!' % tag, padd) -# Check if input path or buffer is AMI UCP image +# Check if input is AMI UCP image def is_ami_ucp(in_file): buffer = file_to_bytes(in_file) diff --git a/Dell_PFS_Extract.py b/Dell_PFS_Extract.py index cae76c9..fb9af1a 100644 --- a/Dell_PFS_Extract.py +++ b/Dell_PFS_Extract.py @@ -7,7 +7,7 @@ Dell PFS Update Extractor Copyright (C) 2018-2022 Plato Mavropoulos """ -TITLE = 'Dell PFS Update Extractor v6.0_a8' +TITLE = 'Dell PFS Update Extractor v6.0_a9' import os import io @@ -206,7 +206,7 @@ def is_pfs_hdr(in_buffer): def is_pfs_ftr(in_buffer): return PAT_DELL_FTR.search(in_buffer) -# Check if input path or buffer is Dell PFS/PKG image +# Check if input is Dell PFS/PKG image def is_dell_pfs(in_file): in_buffer = file_to_bytes(in_file) @@ -304,7 +304,7 @@ def pfs_section_parse(zlib_data, zlib_start, output_path, pfs_name, pfs_index, p # Decompress PFS ZLIB section payload try: - assert not is_zlib_error # ZLIB errors are critical + if is_zlib_error: raise Exception('ZLIB_ERROR') # ZLIB errors are critical section_data = zlib.decompress(compressed_data) # ZLIB decompression except: section_data = zlib_data # Fallback to raw ZLIB data upon critical error diff --git a/Portwell_EFI_Extract.py b/Portwell_EFI_Extract.py new file mode 100644 index 0000000..3546065 --- /dev/null +++ b/Portwell_EFI_Extract.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +#coding=utf-8 + +""" +Portwell EFI Extract +Portwell EFI BIOS Extractor +Copyright (C) 2021-2022 Plato Mavropoulos +""" + +TITLE = 'Portwell EFI BIOS Extractor v2.0_a4' + +import os +import sys +import pefile + +# Stop __pycache__ generation +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.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', + 2 : 'Update.nsh', + 3 : 'Temp.bin', + 4 : 'SaveDmiData.efi' + } + +# Check if input is Portwell EFI executable +def is_portwell_efi(in_file): + in_buffer = file_to_bytes(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 + + return is_mz and is_uu + +# Get PE of Portwell EFI executable +def get_portwell_pe(in_buffer): + pe_file = pefile.PE(data=in_buffer, fast_load=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_buffer, out_path, padding=0): + extract_path = os.path.join(out_path + '_extracted') + + make_dirs(extract_path, delete=True) + + pe_file,pe_data = get_portwell_pe(input_buffer) + + efi_title = get_unpacker_tag(input_buffer, pe_file) + + printer(efi_title, padding) + + efi_files = pe_data.split(FILE_MAGIC) # Split EFI Payload into file chunks + + parse_efi_files(extract_path, efi_files[1:], padding) + +# Get Portwell UEFI Unpacker tag +def get_unpacker_tag(input_buffer, pe_file): + 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') + + # 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 + + 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): + 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(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 + + with open(file_path, 'wb') as out_file: 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 + + if efi_decompress(comp_fname, file_path, padding + 8) == 0: + os.remove(comp_fname) # Successful decompression, delete compressed file + +if __name__ == '__main__': + # Set argparse Arguments + argparser = argparse_init() + arguments = argparser.parse_args() + + # Initialize script (must be after argparse) + exit_code,input_files,output_path,padding = script_init(TITLE, arguments, 4) + + for input_file in input_files: + input_name = os.path.basename(input_file) + + printer(['***', input_name], padding - 4) + + with open(input_file, 'rb') as in_file: input_buffer = in_file.read() + + if not is_portwell_efi(input_buffer): + printer('Error: This is not a Portwell EFI Update Package!', padding) + + continue # Next input file + + extract_path = os.path.join(output_path, input_name) + + portwell_efi_extract(input_buffer, extract_path, padding) + + exit_code -= 1 + + printer('Done!', pause=True) + + sys.exit(exit_code) diff --git a/README.md b/README.md index 23e637b..df5392a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ * [**Dell PFS Update Extractor**](#dell-pfs-update-extractor) * [**AMI UCP BIOS Extractor**](#ami-ucp-bios-extractor) * [**AMI BIOS Guard Extractor**](#ami-bios-guard-extractor) +* [**Portwell EFI BIOS Extractor**](#portwell-efi-bios-extractor) ## **Dell PFS Update Extractor** @@ -98,7 +99,7 @@ Should work at all Windows, Linux or macOS operating systems which have Python 3 To run the utility, you must have the following 3rd party tools at the "external" project directory: -* [TianoCompress](https://github.com/tianocore/edk2/tree/master/BaseTools/Source/C/TianoCompress/) (e.g. [TianoCompress.exe for Windows](https://github.com/tianocore/edk2-BaseTools-win32/) or TianoCompress) +* [TianoCompress](https://github.com/tianocore/edk2/tree/master/BaseTools/Source/C/TianoCompress/) (e.g. [TianoCompress.exe for Windows](https://github.com/tianocore/edk2-BaseTools-win32/) or TianoCompress for Linux) * [7-Zip Console](https://www.7-zip.org/) (i.e. 7z.exe for Windows or 7zz|7zzs for Linux) Optionally, to decompile the AMI UCP \> AMI PFAT \> Intel BIOS Guard Scripts (when applicable), you must have the following 3rd party utility at the "external" project directory: @@ -198,5 +199,74 @@ Some Anti-Virus software may claim that the built/frozen/compiled executable con ![]() +## **Portwell EFI BIOS Extractor** + +![]() + +#### **Description** + +Parses Portwell UEFI Unpacker EFI executables (usually named "Update.efi") and extracts their firmware (e.g. SPI, BIOS/UEFI, EC etc) and utilities (e.g. Flasher etc) components. It supports all known Portwell UEFI Unpacker revisions (v1.1, v1.2, v2.0) and formats (used, empty, null), including those which contain EFI compressed files. The output comprises only final firmware components and utilities which are directly usable by end users. + +#### **Usage** + +You can either Drag & Drop or manually enter Portwell UEFI Unpacker EFI executable file(s). Optional arguments: + +* -h or --help : show help message and exit +* -v or --version : show utility name and version +* -i or --input-dir : extract from given input directory +* -o or --output-dir : extract in given output directory +* -e or --auto-exit : skip press enter to exit prompts +* --static : use static-built external dependencies + +#### **Compatibility** + +Should work at all Windows, Linux or macOS operating systems which have Python 3.8 support. + +#### **Prerequisites** + +To run the utility, you must have the following 3rd party Python module installed: + +* [pefile](https://pypi.org/project/pefile/) + +> pip3 install pefile + +Moreover, you must have the following 3rd party tool at the "external" project directory: + +* [TianoCompress](https://github.com/tianocore/edk2/tree/master/BaseTools/Source/C/TianoCompress/) (e.g. [TianoCompress.exe for Windows](https://github.com/tianocore/edk2-BaseTools-win32/) or TianoCompress for Linux) + +#### **Build/Freeze/Compile with PyInstaller** + +PyInstaller can build/freeze/compile the utility at all three supported platforms, it is simple to run and gets updated often. + +1. Make sure Python 3.8.0 or newer is installed: + +> python --version + +2. Use pip to install PyInstaller: + +> pip3 install pyinstaller + +3. Use pip to install pefile: + +> pip3 install pefile + +4. Place prerequisite at the "external" project directory: + +> TianoCompress + +5. Build/Freeze/Compile: + +> pyinstaller --add-data="external/*;external/" --noupx --onefile \\/Portwell_EFI_Extract.py + +You should find the final utility executable at "dist" folder + +#### **Anti-Virus False Positives** + +Some Anti-Virus software may claim that the built/frozen/compiled executable contains viruses. Any such detections are false positives, usually of PyInstaller. You can switch to a better Anti-Virus software, report the false positive to their support, add the executable to the exclusions, build/freeze/compile yourself or use the Python script directly. + +#### **Pictures** + +![]() + ###### _Donate Button Card Image: [Credit and Loan Pack](https://flaticon.com/free-icon/credit-card_3898076) by **Freepik** under Flaticon license_ ###### _Donate Button Paypal Image: [Credit Cards Pack](https://flaticon.com/free-icon/paypal_349278) by **Freepik** under Flaticon license_ \ No newline at end of file diff --git a/common/efi_comp.py b/common/efi_comp.py index 97c05f9..e2c283f 100644 --- a/common/efi_comp.py +++ b/common/efi_comp.py @@ -47,6 +47,6 @@ def efi_decompress(in_path, out_path, padding=0, comp_type='--uefi'): return 1 - printer('Succesfull EFI/Tiano decompression via TianoCompress!', padding) + printer('Succesfull EFI decompression via TianoCompress!', padding) return 0