From March to September 2019, I had the pleasure to do a six-month internship at Quarkslab to study the boot chains produced by Qualcomm and complete the following tasks:

Two different phone models were used to perform this research: a Google Nexus 6P and a Nokia 6.

Given how widespread Qualcomm hardware is, as stated above, many people have already worked on studying Qualcomm components. In particular, Aleph Security released a series of 5 blogposts (aleph aleph2 aleph3 aleph4 aleph5) (alongside tools), describing how they took over the Nokia 6 boot chain and wrote a debugger. These blogposts served as basis for our own work.

First of all a general overview of the Secure Boot process and especially the one used by Qualcomm is given, then by using Aleph Security's tools, we will dump the Nexus 6P and Nokia 6 bootroms in order to gain code execution in them and inject a small debugger, giving the ability to dump the whole phone from the very beginning. This section will also explain the difficulties we encountered with the payload provided by Aleph Security, and how we managed to get around them. After propagating the control to the next stages of the bootchain, we patched the Qualcomm Secure Execution Environment to add a hook giving us a read/write primitive in the highest privileg level EL3. Finally, we will discuss briefly about the XPU register.

On older telephone models, EDL images ("programmers") have interesting properties, among which are:

When going to EDL, the bootROM waits for the PC to send commands. Among these commands, a particularly interesting one is used to send a signed image over USB. To alleviate this task, Aleph Security created a tool named firehorse .

More importantly, the PBL (bootROM) can execute a recovery mode, called Emergency Download Mode (EDL), if one of the following conditions is met:

The Qualcomm secure boot chain, on non-Samsung devices, is designed as follows:

This certificate can sign another certificate (and so on...), or directly authenticate a signature. The end result is a certificate chain where different parties can sign various sub-components of the system.

The vendor emits a root certificate, keeping the private key for themselves and using the public key in the bootROM. As it has to be implicitely trusted, a hash of its public key is stored in hardware (most of the time, in fuses), to ensure it has not been tampered with.

According to Qualcomm Secure Boot and Image Authentication Technical Overview document [QualcommSB] , the binary authentication is designed more or less in the following way:

A secure boot chain is a chain where every stage loads, authenticates (with e.g. RSA) and then executes the next one. The very first stage, the bootROM, is implicitely trusted, as it is most often stored on a CPU die. While modifying it may be considered feasible in theory, it is not scalable. The result is a full chain of trusted components:

Armv8 systems have multiple privilege levels, called exception levels or ELs, ranging from EL0 to EL3. The lower the exception level number is, the less privileges it has. Additionally, with the software and hardware isolation provided by the TrustZone technology, Armv8 distinguishes between the Non-Secure and Secure exception levels.

Exploiting the Nexus 6P

The Google Nexus6P, codenamed angler , is a phone co-developed by Google and Huawei released in late 2015. Its SoC is a MSM8994.

Accessing EDL Mode Accessing EDL mode is fairly trivial on this phone. It is sufficient to downgrade the phone to a version older than December 2017 — which is trivial because Google themselves give you the needed images and instructions [OTA] — and root it. Once rooted, one simply needs to execute the following commands to access the EDL mode: echo 1 > /sys/module/msm_poweroff/parameters/enable_edl adb reboot edl

Dumping the BootROM (PBL) Once the device has entered EDL mode, it identifies itself as Qualcomm HS-USB 9008 and the bootROM waits for commands. It is then necessary for the PC to communicate with the phone to send the signed EDL image. Using Aleph's tool, firehorse, which is a wrapper for Qualcomm's official tools, the command is: firehorse -c COM3 -t angler fw hello Assuming the phone is mapped to COM3, and that the EDL image (or programmer) is at its correct location. Knowing the address of the bootROM thanks to Aleph blogposts, it is trivial to dump the bootROM (PBL): firehorse -c COM3 -t angler fw peek 0xFC010000 0x20000 pbl.bin In any case, the bootROM is mirrored at physical address 0 too.

Gaining Code Execution Once we can communicate with the EDL programmer, it is possible to use the poke command to write at any address as explained in the Aleph's third blogpost aleph3. With the powerfull primitive, it's possible to inject code directly on the stack. However, on Aarch64 EDL programmers, SCTLR_EL3.WXN is a protection mitigating such attempts. One way this can be worked around is by using ROP (return-oriented programming), which is what Aleph tried to do. However, their ROP chain did not seem to work during our tests and we decided to try a different approach by modifying page table entries. By mapping the 1GB region containing our code as read-only, we can subsequently make our code executable. Moreover, since the virtual address range is otherwise unused, no TLB (Translation lookaside buffer) flush is required. Using the firehorse code base, this gives: addr = 0xFC100000 path = r "hooks.bin" target = t . get () ttbr0 = target . page_table_base FH_FW . upload64 ( addr , path ) I ( "uploaded payload" ) FH_FW . poke64 ( ttbr0 , 0xC0000611 ) # AP=0b10, read only; map 1GB from 0xC0000000 to VA 0 FH_FW . poke64 ( target . saved_lr_addr , addr + 4 - 0xC0000000 ) # jump to our code at addr We've tried disabling SCTLR_EL3.WXN as soon as we gained code execution. Unfortunately, this did not work; we thus had to disable the MMU with the following gadget: .text:00000000F803DF38 loc_F803DF38 ; CODE XREF: write_sctlr+C↑j .text:00000000F803DF38 01 10 1E D5 MSR SCTLR_EL3, X1 .text:00000000F803DF3C .text:00000000F803DF3C locret_F803DF3C ; CODE XREF: write_sctlr+14↑j .text:00000000F803DF3C C0 03 5F D6 RET With this done, we can now try to take over the Nexus 6P boot chain.

Taking Over the Bootchain: First Attempts Given that, on the Google Nexus 6P, the bootROM is executing Aarch32 code (i.e. 32-bit) while the EDL is a 64-bit image, it immediately comes to mind that we need to reset the CPU back into Aarch32 mode. We thus tried to write the reset address to the same hardware register that is used by the bootROM to set the 64-bit entry address, 0xF900D07C , then use the standard Armv8 register, RMR_EL3 , to actually reset the CPU. However, after a few tests, it turned out that this doesn't work: the bootROM is mirrored at the physical address 0; this probably means that the Aarch32 reset vector is hardcoded at address 0;

just resetting the CPU without trying to change the entry address doesn't work either; this may indicate some weird hardware issue.

Reversing the Nexus 6P BootROM The execution of the bootROM, also called PBL, starts at address 0xFC010000 , which directly jumps to 0xFC010050 , then 0xFC010054 . This corresponds to the following code: RAM:FC010754 50 0F 0D EE MCR TPIDRURW, R0 RAM:FC010758 90 1F 0D EE MCR TPIDRPRW, R1 RAM:FC01075C B0 0F 10 EE MRC R0, MPIDR RAM:FC010760 0F 00 00 E2 AND R0, R0, #0xF RAM:FC010764 00 00 50 E3 CMP R0, #0 RAM:FC010768 5B 00 00 1A BNE loc_FC0108DC RAM:FC01076C 48 03 9F E5 LDR R0, =0xFC010000 RAM:FC010770 10 0F 0C EE MCR VBAR, R0 RAM:FC010774 44 13 9F E5 LDR R1, =0xFC400204 RAM:FC010778 00 00 91 E5 LDR R0, [R1] RAM:FC01077C 01 00 80 E3 ORR R0, R0, #1 RAM:FC010780 00 00 81 E5 STR R0, [R1] RAM:FC010784 38 13 9F E5 LDR R1, =0xFC400208 RAM:FC010788 00 00 91 E5 LDR R0, [R1] RAM:FC01078C 01 00 80 E3 ORR R0, R0, #1 RAM:FC010790 00 00 81 E5 STR R0, [R1] The bootROM starts by setting per-thread registers (seemingly used as scratch registers here) TPIDRURW and TPIDRPRW to the contents of r0 and r1 . This is used for improper reset. The bootROM checks if the core ID of the processor executing the code is 0, if not, it jumps to an inifinite loop. It then sets the exception vector base to 0xFC010000 , and sets some miscellaneous hardware registers. Next is the boot procedure selection. It begins with: RAM:FC010794 2C 03 9F E5 LDR R0, =0xFC401780 RAM:FC010798 00 00 90 E5 LDR R0, [R0] RAM:FC01079C 02 08 10 E3 TST R0, #0x20000 RAM:FC0107A0 0C 00 00 0A BEQ loc_FC0107D8 RAM:FC0107A4 20 03 9F E5 LDR R0, =0xFC4BE0F0 RAM:FC0107A8 00 00 90 E5 LDR R0, [R0] RAM:FC0107AC D1 15 00 E3 MOV R1, #0x5D1 RAM:FC0107B0 01 00 50 E1 CMP R0, R1 RAM:FC0107B4 03 00 00 0A BEQ loc_FC0107C8 RAM:FC0107B8 10 03 9F E5 LDR R0, =0xFC4017C0 RAM:FC0107BC 00 00 90 E5 LDR R0, [R0] RAM:FC0107C0 01 00 10 E3 TST R0, #1 RAM:FC0107C4 03 00 00 0A BEQ loc_FC0107D8 RAM:FC0107C8 RAM:FC0107C8 loc_FC0107C8 ; CODE XREF: _init+60↑j RAM:FC0107C8 04 03 9F E5 LDR R0, =sub_FC010040 RAM:FC0107CC A3 00 00 EB BL SetVbar RAM:FC0107D0 00 03 9F E5 LDR R0, =0xFE800000 ; func RAM:FC0107D4 71 2E 00 EB BL resetToAa64 It means that if: (( * ( vu32 * ) 0xFC401780 & 0x20000 ) && ( * ( vu32 * ) 0xFC4BE0F0 == 0x5D1 || ( * ( vu32 * ) 0xFC4017C0 & 1 ) == 1 )) that is, with the two reset methods, if mask 0x20000 was set in the secure register at 0xFC401780 (by default, it isn’t), then the bootROM sets the exception table to infinite loops and immediately resets to 0xFE800000 in Aarch64 mode. This seems to be called improper reset. All 3 involved registers are writable (or in the case of HWIO_GCC_RESET_STATUS_ADDR , one can trigger a watchdog reset). Then: RAM:FC0107D8 FC 02 9F E5 LDR R0, =0xFC102080 RAM:FC0107DC D3 F0 21 E3 MSR CPSR_c, #0xD3 RAM:FC0107E0 00 D0 A0 E1 MOV SP, R0 RAM:FC0107E4 DB F0 21 E3 MSR CPSR_c, #0xDB RAM:FC0107E8 00 D0 A0 E1 MOV SP, R0 RAM:FC0107EC D7 F0 21 E3 MSR CPSR_c, #0xD7 RAM:FC0107F0 00 D0 A0 E1 MOV SP, R0 RAM:FC0107F4 D3 F0 21 E3 MSR CPSR_c, #0xD3 RAM:FC0107F8 00 C0 A0 E3 MOV R12, #0 RAM:FC0107FC 0C 30 A0 E1 MOV R3, R12 ; ............................... boilerplate .................................. meaning that in any other case than the above, the stacks for each Aarch32 execution mode are set, and r3 to r12 are set to 0. Furthermore, RAM:FC010860 78 02 9F E5 LDR R0, =0xFC4BE034 RAM:FC010864 00 10 90 E5 LDR R1, [R0] RAM:FC010868 40 00 11 E3 TST R1, #0x40 RAM:FC01086C 02 00 00 1A BNE loc_FC01087C RAM:FC010870 6C 02 9F E5 LDR R0, =sub_FC010060 RAM:FC010874 79 00 00 EB BL SetVbar RAM:FC010878 68 F2 9F E5 LDR PC, =0xFC100000 if (0xFC4BE034 & 0x40) is true, then the bootROM sets the exception vector table to infinite loops then jumps to 0xFC100000. The concerned register is read-only, it's probably a fuse register (the used register seems to be fuse-related and is involved in determining whether secure boot is enabled). Finally, RAM:FC01087C loc_FC01087C ; CODE XREF: _init+118↑j RAM:FC01087C E7 40 00 EB BL initsysregs RAM:FC010880 64 02 9F E5 LDR R0, =main RAM:FC010884 10 FF 2F E1 BX R0 ; main The boot process continues: further system registers are initialized, after which the code jumps to the main function. The main function, after some miscellaneous initializations, executes a list of functions, passing the same singleton instance to all the functions. There are many functions, but the most notable ones decide whether or not to go to EDL mode: if the EDL test points are shortened, go to EDL;

if some fuses have specific bit sets, go to EDL;

if the three words at 0xFE87FFE0 , written by the Android kernel, are 0x322A4F99 0xC67E4350 0x77777777 , go to EDL.