From a8490f0ac1d4a6452ef3241f8885079e838951b0 Mon Sep 17 00:00:00 2001 From: Yuwei Chen Date: Fri, 28 Apr 2023 16:03:34 +0800 Subject: [PATCH] Update Driver to existing Binary of specified GUID 1. Functions Introduction Functions: 1. Parse the input FV file into a origin binary tree. Currently, we just support FV level replacement operation, so we define two different types of tree nodes - FV/ FFS. 2. Given a new FFS file , parse the new FFS file into a new binary tree and get the new FFS Node from the new binary tree. 3. Find target FFS node from the origin binary tree with new FFS's driver guid. 4. Replace the target FFS node with New FFS node in the binary tree. 5. Encapsulate binary with replaced FFS. Design thoughts: A binary tree will be created for binary file to include all the info. The whole binary file will be seen as a Root Node of the tree. When parse the binary, the FV and FFS part will be seen as the FVNode/FFSNode which is the Child of the ROOT/FVNode. With the tree, both the structure and data of ROOT/FV/FFS... can be saved which can be used for function extensions. 2. Use method Method 1: Step 1: Copy the origin "bios_image.bin" & target "driver_image.ffs" (The file name can not be changed) into the XmlCli repo root folder. Step 2: Open windows console with XmlCli repo root folder, then run "py -3 src\xmlcli\common\bios_fw_parser.py". Then the expected output binary file "replaced_image.bin" will be generated into the root folder. Method 2: Step 1: Copy the origin "bios_image.bin" & target "driver_image.ffs"( with your own file name )into the XmlCli repo root folder. Step 2: Open windows console with XmlCli repo root folder, then run "py -3 src\xmlcli\common\bios_fw_parser.py -r $(input_binary_file) $(target_driver_ffs).ffs $(output_binary_file)". Then the expected output binary file $(output_binary_file) will be generated into the root folder. 3. Known Issue 3.1 Only support replace the first find driver ffs. 3.2 Only support replace the same guid driver ffs file. 3.3 The entry extension in xmlcli_registry_listener.py does not been complemented. 4. Extension direction 4.1 All the Known Issues can be enhanced with more details requirements descriptions. 4.2 The binary tree can be extended to save all level's FV and FFS and Section Node through binary parsing. 4.3 The existed binary Tree can be used to extend more functions like "Add a new driver / Delete a driver...". Signed-off-by: Yuwei Chen Signed-off-by: Yuting Yang --- docs/user_guide/uefi_binary_parsing.md | 41 +++++ src/xmlcli/common/bios_fw_parser.py | 244 +++++++++++++++++++++++-- src/xmlcli/common/compress.py | 6 +- src/xmlcli/common/logger.py | 2 +- src/xmlcli/common/structure.py | 72 +++++++- src/xmlcli/common/tree_and_node.py | 131 +++++++++++++ src/xmlcli/common/utils.py | 4 +- tests/UefiParserTest.py | 32 ++++ tests/UnitTestHelper.py | 10 + tests/tests.config | 2 + 10 files changed, 518 insertions(+), 26 deletions(-) create mode 100644 src/xmlcli/common/tree_and_node.py diff --git a/docs/user_guide/uefi_binary_parsing.md b/docs/user_guide/uefi_binary_parsing.md index 55122a0..19d2f5b 100644 --- a/docs/user_guide/uefi_binary_parsing.md +++ b/docs/user_guide/uefi_binary_parsing.md @@ -5,6 +5,7 @@ The parser is able to generate the json representation from BIOS or IFWI image. Key features: - JSON representation, lightweight database with keys and values with ease of readability - Works with both SUT and offline image +- Replace new driver in a bios image. Initiate the Parsing with below commands: @@ -33,3 +34,43 @@ if uefi_parser.guid_to_store: # Store guid stored result to json file uefi_parser.write_result_to_file(user_guid_out_file, output_dict=uefi_parser.stored_guids) ``` + + + +Replace the Driver Ffs in Binary with below commands: +```python +from xmlcli.common import bios_fw_parser + +bios_image = "absolute-path/to/bios-image.rom" +driver_image = "absolute-path/to/driver_image.ffs" +output_image = "absolute-path/to/replaced_bios_image.rom" + +uefi_parser = bios_fw_parser.UefiParser(bin_file=bios_image, # binary file to parse + parsing_level=0, # parsing level to manage number of parsing features + base_address=0, # (optional) provide base address of bios FV region to start the parsing (default 0x0) + guid_to_store=[] # if provided the guid for parsing then parser will look for every GUID in the bios image + ) + +newffs_parser = bios_fw_parser.UefiParser(bin_file=driver_image, # binary file to parse + parsing_level=0, # parsing level to manage number of parsing features + base_address=0, # (optional) provide base address of bios FV region to start the parsing (default 0x0) + guid_to_store=[] # if provided the guid for parsing then parser will look for every GUID in the bios image + ) + +# parse bios image into a binary_tree +bios_output_dict = uefi_parser.parse_binary() + +# parse driver ffs image into a binary tree node +ffs_output_dict = newffs_parser.parse_binary() +# get the target ffs guid through ffs file, extract the target tree node +TargetFfsGuid = newffs_parser.binary_tree.Position.ChildNodeList[0].Data.Name +newffsnode = newffs_parser.binary_tree.Position.ChildNodeList[0] + +# replace the target ffs with new one +uefi_parser.find_ffs_node(TargetFfsGuid) +uefi_parser.ReplaceFfs(newffsnode, uefi_parser.TargetFfsList[0]) +uefi_parser.binary_tree.WholeTreeData = b'' +uefi_parser.Encapsulate_binary(uefi_parser.binary_tree) +# dump the bios image with replaced ffs info +uefi_parser.dump_binary(replaced_image) +``` \ No newline at end of file diff --git a/src/xmlcli/common/bios_fw_parser.py b/src/xmlcli/common/bios_fw_parser.py index d804801..017db36 100644 --- a/src/xmlcli/common/bios_fw_parser.py +++ b/src/xmlcli/common/bios_fw_parser.py @@ -36,16 +36,20 @@ # Built-in imports import os +import sys +import argparse import json import shutil from collections import namedtuple +sys.path.append(".") # Custom imports -from . import utils -from . import compress -from . import structure -from . import logger -from . import configurations +import utils +import compress +import structure +import logger +import configurations +import tree_and_node __version__ = "0.0.1" @@ -115,6 +119,14 @@ def __init__(self, bin_file, parsing_level=0, **kwargs): "e360bdba-c3ce-46be-8f37b231e5cb9f35": FirmwareVolumeGuid("PFH2", [0xe360bdba, 0xc3ce, 0x46be, 0x8f, 0x37, 0xb2, 0x31, 0xe5, 0xcb, 0x9f, 0x35], self.parse_null, ""), } + self.isffs = False + if os.path.splitext(bin_file)[1].lower() == '.ffs': + self.isffs = True + self.binary_tree = tree_and_node.TreeNode('ROOT') + self.binary_tree.Type = 'ROOT' + self.FvNum = 0 + self.TargetFfsList = [] + def parse_null(self, *args, **kwargs): return {} @@ -242,11 +254,14 @@ def parse_binary(self, **kwargs): bios_size = file_size - buffer_pointer log.result(f"Size of BIOS: {bios_size} bytes ({bios_size // 1024} KB)") - self.output.update(self.parse_firmware_volume(buffer, buffer_pointer, end_point=file_size, bin_dir=bin_dir)) + if self.isffs: + self.output.update(self.parse_ffs(buffer, buffer_pointer, end_point=file_size, par_tree_node=self.binary_tree, is_root_level=True, nesting_level=0, is_compressed=False, bin_dir=bin_dir)) + else: + self.output.update(self.parse_firmware_volume(buffer, buffer_pointer, end_point=file_size, par_tree_node=self.binary_tree, is_root_level=True, bin_dir=bin_dir)) log.result(self.stored_guids) return self.output - def parse_firmware_volume(self, buffer, buffer_pointer, end_point, nesting_level=0, is_compressed=False, **kwargs): + def parse_firmware_volume(self, buffer, buffer_pointer, end_point, par_tree_node=None, nesting_level=0, is_compressed=False, is_root_level=False, **kwargs): """Parse the Firmware Volume(s) from given buffer :param buffer: Buffer to be read to parse firmware volume(s) @@ -282,6 +297,13 @@ def parse_firmware_volume(self, buffer, buffer_pointer, end_point, nesting_level firmware_volume_header = fv.read_from(buffer) # read header structure from buffer log.debug("FV parsed...") + if (firmware_volume_header.HeaderLength - 56) // 8 != 1: + Map_Nums = (firmware_volume_header.HeaderLength - 56) // 8 + newfv = structure.Refine_EfiFirmwareVolumeHeader(Map_Nums) + buffer.seek(buffer_pointer) # seek to specified buffer pointer + firmware_volume_header = newfv.read_from(buffer) # read header structure from buffer + log.debug("FV Header refined...") + # construct Directory name to store file system of the current FV fv_dir = os.path.join(bin_dir, f"FV_0x{buffer_pointer:x}_to_0x{buffer_pointer + firmware_volume_header.FvLength:x}") @@ -289,6 +311,7 @@ def parse_firmware_volume(self, buffer, buffer_pointer, end_point, nesting_level key = f"0x{buffer_pointer:x}-{'FVI'}-0x{firmware_volume_header.FvLength:x}" # construct key to store fv values result[key] = firmware_volume_header.dump_dict() # store result of fv header data in the dictionary fv_guid = firmware_volume_header.FileSystemGuid.guid # Get GUID of the FV + firmware_volume_extended_header = None if fv_guid in self.firmware_volume_guids: # parse only valid FV GUIDs if firmware_volume_header.ExtHeaderOffset: # recalculate fv header length if extended header offset @@ -296,8 +319,8 @@ def parse_firmware_volume(self, buffer, buffer_pointer, end_point, nesting_level # read extended header from buffer firmware_volume_extended_header = structure.EfiFirmwareVolumeExtHeader().read_from(buffer) # Override New Header Length - firmware_volume_header.HeaderLength = firmware_volume_header.ExtHeaderOffset + firmware_volume_extended_header.ExtHeaderSize - result[key]["HeaderLength"] = hex(firmware_volume_header.HeaderLength) # Override Header Length in dictionary + NewHeaderLength = firmware_volume_header.ExtHeaderOffset + firmware_volume_extended_header.ExtHeaderSize + result[key]["HeaderLength"] = hex(NewHeaderLength) result[key]["FvNameGuid"] = firmware_volume_extended_header.FvName.guid # add new unique fv guid key if self.guid_to_store: self.store_guid(dir_path=self.guid_store_dir, @@ -315,7 +338,24 @@ def parse_firmware_volume(self, buffer, buffer_pointer, end_point, nesting_level log.debug(f"Start: 0x{start:x} | END: 0x{end:x} | BIN_FILE_SIZE: 0x{end_point:x}") # Parse the file system according to the data_or_code = self.firmware_volume_guids.get(fv_guid) # get type of Filesystem by GUID - result[key][data_or_code.name] = data_or_code.method(buffer, start, end, nesting_level, is_compressed, bin_dir=fv_dir) + if not is_compressed and is_root_level: + Fv_TreeNode = tree_and_node.TreeNode(key) + Fv_TreeNode.Type = 'FV' + par_tree_node.InsertChildNode(Fv_TreeNode) + Fv_TreeNode.Data = tree_and_node.FvNode() + Fv_TreeNode.Data.Name = 'FV{}'.format(self.FvNum) + self.FvNum += 1 + Fv_TreeNode.Data.IsValid = True + Fv_TreeNode.Data.Header = firmware_volume_header + Fv_TreeNode.Data.ExtHeader = firmware_volume_extended_header + Fv_TreeNode.Data.InitExtEntry(buffer, buffer_pointer) + buffer.seek(start) + Fv_TreeNode.Data.Data = buffer.read(end-start) + buffer.seek(start) + Fv_TreeNode.Data.Info = data_or_code + result[key][data_or_code.name] = data_or_code.method(buffer, start, end, nesting_level, is_compressed, Fv_TreeNode, is_root_level, bin_dir=fv_dir) + else: + result[key][data_or_code.name] = data_or_code.method(buffer, start, end, nesting_level, is_compressed, bin_dir=fv_dir) else: # GUID is invalid or GUID for this parsing method not implemented yet err_msg = f"Parsing firmware volume for GUID: {fv_guid} is not implemented" @@ -336,15 +376,27 @@ def parse_firmware_volume(self, buffer, buffer_pointer, end_point, nesting_level else: break key = f"0x{invalid_fv_start:x}-{'InvalidFVI'}-0x{buffer_pointer - invalid_fv_start:x}" # construct key to store fv values + if not is_compressed and is_root_level: + Fv_TreeNode = tree_and_node.TreeNode(key) + Fv_TreeNode.Type = 'FV' + par_tree_node.InsertChildNode(Fv_TreeNode) + Fv_TreeNode.Data = tree_and_node.FvNode() + Fv_TreeNode.Data.Name = 'FV{}'.format(self.FvNum) + self.FvNum += 1 + Fv_TreeNode.Data.IsValid = False + Fv_TreeNode.Data.Header = firmware_volume_header + buffer.seek(invalid_fv_start) + Fv_TreeNode.Data.Data = buffer.read(buffer_pointer - invalid_fv_start) + buffer.seek(invalid_fv_start) result[key] = fv.dump_dict() log.debug(f"Invalid Signature of FV from 0x{invalid_fv_start:x} to: 0x{buffer_pointer:x}") if not is_sub_fv: if buffer_pointer == 0x2612000: pass - self.output.update(self.parse_firmware_volume(buffer, buffer_pointer, end_point=end_point, nesting_level=nesting_level, is_compressed=is_compressed)) + self.output.update(self.parse_firmware_volume(buffer, buffer_pointer, end_point=end_point, nesting_level=nesting_level, is_compressed=is_compressed, par_tree_node=par_tree_node, is_root_level=is_root_level)) return result - def parse_ffs(self, buffer, buffer_pointer, end_point, nesting_level, is_compressed, **kwargs): + def parse_ffs(self, buffer, buffer_pointer, end_point, nesting_level, is_compressed, par_tree_node=None, is_root_level=False, **kwargs): """Parse File System of type FFS1, FFS2, FFS3 :param buffer: buffer from where filesystem to be parsed @@ -391,6 +443,25 @@ def parse_ffs(self, buffer, buffer_pointer, end_point, nesting_level, is_compres start = align_buffer + ffs_data.cls_size # calculate start pointer of ffs data (first section) end = align_buffer + ffs_data.size # calculate location of next ffs + if not is_compressed and is_root_level: + Ffs_TreeNode = tree_and_node.TreeNode(key) + Ffs_TreeNode.Type = 'FFS' + par_tree_node.InsertChildNode(Ffs_TreeNode) + Ffs_TreeNode.Data = tree_and_node.FfsNode() + Ffs_TreeNode.Data.Header = ffs_data + Ffs_TreeNode.Data.Name = Ffs_TreeNode.Data.Header.Name + Ffs_TreeNode.Data.FfsType = ffs_type + if structure.struct2stream(Ffs_TreeNode.Data.Header).replace(b'\xff', b'') == b'': + Ffs_TreeNode.Type = 'FFS_FREE_SPACE' + buffer.seek(align_buffer) + Ffs_TreeNode.Data.Data = buffer.read(end_point - align_buffer) + buffer.seek(align_buffer) + par_tree_node.Data.Free_Space = end_point - align_buffer + else: + buffer.seek(start) + Ffs_TreeNode.Data.Data = buffer.read(end - start) + buffer.seek(start) + Ffs_TreeNode.Data.PadData = structure.get_pad_size(len(Ffs_TreeNode.Data.Data), structure.FFS_ALIGNMENT) * b'\xff' if self.guid_to_store: self.store_guid(dir_path=self.guid_store_dir, buffer=buffer, @@ -486,6 +557,7 @@ def parse_ffs_section(self, buffer, buffer_pointer, end_point, ffs_data, _type, end_point=end, nesting_level=nesting_level, is_compressed=is_compressed, + is_root_level=False, bin_dir=section_dir) if utils.SORT_FV: result[key]["FV"] = self.sort_output_fv(result[key]["FV"]) @@ -688,6 +760,154 @@ def write_result_to_file(self, file_path, **kwargs): log.info(f"File successfully stored at: {os.path.abspath(file_path)}") return True + def find_ffs_node(self, TargetFfsGuid): + self.binary_tree.FindNode(TargetFfsGuid, self.TargetFfsList) + + def dump_binary(self, outputfile_path): + with open(outputfile_path, 'wb') as outputfile: + outputfile.write(self.binary_tree.WholeTreeData) + + def Encapsulate_binary(self, BinaryTree): + if BinaryTree.Type == 'ROOT': + log.info('Encapsulating ROOT node!') + elif BinaryTree.Type == 'FV': + log.info('Encapsulation FV Node!') + if BinaryTree.Data.IsValid: + if BinaryTree.HasChild(): + self.binary_tree.WholeTreeData += structure.struct2stream(BinaryTree.Data.Header) + else: + self.binary_tree.WholeTreeData += structure.struct2stream(BinaryTree.Data.Header) + BinaryTree.Data.Data + else: + self.binary_tree.WholeTreeData += BinaryTree.Data.Data + elif BinaryTree.Type == 'FFS': + log.info(f"Encapsulation Ffs Node: {BinaryTree.Data.Header.Name}!") + if BinaryTree.Position.ParentNode.Type != 'ROOT': + self.binary_tree.WholeTreeData += structure.struct2stream(BinaryTree.Data.Header) + BinaryTree.Data.Data + BinaryTree.Data.PadData + else: + self.binary_tree.WholeTreeData += structure.struct2stream(BinaryTree.Data.Header) + BinaryTree.Data.Data + elif BinaryTree.Type == 'FFS_FREE_SPACE': + self.binary_tree.WholeTreeData += BinaryTree.Data.Data + for Child in BinaryTree.Position.ChildNodeList: + self.Encapsulate_binary(Child) + + def ModifyFvExtData(self, TargetFv): + FvExtData = b'' + if TargetFv.Data.Header.ExtHeaderOffset: + FvExtHeader = structure.struct2stream(TargetFv.Data.ExtHeader) + FvExtData += FvExtHeader + if TargetFv.Data.Header.ExtHeaderOffset and TargetFv.Data.ExtEntryExist: + FvExtEntry = structure.struct2stream(TargetFv.Data.ExtEntry) + FvExtData += FvExtEntry + if FvExtData: + InfoNode = TargetFv.Position.ChildNodeList[0] + InfoNode.Data.Data = FvExtData + InfoNode.Data.Data[TargetFv.Data.ExtHeader.ExtHeaderSize:] + InfoNode.Data.ModCheckSum() + + def UpdateFvData(self, TargetFv): + TargetFv.Data.Data = b'' + for item in TargetFv.Position.ChildNodeList: + if item.Type == 'FFS_FREE_SPACE': + TargetFv.Data.Data += item.Data.Data + else: + TargetFv.Data.Data += structure.struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + + def UpdateFvHeader(self, TargetFv): + TargetFv.Data.ModFvExt() + TargetFv.Data.ModFvSize() + TargetFv.Data.ModExtHeaderData() + self.ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + + def ReplaceFfs(self, NewFfs, TargetFfs): + TargetFv = TargetFfs.Position.ParentNode + TargetIndex = TargetFv.Position.ChildNodeList.index(TargetFfs) + FinalFfs = TargetFv.Position.ChildNodeList[-1] + + if TargetFv.Data.Header.Attributes & structure.EFI_FVB2_ERASE_POLARITY: + NewFfs.Data.Header.State = structure.ctypes.c_uint8(~NewFfs.Data.Header.State) + + if len(NewFfs.Data.Data) + len(NewFfs.Data.PadData) <= len(TargetFfs.Data.Data) + len(TargetFfs.Data.PadData): + Released_Space = len(TargetFfs.Data.Data) + len(TargetFfs.Data.PadData) - len(NewFfs.Data.Data) - len(NewFfs.Data.PadData) + if FinalFfs.Type == 'FFS_FREE_SPACE': + TargetFv.InsertChildNode(NewFfs, TargetIndex) + TargetFv.RemoveChildNode(TargetFfs) + FinalFfs.Data.Data += Released_Space * b'\xff' + TargetFv.Data.Free_Space = len(FinalFfs.Data.Data) + elif FinalFfs.Type == 'FFS': + NewFinalFfs = tree_and_node.TreeNode('FFS_FREE_SPACE') + NewFinalFfs.Type = 'FFS_FREE_SPACE' + NewFinalFfs.Data = tree_and_node.FfsNode() + NewFinalFfs.Data.Name = b'\xff' * 16 + NewFinalFfs.Data.Data = Released_Space * b'\xff' + TargetFv.Data.Free_Space = len(NewFinalFfs.Data.Data) + TargetFv.InsertChildNode(NewFfs, TargetIndex) + TargetFv.RemoveChildNode(TargetFfs) + TargetFv.InsertChildNode(NewFinalFfs) + elif len(NewFfs.Data.Data) + len(NewFfs.Data.PadData) > len(TargetFfs.Data.Data) + len(TargetFfs.Data.PadData): + Needed_Space = len(NewFfs.Data.Data) + len(NewFfs.Data.PadData) - len(TargetFfs.Data.Data) - len(TargetFfs.Data.PadData) + if FinalFfs.Type == 'FFS': + log.error('The FV does not have enough space to replace the ffs file!') + raise MemoryError + elif FinalFfs.Type == 'FFS_FREE_SPACE': + if Needed_Space > len(FinalFfs.Data.Data) + len(FinalFfs.Data.PadData): + log.error('The FV does not have enough space to replace the ffs file!') + raise MemoryError + elif Needed_Space == len(FinalFfs.Data.Data) + len(FinalFfs.Data.PadData): + TargetFv.InsertChildNode(NewFfs, TargetIndex) + TargetFv.RemoveChildNode(TargetFfs) + TargetFv.RemoveChildNode(FinalFfs) + TargetFv.Data.Free_Space = 0 + else: + FinalFfs.Data.Data = (len(FinalFfs.Data.Data) - Needed_Space) * b'\xff' + TargetFv.Data.Free_Space = len(FinalFfs.Data.Data) + TargetFv.InsertChildNode(NewFfs, TargetIndex) + TargetFv.RemoveChildNode(TargetFfs) + self.UpdateFvHeader(TargetFv) + self.UpdateFvData(TargetFv) + +parser = argparse.ArgumentParser(description=''' +Bios firmware Parser. +''') +parser.add_argument("-r", "--Replace", dest="Replace", nargs='+', + help="Replace a Ffs in a FV: '-r bios_image.bin driver_image.ffs replaced_image.bin") if __name__ == "__main__": print("Please run test suite for the testing") + bios_image = 'bios_image.bin' + driver_image = 'driver_image.ffs' + replaced_image = 'replaced_image.bin' + + args=parser.parse_args() + if args.Replace: + bios_image = args.Replace[0] + driver_image = args.Replace[1] + replaced_image = args.Replace[2] + + logger.log.info(f"{'=' * 50}\n>>>>>>>>> REPLACING DRIVER: {driver_image} <<<<<<<<<\n{'=' * 50}") + uefi_parser = UefiParser(bin_file=bios_image, # binary file to parse + parsing_level=0, # parsing level to manage number of parsing features + base_address=0, # (optional) provide base address of bios FV region to start the parsing (default 0x0) + guid_to_store=[] # if provided the guid for parsing then parser will look for every GUID in the bios image + ) + + newffs_parser = UefiParser(bin_file=driver_image, # binary file to parse + parsing_level=0, # parsing level to manage number of parsing features + base_address=0, # (optional) provide base address of bios FV region to start the parsing (default 0x0) + guid_to_store=[] # if provided the guid for parsing then parser will look for every GUID in the bios image + ) + + # parse bios image into a binary_tree + bios_output_dict = uefi_parser.parse_binary() + + # parse driver ffs image into a binary tree node + ffs_output_dict = newffs_parser.parse_binary() + # get the target ffs guid through ffs file, extract the target tree node + TargetFfsGuid = newffs_parser.binary_tree.Position.ChildNodeList[0].Data.Name + newffsnode = newffs_parser.binary_tree.Position.ChildNodeList[0] + + # replace the target ffs with new one + uefi_parser.find_ffs_node(TargetFfsGuid) + uefi_parser.ReplaceFfs(newffsnode, uefi_parser.TargetFfsList[0]) + uefi_parser.binary_tree.WholeTreeData = b'' + uefi_parser.Encapsulate_binary(uefi_parser.binary_tree) + uefi_parser.dump_binary(replaced_image) \ No newline at end of file diff --git a/src/xmlcli/common/compress.py b/src/xmlcli/common/compress.py index a24f9be..b7381dd 100644 --- a/src/xmlcli/common/compress.py +++ b/src/xmlcli/common/compress.py @@ -8,9 +8,9 @@ from datetime import datetime # Custom imports -from . import utils -from . import logger -from . import configurations +import utils +import logger +import configurations __author__ = "Gahan Saraiya" diff --git a/src/xmlcli/common/logger.py b/src/xmlcli/common/logger.py index c98c443..3afbf16 100644 --- a/src/xmlcli/common/logger.py +++ b/src/xmlcli/common/logger.py @@ -9,7 +9,7 @@ from datetime import datetime # Custom imports -from .configurations import XMLCLI_CONFIG, XMLCLI_DIR, OUT_DIR, PLATFORM, PY_VERSION, ENCODING, STATUS_CODE_RECORD_FILE +from configurations import XMLCLI_CONFIG, XMLCLI_DIR, OUT_DIR, PLATFORM, PY_VERSION, ENCODING, STATUS_CODE_RECORD_FILE ############################################################################### # START: LOG Settings ######################################################### diff --git a/src/xmlcli/common/structure.py b/src/xmlcli/common/structure.py index 0fe3193..c6846a9 100644 --- a/src/xmlcli/common/structure.py +++ b/src/xmlcli/common/structure.py @@ -5,14 +5,9 @@ from collections import namedtuple # custom imports -from . import utils -from .logger import log -try: - from ..restricted.structure_restricted import * -except ModuleNotFoundError as e: - DescriptorRegion = None - log.warn("You are working with limited set of feature which are not available with this version of XmlCli") - +import utils +from logger import log +DescriptorRegion = None __version__ = "0.0.1" __author__ = "Gahan Saraiya" @@ -54,6 +49,16 @@ def is_bios(bin_file): bios_header = EfiFirmwareVolumeHeader.read_from(bin_file) return FV_SIGNATURE == bios_header.Signature +def struct2stream(target_struct): + length = ctypes.sizeof(target_struct) + p = ctypes.cast(ctypes.pointer(target_struct), ctypes.POINTER(ctypes.c_char * length)) + return p.contents.raw + +def get_pad_size(size: int, alignment: int): + if size % alignment == 0: + return 0 + pad_Size = alignment - size % alignment + return pad_Size def read_structure(method, base_structure, buffer, buffer_pointer): """Read Valid structure @@ -171,6 +176,7 @@ def process_efi_cert_type_rsa2048_sha256_guid(buffer, buffer_pointer, section): # i.e. EFI_FVB2_ALIGNMENT_128 will be 0x00070000 EFI_FVB2_ALIGNMENT = lambda size=None: "0x001F0000" if not size else f"0x{size:0>4x}0000" EFI_FVB2_WEAK_ALIGNMENT = 0x80000000 +EFI_FVB2_ERASE_POLARITY = 0x00000800 class EfiTime(utils.StructureHelper): # 16 bytes @@ -527,6 +533,33 @@ def dump_dict(self): result.pop("Checksum") return result +def Refine_EfiFirmwareVolumeHeader(nums): + class EfiFirmwareVolumeHeader(utils.StructureHelper): + # source: Edk2/BaseTools/Source/C/Include/Common/PiFirmwareVolume.h + _fields_ = [ + ("ZeroVector", ctypes.ARRAY(ctypes.c_uint8, 16)), + ("FileSystemGuid", utils.Guid), # EFI_GUID + ("FvLength", ctypes.c_uint64), + ("Signature", ctypes.ARRAY(ctypes.c_char, 4)), # actually it's a signature of 4 characters... (UINT32 Signature) + ("Attributes", EFI_FVB_ATTRIBUTES_2), # UINT32 EFI_FVB_ATTRIBUTES_2 + ("HeaderLength", ctypes.c_uint16), + ("Checksum", ctypes.c_uint16), + ("ExtHeaderOffset", ctypes.c_uint16), + ("Reserved", ctypes.ARRAY(ctypes.c_uint8, 1)), + ("Revision", ctypes.c_uint8), + ("BlockMap", ctypes.ARRAY(EfiFvBlockMapEntry, nums)) # EFI_FV_BLOCK_MAP_ENTRY + ] + + @property + def get_guid(self): + return self.FileSystemGuid + + def dump_dict(self): + result = super(EfiFirmwareVolumeHeader, self).dump_dict() + result.pop("BlockMap") + result.pop("Checksum") + return result + return EfiFirmwareVolumeHeader class EfiFirmwareVolumeExtHeader(utils.StructureHelper): # source: Edk2/BaseTools/Source/C/Include/Common/PiFirmwareVolume.h @@ -555,6 +588,15 @@ class EfiFirmwareVolumeExtEntryOemType(utils.StructureHelper): ("TypeMask", ctypes.c_uint32) ] +def Refine_FV_EXT_ENTRY_OEM_TYPE_Header(nums: int): + class EfiFirmwareVolumeExtEntryOemType(utils.StructureHelper): + # source: Edk2/BaseTools/Source/C/Include/Common/PiFirmwareVolume.h + _fields_ = [ + ("Hdr", EfiFirmwareVolumeExtEntry), + ("TypeMask", ctypes.c_uint32), + ('Types', ctypes.ARRAY(utils.Guid, nums)) + ] + return EfiFirmwareVolumeExtEntryOemType() class EfiFirmwareVolumeExtEntryGuidType(utils.StructureHelper): # source: Edk2/BaseTools/Source/C/Include/Common/PiFirmwareVolume.h @@ -567,6 +609,20 @@ class EfiFirmwareVolumeExtEntryGuidType(utils.StructureHelper): def get_guid(self): return self.FormatType +def Refine_FV_EXT_ENTRY_GUID_TYPE_Header(nums: int): + class EfiFirmwareVolumeExtEntryGuidType(utils.StructureHelper): + # source: Edk2/BaseTools/Source/C/Include/Common/PiFirmwareVolume.h + _fields_ = [ + ("Hdr", EfiFirmwareVolumeExtEntry), + ("FormatType", utils.Guid), + ('Data', ctypes.ARRAY(ctypes.c_uint8, nums)) + ] + @property + + def get_guid(self): + return self.FormatType + + return EfiFirmwareVolumeExtEntryGuidType() class EfiFirmwareVolumeExtEntryUsedSizeType(utils.StructureHelper): # source: Edk2/BaseTools/Source/C/Include/Common/PiFirmwareVolume.h diff --git a/src/xmlcli/common/tree_and_node.py b/src/xmlcli/common/tree_and_node.py new file mode 100644 index 0000000..9564fdc --- /dev/null +++ b/src/xmlcli/common/tree_and_node.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- + +# Built-in imports +from collections import namedtuple + +# custom imports +import structure +from logger import log + + +__version__ = "0.0.1" +__author__ = "Christine Chen & Yuting2 Yang" + +class TreeStructure: + def __init__(self) -> None: + self.ParentNode = None + self.ChildNodeList = [] + +class TreeNode: + def __init__(self, Key) -> None: + self.Key = Key + self.Type = None + self.Data = b'' + self.WholeTreeData = b'' + self.Position = TreeStructure() + + def HasChild(self): + if self.Position.ChildNodeList != []: + return True + else: + return False + + def InsertChildNode(self, NewChild, order=None): + if order is not None: + self.Position.ChildNodeList.insert(order, NewChild) + NewChild.Position.ParentNode = self + else: + self.Position.ChildNodeList.append(NewChild) + NewChild.Position.ParentNode = self + + def RemoveChildNode(self, RemoveChild): + self.Position.ChildNodeList.remove(RemoveChild) + + def FindNode(self, key, findlist) -> None: + if self.Type == 'FFS' and self.Data.Name.guid==key.guid: + findlist.append(self) + for item in self.Position.ChildNodeList: + item.FindNode(key, findlist) + +class FvNode: + def __init__(self): + self.Name = '' + self.Header = None + self.ExtHeader = None + self.Data = b'' + self.Info = None + self.IsValid = None + self.Free_Space = 0 + + def InitExtEntry(self, buffer, buffer_pointer): + self.ExtEntryOffset = self.Header.ExtHeaderOffset + 20 + buffer.seek(buffer_pointer+self.ExtEntryOffset) + if self.ExtHeader.ExtHeaderSize != 20: + self.ExtEntryExist = 1 + self.ExtEntry = structure.EfiFirmwareVolumeExtEntry.read_from(buffer) + self.ExtTypeExist = 1 + if self.ExtEntry.ExtEntryType == 0x01: + nums = (self.ExtEntry.ExtEntrySize - 8) // 16 + self.ExtEntry = structure.Refine_FV_EXT_ENTRY_OEM_TYPE_Header(nums).read_from(buffer) + elif self.ExtEntry.ExtEntryType == 0x02: + nums = self.ExtEntry.ExtEntrySize - 20 + self.ExtEntry = structure.Refine_FV_EXT_ENTRY_GUID_TYPE_Header(nums).read_from(buffer) + elif self.ExtEntry.ExtEntryType == 0x03: + self.ExtEntry = structure.EfiFirmwareVolumeExtEntryUsedSizeType.read_from(buffer) + else: + self.ExtTypeExist = 0 + else: + self.ExtEntryExist = 0 + + def ModCheckSum(self): + # Fv Header Sums to 0. + Header = structure.struct2stream(self.Header)[::-1] + Size = self.Header.HeaderLength // 2 + Sum = 0 + for i in range(Size): + Sum += int(Header[i*2: i*2 + 2].hex(), 16) + if Sum & 0xffff: + self.Header.Checksum = 0x10000 - (Sum - self.Header.Checksum) % 0x10000 + + def ModFvExt(self): + # If used space changes and self.ExtEntry.UsedSize exists, self.ExtEntry.UsedSize need to be changed. + if self.Header.ExtHeaderOffset and self.ExtEntryExist and self.ExtTypeExist and self.ExtEntry.Hdr.ExtEntryType == 0x03: + self.ExtEntry.UsedSize = self.Header.FvLength - self.Free_Space + + def ModFvSize(self): + # If Fv Size changed, self.Header.FvLength and self.Header.BlockMap[i].NumBlocks need to be changed. + BlockMapNum = len(self.Header.BlockMap) + for i in range(BlockMapNum): + if self.Header.BlockMap[i].Length: + self.Header.BlockMap[i].NumBlocks = self.Header.FvLength // self.Header.BlockMap[i].Length + + def ModExtHeaderData(self): + if self.Header.ExtHeaderOffset: + ExtHeaderData = structure.struct2stream(self.ExtHeader) + ExtHeaderDataOffset = self.Header.ExtHeaderOffset - self.Header.HeaderLength + self.Data = self.Data[:ExtHeaderDataOffset] + ExtHeaderData + self.Data[ExtHeaderDataOffset+20:] + if self.Header.ExtHeaderOffset and self.ExtEntryExist: + ExtHeaderEntryData = structure.struct2stream(self.ExtEntry) + ExtHeaderEntryDataOffset = self.Header.ExtHeaderOffset + 20 - self.HeaderLength + self.Data = self.Data[:ExtHeaderEntryDataOffset] + ExtHeaderEntryData + self.Data[ExtHeaderEntryDataOffset+len(ExtHeaderEntryData):] + + +class FfsNode: + def __init__(self): + self.Name = '' + self.Header = None + self.Data = b'' + self.PadData = b'' + self.Info = None + self.FfsType = None + + def ModCheckSum(self) -> None: + HeaderData = structure.struct2stream(self.Header) + HeaderSum = 0 + for item in HeaderData: + HeaderSum += item + HeaderSum -= self.Header.State + HeaderSum -= self.Header.IntegrityCheck.Checksum.File + if HeaderSum & 0xff: + Header = self.Header.IntegrityCheck.Checksum.Header + 0x100 - HeaderSum % 0x100 + self.Header.IntegrityCheck.Checksum.Header = Header % 0x100 diff --git a/src/xmlcli/common/utils.py b/src/xmlcli/common/utils.py index 04156c1..600e830 100644 --- a/src/xmlcli/common/utils.py +++ b/src/xmlcli/common/utils.py @@ -19,8 +19,8 @@ from collections import OrderedDict # Custom imports -from .logger import log -from .configurations import XMLCLI_CONFIG, ENCODING, XMLCLI_DIR, OUT_DIR, PY3, STATUS_CODE_RECORD_FILE +from logger import log +from configurations import XMLCLI_CONFIG, ENCODING, XMLCLI_DIR, OUT_DIR, PY3, STATUS_CODE_RECORD_FILE try: from defusedxml import ElementTree as ET diff --git a/tests/UefiParserTest.py b/tests/UefiParserTest.py index 13022a8..a1124fd 100644 --- a/tests/UefiParserTest.py +++ b/tests/UefiParserTest.py @@ -96,6 +96,9 @@ def test_parse_multiple_binaries(self): def test_write_to_json(self): self.parse_image(self.bios_image) + def test_replace_driver(self): + self.replace_ffs(self.bios_image, self.new_driver_file, self.replaced_image_file) + def parse_image(self, bios_image): self.log.info(f"{'=' * 50}\n>>>>>>>>> PROCESSING IMAGE: {bios_image} <<<<<<<<<\n{'=' * 50}") binary_file_name = os.path.splitext(bios_image)[0] # get filename without extension @@ -127,6 +130,35 @@ def parse_image(self, bios_image): # Validate whether content written in json or not self.assertGreater(os.path.getsize(user_guid_out_file), 0) + def replace_ffs(self, bios_image, driver_image, replaced_image): + self.log.info(f"{'=' * 50}\n>>>>>>>>> REPLACING DRIVER: {driver_image} <<<<<<<<<\n{'=' * 50}") + uefi_parser = bios_fw_parser.UefiParser(bin_file=bios_image, # binary file to parse + parsing_level=0, # parsing level to manage number of parsing features + base_address=0, # (optional) provide base address of bios FV region to start the parsing (default 0x0) + guid_to_store=[] # if provided the guid for parsing then parser will look for every GUID in the bios image + ) + + newffs_parser = bios_fw_parser.UefiParser(bin_file=driver_image, # binary file to parse + parsing_level=0, # parsing level to manage number of parsing features + base_address=0, # (optional) provide base address of bios FV region to start the parsing (default 0x0) + guid_to_store=[] # if provided the guid for parsing then parser will look for every GUID in the bios image + ) + + # parse bios image into a binary_tree + bios_output_dict = uefi_parser.parse_binary() + + # parse driver ffs image into a binary tree node + ffs_output_dict = newffs_parser.parse_binary() + # get the target ffs guid through ffs file, extract the target tree node + TargetFfsGuid = newffs_parser.binary_tree.Position.ChildNodeList[0].Data.Name + newffsnode = newffs_parser.binary_tree.Position.ChildNodeList[0] + + # replace the target ffs with new one + uefi_parser.find_ffs_node(TargetFfsGuid) + uefi_parser.ReplaceFfs(newffsnode, uefi_parser.TargetFfsList[0]) + uefi_parser.binary_tree.WholeTreeData = b'' + uefi_parser.Encapsulate_binary(uefi_parser.binary_tree) + uefi_parser.dump_binary(replaced_image) class OfflineTest(UefiParserTest): @property diff --git a/tests/UnitTestHelper.py b/tests/UnitTestHelper.py index 52451fc..79e104e 100644 --- a/tests/UnitTestHelper.py +++ b/tests/UnitTestHelper.py @@ -32,6 +32,8 @@ ACCESS_METHOD = TEST_SUITE_CONFIG.get("TEST_SETTINGS", "ACCESS_METHOD") RUN_OPTIONAL_TEST = TEST_SUITE_CONFIG.getboolean("TEST_SETTINGS", "RUN_OPTIONAL_TEST") BIOS_IMAGES_DIR = os.path.abspath(TEST_SUITE_CONFIG.get("TEST_SETTINGS", "BIOS_IMAGES_DIR")) +NEW_DRIVER_FILE = os.path.abspath(TEST_SUITE_CONFIG.get("TEST_SETTINGS", "NEW_DRIVER_FILE")) +REPLACED_IMAGE_FILE = os.path.abspath(TEST_SUITE_CONFIG.get("TEST_SETTINGS", "REPLACED_IMAGE_FILE")) LITE_FEATURE_TESTING = TEST_SUITE_CONFIG.getboolean("TEST_SETTINGS", "LITE_FEATURE_TESTING") LOG_TITLE = TEST_SUITE_CONFIG.get("LOG_SETTINGS", "LOGGER_TITLE") @@ -88,6 +90,14 @@ def access_method(self): def bios_image_dir(self): return BIOS_IMAGES_DIR + @property + def new_driver_file(self): + return NEW_DRIVER_FILE + + @property + def replaced_image_file(self): + return REPLACED_IMAGE_FILE + @property def bios_image(self): return self.bios_roms[0] diff --git a/tests/tests.config b/tests/tests.config index de579f2..1c42798 100644 --- a/tests/tests.config +++ b/tests/tests.config @@ -12,6 +12,8 @@ ACCESS_METHOD = linux BIOS_IMAGES_DIR = /bios_images RUN_OPTIONAL_TEST = False LITE_FEATURE_TESTING = True +NEW_DRIVER_FILE = new_driver.ffs +REPLACED_IMAGE_FILE = replaced_image.rom [LOG_SETTINGS] # Set logging title which is to be displayed in log file