Chip Security Testing 
Binary Security Analysis 
Resources 
Blog
Contact us
Back to all articles
Chip Security

MCUBoot Under (good) Pressure | Part 2: From the too-much to the achievable

11 min read
Edit by Guillaume Vinet • Jun 12, 2023
Share

Part 1 | Part 2 | Part 3


 

We keep going on with our blog post about assessing the strength of MCUBoot against fault injection attacks thanks to simulation. In the previous blog post, we presented MCUBoot, and described an attack scenario intending to make it execute arbitrary code.

In this blog post, we are going to present how to compile MCUBoot, then how to execute it with our simulation tool, esReverse, and ultimately how we will attack MCUBoot.

 

MCUBoot code generation

First thing first, we need to choose the MCUBoot code version that we wish to attack and compile it. The straightforward way is to start from the ARM Trusted Firmware-M (ARM TF-M) project. ARM TF-M is a reference implementation of the Platform Security Architecture (PSA) IoT Security Framework for ARMv7-M and Armv8-M architecture where the default secure bootloader is MCUBoot.

The Getting Starting Guides documentation explains step-by-step how to download and compile ARM TF-M, it is therefore the perfect solution to have both a MCUBoot binary code, and as well a signed image to feed it.

Let us skip the installation of the ARM TF-M compilation (python dependency and toolchain) which is well explained in the documentation, and directly jump to the code downloading where we take the latest tagged version TF-Mv1.7.0 (when this post was written). It contains the version 1.9.0 of MCUBoot code. Then, we compile the project with the cmake tool in two steps.

In the first step, the compilation environment is prepared, and customized with preprocessor directives.

cd trusted-firmware-m/ cmake -S . \ -B cmake_build \ -DMCUBOOT_LOG_LEVEL=OFF \ -DTFM_SPM_LOG_LEVEL=TFM_SPM_LOG_LEVEL_INFO \ -DTFM_PLATFORM=arm/mps2/an521 \ -DTFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake \ -DTEST_NS=ON \ -DTEST_S=ON \ -DTFM_PSA_API=ON \ -DMCUBOOT_FIH_PROFILE=OFF \ -DBL2=ON

Let us review some of these directives:

  • MCUBOOT_LOG_LEVEL and TFM_SPM_LOG_LEVEL configure the level of logging for MCUBoot or ARM TF-M.
  • MCUBOOT_FIH_PROFILE. ARM has embedded, in MCUBoot, protections against fault injection attacks with different security levels: OFF, LOW, MEDIUM and HIGH. These protections will be a topic of the next blog post, but for this one we will use the profile without protection: OFF.
  • TFM_TOOLCHAIN_FILE indicates the compilation toolchain. We use gcc-arm-none-eabi-10.3-2021.10.
  • TFM_PLATFORM indicates the targeted hardware platform to compile the code. We choose the ARM MPS2+ AN521 board since it is among the ones supported by esReverse.
  • TEST_NS and TEST_S enable building an image containing both secure and non-secure regression tests; it fits our needs.

Then, the code is compiled.

cmake --build cmake_build -- install

Once this process is finished, we get the compiled files inside ./trusted-firmware-m/cmake_build/bin directory. The more valuable file are:

  • The MCUBoot bootloader code (bl2.axf), which is our target for the fault injection.
  • tfm_s_ns_signed.bin file corresponds to the full image that will be checked by MCUBoot.

Now, let us present our emulation tool: esReverse.

 

esReverse

esReverse is a tool providing a way to emulate your code running in a hardware device and subsequently perform dynamic analyses of the firmware. Close to the hardware device functional behavior, the esReverse’s emulation environment gives access to runtime data (registers, memory) for different architectures (ARM, Intel, RISC-V). It provides on one hand the capability to trace the firmware execution to generate Side Channel traces (with customized models), and on the other hand, to stress it against physical code injection, under user defined models. Additionally, it facilitates code profiling or fuzzing.

To simplify its usage, it can emulate different types of targets; a piece of code, a Linux ELF executable or a full system on a chip (SoC). A Python library and graphical widgets are provided to make it user-friendly. Finally, it is compatible with GDB to ease investigation.

The pieces of the puzzle are here, now we can enter in the emulation part.

 

MCUBoot code emulation with esReverse

Consistently with the build of MCUBoot and the target platform, setting esReverse is straightforward:

import esReverse target = esReverse.TargetRawCode() target.board = "mps2-an521"

Then, we indicate the ELF file containing the MCU-Boot code with the kernel option. Automatically, each piece of code will be copied in the right memory area.

target.kernel = "./trusted-firmware-m/cmake_build/bin/bl2.axf"

After that, let us indicate when to start and stop the code emulation with the Program Counter (PC):

  • We set the PC start value to -1 to indicate that we will start automatically at the start address indicated in the bl2.axf file,
  • We will not stop the emulation when a specific PC is reached, so we set the PC end value to -1 as well.
target.pc_start = -1 target.pc_end = -1

esReverse offers a nice option called timeout_inst to enable a timeout measured in terms of executed instructions. In our case, we just chose this option to stop the program, so arbitrarily, we set a big value. But, in the context of security testing (with fault injection in particular), it is a good practice to set this option with the intended to handle endless loops.

target.timeout_inst = 0x8F00000

In order to set or retrieve data, two functions (process_input and process_output) serve as an interface to read or write data in specific memory addresses. In our case, no specific input is required, and the received output is collected as such from the emulation.

target.process_input = lambda data : [] target.process_output = lambda data : data

We have loaded the MCUBoot code, but the image containing the secure and non-secure OS is still missing in FLASH. The FLASH is emulated by creating a memory segment located at the address 0x10080000 and its size is 0x1000000.

target.mem_map = [{'address': 0x10080000, 'size': 0x100000}]

Then, we indicate the image file to copy its content at the address 0x10080000.

target.mem_set = [{'address': 0x10080000, 'filename': './trusted-firmware-m/cmake_build/bin/tfm_s_ns_signed.bin'}]

Everything is ready to launch the code:

print(target.run(""))

We obtain the expected output as you can see in the snippet below.

[WRN] TFM_DUMMY_PROVISIONING is not suitable for production! This device is NOT SECURE [Sec Thread] Secure image initializing! Booting TF-M v1.7.0 #### Execute test suites for the Secure area #### Running Test Suite SFN Backend Secure test (TFM_S_SFN_TEST_1XXX)... > Executing 'TFM_S_SFN_TEST_1001' Description: 'Get PSA framework version' TEST: TFM_S_SFN_TEST_1001 - PASSED! > Executing 'TFM_S_SFN_TEST_1002' Description: 'Get version of an RoT Service' TEST: TFM_S_SFN_TEST_1002 - PASSED! > Executing 'TFM_S_SFN_TEST_1003' Description: 'Request a connection-based RoT Service' TEST: TFM_S_SFN_TEST_1003 - PASSED! > Executing 'TFM_S_SFN_TEST_1004' Description: 'Request a stateless RoT Service' TEST: TFM_S_SFN_TEST_1004 - PASSED!

With the Tracer we can get a trace of the PC from the execution:

tracer = esReverse.Tracer(target) tracer.targets = ["pc"] tracer.generate_traces([""])

We obtain a trace file where each PC value is a 32 bit integer. Below, we display it with annotations to have a better understanding of the code execution.

  • The x-axis represents the instruction number: in fact, it is the temporal axis,
  • The y-axis represents the value of the PC register.

trace_1-2.png Figure 1: Firmware verification by MCUBoot - PC register trace.

We observe two main items: the authentication of the non-secure OS image, and then the secure-OS image. The second iteration is slightly bigger than the first one: it is perfectly understandable since the size of the secure image is bigger.

The same operations are performed for both images, so let zoom on the first authentication to have a better view:

  • First, the purple area corresponds to the computation of the SHA-256 of the image. This hash is used to check the signature,
  • Then, the orange area corresponds to the modular exponentiation of the area containing the RSA signature of the image corresponding to the public key stored within MCUBoot code area. The obtained result is name encoded message,
  • Finally, MCUBoot checks the consistency of the encoded message with the hash generated previously.

trace_2.png Figure 2: First image verification by MCUBoot - PC register trace.

We managed to emulate MCUBoot code in a few lines with esReverse and performed a quick graphical analysis of the code execution. In the previous blog post, I have pointed out two attack scenarios. But, for the sake of simplicity, I will focus here on the second one. Just recall the objective of this scenario: to put an arbitrary code inside the image and execute it. Because we change the image, the code signature will be inconsistent, and so I need to disturb the code authentication to get it accepted as genuine.

 

The Attack

To create an altered image, it is as easy as pie; we just need to modify the signature of the genuine code by changing arbitrarily one byte.

The consequence is that the signature will be rejected. As described in the part 1 "Walk around the code", the verification will fail inside the main function. More precisely, according to the result of the comparison, stored in the fih_rc variable, we will enter in an endless loop as seen in the code snippet below.

// bl2_main.c:154-158 if (fih_not_eq(fih_rc, FIH_SUCCESS)) { BOOT_LOG_ERR("Unable to find bootable image"); FIH_PANIC; }

If I perform a small reverse engineering of the bl2.axf file, I see that the if statement is implemented in two instructions:

  • First, at address 0x10000604, the register r3 contains the value coding the comparison success (0), which is compared to fih_rc value(stored in the register r0).
  • Then, at address 0x10000606, the equality is tested, and if the two values are not equal, the processing enters an endless loop (0x10000608), otherwise it jumps to continue the nominal program execution.
.text:10000604 CMP R3, fih_rc .text:10000606 BEQ loc_1000060A .text:10000608 B loc_10000608

We need a way to check that the attack is successful. According to the code, we can stop the emulation at the address 0x10000606 and check the value of the register r0. If it is equal to zero, the attack is considered as successful since we do not enter in the endless loop.

It is useless to start tampering with the program as soon as it starts. In accordance with the code analysis that we made in the previous post, we can start to disturb it when the bootutil_img_validate function is called, corresponding to the address 0x10001648. We update our python script to take these changes into account.

tracer.trig_and_acts = [[0x10001648, 1, "start-trace"]]

We indicate when to stop the emulation.

target.pc_end = 0x10000606

And we ask to display the r0 register at the end of the emulation.

target.ending_display = [{'register': "r0"}]

We generate a new trace and display its length:

tracer.generate_traces([""]) pc_full_np = np.fromfile("./full_trace_pc/_0x1.bin", dtype='uint32') print(len(pc_full_np))

There are more than 16,000,000 executed instructions, and so the same number of instructions to tamper with! This is the classic problem of simulation which is the combinatorial explosion due to the number of different parameters to test, in particular the instructions where to inject the fault. Moreover, the complexity is also increased by the number of input parameters, and the possible fault models.

We can tackle this issue in two ways:

  • First approach, by taking advantage of big computational capabilities or accepting long duration campaigns; and so, just execute this huge campaign,
  • Another approach is to restrict the faulting area to decrease the number of faulted instructions, by identifying points of interest.

We made the choice of the former approach. We analyze the PC distribution of the executed instructions. Perturbing the cryptographic functions to obtain our expected results (code and signature consistent) is complex. Since the memset and memcpy are the majority of called functions, let us remove them from the scope and see what we can get. So, in our case, we can remove from the scope of the faulted instructions corresponding to:

  • cryptography related functions related to mbedtls or mpi_montmul,
  • memset, memcpy functions.

With esReverse, we can easily trace only a specific list of instructions according to their PC values. With a small reverse engineering of the binary, we create the list of the PC to acquire while dropping the instructions related to the functions listed previously. We name this list pc_to_trace, and give it to esReverse tracer.

tracer.filt_pre.in_pc_.pts = pc_to_trace

We regenerate the trace, and display its size.

tracer.generate_traces([""]) pc_full_np = np.fromfile("./full_trace_pc/_0x1.bin", dtype='uint32') print(len(pc_full_np))

Now, we render with only 66,000 instructions. With such a number of instructions to be faulted, a fault campaign became realistic, and will be the topic of our next post where we will attack MCUBoot implementation!

 

Thanks to Pylou for proofreading this article.

esReverse Release-02.png

Share

Categories

All articles
(102)
Binary Analysis
(57)
Chip Security
(40)
Corporate News
(15)
Expert Review
(5)
Time Travel Analysis
(13)

you might also be interested in

Chip Security
Binary Analysis

"Shifting left" secures PQC implementations from physical attacks

13 min read
Edit by Hugues Thiebeauld • Jun 20, 2025
CopyRights eShard 2025.
All rights reserved
Privacy policy | Legal Notice
CHIP SECURITY
esDynamicExpertise ModulesInfraestructureLab Equipments