I recently spent some time dissecting the bootloader used on Motorola’s latest Android devices, the Atrix HD, Razr HD, and Razr M. The consumer editions of these devices ship with a locked bootloader, which prevents booting kernel and system images not signed by Motorola or a carrier. In this blog post, I will present my findings, which include details of how to exploit a vulnerability in the Motorola TrustZone kernel to permanently unlock the bootloaders on these phones.
These three devices are the first Motorola Android phones to utilize the Qualcomm MSM8960 chipset, a break from a long tradition of OMAP-based Motorola devices. Additionally, these three devices were released in both “consumer” and “developer” editions. The developer editions of these models support bootloader unlocking, allowing the user to voluntarily void the manufacturer warranty to allow installation of custom kernels and system images not signed by authorized parties. However, the consumer editions ship with a locked bootloader, preventing these types of modifications.
Supported Bootloader Unlocking
From the perspective of the user, unlocking the bootloader on a developer edition device is fairly straightforward. The user must boot into “bootloader mode” using a hardware key combination (usually Vol Up + Vol Down + Power) at boot. Next, the standard “fastboot” utility can be used to issue the following command:
fastboot oem get_unlock_data
In response to this command, an ASCII blob will be returned to the user. The user must then submit this blob to the Motorola bootloader unlock website (https://motorola-global-portal.custhelp.com/app/standalone/bootloader/unlock-your-device-a). If the user’s device is supported by the bootloader unlocking program (i.e. if it’s a developer edition device), the website will provide a 20-character “unlock token”.
To complete the process, the user issues a final fastboot command:
fastboot oem unlock [token]
At this point, the bootloader unlocks, and the user may use fastboot to flash custom kernel and system images that have not been signed by Motorola or the carrier.
QFuses and Trusted Boot
Checking the Bootloader Status
int handle_fboot_oem_unlock(char *cmd)
{
char *token;
if ( is_unlocking_allowed() != 0xff )
{
print_console("INFO", "fastboot oem unlock disabled!");
return 3;
}
if ( is_device_locked() != 0xff )
{
print_console("INFO", "Device already unlocked!");
return 3;
}
token = cmd + 12; /* offset of token in "oem unlock [token]" */
if ( strnlen(token, 21) - 1 > 19)
{
print_console("INFO", "fastboot oem unlock [ unlock code ]");
return 0;
}
if ( !validate_token_and_unlock(token) )
{
print_console("INFO", "OEM unlock failure!");
return 3;
}
return 0;
}
Of particular note are the is_unlocking_allowed() and is_device_locked() functions. Further reversing revealed that these functions query values stored in particular QFuses by accessing the QFPROM region, which represents the contents of the QFuses, memory-mapped at physical address 0x700000. In particular, these two functions invoke another function I called get_mot_qfuse_value(), which queries the value stored in a specific QFuse register.
Having reversed this behavior, the implementation of is_unlocking_allowed() is simple: it returns the “success” value (0xff) if get_mot_qfuse_value(0x85) returns zero, indicating the value in the QFuse Motorola identifies with 0x85 is zero (this register happens to be mapped to physical address 0x700439). In other words, by blowing this particular QFuse, unlocking may be permanently disabled on these devices. Fortunately, this has not been performed on any of the consumer editions of these devices.
The logic behind is_device_locked() is a bit more complex. It invokes a function I called get_lock_status(), which queries a series of QFuse values to determine the status of the device. Among others, it checks the QFuse values for identifiers 0x85 (“is unlocking disabled?”) and 0x7b (“is the Production QFuse blown?”). If the Production bit isn’t set, get_lock_status() returns a value indicating an unlocked bootloader, but this QFuse has been blown on all released models. Otherwise, if unlocking hasn’t been permanently disabled, the result is based on two additional conditions.
If the QFuse with identifier 0x84, which is mapped to physical address 0x700438, hasn’t been blown, the status is returned as “locked”. It turns out this is the QFuse we’re looking for, since blowing it will result in unlocking the bootloader! If this QFuse has been blown, there is one final condition that must be satisfied before get_lock_status() will return an “unlocked” status: there must not be a signed token in the SP partition of the phone. Further investigation revealed that this token is only added when the user re-locks their bootloader using “fastboot oem lock”, so it does not pose any obstacle when trying to unlock the bootloader.
Token Validation
- The CID partition is read from the device.
- A digital signature on the CID partition is verified using a certificate stored in the CID partition.
- The authenticity of the certificate is verified by validating a trust chain rooted in cryptographic values stored in blown QFuses.
- The user-provided token is hashed together with a key blown into the QFuses using a variant of SHA-1.
- This hash is compared against a hash in the CID partition, and if it matches, success is returned.
Edit: The original post claimed the algorithm used was MD4. I mistakenly identified the algorithm because MD4 and SHA-1 evidently share some of the same constant values used during initialization. Thanks to Tom Ritter, Melissa Elliott, and Matthew Green for discussing this and inspiring me to take another look.
Introducing TrustZone
Communicating with TrustZone
struct scm_command {
u32 len;
u32 buf_offset;
u32 resp_hdr_offset;
u32 id;
u32 buf[0];
};
Inside TrustZone
int handle_smc(int code, int arg1, int arg2, int arg3)
{
int ret;
switch (code) {
...
case 2:
if (global_flag) {
ret = -1001;
}
else {
/* Perform unlock */
...
ret = 0;
}
break;
case 3:
global_flag = 1;
ret = 0;
break;
...
}
return ret;
}
Based on this code, it appears that the first word in the SMC buffer represents a command code (in our case it’s 0x2). The reason TrustZone is returning an error on our SMC call from the Android kernel is a particular one-way global flag residing in TrustZone secure memory has apparently been set by MBM before booting the Android OS, preventing all subsequent calls to blow QFuses. Going back to the MBM code, I confirmed this by identifying an SMC call with a command code of 0x3 invoked immediately before booting Linux. Another dead end.
Exploiting TrustZone
switch (code) {
...
case 9:
if ( arg1 == 0x10 ) {
for (i = 0; i < 4; i++)
*(unsigned long *)(arg2 + 4*i) = global_array[i];
ret = 0;
} else
ret = -2020;
break;
...
}
This SMC call, invoked with command code 0x9, is evidently intended to allow the Non-secure Linux kernel to obtain values stored in a particular region of Secure memory. The value provided as the second argument in the SMC buffer is used as the physical address at which this memory is copied. This code does not check that the provided physical address corresponds to a Non-secure memory region, so it can be used to overwrite memory in a Secure region, including our global flag.
At this point, I had everything I needed. As a test, I issued the vulnerable SMC call while providing a physical address in Non-secure memory that I could read back. Fortunately, it appears that all but the first of the four words that are written by the vulnerable code are zeroes, allowing me to clear our global flag preventing a bootloader unlock. Finally, I put it all together by aiming the arbitrary TrustZone memory write to zero the flag and issuing the SMC call with command code 0x9, and then finally issuing the SMC call with command code 0x2 to unlock the bootloader. Rebooting my test device into bootloader mode and checking the bootloader status with “fastboot getvar all” showed I had been successful, and the bootloader was now unlocked!
As previously mentioned, this exploit will work on all Qualcomm-based Motorola Android phones, which includes the Razr HD, Atrix HD, and Razr M.