From 0f05e9b0bea7777407c04c5bd9d995467a3c1f1a Mon Sep 17 00:00:00 2001
From: platomav <platomav@users.noreply.github.com>
Date: Mon, 14 Mar 2022 00:57:18 +0200
Subject: [PATCH] Insyde iFlash Image Extractor v1.0

Parses Insyde iFlash images and extracts their raw components (e.g. SPI/BIOS/UEFI, EC, ME, Flasher, Configuration etc)
---
 .../Insyde_iFlash_Extract.py                  | 270 ++++++++++++++++++
 LICENSE                                       |   2 +-
 README.md                                     |  57 ++++
 3 files changed, 328 insertions(+), 1 deletion(-)
 create mode 100644 Insyde iFlash Image Extractor/Insyde_iFlash_Extract.py

diff --git a/Insyde iFlash Image Extractor/Insyde_iFlash_Extract.py b/Insyde iFlash Image Extractor/Insyde_iFlash_Extract.py
new file mode 100644
index 0000000..d8ca64c
--- /dev/null
+++ b/Insyde iFlash Image Extractor/Insyde_iFlash_Extract.py	
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+#coding=utf-8
+
+"""
+Insyde iFlash Extract
+Insyde iFlash Image Extractor
+Copyright (C) 2022 Plato Mavropoulos
+"""
+
+title = 'Insyde iFlash Image Extractor v1.0'
+
+import sys
+
+# Detect Python version
+sys_py = sys.version_info
+
+# Check Python version
+if sys_py < (3,7):
+    sys.stdout.write('%s\n\nError: Python >= 3.7 required, not %d.%d!\n' % (title, sys_py[0], sys_py[1]))
+    
+    if '--auto-exit' not in sys.argv and '-e' not in sys.argv:
+        (raw_input if sys_py[0] <= 2 else input)('\nPress enter to exit') # pylint: disable=E0602
+    
+    sys.exit(1)
+
+# Detect OS platform
+sys_os = sys.platform
+
+# Check OS platform
+if sys_os == 'win32':
+    sys.stdout.reconfigure(encoding='utf-8') # Fix Windows Unicode console redirection
+elif sys_os.startswith('linux') or sys_os == 'darwin' or sys_os.find('bsd') != -1:
+    pass # Supported/Tested
+else:
+    print('%s\n\nError: Unsupported platform "%s"!\n' % (title, sys_os))
+    
+    if '--auto-exit' not in sys.argv and '-e' not in sys.argv: input('Press enter to exit')
+    
+    sys.exit(1)
+
+# Python imports
+import os
+import re
+import ctypes
+import inspect
+import pathlib
+import argparse
+import traceback
+
+# Set ctypes Structure types
+char = ctypes.c_char
+uint32_t = ctypes.c_uint
+
+class IflashHeader(ctypes.LittleEndianStructure):
+    _pack_ = 1
+    _fields_ = [
+        ('Signature',            char*9),        # 0x00 $_IFLASH_
+        ('ImageTag',             char*7),        # 0x08
+        ('TotalSize',            uint32_t),      # 0x10 from header end
+        ('ImageSize',            uint32_t),      # 0x14 from header end
+        # 0x18
+    ]
+    
+    def ifl_print(self, padd):
+        p = ' ' * (padd - 1)
+        
+        print(p, 'Signature : %s' % self.Signature.decode('utf-8','ignore'))
+        print(p, 'Image Name: %s' % self.ImageTag.decode('utf-8','ignore'))
+        print(p, 'Total Size: 0x%X' % self.TotalSize)
+        print(p, 'Image Size: 0x%X' % self.ImageSize)
+
+class InsydeIflash:
+    def __init__(self, in_data, out_path, in_padd, in_verbose):
+        self.fw_data = in_data
+        self.ex_path = out_path
+        self.padding = in_padd
+        self.verbose = in_verbose
+        
+        self.hdr_len = ctypes.sizeof(IflashHeader)
+        
+        self.mod_names = {
+            'DRV_IMG':['isflash','efi'],
+            'INI_IMG':['platform','ini'],
+            'BIOSIMG':['BIOS','bin'],
+            'ME_IMG_':['ME','bin'],
+            'EC_IMG_':['EC','bin'],
+            'OEM_ID_':['OEM_ID','bin'],
+            'BIOSCER':['Certificate','bin'],
+            'BIOSCR2':['Certificate_2','bin'],
+            }
+
+    def iflash_parse(self):
+        all_ins_ifl = pat_ins_ifl.finditer(self.fw_data)
+        
+        if not all_ins_ifl: return 1
+        
+        if not os.path.isdir(self.ex_path): os.mkdir(self.ex_path)
+        
+        for ins_ifl in all_ins_ifl:
+            ifl_off = ins_ifl.start()
+            
+            ifl_hdr = get_struct(self.fw_data, ifl_off, IflashHeader)
+            
+            if self.verbose:
+                print('\n%sInsyde iFlash Module @ 0x%0.8X\n' % (' ' * self.padding, ifl_off))
+                
+                ifl_hdr.ifl_print(self.padding + 4)
+            
+            mod_bgn = ifl_off + self.hdr_len
+            mod_end = mod_bgn + ifl_hdr.ImageSize
+            mod_bin = self.fw_data[mod_bgn:mod_end]
+            
+            if not mod_bin: continue # Empty/Missing Module
+            
+            mod_tag = ifl_hdr.ImageTag.decode('utf-8','ignore')
+            out_tag = self.mod_names[mod_tag][0] if mod_tag in self.mod_names else mod_tag
+            out_ext = self.mod_names[mod_tag][1] if mod_tag in self.mod_names else 'bin'
+            
+            out_name = get_safe_name('%s [0x%0.8X-0x%0.8X].%s' % (out_tag, mod_bgn, mod_end, out_ext))
+            out_path = os.path.join(self.ex_path, out_name)
+
+            with open(out_path, 'wb') as out: out.write(mod_bin)
+            
+            print('\n%sExtracted' % (' ' * (self.padding + 8 if self.verbose else self.padding)), out_name)
+
+        return 0
+
+# Process ctypes Structure Classes
+# https://github.com/skochinsky/me-tools/blob/master/me_unpack.py by Igor Skochinsky
+def get_struct(buffer, start_offset, class_name, param_list=None):
+    if param_list is None: param_list = []
+
+    structure = class_name(*param_list) # Unpack parameter list
+    struct_len = ctypes.sizeof(structure)
+    struct_data = buffer[start_offset:start_offset + struct_len]
+    fit_len = min(len(struct_data), struct_len)
+
+    ctypes.memmove(ctypes.addressof(structure), struct_data, fit_len)
+
+    return structure
+
+# Get absolute file path (argparse object)
+def get_absolute_path(argparse_path):
+    if not argparse_path:
+        absolute_path = get_script_dir() # Use input file directory if no user path is specified
+    else:
+        # Check if user specified path is absolute, otherwise convert it to input file relative
+        if pathlib.Path(argparse_path).is_absolute(): absolute_path = argparse_path
+        else: absolute_path = os.path.join(get_script_dir(), argparse_path)
+    
+    return absolute_path
+
+# Get list of files from absolute path
+def get_path_files(abs_path):
+    file_list = [] # Initialize list of files
+    
+    # Traverse input absolute path
+    for root,_,files in os.walk(abs_path):
+        file_list = [os.path.join(root, name) for name in files]
+    
+    return file_list
+
+# Fix illegal/reserved Windows characters
+def get_safe_name(file_name):
+    raw_name = repr(file_name).strip("'")
+
+    return re.sub(r'[\\/*?:"<>|]', '_', raw_name)
+
+# Get python script working directory
+# https://stackoverflow.com/a/22881871 by jfs
+def get_script_dir(follow_symlinks=True):
+    if getattr(sys, 'frozen', False):
+        path = os.path.abspath(sys.executable)
+    else:
+        path = inspect.getabsfile(get_script_dir)
+    if follow_symlinks:
+        path = os.path.realpath(path)
+
+    return os.path.dirname(path)
+
+# Pause after any unexpected Python exception
+# https://stackoverflow.com/a/781074 by Torsten Marek
+def show_exception_and_exit(exc_type, exc_value, tb):
+    if exc_type is KeyboardInterrupt :
+        print('\n')
+    else:
+        print('\nError: %s crashed, please report the following:\n' % title)
+        traceback.print_exception(exc_type, exc_value, tb)
+        if not bool(args.auto_exit): input('\nPress enter to exit')
+    
+    sys.exit(1) # Crash exceptions are critical
+
+# Insyde iFlash Section Signature
+pat_ins_ifl = re.compile(br'\$_IFLASH_')
+
+if __name__ == '__main__':
+    # Show script title
+    print('\n' + title)
+
+    # Set console/shell window title
+    user_os = sys.platform
+    if user_os == 'win32': ctypes.windll.kernel32.SetConsoleTitleW(title)
+    elif user_os.startswith('linux') or user_os == 'darwin' or user_os.find('bsd') != -1: sys.stdout.write('\x1b]2;' + title + '\x07')
+
+    # Set argparse Arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument('images', type=argparse.FileType('r'), nargs='*')
+    parser.add_argument('-v', '--verbose', help='show iFlash structure information', action='store_true')
+    parser.add_argument('-e', '--auto-exit', help='skip press enter to exit prompts', action='store_true')
+    parser.add_argument('-o', '--output-dir', help='extract in given output directory')
+    parser.add_argument('-i', '--input-dir', help='extract from given input directory')
+    args = parser.parse_args()
+    
+    # Set pause-able Python exception handler (must be after args)
+    sys.excepthook = show_exception_and_exit
+    
+    # Initialize Dell PFS input file list
+    iflash_input_images = []
+
+    # Process input files
+    if len(sys.argv) >= 2:
+        # Drag & Drop or CLI
+        if args.input_dir:
+            input_path_user = get_absolute_path(args.input_dir)
+            iflash_input_images = get_path_files(input_path_user)
+        else:
+            iflash_input_images = [image.name for image in args.images]
+        
+        output_path_user = get_absolute_path(args.output_dir or args.input_dir)
+    else:
+        # Script w/o parameters
+        input_path_user = get_absolute_path(input('\nEnter input directory path: '))
+        iflash_input_images = get_path_files(input_path_user)
+        
+        output_path_user = get_absolute_path(input('\nEnter output directory path: '))
+
+    # Initialize global variables
+    exit_code = len(iflash_input_images) # Initialize exit code with input file count
+    is_verbose = bool(args.verbose) # Set Verbose output mode optional argument
+    
+    for input_file in iflash_input_images:
+        input_name = os.path.basename(input_file)
+        input_padd = 8
+        
+        print('\n*** %s' % input_name)
+        
+        # Check if input file exists
+        if not os.path.isfile(input_file):
+            print('\n%sError: This input file does not exist!' % (' ' * input_padd))
+            continue # Next input file
+        
+        with open(input_file, 'rb') as in_file: input_data = in_file.read()
+        
+        # Search input image for Insyde iFlash Sections
+        is_ins_ifl = pat_ins_ifl.search(input_data)
+        
+        if not is_ins_ifl:
+            print('\n%sError: This is not an Insyde iFlash image!' % (' ' * input_padd))
+            continue # Next input file
+        
+        # Set main extraction path (optional user specified path taken into account)
+        output_path = os.path.join(output_path_user, input_name + '_extracted')
+        
+        InsydeIflash(input_data, output_path, input_padd, is_verbose).iflash_parse()
+        
+        exit_code -= 1 # Adjust exit code to reflect extraction progress
+    
+    if not bool(args.auto_exit): input('\nDone!')
+    
+    sys.exit(exit_code)
diff --git a/LICENSE b/LICENSE
index 47e82f4..06831fb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2019-2021 Plato Mavropoulos
+Copyright (c) 2019-2022 Plato Mavropoulos
 
 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
diff --git a/README.md b/README.md
index fb8804c..0f77960 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@
 * [**AMI UCP BIOS Extractor**](#ami-ucp-bios-extractor)
 * [**AMI BIOS Guard Extractor**](#ami-bios-guard-extractor)
 * [**Phoenix SCT BIOS Extractor**](#phoenix-sct-bios-extractor)
+* [**Insyde iFlash Image Extractor**](#insyde-iflash-image-extractor)
 * [**Portwell EFI BIOS Extractor**](#portwell-efi-bios-extractor)
 * [**Panasonic BIOS Update Extractor**](#panasonic-bios-update-extractor)
 * [**VAIO Packaging Manager Extractor**](#vaio-packaging-manager-extractor)
@@ -262,6 +263,62 @@ Some Anti-Virus software may claim that the built/frozen/compiled executable con
 
 ![](https://i.imgur.com/Td6F5mm.png)
 
+## **Insyde iFlash Image Extractor**
+
+![](https://i.imgur.com/13GJjwO.png)
+
+#### **Description**
+
+Parses Insyde iFlash images and extracts their raw components (e.g. SPI/BIOS/UEFI, EC, ME, Flasher, Configuration etc).
+
+#### **Usage**
+
+You can either Drag & Drop or manually enter the full path of a folder containing Insyde iFlash images. Optional arguments:
+  
+* -h or --help : show help message and exit
+* -v or --verbose : show iFlash structure information
+* -e or --auto-exit : skip press enter to exit prompts
+* -o or --output-dir : extract in given output directory
+* -i or --input-dir : extract from given input directory
+
+#### **Download**
+
+An already built/frozen/compiled binary is provided by me for Windows only. Thus, **you don't need to manually build/freeze/compile it under Windows**. Instead, download the latest version from the [Releases](https://github.com/platomav/BIOSUtilities/releases) tab. To extract the already built/frozen/compiled archive, you need to use programs which support RAR5 compression. Note that you need to manually apply any prerequisites.
+
+#### **Compatibility**
+
+Should work at all Windows, Linux or macOS operating systems which have Python 3.7 support. Windows users who plan to use the already built/frozen/compiled binary must make sure that they have the latest Windows Updates installed which include all required "Universal C Runtime (CRT)" libraries.
+
+#### **Prerequisites**
+
+To run the utility, you do not need any 3rd party tool.
+
+#### **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.7.0 or newer is installed:
+
+> python --version
+
+2. Use pip to install PyInstaller:
+
+> pip3 install pyinstaller
+
+3. Build/Freeze/Compile:
+
+> pyinstaller --noupx --onefile Insyde_iFlash_Extract.py
+
+At dist folder you should find the final utility executable
+
+#### **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**
+
+![](https://i.imgur.com/Pn4JNiG.png)
+
 ## **Portwell EFI BIOS Extractor**
 
 ![](https://i.imgur.com/ySdUSgf.png)