Technical
December 31, 2019
By Leo Dorrendorf

Setting Up U-Boot to Harden the Boot Process

With guest writer Elliot Cooper

In this article, we will discuss setting up the popular U-Boot bootloader to achieve a secure configuration, hardening the boot process by removing unnecessary and insecure functionality such as shell and network access. Attackers with physical access to a device commonly subvert its boot process to run their own code, dump and analyze the device's firmware, extract its secrets, and then reconfigure or reprogram it to suit their purposes. This is why having a secure boot-up process is a critical factor in ensuring device security and trustworthy software execution.

U-Boot is a highly customizable boot loader that is very popular in embedded systems and IoT devices. A boot loader is a program initiated by the system's ROM or BIOS, which in turn loads a kernel and initiates the operating system's boot process. U-Boot is a popular choice for embedded and IoT systems because these devices have diverse hardware configurations that may not be possible to boot using mainstream boot loaders such as GRUB.

In this guide, we will walk through the process of compiling a U-Boot binary, loading it onto a Raspberry Pi (hereafter, RPI for brevity), and using it to boot the system. We will then take advantage of U-Boot's customizability to remove some features in order to make the RPI's boot process more secure.

To work according to the instructions in this guide you will need either a monitor and keyboard or a serial console plugged into your RPI, because it won't be accessible via SSH or a remote desktop session during the very early stages of the boot process. As always when working with the RPI, you will need a MicroSD card to store the firmware image.


Ensure optimal security for your connected products across their entire lifecycle See VDOO in action


RPI Boot Process

The RPI has a boot process that, while non-standard, is typical of embedded and IoT devices. To simplify the tasks you will perform later in this guide, we will attempt to present a clear idea of how an RPI boots from power-on until the operating system starts running.

The standard (non-U-Boot) boot process is as follows:

The standard (non-U-Boot) boot process

Note that on the RPI, the GPU is used to initialize the system and perform the initial stages of the boot.

The boot process with U-Boot enabled is as follows:

The boot process with U-Boot enabled

The u-boot.bin binary that will load the system kernel will be created at Stage 3 of the boot process, and will have the security settings hard-coded so that they cannot be changed after the file has been created.

Creating a Cross-Compile Environment

Cross-compiling means compiling a software binary that is targeted at, or intended to run on, a CPU architecture that is different to the one on which it is compiled. This is commonly done because compiling is a CPU-intensive task and the RPI has a modest CPU. Compiling the u-boot.bin binary on your local computer would be a great deal faster than doing it directly on the RPI, and with cross-compilation, you can create native RPI binaries from the comfort of the regular development environment on your host PC.

We need to cross-compile because your host PC almost certainly has an x86 CPU, while the RPI has an ARM CPU. We will, therefore, provide the tools that the compiler needs to build ARM code by installing the packages that contain these tools. These packages have different names on different distributions.

We will be building 32-bit ARM code, since the RPI kernel on Raspbian is a 32-bit kernel.

The following packages need to be installed on the system that you intend to use to build the U-Boot binary. Run these commands in a terminal as a non-root, sudo-enabled user:

Debian / Ubuntu:
$ sudo apt install git bison flex u-boot-tools libncurses5-dev build-essential libssl-dev gcc-arm-linux-gnueabi

Redhat / Centos:
$ sudo dnf install git bison flex uboot-tools ncurses-devel @development-tools openssl-devel arm-none-eabi-gcc-cs

Archlinux:
$ sudo pacman -S git bison flex uboot-tools ncurses base-devel arm-none-eabi-gcc

Next, you need to set a new environment variable, CROSS_COMPILE, that points to the ARM cross-compile tools. When this variable is set, the compiler will use the cross-compilation tools instead of the native ones.

The following commands will need to be run any time you open a new terminal to compile U-Boot, as the variable is only set in the terminal where it is run.

The following commands set the necessary variables on the listed Linux distributions:

Debian / Ubuntu:
export CROSS_COMPILE=arm-linux-gnueabi-

Redhat / Centos:
export CROSS_COMPILE=arm-none-eabi-

Archlinux:
export CROSS_COMPILE=/usr/bin/arm-none-eabi-

You are now ready to start building the U-Boot binary.

Building a U-Boot Binary

Now that your local system is configured to cross-compile code for the RPI, we can create a test u-boot.bin binary and use it to boot the RPI.

First, download all the U-Boot source code from its repository using git, and change into the new directory:

$ git clone git://git.denx.de/u-boot.git
cd u-boot

The next command will create the build and configuration files that will be used in the following step to create the u-boot.bin file:

$ make <configuration-file>

The U-Boot repository contains pre-made configuration files for many different IoT boards. They are all contained in u/boot/configs/. This directory has pre-made configuration files for all versions of the RPI except, at the time of writing, the RPI 4. In this guide, we target the Raspberry PI 3B running Raspbian Buster with a 32-bit kernel, so we will use rpi_3_32b_defconfig.

Substituting this file name into the previous command gives:

$ make rpi_3_32b_defconfig

This is the where you will customize the default configuration to make it more secure later in the guide. For now, you will build the defaults so that you have a working base configuration to start from.

To parallelize the build process and save time, build the u-boot.bin with the following command:

$ make -j <CPU cores + 1>

On a 4 CPU core system this becomes:

$ make -j 5

We set the number of simultaneous jobs with the -j flag to utilize every core on a multi-core CPU.

You have now created a u-boot.bin binary file in the same directory where you ran the make command. Copy the u-boot.bin file to the /boot/ folder on your RPI.

Configuring the RPI to Boot Using U-Boot

Log into a terminal on your RPI as a non-root, sudo-enabled user and move to the /boot/ directory:

$ cd /boot

The /boot directory contains all of the RPI's boot configuration, files, and kernels. Now we need to configure the stage-3 boot loader to load u-boot.bin instead of the kernel.

You will do this by editing the /boot/config.txt file that the stage-3 boot loader reads to pick up any additional configuration.

Rather than delete the original, you should change its name to create a backup copy:

$ sudo mv config.txt config.txt-pre-uboot

Then create a new file by opening it with a text editor. Here, nano is used:

$ nano config.txt

Add the following line:

kernel=u-boot.bin

If you are accessing the RPI via a serial console, add the additional line:

enable_uart=1

Save and exit the editor.

Booting the RPI with the U-Boot Shell

The RPI is now ready to boot using U-Boot. However, it will not boot automatically into the OS. In its current configuration, when you re-boot the RPI it will boot into the U-Boot shell. From that shell, you can issue commands to boot a selected kernel.

We will automate this process in the next step, but right now, you can issue the commands manually to familiarize yourself with the U-Boot shell. This will be helpful for later testing.

Ensure that you have the keyboard and mouse or serial console connected, and reboot your RPI:

$ sudo reboot

In place of the regular kernel output, you will see the following output:

Starting kernel ...                                                             

MMC:   mmc@7e202000: 0, sdhci@7e300000: 1                                       
Loading Environment from FAT... *** Warning - bad CRC, using default environment

In:    serial                                                                   
Out:   vidconsole                                                               
Err:   vidconsole                                                               
Net:   No ethernet found.                                                       
starting USB...                                                                 
Bus usb@7e980000: scanning bus usb@7e980000 for devices... 3 USB Device(s) found
       scanning usb for storage devices... 0 Storage Device(s) found            
Hit any key to stop autoboot:  2

Before the countdown reaches "0", press a key. When you do this, you will be dropped into the U-boot shell with the following prompt:

U-Boot>

From here you can issue the commands needed to boot the OS. The complete set of commands that you need to run are as follows:

U-Boot> mmc dev 0
U-Boot> fatload mmc 0:1 ${kernel_addr_r} kernel7.img
U-Boot> fatload mmc 0:1 ${fdt_addr_r} bcm2710-rpi-3-b.dtb
U-Boot> setenv bootargs root=/dev/mmcblk0p2 rootfstype=ext4 rootwait
U-Boot> bootz ${kernel_addr_r} - ${fdt_addr_r}

These commands mean:

  • mmc dev 0 - Set the MicroSD card as device 0 from which the kernel and DTB file will be read.
  • fatload mmc 0:1 ${kernel_addr_r} kernel7.img - Load the default kernel from the MicroSD card into memory.
  • fatload mmc 0:1 ${fdt_addr_r} bcm2710-rpi-3-b.dtb - Load the DTB file for the MicroSD card into memory.
  • setenv bootargs... - Pass these arguments to the kernel.
  • bootz ${kernel_addr_r} - ${fdt_addr_r} - Run the kernel and dtb file.

When you have issued these commands, your RPI will boot into Raspbian as usual. After your RPI has finished booting, log back in so that you can make this process automatic.

First, install the U-Boot suite of tools:

$ apt update
$ apt upgrade
$ apt install u-boot-tools

Now, create a new file that contains the commands you ran in the U-Boot shell:

$ sudo nano /boot/boot_commands.scr

Add the lines:

mmc dev 0
fatload mmc 0:1 ${kernel_addr_r} kernel7.img
fatload mmc 0:1 ${fdt_addr_r} bcm2710-rpi-3-b.dtb
setenv bootargs root=/dev/mmcblk0p2 rootfstype=ext4 rootwait
bootz ${kernel_addr_r} - ${fdt_addr_r}

As you can see, they are the exact commands you entered manually in the previous step.

Now, use this file to generate a new file that U-Boot will use to automatically execute these commands:

$ sudo mkimage -A arm -O linux -T script -C none -n boot.scr -d /boot/boot_commands.scr /boot/boot.scr.uimg

The RPI will now automatically run those commands at the end of the countdown if no key is pressed. This allows you to have a working configuration but also to try different kernels or configurations by pressing a key and entering the U-Boot shell.

Recovering from a Non-Booting RPI

If you create a U-Boot binary that is unable to boot your RPI, which is quite likely to happen while you are testing different configurations, then you will need to do the following:

  1. Power off the RPI.
  2. Extract the MicroSD card.
  3. Insert the MicroSD card into your local computer.
  4. Mount the first partition (which is the Raspbian /boot/ partition) of the MicroSD card.
  5. Replace the broken u-boot.bin with a known working one.
  6. Insert the MicroSD card in your RPI.
  7. Power on the RPI.

The u-boot.bin that you created in the first section is a good working u-boot.bin binary to use for this purpose. Keep a copy in /boot/ on your RPI with a descriptive name such as u-boot.bin-defaults-working.

Securing U-Boot

The default U-Boot configuration has other security issues as well. Now that you can build U-Boot binaries and use them to boot your RPI, you can move on to creating custom binaries with secure configurations.

Configuring U-Boot

When you compile the u-boot.bin binary, the compiler will follow configuration options from several locations. The first is in the default configuration file for the RPI board, located at u-boot/configs/rpi_3_32b_defconfig. The second is a temporary file that contains overrides to the default file. This file is located at u-boot/.config.

You can make the configuration edits to the default file at u-boot/configs/rpi_3_32b_defconfig, but doing so has some drawbacks, the main one being that you will need to merge your edits any time the U-Boot repository maintainers update this file.

There are several tools available to update the kernel configuration. The nconfig tool, unlike a regular config tool, has a more comfortable graphic text-based colored menus utilizing a ncurses menu-based program and it is recommended method to edit the default configuration included with the U-Boot source.

This menu system will ensure that you do not create conflicting configuration options and also provides a logical order to the configurable settings. The changes that you make will be saved to u-boot/.config. You can keep copies of this file under descriptive names and use them to create customized builds in the future.

You can access this menu system after you perform the initial make rpi_3_32b_defconfg by running the following command:

$ make nconfig

Going forth, this menu system will be referred to as nconfig, and navigating inside it will be shown as follows:

nconfig -> Device Tree Control -> [ ] Enable use of a live tree

This shows that you should select the Device Tree Control sub-menu and enable or disable the option Enable use of a live tree .

When you want to start defining your configurations from scratch, U-Boot provides a way to clear out any previous configuration and binaries. You should also run this command each time when you want to test a new configuration independent of all previous work:

$ make distclean

This will delete the .config along with all the files that were created by make rpi_3_32b_defconfig and the make commands, ensuring that you start with only the default configuration settings.

After you have run make distclean, create the default configuration again with this command:

$ make rpi_3_32b_defconfig

Disable the U-Boot Shell

The U-Boot shell is the pre-OS environment where you entered the commands to boot the RPI manually in the first section of this guide. The command shell is very useful for developing and debugging, but it should not be left enabled in the production image. An attacker could easily use the shell to dump the firmware image or replace it with a maliciously modified image.

The following procedure will create the configuration that will create a u-boot.bin binary that has the U-Boot command shell completely disabled. The RPI will not allow any shell commands to be issued, nor will it be possible to enter the command shell, when a u-boot.bin binary is created with this configuration.

Detailed Guidance

First, create the default configuration for the RPI:

$ make rpi_3_32b_defconfig

The configuration that you have to set to disable the U-Boot shell is as follows:

CONFIG_DISABLE_CONSOLE=y
CONFIG_SILENT_CONSOLE=y
CONFIG_SILENT_CONSOLE_UPDATE_ON_RELOC=y
CONFIG_BOOTDELAY=0
CONFIG_SYS_DEVICE_NULLDEV
CONFIG_OF_SEPARATE=y

Enable (set the checkboxes on) the following options:

nconfig -> Console -> [ ] Console recording
nconfig -> Console -> [ ] Support a silent console
nconfig -> Device Tree Control -> Provider of DTB for DT control (Separate DTB for DT control) -> Separate DTB for DT control

Set this option to 0 seconds:

nconfig -> (2) delay in seconds before automatically booting

Save and exit nconfig (press F6 to save and F9 to exit).

The final setting cannot be made in nconfig, so you need to add it manually to the .config file by opening it with a text editor:

$ nano .config

Add this line to the bottom of the file:

CONFIG_SYS_DEVICE_NULLDEV

The final configuration change must be made to the RPI's C code. Open this file board/raspberrypi/rpi/rpi.c in an editor:

$ nano board/raspberrypi/rpi/rpi.c

and add the following to the bottom of the file:

int board_early_init_f(void)
{
  gd->flags |= (GD_FLG_SILENT | GD_FLG_DISABLE_CONSOLE);
  return 0;
}

Finally, build the binaries:

$ make

The CONFIG_OF_SEPARATE=y configuration option prompts U-Boot to create two files, u-boot.bin and u-boot.dtb, that need to be joined to create a u-boot.bin binary. Run the following command to join the two files and create a working u-boot.bin:

$ cat u-boot.bin u-boot.dtb > u-boot.bin

Finally, copy the u-boot.bin binary to the /boot/ folder on your RPI, and reboot to test if your binary works.

Disable U-Boot Network Functionality

The default configuration for the U-Boot shell includes network functionality and network tools. These tools are useful during development and testing, but are not needed for a production device. This network functionality can easily be compromised by network attackers, as U-Boot uses many unauthenticated network protocols such as TFTP and BOOTP. Therefore, the U-Boot network functionality should be disabled for production devices.

Detailed Guidance

First, create the default configuration for the RPI:

$ make rpi_3_32b_defconfig

The option that you need to set to disable networking is the following:

# CONFIG_NET is not set

The default is for networking to be enabled. This option is unset here:

nconfig -> [ ] Networking support

Now, build U-Boot:

$ make

Finally, copy the u-boot.bin binary to /boot/ on your RPI and reboot to test that it is working.

Hardening U-Boot

The default configuration for U-Boot includes functionality that is helpful during testing but is not needed to boot a production device. This functionality increases the attack surface of the device, and so it should be disabled in production, leaving only the minimum required functionality.

Removing features from the bootloader will make it harder for attackers to accomplish certain goals, even if the bootloader has been compromised. For example, the attacker cannot easily bypass secure boot without memory-modification commands and kernel command line control.

Detailed Guidance

The following sections will disable non-required functionality from the U-Boot binary.

In order to avoid memory modification, undefine the following configuration parameters:

CONFIG_CMD_BINOP
CONFIG_CMD_CRC32
CONFIG_CMD_EEPROM
CONFIG_CMD_LOOPW
CONFIG_CMD_MD5SUM
CONFIG_CMD_MEMINFO
CONFIG_CMD_MEMORY
CONFIG_CMD_MEMTEST
CONFIG_CMD_MX_CYCLIC
CONFIG_CMD_SHA1SUM
CONFIG_CMD_STRINGS

These parameters are unset at:

nconfig -> Command line interface -> Memory commands

Un-check every option on the page of nconfig.

Next, disable storage modification by undefining the following configs:

CONFIG_CMD_NAND
CONFIG_CMD_MTD
CONFIG_CMD_MMC

They are un-set at:

nconfig -> Command line interface -> Device access commands

Ensure the following lines are unchecked:

[ ] mmc
[ ] mtd
[ ] nand

Disable USB support by undefining the following options:

CONFIG_CMD_USB
CONFIG_USB_UHCI
CONFIG_USB_KEYBOARD
CONFIG_USB_STORAGE
CONFIG_USB_HOST_ETHER

They are un-set by un-checking these lines:

nconfig -> Command line interface -> Device access commands -> [ ] usb
nconfig -> Device Drivers -> [ ] USB support

Next, remove configuration options related to non-volatile memory, such as:

CONFIG_ENV_IS_IN_MMC
CONFIG_ENV_IS_IN_EEPROM
CONFIG_ENV_IS_IN_FLASH
CONFIG_ENV_IS_IN_DATAFLASH
CONFIG_ENV_IS_IN_MMC
CONFIG_ENV_IS_IN_FAT
CONFIG_ENV_IS_IN_NAND
CONFIG_ENV_IS_IN_NVRAM
CONFIG_ENV_IS_IN_ONENAND
CONFIG_ENV_IS_IN_SPI_FLASH
CONFIG_ENV_IS_IN_REMOTE
CONFIG_ENV_IS_IN_UBI

Also, set the following option:

CONFIG_ENV_IS_NOWHERE

These options above are set and un-set at:

nconfig -> Environment

Ensure that every line is un-checked except the following lines, which must be checked:

[*] Environment is not stored
[*] Environment is in a FAT filesystem
[*] Add run-time information to the environment

Finally, you should ensure that any unused device drivers are unchecked at:

nconfig -> Device drivers

and that any unused boot media are unchecked at:

nconfig -> Boot media

The default configuration for rpi_3_32b_defconfig is to have these options unchecked. However, this may not be the case for other boards, including different versions of the RPI.

Summary

This post has walked you through the steps of hardening the U-Boot configuration, disabling unnecessary functionality and securing the device's boot process. By following this guide, you should be able to achieve a configuration that's much harder for attackers to hack because they would be forced to attack the hardware directly using intrusive physical methods, instead of a local serial console, keyboard, or network connection.


Ensure optimal security for your connected products across their entire lifecycle Contact us


References

Share this post

You may also like