diff --git a/docs/user_guide/uefi_binary_parsing.md b/docs/user_guide/uefi_binary_parsing.md index f6e6df2..2305cb9 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. Working with SUT: @@ -44,3 +45,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 ef6211e..e8c8389 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" @@ -121,6 +125,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 {} @@ -248,11 +260,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) @@ -288,6 +303,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}") @@ -295,6 +317,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 @@ -302,8 +325,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, @@ -321,7 +344,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" @@ -342,15 +382,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 @@ -397,6 +449,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, @@ -496,6 +567,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"]) @@ -721,6 +793,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 481b34a..94aab45 100644 --- a/src/xmlcli/common/structure.py +++ b/src/xmlcli/common/structure.py @@ -12,7 +12,6 @@ except ModuleNotFoundError as e: DescriptorRegion = None - __version__ = "0.0.1" __author__ = "Gahan Saraiya" @@ -53,6 +52,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 @@ -170,6 +179,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 @@ -526,6 +536,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 @@ -554,6 +591,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 @@ -566,6 +612,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 e6767db..34f66d4 100644 --- a/src/xmlcli/common/utils.py +++ b/src/xmlcli/common/utils.py @@ -20,8 +20,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 9885ae5..600d12f 100644 --- a/tests/UefiParserTest.py +++ b/tests/UefiParserTest.py @@ -103,6 +103,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 @@ -134,6 +137,36 @@ 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) + def test_compare_with_old(self): for bios_image in self.bios_roms: with open(bios_image, 'rb') as BiosBinFile: diff --git a/tests/UnitTestHelper.py b/tests/UnitTestHelper.py index 52476da..c31b7ab 100644 --- a/tests/UnitTestHelper.py +++ b/tests/UnitTestHelper.py @@ -33,6 +33,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") @@ -89,6 +91,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 a7b13b8..b1eeeec 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 [KNOB_TESTING] SKIP_RANDOM_KNOB_TESTING = False