Tying It All Together - Pwning To Own on LG phones
Last year I detailed a secure EL3 vulnerability which affected (and still affects, for devices with discontinued updates) LG Android devices. However, this vulnerability alone isn't actually all that useful for a number of reasons, the more immediate being that many phones simply do not allow writing to eMMC without root or a custom recovery. Additionally, gaining full control over all privilege levels requires draining the battery to below 0%, which while it would be possible to create a modchip that facilitated this, is impractical. To finish off my exploit chain, I would like to detail two additional vulnerabilities that I have found and utilized in my coldboot process. It's worth noting though that these vulnerabilities were reported to LG and may be patched on updated units.
The first exploit is an obvious necessity: In order to write the rle888 payload into the eMMC's boot graphics, I need to be able to achieve an arbitrary partition write. While exploiting Android *is* an option for this (as are hardware methods), I instead opted to attack LAF, LG's recovery/flashing component. While many Android phones in the past have used fastboot in order to flash radios and other system components to eMMC, fastboot has been completely removed on the Q710/Q720. Some phones such as the Nexus 5 actually maintain both fastboot methods and LAF, but for maximum spread, LAF is the clear target.
LAF is designed to work with LGUP, a frequently-leaked LG-internal flashing tool that allows flashing KDZ update files. While LAF in the past was able to read and write eMMC partitions without any restriction, in recent years LG has opted to sign all of their KDZ files in order to make it more difficult for things like cross-carrier flashing, version mixing/matching between partitions and other modifications to occur. Flashing is done via USB, and most of the protocol has been documented at https://github.com/Lekensteyn/lglaf.
The LAF update process largely consists of an ioctl-over-USB shim: The OPEN command is able to open a partition block device, and READ/WRTE will seek into the file and write contents. However, reading and writing are explicitly blocked until a list of partitions, their eMMC offsets, their KDZ content offsets, and their content hashes is sent via the SIGN command, all of which is hashed and signed by LG. If the contents of the partitions in the KDZ are modified, the partition list hashes will fail to verify, and modifying the hashes in the partition list will make the SIGN check fail. The private key is not stored in LGUP; KDZs are downloaded from LG's servers, signed presumably by their build servers.
So, how can we manage to activate WRTE commands, with valid partition content hashes of our arbitrary contents, if we cannot sign our own? To start, I investigated how the WRTE commands actually handled hash checking--if the partition list is sent with SIGN, then at some point the WRTE command must be able to figure out which partition the current write is for, and the current partition's contents must be buffered in RAM somewhere along with an updating SHA context, because if the SHA check fails, then it shouldn't write at all. As it turns out, most of the checks in this area were fairly solid (the write must be in the range of a partition in the list, the entire transaction is one bulk packet of the hashed size, etc). However, this led me to realize: The partition list signature is only checked once, and there is nothing stopping me from, say, sending another SIGN command.
The SIGN verification process works as follows:
The partition information is sent along with a signature in one bulk transaction.
The partition information is copied into a global .bss array from the USB buffer with a fixed size.
The partition information hash buffer is prepared: An allocation is made for N partitions and an optional string, the string being the device model (to prevent cross-flashing). The number of partitions is determined by a signed portion of the header. If the allocation fails, an error is returned.
The partition information is copied again into this allocation along with the string, and the contents are hashed. The signature is crypted with the public key and the signature hash is verified against the partition info hash. If the check fails, the global .bss array is cleared and an error is returned. If the check passes, some write threads and structs are initialized and a success value is returned.
The .bss buffer storing partition info (used by other functions) is copied to before the packet is verified
The .bss buffer is cleared when the signature mismatches, but not with this malloc fail...?
The flaw here is subtle, but not terribly difficult to notice: The number of partitions is user-controlled even though it is signed, and the partition info was copied into a global variable before verification. In all other error conditions, LAF will memset the partition information before returning an error code, however if the hashing allocation fails (ie by setting the number of partitions to -1), then the allocation will fail and an error is returned without clearing the partition information. Thus, we can fakesign our own update KDZs by
Sending a valid SIGN command, which will start the write threads
Sending a fakesigned SIGN command with the number of partitions set to -1, and all partition information set however we want. The partition information in .bss is now set without a signature being checked.
While this fakesign has the potential to hang WRTE commands while due to the number of partitions being set much larger than the global partition array, all loops when WRTE checks the partition list hashes will break once a valid partition is found. So, as long as the hash contents of the WRTE command are existent in the first few entries, it will not hang, however any writes sent that do not match will hang lafd.
Another S-EL3 vuln to wrap it all up
This might seem a bit pointless given that the former vulnerability paired with 🔋 📱❄️🥾🔓 at aboot is more than enough to unlock bootloaders, since aboot is usually the code that handles bootloader unlocking/wiping/boot image signature verification, but the downside to unlocking your bootloader is that you lose SafetyNet. To most effectively mitigate SafetyNet issues you basically need an S-EL3 exploit in order to patch Qualcomm's TrustZone to spoof a locked bootloader. While 🔋 📱❄️🥾🔓 has a vector for S-EL3 code execution via SBL1 and its charging graphic, it only triggers at extremely low battery voltages and it would be more convenient to find an alternative means to gaining S-EL3 code execution via aboot, which runs at EL2/EL1.
One of the first things I noticed when I began to look for SBL vulnerabilities, and actually the reason I looked at SBL in the first place is its crash handler. Since at least the Nexus 5, LG has shipped its "Demigod Crash Handler" which can print registers and stack information and RAM console logs from EL1 kernel, S-EL3 SBL, TrustZone, etc. I first discovered it while trying to exploit a kernel stack overflow. It also allows the user to dump memory contents over USB via its Sahara protocol which also gets used in PBL for Firehose bootstrapping.
Naturally, SBL cannot know the exact details of every execution environment it displays stack dumps for, it requires the faulting environment to store that information before warm-resetting into SBL. Consequently, this means there are portions of RAM writable by EL1 which will be later parsed at an S-EL3 execution level, and of course to make matters worse it also expects EL1 to handle the memory allocation for both the RAM console as well as for the framebuffer. These structures are also plainly visible in LG's kernel sources available in their Android OSS zips.
Above roughly shows the arbitrary write which is possible with this ramconsole parsing. The ramconsole offset is not bounds checked, so we are able to achieve an arbitrary write to a limited set of addresses based on ramconsole_offs, the limit being that the offset factors both into what you write and where you write it. However, I found that since DRAM takes up such a significant portion of the address space, it was more than enough to specifically write a function pointer to the stack. To keep the exploit as simple as possible, I chose to force console_init_maybe to return to the missing battery graphic draw routine, which then triggered 🔋 📱❄️🥾🔓 without the need to drain the battery below 1% and made loading additional payloads significantly easier.
As an interesting sidenote, this vulnerability is extremely similar to hexkyz's Wii U boot1 exploit, which also abuses warmboot behavior to take over the secondary bootloader of the Wii U's ARM boot processor. In that case, however, the Wii U encrypted its PRSH/PRST structure in RAM, and rather than displaying syslogs, it uses the structure to store boot timings and other info between IOS reboots.
For most practical usecases, this vulnerability is a bit difficult to exploit, due to SBL's text and stack differing between devices. However, S-EL3 vulnerabilities aren't all that frequently documented on Android, so I hope that it will at least be useful for anyone interested in examining Qualcomm's TrustZone components or avoiding weird SafetyNet junk.
Code for both of these exploits can be seen at https://github.com/shinyquagsire23/Q710-SIGNhax-EL3-Warmboot