I should probably preface all of this by saying that I’m not really a security professional in the sense that I don’t actually do security stuff for a living; I reported this vulnerability in March and gave a 90 day delay on releasing specific details mostly just because that’s A Thing That Security Researchers Do. Also the vulnerability doesn’t require user interaction from coldboot so it’s a bit nasty in that regard. But also this vulnerability sat around for 7 years so it could be argued that, if anything, 90 days is too long.

Anyhow jumping into things, this is a writeup documenting CVE-2020-12753, a bootloader vulnerability affecting most Qualcomm-based LG phones since the Nexus 5, all the way up to the my test device, the LG Stylo 4 Q710 (and 5 Q720), and probably others. While working on the implementation of this vulnerability I thought it was odd how few bootloader vulnerabilities for Android actually get properly documented, and given the sheer spread of affected devices of this particular vuln I thought it’d be interesting to document it in detail.

A Quick Primer on the (Qualcomm) Android Boot Process

The device I’m working with, the Stylo 4, operates on 2013-2016 variant of Qualcomm’s boot sequence described at https://lineageos.org/engineering/Qualcomm-Firmware/:

image

- On power-on, the Primary Bootloader (PBL) initializes DRAM, eMMC, etc and then loads and verifies SBL1 (Secondary Bootloader 1) from eMMC.
- SBL1 then loads and verifies the Trusted Execution Environment (TEE), aboot, and a few other bits and pieces and then jumps to the TEE, in this case Qualcomm’s Secure Execution Environment (QSEE)
- QSEE sets up secure EL3/EL1 (TrustZone) and jumps down to aboot (non-secure EL1)
- aboot loads and verifies the Linux kernel and jumps to it

Some Android devices allow “bootloader unlocking”, which allows unsigned kernels to be loaded and run. Generally this unlocking occurs via aboot, and the implementation varies from vendor to vendor, however in most cases what happens is that a fastboot command gets sent to the phone to unlock/lock the phone, and as part of Android’s Verified Boot, the phone’s storage is wiped on this transition. There’s also some requirements on user verification so that, in theory, this unlock cannot occur without user interaction.

Additionally, with verified boot enabled, Android will use dm-verity to verify all files on the root/system partition, and SELinux is run as Enforcing.

Variants on the Boot Process, added by LG

In practice, the boot process isn’t quite so simple: Vendors are able to add modifications to the boot process as they see fit. In LG’s case, these differences can be summarized as follows:
- Hardware bringup in SBL for charging PMICs, LEDs, and other misc hardware
- Misc logging/debugging modifications
- Additional TEE processes for SIM unlocking, backed by RPMB
- In aboot, vendor-specific fastboot commands (or no fastboot at all in the case of my device), restrictions on unlocking via certificates, verification modifications, additional boot args for Linux, etc
- Vendor-specific recoveries/flashers, LAF in the case of LG

While I initially started in a privesc from within Linux (and got ~close to getting kernel execution), Google has done a lot of work to ensure that vendors can’t mess up Android security. However, bootloaders have a lot less oversight, so going after these vendor-specific bits of hardware bringup seemed extremely opportune for errors.

Introducing: raw_resources

At an undetermined point in time (likely prior to the Nexus 5 releasing), LG added an “imgdata” partition on eMMC to store boot graphics for Download Mode, fastboot graphics, charging graphics, the unlock graphic and so on. Image data is stored RLE compressed and for each image, metadata for the image width, height, x and y position are specified. The Nexus 5’s final bootloader image, as far as I can tell, only accesses this partition from aboot; SBL1 is not affected on this device. For the curious, I have a Python3 script which can extract these images at https://gist.github.com/shinyquagsire23/ba0f6209592d50fb8e4166620228aaa5.

image
image

A few examples of Nexus 5 imgdata resources

imgdata later became raw_resources, and at an undetermined point, the same RLE decompression and metadata interpreting was copied into SBL1 for use in boot paths where the battery has discharged significantly. If the battery is discharged too far, SBL1’s pm_sbl_chg_check_weak_battery_status will display LGE_PM_NO_CHARGER for boot attempts made without a charger connected, LGE_PM_WEAK_CHARGING_ON for boot attempts with a charger connected, and LGE_PM_NO_BATERY_ANI_* for boot attempts made without a battery. A script for extracting raw_resources can be found at https://gist.github.com/shinyquagsire23/b69ca343fd2f246aee882ecb5af702bd.

image
image

A few examples of Q710 resources

On normal boot paths, aboot reads raw_resources to display the boot logo, download mode graphic, and verified boot statuses for devices which allow unlocking. In my case, the Q710/Q720 does not allow for unlocking, so this boot path is never reached on these devices. However, the graphics still exist I guess on the off chance that they allowed it to happen.

For the C inclined, the format of raw_resources can be summarized in these structs:
typedef struct boot_img_header
{
   char magic[0x10];
   uint32_t num_imgs;
   uint32_t version;
   char device[0x10];
   uint32_t signature_offs;
} boot_img_header;

typedef struct img_info
{
   char name[0x28];
   uint32_t data_offset;
   uint32_t data_size;
   uint32_t width;
   uint32_t height;
   uint32_t offs_x;
   uint32_t offs_y;
} img_info;

The following calculation is performed in order to determine the output pointer to be used during decompression:
bpp = 24
screen_stride = fbinfo->screen_width;
fbuf_offset = offs_x + (screen_stride * offs_y);
fbuf_out = (fbuf_offset * (bpp / 8)) + fbinfo->buffer;

offs_x and offs_y are not bounds checked, and fbinfo->buffer is known in SBL1 and aboot, allowing for a controlled arbitrary write in both environments.

SBL1 load_res_888rle_image Arbitrary Write

This boot path requires discharging the battery to below 0%. While this is less feasible for any practical usage, performing the arbitrary write at this point allows patching SBL1 to disable signature verification before TEE and aboot are loaded. Any of the LGE_PM_* images can be hijacked selectively for arbitrary code execution, though for my PoC I used LGE_PM_NO_CHARGER specifically because I didn’t want to accidentally brick myself (or well, I didn’t want to have to beep out eMMC wires on the board to unbrick).

A 32-bit x offset can be calculated for any given address divisible by 3 using the following calculation:
offset_x = (((0x100000000 + target_addr) - 0x90000000) / 3) & 0xFFFFFFFF

Data written to this arbitrary address can be kept contiguous by specifying the image width to be the same as the screen width. The height should then be rounded up from the payload size to ensure all data is written properly. So really by the end, this isn’t an arbitrary write so much as it is an arbitrary memcpy at Secure EL3.

aboot Arbitrary Write

The aboot arbitrary write functions identically to SBL1: Any image can be selected to perform the arbitrary write. Most notably, this includes any lglogo_image_* graphic, which is displayed by default on every boot. The framebuffer is generally fixed to address 0x90001000, which means that by using the arbitrary write to gain code execution, the original graphic which was used to obtain the arbitrary write can be written to the screen following hijacking to, in effect, make it appear as if boot flow has not been modified at all, for better or for worse.

A good question that might be raised after looking briefly at the structs earlier would be, “wait, there’s a signature offset, why does any of this work if raw_resources contains a signature?” And yes, raw_resources contains a signature! But it’s a useless signature because this signature is only checked in aboot while displaying verifiedboot_* images. At some point, XDA users found out that you could swap the LG boot logos over the verifiedboot_* images so that when they unlocked their devices they wouldn’t have to see the AVB boot nag messages. Naturally, this defeats the point of Google’s Verified Boot spec since it would potentially allow a bootloader be unlocked without the user knowing, so LG added a signature. But it’s only checked for the verified boot images.

As a minor note, unlike SBL1, aboot will also select between raw_resources_a and raw_resources_b depending on the A/B boot slot.

Practical Exploitation

I started by exploiting SBL1, partially because Secure EL3 is just cooler than nonsecure EL1, but also because the framebuffer address was more obviously seen than in aboot (though I later found the aboot framebuffer address anyhow). At this point in execution all of the hardware is initialized and no other bootloaders have been loaded, so we’re basically free to patch sigchecks and control the entire phone!

As it turns out though, SBL1 takes a bit more work to actually exploit, because unlike aboot, its segments aren’t set RWX. I’m not really sure why aboot has all of its segments RWX, like at that point it’s more of a ‘boot’ than a 'secure boot’ if they can’t even bother to use the easiest security option available.

In any case, ROP is required briefly to bypass the MMU’s NX bit. This isn’t too terrible, since SBL1 actually has a routine we can jump to to disable the MMU, though it requires a bit of finnagling to get correct.

So in summary, the exploitation process goes as follows:
- Flash raw_resources_a.img to eMMC (ie via kernel execution, LG LAF, soldered wires and a hardware flasher, etc).
- For SBL1 hax, drain the battery to below 0%. For my Q710, I drained most of the battery by leaving the screen at max brightness with sleep disabled until the phone powered off on its own after several hours. To drain the remaining battery, I charged enough to enter LAF download mode and left it to drain with the screen on for 2-3 hours.
- Hold Power and Volume Down until the phone restarts. If the phone discharged enough, the phone should restart into the payload.
- The phone can be plugged in to boot into aboot and Android normally, but the payload will now execute every time the payload-injected graphic is displayed.

To exploit SBL1:
- raw_resources_a is modified such that the x offset is set to 0x2801CF5C, the width to 1080, and the height to 5. This will decompress LGE_PM_NO_CHARGER (now containing the contents of payload.bin) to 0x08056E14, slightly higher than the stack pointer during decompression.
- The image is decompressed and load_res_888rle_image exits. The previous LR has now been overwritten by a pointer to a Thumb-mode pop {pc} ROP slide, to account for possible offsetting error.
- At the end of the ROP slide, the following ROP instruction sequence is executed:

pop {r4-r12, pc}    ; r8 is now set to SBL1_ARM_MMU_DISABLE, and r12 is now set to SBL1_THUMB_BX_R8

pop {r4-r6, lr}     ; LR is set to the payload pointer
orr r12, r12, #0x1
bx r12 ; jump to SBL1_THUMB_BX_R8

bx r8 ; jump to SBL1_ARM_MMU_DISABLE with lr now set to our payload

Proof-of-Concept, and Future Plans

For those interested in experimenting, a PoC can be found here. Note that this is specifically for the LG Q710, and contains offset specific to the AMZ LG Stylo 4 (Q710ULM, 20c_00_AMZ_US_OP_1121). I’m planning on polishing things up further, however my current boot takeover involves two other vulnerabilities that I’d like to give a bit more time to make sure they’re actually fixed/close to being fixed before releasing.

In the meantime I’d be interested in seeing if anyone else is interested in porting this to other LG devices, since I only own a Nexus 5, a Q710 and a Q720. As it is, these vulns will allow bootloader unlocking at a minimum on most older Qualcomm LG devices, and secure EL3 on newer ones, which should be very interesting for anyone interested in finding vulnerabilities in QSEE/similar on LG devices.

  1. jarno83-blog said: Hi, After finding your great post I dug out my old flex 2 it’s a lg-h955. Is it possible to use this method on that, so costum ROMs can be created and loaded?
  2. valshaped reblogged this from shinyquagsire23
  3. foldingcookie2 reblogged this from shinyquagsire23
  4. shinyquagsire23 posted this