This blog post was originally published on May 23, 2013 on the now-defunct Azimuth Security blog.
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!
Thanks to ralekdev for the helpful exchange of ideas and suggestions.