From c144ad804c7077f5f603ed77ce9bf245658352bb Mon Sep 17 00:00:00 2001 From: platomav Date: Mon, 15 Aug 2022 18:29:58 +0300 Subject: [PATCH] Added Apple EFI Image Identifier v2.0_a3 Fixed argparse lock of input files --- Apple_EFI_Identify.py | 193 ++++++++++++++++++++++++++++++++++++++++++ README.md | 70 ++++++++++++++- common/externals.py | 15 ++++ common/path_ops.py | 9 +- common/patterns.py | 1 + 5 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 Apple_EFI_Identify.py diff --git a/Apple_EFI_Identify.py b/Apple_EFI_Identify.py new file mode 100644 index 0000000..d203d5b --- /dev/null +++ b/Apple_EFI_Identify.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +#coding=utf-8 + +""" +Apple EFI Identify +Apple EFI Image Identifier +Copyright (C) 2018-2022 Plato Mavropoulos +""" + +TITLE = 'Apple EFI Image Identifier v2.0_a3' + +import os +import sys +import zlib +import struct +import ctypes +import subprocess + +# Stop __pycache__ generation +sys.dont_write_bytecode = True + +from common.externals import get_uefifind_path, get_uefiextract_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.system import argparse_init, printer, script_init +from common.text_ops import file_to_bytes + +class IntelBiosId(ctypes.LittleEndianStructure): + _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 + # 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 +def is_apple_efi(input_file): + 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: + return False + +# Parse & Identify (or Rename) Apple EFI image +def apple_efi_identify(input_file, output_path, padding=0, rename=False): + 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 + + 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] + + temp_dir = os.path.join(f'{output_path}_uefiextract') + + del_dirs(temp_dir) # UEFIExtract must create its output folder itself, make sure it is not present + + _ = subprocess.run([get_uefiextract_path(), input_file, bios_id_res, '-o', temp_dir, '-m', 'body'], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + with open(os.path.join(temp_dir, '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_hdr = get_struct(body_buffer, bios_id_match.start(), IntelBiosId) + + del_dirs(temp_dir) # Successful UEFIExtract extraction, remove its output (temp) folder + except: + printer('Error: Failed to parse compressed $IBIOSI$ pattern!', 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)[0] + + 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}' + + 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 + + printer(f'Renamed input to {output_name}', padding) + + return 0 + +PAT_UEFIFIND = f'244942494F534924{"."*32}2E00{"."*12}2E00{"."*16}2E00{"."*12}2E00{"."*40}0000' + +if __name__ == '__main__': + # Set argparse Arguments + argparser = argparse_init() + argparser.add_argument('-r', '--rename', help='rename EFI image based on its tag', action='store_true') + arguments = argparser.parse_args() + + rename = arguments.rename # Set EFI image tag renaming optional argument + + # 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) + + if not is_apple_efi(input_file): + printer('Error: This is not an Apple EFI image!', padding) + + continue # Next input file + + extract_path = os.path.join(output_path, input_name) + + if apple_efi_identify(input_file, extract_path, padding, rename) == 0: + exit_code -= 1 + + printer('Done!', pause=True) + + sys.exit(exit_code) diff --git a/README.md b/README.md index 7bfac61..d8eaaa1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ * [**AMI BIOS Guard Extractor**](#ami-bios-guard-extractor) * [**AMI UCP Update Extractor**](#ami-ucp-update-extractor) +* [**Apple EFI Image Identifier**](#apple-efi-image-identifier) * [**Award BIOS Module Extractor**](#award-bios-module-extractor) * [**Dell PFS/PKG Update Extractor**](#dell-pfspkg-update-extractor) * [**Fujitsu SFX BIOS Extractor**](#fujitsu-sfx-bios-extractor) @@ -103,7 +104,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 for Linux) +* [TianoCompress](https://github.com/tianocore/edk2/tree/master/BaseTools/Source/C/TianoCompress/) (i.e. [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 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: @@ -142,6 +143,67 @@ Some Anti-Virus software may claim that the built/frozen/compiled executable con ![]() +## **Apple EFI Image Identifier** + +![]() + +#### **Description** + +Parses Apple EFI images and identifies them based on Intel's official $IBIOSI$ tag, which contains info such as Model, Version, Build, Date and Time. Optionally, the utility can rename the input Apple EFI image based on the retrieved $IBIOSI$ tag info, while also making sure to differentiate any EFI images with the same $IBIOSI$ tag (e.g. Production, Pre-Production) by appending a checksum of their data. + +#### **Usage** + +You can either Drag & Drop or manually enter Apple EFI image 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 +* -r or --rename : rename EFI image based on its tag + +#### **Compatibility** + +Should work at all Windows, Linux or macOS operating systems which have Python 3.10 support. + +#### **Prerequisites** + +To run the utility, you must have the following 3rd party tools at the "external" project directory: + +* [UEFIFind](https://github.com/LongSoft/UEFITool/) (i.e. [UEFIFind.exe for Windows or UEFIFind for Linux](https://github.com/LongSoft/UEFITool/releases)) +* [UEFIExtract](https://github.com/LongSoft/UEFITool/) (i.e. [UEFIExtract.exe for Windows or UEFIExtract for Linux](https://github.com/LongSoft/UEFITool/releases)) + +#### **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.10.0 or newer is installed: + +> python --version + +2. Use pip to install PyInstaller: + +> pip3 install pyinstaller + +3. Place prerequisites at the "external" project directory: + +> UEFIFind\ +> UEFIExtract + +4. Build/Freeze/Compile: + +> pyinstaller --add-data="external/*;external/" --noupx --onefile \\/Apple_EFI_Identify.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** + +![]() + ## **Award BIOS Module Extractor** ![]() @@ -344,7 +406,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 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) +* [TianoCompress](https://github.com/tianocore/edk2/tree/master/BaseTools/Source/C/TianoCompress/) (i.e. [TianoCompress.exe for Windows](https://github.com/tianocore/edk2-BaseTools-win32/) or TianoCompress for Linux) #### **Build/Freeze/Compile with PyInstaller** @@ -585,7 +647,7 @@ To run the utility, you must have the following 3rd party Python module installe 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) +* [TianoCompress](https://github.com/tianocore/edk2/tree/master/BaseTools/Source/C/TianoCompress/) (i.e. [TianoCompress.exe for Windows](https://github.com/tianocore/edk2-BaseTools-win32/) or TianoCompress for Linux) #### **Build/Freeze/Compile with PyInstaller** @@ -647,7 +709,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 tool at the "external" project directory: -* [ToshibaComExtractor](https://github.com/LongSoft/ToshibaComExtractor) (e.g. comextract.exe for Windows or comextract for Linux) +* [ToshibaComExtractor](https://github.com/LongSoft/ToshibaComExtractor) (i.e. [comextract.exe for Windows or comextract for Linux](https://github.com/LongSoft/ToshibaComExtractor/releases)) #### **Build/Freeze/Compile with PyInstaller** diff --git a/common/externals.py b/common/externals.py index 28b7631..5aa17ae 100644 --- a/common/externals.py +++ b/common/externals.py @@ -5,6 +5,9 @@ Copyright (C) 2022 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(): @@ -14,3 +17,15 @@ def get_bgs_tool(): BigScript = None return BigScript + +# 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]) + +# 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]) diff --git a/common/path_ops.py b/common/path_ops.py index 8381d56..8e093fe 100644 --- a/common/path_ops.py +++ b/common/path_ops.py @@ -136,6 +136,8 @@ def get_argparse_path(argparse_path): # Process input files (argparse object) def process_input_files(argparse_args, sys_argv=None): + input_files = [] + if sys_argv is None: sys_argv = [] @@ -146,7 +148,12 @@ def process_input_files(argparse_args, sys_argv=None): input_path_full = get_argparse_path(input_path_user) if input_path_user else '' input_files = get_path_files(input_path_full) else: - input_files = [file.name for file in argparse_args.files] + # Parse list of input files (i.e. argparse FileType objects) + for file_object in argparse_args.files: + # Store each argparse FileType object's name (i.e. path) + 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(input_files[0]) if input_files else None diff --git a/common/patterns.py b/common/patterns.py index 92ab4a6..018833f 100644 --- a/common/patterns.py +++ b/common/patterns.py @@ -9,6 +9,7 @@ import re PAT_AMI_PFAT = re.compile(br'_AMIPFAT.AMI_BIOS_GUARD_FLASH_CONFIGURATIONS', re.DOTALL) PAT_AMI_UCP = re.compile(br'@(UAF|HPU).{12}@', re.DOTALL) +PAT_APPLE_EFI = re.compile(br'\$IBIOSI\$.{16}\x2E\x00.{6}\x2E\x00.{8}\x2E\x00.{6}\x2E\x00.{20}\x00{2}', re.DOTALL) PAT_AWARD_LZH = re.compile(br'-lh[04567]-') 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)