Security Hardening of U-Boot
This is a detailed description of the security hardening modifications to U-Boot carried out by Toradex. They are part of our Secure Boot implementation whose main article is Secure Boot on Torizon OS. The acronyms used here are defined in that article.
Introduction
U-Boot stands as a robust bootloader with widespread adoption in embedded systems and it is used by Toradex both in its Torizon OS and BSP reference images.
In implementing Secure Boot, Toradex has undertaken a series of modifications to U-Boot, enhancing its security features and thereby fortifying the overall security of the system. In this article, we explain what these security hardening modifications are, why they were done and how they work overall.
Let us start by answering some fundamental questions.
Why does U-Boot need security hardening after all?
The Secure Boot solution provided by Toradex is based on a Chain of Trust (CoT) system, initiated by the ROM code loading the bootloader from a storage device, validating it (checking integrity and authenticity), and executing it. This initial process corresponds to the first link in the CoT. It's worth noting that the details of this boot step are mostly hard-coded into the system.
Once the bootloader is executed, it proceeds with its primary function, which, in concept, mirrors what the ROM code does. It loads the kernel from storage, specifically a binary file that bundles the kernel and related artifacts, validates, and executes it, establishing the second link in the CoT. In the case of U-Boot, the details of this second step are controlled by a script that it executes. There is a wide range of commands available to the script, providing flexibility to adapt to various booting scenarios without requiring changes to the bootloader code itself. While this flexibility is advantageous, it also presents multiple avenues for attackers to compromise the second link in the CoT.
Through hardening modifications, Toradex aims to prevent such compromises, ensuring that the system remains secure even if an attacker manages to modify or inject changes into the boot script.
Why are we following this approach?
In light of the above, one might wonder: why not simply hard code the boot script and eliminate the concern entirely? While hard-coding the boot script proves effective in certain scenarios, it introduces limitations, especially in situations where the operating system accepts updates (as does Torizon OS). This approach impedes the ability to modify the boot logic upon such updates. For systems based on OSTree, like Torizon OS, the integration between U-Boot and the OS relies on a boot script that is dynamically generated at deployment-time. Hard-coding would complicate this process as well. Therefore, it is important to recognize that Toradex's choice to pursue the hardening path is a deliberate design decision, taking into account various system constraints.
What are the goals of the hardening?
Overall the hardening modifications intend to:
- Prevent modification to the running software: since an attacker might use U-Boot commands to modify the running U-Boot itself and thereby try to bypass any other protections.
- Prevent execution of unsigned software: to ensure only authentic and trustworthy software can be executed.
- Prevent injection of kernel arguments: since the kernel command line can heavily influence the kernel behavior, we want to make sure that this piece of information that is passed from the bootloader to the kernel is also authentic.
In spite of the constraints the modifications may add, we still want to:
- Keep most of the flexibility generally provided by U-Boot: at least until the device is in the production line. We do this by implementing different behaviors depending on whether the device is in an open or closed state with regards to the underlying Secure Boot technology (HAB or AHAB, in case of the i.MX-based SoMs).
How does the hardening add value to the solution?
In terms of security, a system based on a CoT is as strong as its weakest link. Thus, making all links strong is always important.
The hardening of U-Boot deals with the second link of the chain, one that could be easily broken under special circumstances, e.g.:
- If an attacker gains physical access to the device, they could employ specialized hardware to modify the contents of the flash storage where the U-Boot environment variables (including part of the boot script) reside or they could change parts of the boot script living in the filesystem.
- Similarly, if an attacker has physical access to the device and the U-Boot Command Line Interface (CLI) is accessible through a serial port, they could potentially execute the same modifications.
- The same risk exists if an attacker successfully exploits a system vulnerability and acquires root shell access to the system; this case would not necessarily involve physical access to the device though.
In our threat assessment, we assume these attacks could happen (actually, there is nothing the software can do to prevent the hardware attacks) and we seek to prevent further damage leading from them. Thus, thanks to the hardening, even if these attacks were performed, the boot script would remain restricted to initiating the execution of a genuine kernel with authentic kernel arguments so that the CoT would remain intact. That implies an attacker would be thwarted from executing their tampered-with kernel, which could otherwise be employed to carry out various malicious activities.
Hardening features introduced by Toradex
To safeguard the second link of the CoT, we identified the need for the following distinct types of hardening:
- Command whitelisting: is responsible for allowing the sole execution of bootloader commands that are deemed safe, where "safe" means the command cannot execute code or change the running U-Boot code.
- Exclusive signed software execution: for strictly-required commands that execute binary code, this allows only code-paths where validated signed software can be executed.
- Self-overwriting protection: since code needs to be loaded into RAM before being executed, this part of the hardening would strive to ensure the running U-Boot code (that is also in RAM) does not get overwritten by the various loading commands.
- CLI access prevention: optionally disables the U-Boot CLI to help improve security by eliminating an attack vector.
- Kernel command-line protection: this prevents changes to the
bootargs
variable which is used for passing the command line arguments from the bootloader to the kernel.
We implemented all of those protections as part of our security Yocto/OpenEmbedded meta layer: meta-toradex-security.
Let us learn a bit more about each one of them.
Command whitelisting
The U-Boot build system offers the capability to enable or disable commands during the build process, determining their availability at runtime. While booting the system typically necessitates a limited set of commands, Toradex, in the interest of providing flexibility, often leaves numerous commands (over 200) accessible. This extensive range serves to assist customers in hardware debugging or conducting tests during production. While beneficial, it introduces some security concerns, as numerous commands could potentially be exploited to compromise the integrity of the CoT link.
To solve the problem while keeping flexibility, we introduce the "command whitelisting" feature which allows or denies command execution based on the state of the device with regards to the underlying Secure Boot technology (for NXP-based SoMs, this means HAB or AHAB). Normally, if the device is open most commands are allowed whereas if closed only a small set of them are permitted (basically, those required for booting the device plus a few considered useful and safe). This is the default configuration and we expect it to work without customization for most users, but it can be configured when necessary.
In fact, the feature provides both a whitelist and a blacklist, with the latter having a higher priority over the former. One can specify those lists independently for when the device is in open or closed state. The lists are kept inside U-Boot's control device-tree blob (also known as "control DTB" or "control FDT") and their entries identify command categories rather than individual commands; each command was assigned one or more categories. Presently, the lists can be modified only at build-time in Yocto but the reason for storing the information within the control DTB is to facilitate its modification through dedicated tooling in the future.
Here is what the default control DTB looks like (with some explanatory comments):
/ {
chosen {
toradex,secure-boot { /* if not present: disable Toradex hardening */
disabled; /* optional: disable Toradex hardening */
bootloader-commands {
allow-open = <CMD_CAT_ALL>;
deny-open = <CMD_CAT_ALL_UNSAFE>; /* optional, discouraged */
allow-closed = <CMD_CAT_NEEDED CMD_CAT_SAFE>;
deny-closed = <CMD_CAT_ALL_UNSAFE>; /* optional, discouraged */
};
};
};
};
This should be straightforward to read. For example, we see that when the device is open, all
commands are permitted (as defined by allow-open
), except those deemed unsafe (as set by
deny-open
).
Exclusive signed software execution (bootm
protection)
With the command whitelisting we can prevent the execution of almost all commands that execute
binary code. However, we need at least one of such execution commands, bootm
, to boot the
authentic OS. This command is very general in that it is capable of booting legacy U-Boot images,
Android boot images and FIT images. When hardening is enabled (as defined by the control DTB) we try
to ensure only FIT images signed with the proper keys can be booted.
A FIT image usually contains multiple components (or sub-images) in it, such as the kernel, the
initial ramdisk, device-trees and device-tree overlays. It may also contain nodes describing
configurations which refer to those sub-images. When building for Secure Boot (in particular with
UBOOT_SIGN_ENABLE
set to 1), these configurations are signed at build-time; this is good because
the signature covers the configuration plus the actual images. At runtime the boot scripts load the
FIT image by specifying one of these configurations which is achieved by employing a special
notation with the bootm
command:
bootm [fit_addr]#<conf>[#extra-conf]
As part of the hardening, we also restrict the utilization of alternative notations offered by bootm
(refer to bootm.rst for more details).
This precaution is crucial, as certain notations do not mandate signature checks,
at least in versions of U-Boot in use by Toradex at the time of this writing. That is the case, for
example, of the direct sub-image notations below (which are all denied):
bootm [<addr1>]:<subimg1>
bootm [<addr1>]:<subimg1> [<addr2>]:<subimg2>
bootm [<addr1>]:<subimg1> [<addr2>]:<subimg2> [<addr3>]:<subimg3>
Self-overwriting protection
Some U-Boot commands can write to arbitrary RAM addresses and that property makes them unsafe
because they could be employed by an attacker to overwrite U-Boot itself that is running in
RAM. Because of that, when the hardening is enabled, we prevent the use of most of those commands
when the device is in closed state (via whitelisting). However, some commands that write to RAM are
actually required by the boot process, such as those capable of loading files from the filesystem
(e.g. load
, fatload
and ext4load
). In such cases, the commands must have extra protections to
prevent overwriting of the running U-Boot. Fortunately, U-Boot already has protections against
overwriting the so-called reserved memory areas (where the U-Boot code and data reside at runtime)
at least for the commands currently required by our boot scripts. So, in our patches we only ensure
that the appropriate Kconfig
option is set so that the protections are in place.
CLI access prevention
With this feature, access to the U-Boot CLI is disabled when the device is in closed state. This is not strictly needed to guarantee the security of the system (thanks to the other hardening features) but it is helpful in that it eliminates a vector of attack. Moreover, since it only takes place when the device is in closed state, the feature is not expected to cause any difficulties to customers with their devices in the field and because of that the feature is enabled by default.
The behavior of U-Boot with the "CLI access prevention" patch applied is as follows:
- If the hardening is disabled then no CLI access protection takes place.
- Otherwise, access to the CLI is enabled when the device is open and is disabled (by default) when the device is closed.
- If one wants to keep CLI access enabled even after the device is closed, then they can indicate
that by simply adding the property
/chosen/toradex,secure-boot/enable-cli-when-closed
(without a value) to the control DTB. We intend to make this possible through tooling in the future.
Notice that in any case, the CLI is enabled when the device is open. This is useful for Secure Boot because CLI access is expected to be normally needed during production as the HAB/AHAB key programming and closing procedure is done through that CLI.
Kernel command-line protection
The bootloader can pass arguments to the Linux kernel
(command line parameters)
and those arguments can heavily influence the system operation; for example, one can force the
loading of modules or change the init program to gain shell access to the system upon boot. In case
of U-Boot, the kernel arguments are normally defined by the contents of the U-Boot environment
variable called bootargs
. In Torizon OS, both the U-Boot environment and the boot script
interfacing with OSTree (which dynamically sets bootargs
at runtime) are in the eMMC storage
device, being thus subject to offline tampering. To prevent that, we implement a protection as part
of our hardening improvements to U-Boot.
Overall, the protection involves storing a copy of the expected kernel command line inside the signed FIT image and checking that information against the passed arguments at runtime. The choice of having a copy rather solely storing the command-line inside the FIT image was driven by the desire to ensure compatibility between older boot loaders and newer OS images. This approach facilitates a seamless transition from the implementation of the CoT covering only the bootloader (what we call minimal CoT, or MCoT) to the one also covering the kernel artifacts (called basic CoT, or BCoT).
When hardening is enabled at build-time the following will take place:
- An overlay will be automatically generated containing the expected/required fixed part of the
kernel arguments string; the overlay will have the following structure:
&{/} {
chosen {
toradex,secure-boot {
required-bootargs = "${TDX_SECBOOT_REQUIRED_BOOTARGS}";
};
};
}; - The overlay will be added to the kernel FIT image like any other overlay; the required boot
arguments will be set by the OE variable
TDX_SECBOOT_REQUIRED_BOOTARGS
, as shown in the snippet above. - The
uEnv.txt
boot script will be patched to automatically apply this new overlay.
Then, at runtime (when enabled):
- The overlay will be applied to the device-tree passed to the Linux kernel.
- The boot arguments will be checked by the bootloader: U-Boot will check the initial part of the
bootargs string against the information in the device-tree and it will also validate the rest of
the bootargs string (the variable part, which cannot be defined at build-time) according to
hard-coded rules. At the moment, the only expected variable argument is one in the form of
ostree=<path>
which is supposed to specify a path to the root directory for the OSTree deployment being booted. - Upon validation success, U-Boot will show a message indicating that fact:
## Validation of bootargs succeeded.
- Upon validation failure, U-Boot will also show a message indicating that occurrence, such as:A developer could use that information to correctly set variable
## Unexpected argument in bootargs: nowb...
## WARNING: Validation of the variable part of bootargs failed; the full bootargs string (A) and its fixed part (as defined in the 'required-bootargs' property inside the device-tree) follow:
## A: "user_debug=30 root=LABEL=otaroot rootfstype=ext4 quiet logo.nologo vt.global_cursor_default=0 plymouth.ignore-serial-consoles splash fbcon=map:3 ostree=/ostree/boot.1/torizon/e81fca340cb3a640834888d6b27b060ed91306ab38693382737678a6f2bf9193/0 nowb"
## B: "user_debug=30 root=LABEL=otaroot rootfstype=ext4 quiet logo.nologo vt.global_cursor_default=0 plymouth.ignore-serial-consoles splash fbcon=map:3"
## WARNING: Allowing boot while device is open; please fix bootargs before closing device.TDX_SECBOOT_REQUIRED_BOOTARGS
.
Conclusion
Ensuring the strength of every link in a Chain of Trust is fundamental to guarantee a robust Secure Boot solution. Through the hardening of U-Boot, Toradex elevates the security of the second link in the chain, laying a solid foundation for subsequent links and paving the way towards a truly secure solution.
Next steps
We suggest interested readers to take a look at:
- Secure Boot on Torizon OS: main article on the topic.
- meta-toradex-security: our open-source Yocto/OE meta layer implementing Secure Boot.