Launched in April 2013, the Samsung Galaxy S4 is expected to be one of the top-selling smartphones of the year, having sold 10 million units in its first month of sales. While the majority of released models include an unlocked bootloader, which allows users to flash custom kernels and make other modifications to the software on their own devices, AT&T and Verizon branded devices ship with a locked bootloader that prevents these types of modifications. In this post, I'll provide details on how Samsung implement this locking mechanism, and publish a vulnerability in the implementation that allows bypassing the signature checks to run custom unsigned kernels and recovery images.
Both the AT&T (SGH-I337) and Verizon (SCH-I545) models utilize the Qualcomm APQ8064T chipset. As described in my previous blog post on Motorola's bootloader, Qualcomm leverages software-programmable fuses known as QFuses to implement a trusted boot sequence. In summary, each stage of the boot process cryptographically verifies the integrity of the subsequent stage, with the trust root originating in QFuse values. After the early boot stages bootstrap various hardware components, Samsung's APPSBL ("Application Secondary Bootloader") is loaded and run. This bootloader differs between "locked" and "unlocked" variants of the Galaxy S4 in its enforcement of signature checks on the boot*and*recovery*partitions.
A quick glance at aboot*(adopting the name of the partition on which this bootloader resides) revealed that it is nearly identical to the open source lk*("Little Kernel") project, which undoubtedly saved me many hours of tedious reverse engineering. By locating cross-references to strings found in both lk*and aboot, I was able to quickly identify the functions that implement signature verification and booting of the Linux kernel.
The central logic to load, verify, and boot the Linux kernel and ramdisk contained in either the boot*or recovery*partitions is implemented in the boot_linux_from_mmc()*function. First, the function determines whether it is booting the main boot*partition, containing the Linux kernel and ramdisk used by the Android OS, or the recovery*partition, which contains the kernel and ramdisk used by the Android recovery subsystem. Then, the first page of the appropriate partition is read into physical memory from the eMMC flash storage:
if (!boot_into_recovery) { index = partition_get_index("boot"); ptn = partition_get_offset(index); if (ptn == 0) { dprintf(CRITICAL, "ERROR: No boot partition found\n"); return -1; } } else { index = partition_get_index("recovery"); ptn = partition_get_offset(index); if (ptn == 0) { dprintf(CRITICAL, "ERROR: No recovery partition found\n"); return -1; } } if (mmc_read(ptn + offset, (unsigned int *) buf, page_size)) { dprintf(CRITICAL, "ERROR: Cannot read boot image header\n"); return -1; }
This code is straight out of lk's implementation. Next, after performing some sanity-checking of the boot image, which contains a custom header format, the function loads the kernel and ramdisk into memory at the addresses requested in the boot image header:
hdr = (struct boot_img_hdr *)buf; image_addr = target_get_scratch_address(); kernel_actual = ROUND_TO_PAGE(hdr->kernel_size, page_mask); ramdisk_actual = ROUND_TO_PAGE(hdr->ramdisk_size, page_mask) + 0x200; imagesize_actual = (page_size + kernel_actual + ramdisk_actual); memcpy(image_addr, hdr, page_size); offset = page_size; /* Load kernel */ if (mmc_read(ptn + offset, (void *)hdr->kernel_addr, kernel_actual)) { dprintf(CRITICAL, "ERROR: Cannot read kernel image\n"); return -1; } memcpy(image_addr + offset, hdr->kernel_addr, kernel_actual); offset += kernel_actual; /* Load ramdisk */ if (mmc_read(ptn + offset, (void *)hdr->ramdisk_addr, ramdisk_actual)) { dprintf(CRITICAL, "ERROR: Cannot read ramdisk image\n"); return -1; } memcpy(image_addr + offset, hdr->ramdisk_addr, ramdisk_actual); offset += ramdisk_actual;
This is still essentially identical to lk's implementation, with the addition of code to copy the individual pieces of the boot image to the image_addr*location. Finally, the function performs signature verification of the entire image. If signature verification succeeds, the kernel is booted; otherwise, a tampering warning is displayed and the device fails to boot:
if (check_sig(boot_into_recovery)) { if (!is_engineering_device()) { dprintf("kernel secure check fail.\n"); print_console("SECURE FAIL: KERNEL"); while (1) { /* Display tampered screen and halt */ ... } } } /* Boot the Linux kernel */ ...
The is_engineering_device()*function simply returns the value of a global variable that is set at an earlier stage in the boot process based on whether or not the chipset ID (an unchangeable hardware value) of the device indicates it is an engineering or production device.
Examining the check_sig()*function in more detail revealed that*aboot*uses the open-source mincrypt*implementation of RSA for signature validation. The bootloader uses an RSA-2048 public key contained in aboot*to decrypt a signature contained in the boot image itself, and compares the resulting plaintext against the*SHA1 hash of the boot image. Since any modifications to the boot image would result in a different SHA1 hash, it is not possible to generate a valid signed boot image without breaking RSA-2048, generating a specific SHA1 collision, or obtaining Samsung's private signing key.
The astute reader will have already noticed the design flaw present in the above program logic. Notice the order in which the steps are performed: first, aboot*loads the kernel and ramdisk into memory at the addresses requested by the boot image header, and then signature validation is performed after this loading is complete. Because the boot image header is read straight from eMMC flash prior to any signature validation, it contains essentially untrusted data. As a result, it's possible to flash a maliciously crafted boot image whose header values cause aboot*to read the kernel or ramdisk into physical memory directly on top of aboot*itself!
Exploitation of this flaw proved to be fairly straightforward. I prepare a specially crafted boot image that specifies a ramdisk load address equal to the address of the check_sig() function in aboot physical memory. In my malicious boot image, I place shellcode where the ramdisk is expected to reside. I flash this image by leveraging root access in the Android operating system to write to the boot block device. When aboot reads the supposed ramdisk from eMMC flash, it actually overwrites the check_sig() function with my shellcode, and then invokes it. The shellcode simply patches up the boot image header to contain sane values, copies the actual kernel and ramdisk into appropriate locations in memory, and returns zero, indicating the signature verification succeeded. At this point, aboot continues the boot process and finally boots my unsigned kernel and ramdisk. Victory!