Motorola Bootloader Unlocking

Credit: Dan Rosenberg
Risk: High
Local: Yes
Remote: No

CVSS Base Score: 6.2/10
Impact Subscore: 10/10
Exploitability Subscore: 1.9/10
Exploit range: Local
Attack complexity: High
Authentication: No required
Confidentiality impact: Complete
Integrity impact: Complete
Availability impact: Complete

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 ( 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 Much of Qualcomm's security architecture is implemented using QFuses, which are software-programmable fuses that allow one-time configuration of device settings and cryptographic materials such as hashes or keys. Because of their physical nature, once a QFuse has been blown, it is impossible to "unblow" it to revert its original value. If the FORCE_TRUSTED_BOOT QFuse is blown, as is the case on all production Motorola devices, each stage of the boot chain is cryptographically verified to ensure only authorized bootloader stages may be run. In particular, the PBL ("Primary Bootloader"), which resides in mask ROM, verifies the integrity of the SBL1 ("Secondary Bootloader") via a SHA1 hash. Each stage of the boot chain verifies the next stage using RSA signatures, until finally Motorola's APPSBL ("Application Secondary Bootloader"), "MBM", is loaded and run. Checking the Bootloader Status The entirety of the Android OS signature verification and bootloader unlocking process is implemented in MBM. To study the implementation, I examined the "emmc_appsboot.mbn" binary included in a leaked SBF update package for the Motorola Atrix HD. It's also possible to pull this partition directly from a device via the /dev/block/mmcblk0p5 block device. To start, because the binary blob is an undocumented format, I assisted IDA Pro in identifying entry points for disassembly. Next, I searched for cross-references to strings referring to unlocking the bootloader. After getting my bearings, I identified the function responsible for handling the "fastboot oem unlock" command. The reverse engineered pseudocode looks something like this: 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 If the bootloader has not already been unlocked, MBM will attempt to validate the token provided by the user. More reversing revealed that the following logic is used: 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. As a result, there is no way for a user to generate his or her own valid unlock token without either breaking RSA to violate the integrity of the CID partition, or by performing a pre-image attack against SHA-1, both of which are computationally infeasible in a reasonable amount of time. 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 Having run into this dead-end, I examined what actually takes place when a successful unlock token is provided. We already know that MBM must somehow blow the QFuse with Motorola identifier 0x84 in order to mark the bootloader as "unlocked". It accomplishes this by calling a function that invokes an ARM assembly instruction that may be unfamiliar to some: SMC (Secure Monitor Call). This instruction is used to make a call to the ARM TrustZone kernel running on the device. TrustZone is an approach to security integrated into many modern ARM processors. TrustZone operates in what's known in ARM parlance as the "Secure world", a trusted execution mode whose security is enforced by the processor itself. Among other tasks, TrustZone may designate "Secure memory", which cannot be read from the "Non-secure world", regardless of privilege level. Even if code is running in SVC (kernel) mode, attempts to access a Secure memory region from a Non-secure execution context will cause the CPU to abort. The Secure world stack is often implemented as a small trusted kernel. On these particular Motorola devices, the TrustZone kernel resides on the TZ partition of the device and is loaded at early boot, prior to MBM. The Non-secure world may issue requests to the Secure world using the privileged SMC instruction. In this case, it became clear that MBM issues a specific SMC call to request that the TrustZone kernel blow the appropriate QFuse to unlock the bootloader on the device. Communicating with TrustZone For a first naive attempt at unlocking the bootloader, I decided to deconstruct the syntax of the SMC call made by MBM and make an identical call from kernel mode in the Android OS. Fortunately, Qualcomm-based Linux kernel trees have source code that reveals the syntax of these SMC calls. Looking at arch/arm/mach-msm/scm.c reveals that calls to Qualcomm-based TrustZone kernels are expected to pass arguments using the following data structure: struct scm_command { u32 len; u32 buf_offset; u32 resp_hdr_offset; u32 id; u32 buf[0]; }; Understanding this data structure clarified the SMC calling convention I saw in MBM. In particular, when issuing the call to unlock the bootloader, MBM sets an id of 0x3f801 and provides a buf array of four words, containing the values 0x2, 0x4, 0x1, and 0x0, in that order. Based on looking at additional MBM code, it appears that the 0x4 represents a word offset within the QFuse bank beginning at 0x700428, and 0x1 represents a bitmask indicating which bits within that word should be blown. This makes sense, since this call would result in blowing the QFuse at physical address 0x700438, which we already determined would unlock the bootloader. With this knowledge in hand, I threw together a kernel module that would issue an identical SMC call to hopefully unlock the bootloader. I loaded this module into the Android OS's Linux kernel returned an error code of -1001. Inside TrustZone To see what was going on here, I knew I would need to reverse engineer portions of the TrustZone kernel running on the device. After loading the tz.mbn binary blob contained in an SBF update package into IDA Pro, rather than reversing the entire kernel I made a quick search for the immediate value 0xfffffc17 (-1001) to see if I could skip straight to the code I was interested in. Sure enough, there was only one function containing this value. Taking a look at this function revealed the following pseudocode: 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 At this point, the end was in sight, but I knew I would need a vulnerability in the TrustZone kernel in order to set this flag to zero, allowing my SMC call to blow the QFuse required to unlock the bootloader. Fortunately, I didn't have to look long, since one of the other SMC commands in the same section of the TrustZone kernel contains a fairly obvious arbitrary memory write vulnerability: 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.


Vote for this issue:


Thanks for you vote!


Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.

(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2023,


Back to Top