Booting Ubuntu on Google Chromecast With Google TV

Posted on Mon 29 November 2021 in Article

In a previous post, we detailed a vulnerability in the Amlogic System-On-Chip bootROM that allows arbitrary code execution at EL3. Since the Chromecast with Google TV (CCwGTV) is one of the devices affected by this issue, it opens the possibility to run a custom OS like Ubuntu.

This post describes the journey to boot Ubuntu on CCwGTV starting from arbitrary code execution in its bootROM. All the resources (bootloaders/tools/scripts) are available on GitHub, as well as a step-by-step guide.

Disclaimer

You are solely responsible for any damage caused to your hardware/software/keys/DRM licences/warranty/data/cat/etc...

A straightforward plan

CCwGTV, released on September 30th 2020, is based on the Amlogic S905D3 System-on-Chip and runs Android OS. The objective is to boot Ubuntu instead, and doing so from an USB drive to keep the original OS untouched.

The device implements Secure Boot, which means each software component of the boot chain is digitally signed to prevent non-official code execution. In order to boot a custom OS like Ubuntu, we'll rely on the bootROM vulnerability to break the Secure Boot and run custom code via the USB interface.

Google has released Open Source code of U-Boot and Linux for this device, which is very helpful for this project.

First, we'll obtain the bootloader image and extract BL2 image to customize it. Then, we'll build a custom U-Boot image that boots from USB flash drive by default. Finally, we'll repack the bootloader image with our custom U-Boot image.

Boot chain overview

The boot chain of this device is based on ARM Trusted Firmware (ATF). In this design, the boot flow is divided in 5 steps executed one after another :

  • Boot Loader stage 1 (BL1) AP Trusted ROM
  • Boot Loader stage 2 (BL2) Trusted Boot Firmware
  • Boot Loader stage 3-1 (BL31) EL3 Runtime Software
  • Boot Loader stage 3-2 (BL32) Secure-EL1 Payload (optional) ↴
  • Boot Loader stage 3-3 (BL33) Non-trusted Firmware

The first stage of the boot flow, referred to as BL1, is the SoC bootROM. All firmware images for the next stages are stored in the bootloader partition on the internal flash memory. The last stage, referred to as BL33, is the U-Boot image; unique Open Source firmware in this boot chain.

Using the vulnerability, we can exploit BL1 to run arbitrary code as BL2 stage. But we can't just run U-Boot as a BL2 image: all the firmwares are required to properly initialize the board (PLL, clocks, DDR training, ...). So we have to load all the firmwares, and only replace the last one (BL33) with our custom U-Boot image.

Fortunately, the entire boot chain is designed to support loading over USB : if BL2 image detects that it has been loaded (in SRAM) via USB, it loads all the next stages (BL3x) over USB instead of the internal eMMC storage memory. The host-side tool to send the bootloaders via USB is even provided by Khadas.

Getting the firmware : the hard way

When I started this research, no firmware or update files were available to download. So I had to remove the eMMC flash chip to dump the firmware and get a copy of the bootloader partition.

eMMC chip-off

I used a YIHUA 8786D hot air rework station to remove the chip, and a ALLSOCKET eMMC153/169-SD Adapter to dump its content. Since it was my first attempt at eMMC chip-off technique, I followed the instructions given in a great talk from Exploitee.rs at Black Hat 2017.

Later on, few OTA update files appeared online, so this hardware modification is no longer required to get the bootloader partition image.

A quick analysis of this bootloader partition doesn't reveal any standard filesystem, this file format seems proprietary. But since we also dumped the bootROM, we can still analyze the code that loads the BL2 image to find its location in the bootloader partition :

OFFSET SIZE DESCRIPTION
0x0 0x1000 BL2 header
0x1000 0xF000 BL2 code
0x10000 n Encrypted (BL3x)

Payload for BL1 exploitation

The exploitation tool amlogic-usbdl allows us to load BL2 image in SRAM and execute it without signature check. However, the bootROM exploitation leaves the USB controller in an invalid state, and this will prevent U-Boot from using it.

So we need a payload that restores the USB controller state before jumping to BL2 image :

#define _clear_icache() ((void (*)(void))0xffff048c)()
#define _dwc_pcd_irq() ((void (*)(void))0xffff8250)()
#define _jmp_bl2() ((void (*)(void))0xfffa1000)()
void _start()
{
    _clear_icache(); //clear instruction cache
    _dwc_pcd_irq();  //clear USB state
    _dwc_pcd_irq();  //after exploitation
    _jmp_bl2();      //jump to BL2 entrypoint
}

Function _dwc_pcd_irq is the USB interruption handler implemented in the bootROM USB stack. When the bootROM vulnerability is exploited, the code flow is diverted from this function, which obviously prevents USB events to be fully and properly handled. Luckily for us, executing that USB handler a couple of times is enough to restore USB controller state.

This payload is copied at the beginning of the BL2 image. The data overwritten are headers that aren't used because the exploit jumps directly to this payload, which then jumps to BL2 entrypoint.

Hacking BL2 image

As previously mentioned, we use the exploitation tool amlogic-usbdl to load and run BL2 image. But the original BL2 image implements few features we must first disable to boot properly. This is where it gets dirty. We are free to modify BL2 code because it's signature won't be checked.

Disabling BL33 Authentication

Once loaded via USB and run, the BL2 code expects to receive the rest of the boot chain. If Secure Boot is enabled, BL2 checks the signature of all images including BL33 (U-Boot). We don't have the right key to sign our custom BL33 image, therefore we need to disable the BL33 signature check. We modify BL2 code to ignore the result of the signature check on BL33 image :

--- <sabrina.bl2.factory.2020-07-13.img>
+++ <sabrina.bl2.noSB.img>
@@ -3,5 +3,5 @@
 fffabd70 81 c2 00 91     add        x1,x20,#0x30
 fffabd74 02 04 80 d2     mov        x2,#0x20
 fffabd78 8a 02 00 94     bl         secure_memcmp
-fffabd7c 60 03 00 35     cbnz       w0,auth_fail
+fffabd7c 00 00 80 52     mov        w0,#0x0
 fffabd80 80 72 40 39     ldrb       w0,[x20, #0x1c]

Disabling Anti-RollBack

Anti-rollback (ARB) feature prevents execution of firmware images older than a specific version, which is stored in One Time Programmable (OTP a.k.a. efuses) memory. When firmware images are updated to a higher ARB version number, the OTP version is bumped-up by burning additional efuses.

So two kind of issues could arise depending on the bootloader version we run :

  • if older than the version in OTP: ARB check will fail and it just won't boot.
  • if more recent than version in OTP : OTP version will be increased, and the ARB protection will block the original firmware (older version) from booting.

In order to prevent such issues, we picked the oldest bootloader partition available (factory firmware), and we disable ARB feature in BL2 code :

--- <sabrina.bl2.noSB.img>
+++ <sabrina.bl2.noSB.noARB.img>
@@ -1,8 +1,8 @@
                      uint __cdecl IS_FEAT_ANTIROLLBACK_ENABLE(void)
      uint              w0:4           <RETURN>
                      IS_FEAT_ANTIROLLBACK_ENABLE
 fffa1744 00 06 80 d2     mov        x0,#0x30
 fffa1748 60 ec bf f2     movk       x0,#0xff63, LSL #16
 fffa174c 00 00 40 b9     ldr        w0,[x0]=>SEC_EFUSE_LIC0
-fffa1750 00 18 46 d3     ubfx       x0,x0,#0x6,#0x1
+fffa1750 00 00 80 d2     mov        x0,#0x0
 fffa1754 c0 03 5f d6     ret

Size reduction

We remove 0x100 bytes of padding at the end of the BL2 image (original size is 0x10000 bytes) to comply with the 0xFF00 bytes payload size limit imposed by the exploitation tool.

Building & customizing BL33

Unlike BL2, BL33 is based on Open Source software U-Boot, so building & customizing it is pretty straightforward.

U-Boot boot sequence is defined with a scripting language that supports variables (global, local), control structures (if, while, etc...), and commands. We modify the original script that boots from internal eMMC to instead load and run another script named s905_autoscript from an USB drive :

--- sabrina-uboot/board/amlogic/configs/sm1_sabrina_v1.h
+++ sabrina-uboot/board/amlogic/configs/sm1_sabrina_v1.h
@@ -297,17 +299,14 @@
             "if test ${active_slot} != normal; then "\
                 "setenv bootargs ${bootargs} androidboot.slot_suffix=${active_slot};"\
             "fi;"\
-            "if test ${avb2} = 0; then "\
-                "if test ${active_slot} = _a; then "\
-                    "setenv bootargs ${bootargs} root=/dev/mmcblk0p23;"\
-                "else if test ${active_slot} = _b; then "\
-                    "setenv bootargs ${bootargs} root=/dev/mmcblk0p24;"\
-                "fi;fi;"\
-            "fi;"\
-            "if imgread kernel ${boot_part} ${loadaddr}; then "\
-                "bootm ${loadaddr};"\
-            "fi;"\
-            "run fallback;"\
+            "echo Sleep 10 secs before USB DRIVE boot from ${loadaddr} ...; "\
+            "sleep 10; "\
+            "usb start 0; "\
+            "usb tree; "\
+            "usb info; "\
+            "fatload usb 0 ${loadaddr} s905_autoscript;"\
+            "setenv autoscript_source usb; "\
+            "autoscr ${loadaddr};"\
             "\0"\
         "upgrade_check=" \
             "echo upgrade_step=${upgrade_step}; "\

By loading and running a script from an external media, we have the possibility to change the boot script without recompiling U-Boot or repacking the bootloader image.

Just like other bootloaders in the chain, U-Boot detects if it's been loaded from USB. In such case, U-Boot skips the normal boot sequence and starts an USB recovery interface (a.k.a. USB burning mode) instead. We have to disable that behavior to ensure the boot sequence we just modified is run :

--- sabrina-uboot/board/amlogic/sm1_sabrina_v1/sm1_sabrina_v1.c
+++ sabrina-uboot/board/amlogic/sm1_sabrina_v1/sm1_sabrina_v1.c
@@ -583,13 +583,6 @@ void set_i2c_ao_pinmux(void)
 int board_init(void)
 {
        sys_led_init();
-    //Please keep CONFIG_AML_V2_FACTORY_BURN at first place of board_init
-    //As NOT NEED other board init If USB BOOT MODE
-#ifdef CONFIG_AML_V2_FACTORY_BURN
-       if ((0x1b8ec003 != readl(P_PREG_STICKY_REG2)) && (0x1b8ec004 != readl(P_PREG_STICKY_REG2))) {
-                               aml_try_factory_usb_burning(0, gd->bd);
-       }
-#endif// #ifdef CONFIG_AML_V2_FACTORY_BURN
 #ifdef CONFIG_USB_XHCI_AMLOGIC_V2
        board_usb_pll_disable(&g_usb_config_GXL_skt);
        board_usb_init(&g_usb_config_GXL_skt,BOARD_USB_MODE_HOST);

Nevertheless, this recovery interface can still be useful in some cases, so we repurpose the physical button GPIOAO_10 :

--- sabrina-uboot/board/amlogic/configs/sm1_sabrina_v1.h
+++ sabrina-uboot/board/amlogic/configs/sm1_sabrina_v1.h
@@ -397,9 +396,7 @@
         "upgrade_key=" \
             "if gpio input GPIOAO_10; then " \
                 "echo GPIOAO_10 pressed;" \
-                "if usb start 0; then " \
-                    "run recovery_from_udisk;" \
-                "fi;" \
+                "update 0;" \
             "fi;" \
             "\0"

When the physical button is hold while U-Boot starts, the USB recovery interface is started by executing U-Boot command "update".

U-Boot source tree with these modifications is available on GitHub, as well as building instructions in the README file. A prebuilt image is also available.

Repacking bootloader with custom BL33

Essentially, we just need to replace the original BL33 image with the custom one in the bootloader partition. However in practice, it's a trickier task than it appears. First, the bootloader partition is encrypted (except BL2). Then, its format is proprietary. Last, BL33 image is compressed using LZ4 algorithm.

Ideally, we would write a complete (un)packing tool with encryption/compression support for the bootloader partition. It didn't happen. Instead, we'll hack it quick 'n dirty. The following steps are implemented in script repack_bootloader.sh.

AES decryption

By analyzing the bootROM, we learn that the bootloader partition is encrypted with AES-256-CBC and the key is stored at address 0xFFFE0020. This key can be dumped using the exploitation tool amlogic-usbdl with the memdump_over_usb payload. We can then use OpenSSL to decrypt the bootloader :

$ openssl enc -aes-256-cbc -nopad -d -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 -in bootloader.enc -out bootloader.img

LZ4 compression

We compress our custom BL33 image using the official tool aml_encrypt_g12a provided by Khadas.

$ aml_encrypt_g12a --bl3sig --input u-boot.bin --compress lz4 --output u-boot.enc --level v3 --type bl33

dd editing

Compressed BL33 image starts with a magic value LZ4C. Thanks to this marker, we can identify the BL33 image offset in the bootloader partition, and simply overwrite it with our custom, compressed BL33 u-boot.enc :

$ IN_OFFSET=`grep --byte-offset --only-matching --text LZ4C u-boot.enc | head -1 | cut -d: -f1`
$ OUT_OFFSET=`grep --byte-offset --only-matching --text LZ4C bootloader.img | head -1 | cut -d: -f1`
$ dd if=u-boot.enc of=bootloader.img skip=$IN_OFFSET seek=$OUT_OFFSET bs=1 conv=notrunc

AES encryption

Finally, the modified bootloader image is encrypted back using OpenSSL again.

Preparing bootable USB flash drive

With our customized bootchain ready to boot from USB, it's time to prepare the OS on an USB flash drive.

First, we write a preinstalled image of Ubuntu for ARM64 on the USB flash drive :

$ dd if=ubuntu-20.10-preinstalled-desktop-arm64+raspi.img of=/dev/<device> bs=1M

Then, we add few files in boot partition :

Precompiled files and detailed instructions are available on GitHub.

Booting Ubuntu from USB

We can finally boot Ubuntu from the USB flash drive by following these steps :

  • Connect target to host via USB to exploit bootROM and run modified BL2
  • Send over USB the custom bootloader image (which includes the custom U-Boot image)
  • Connect USB flash drive that contains boot script, kernel image and the rest of the OS.

Since CCwGTV only offers a single USB-C port, we also need an USB-C adapter that splits power and data, so we can connect simultaneously a power source and an USB device.

Detailed instructions are available in the README file on GitHub, along with all the resources (bootloaders/tools/scripts).

Conclusion

Due to a bootROM vulnerability, the entire software stack of CCwGTV can be replaced. This means booting an alternative OS like Ubuntu is theoretically possible. But in practice, replacing proprietary software components (like bootloaders or TEE) requires significant engineering efforts, especially without debugging capabilities.

Instead, we preferred to reuse as many original software components as possible, which we minimally modified to boot a custom OS.

As explained in the previous blog post, the vulnerability has since been mitigated with a software update. It doesn't fix the bug, as bootROM is read-only. But, luckily, the SoC allows OEMs to restrict access to the USB download mode by burning eFuses. This software update burns eFuses to make the vulnerable code inaccessible to attackers.

However, once in possession of a vulnerable device (unpatched eFuses), attackers can modify software updates before applying them to neutralize the eFuse patch. Therefore, they can run an official up-to-date OS while keeping the vulnerable USB download mode accessible for exploitation.

This issue is serious for devices like CCwGTV that are supposed to guarantee a Trusted Execution Environment for DRM. Once root-of-trust has been compromised, restoring it is a difficult challenge for OEMs.