It’s time to get technical! In our previous blog post we notified you already about the vulnerabilities we identified in Qualcomm’s Secure Execution Environment (QSEE). This Trusted Execution Environment (TEE) is found on many Qualcomm-based devices like mobile phones. The vulnerabilities we identified are applicable to (most) devices that are designed around the Qualcomm IPQ40xx SoC family, which is used by major manufacturers like Linksys, Netgear and Cisco. Qualcomm disclosed these vulnerabilities publicly during their January 2021 Security Bulletin after our coordinated disclosure process was finished.
These vulnerabilities allowed us to achieve code execution within QSEE, effectively providing us the highest privileges available on the device. Any attack able to issue a Secure Monitor Call (SMC) is able to exploit these vulnerabilities. In this blog post, we describe the following vulnerabilities:
- CVE-2020-11256 tzbsp_blow_fuses_and_reset (advisory)
- CVE-2020-11257 usb_calib (advisory)
- CVE-2020-11258 tzbsp_version_set (advisory)
- CVE-2020-11259 tzbsp_version_get (advisory)
Don’t worry if you are unfamiliar with TEEs and if the above sounds somewhat cryptic. We will describe step-by-step how we identified the vulnerabilities before we dive into the actual nitty gritty details. We finalize this post by explaining what impact these vulnerabilities have on a vulnerable device.
Target overview
The Linksys EA8300 is a AC2200 Wi-Fi Tri-Band Router. Some of the information we used was obtained from OpenWRT’s website and FCCID’s website. This device is designed around the Qualcomm IPQ4019 SoC of which a picture is shown below.
Our interest was immediately piqued after reading its product description as it supports two of our favorite security features Secure Boot and a TEE. At Raelize, we often analyze these technologies in order to provide adequate guidance to our clients.
It’s always interesting to start analyzing a new device in a black-box setting and with much anticipation we were looking forward to the activities that lied ahead. We never know what exactly to expect, but we may easily end up into our favourite activity: identifying and exploiting vulnerabilities.
Serial interface
Hardware hacking often starts with opening the device. The OpenWRT website actually provides useful instructions for it. Then, the next step is typically scoping out a serial interface, which, hopefully, provides a (root) shell. It’s not uncommon for these type of devices that a serial interface is clearly marked on the printed circuit board (PCB). The Linksys EA8300 we are analyzing exposes the serial interface on an unpopulated connector marked J3
as is shown in the picture below.
Again, very conveniently, the pin-out and other information required for communicating with the serial interface can be found on the device page of the OpenWRT website. This spares us the probing of the pins for determining the needed parametes. There’s no harm done standing on the shoulders of others!
Boot log
After connecting to the serial interface, we started the device. Immediately we are presented with a stream of interesting print statements. The printing that’s shown below is done by the PBL and SBL1 bootloaders, which are developed by Qualcomm. These are likely provided to the OEM (i.e. Linksys) as part of the IPQ40xx SDK. We’ve highlighted the most interesting print statements in red.
+ 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.1.1-00108
+ S - IMAGE_VARIANT_STRING=DAACANAZA
+ S - OEM_IMAGE_VERSION_STRING=CRM
+ S - Boot Config, 0x00000025
+ S - Reset status Config, 0x00000010
+ S - Core 0 Frequency, 0 MHz
- B - 261 - PBL, Start
+ B - 1339 - bootable_media_detect_entry, Start
+ B - 2612 - bootable_media_detect_success, Start
+ B - 2626 - elf_loader_entry, Start
+ B - 4036 - auth_hash_seg_entry, Start
+ B - 6190 - auth_hash_seg_exit, Start
+ B - 74253 - elf_segs_hash_verify_entry, Start
- B - 196174 - PBL, End
- B - 196198 - SBL1, Start
+ B - 288239 - pm_device_init, Start
+ D - 7 - pm_device_init, Delta
+ B - 289733 - boot_flash_init, Start
+ D - 87192 - boot_flash_init, Delta
+ B - 381232 - boot_config_data_table_init, Start
+ D - 13975 - boot_config_data_table_init, Delta - (419 Bytes)
+ B - 397970 - clock_init, Start
+ D - 7587 - clock_init, Delta
+ B - 408990 - CDT version:2,Platform ID:8,Major ID:1,Minor ID:0,Subtype:6
+ B - 412402 - sbl1_ddr_set_params, Start
+ B - 417495 - cpr_init, Start
+ D - 2 - cpr_init, Delta
+ B - 421878 - Pre_DDR_clock_init, Start
+ D - 4 - Pre_DDR_clock_init, Delta
+ D - 13170 - sbl1_ddr_set_params, Delta
+ B - 435181 - pm_driver_init, Start
+ D - 2 - pm_driver_init, Delta
+ B - 504960 - sbl1_wait_for_ddr_training, Start
+ D - 28 - sbl1_wait_for_ddr_training, Delta
+ B - 520319 - Image Load, Start
+ D - 143867 - QSEE Image Loaded, Delta - (269176 Bytes)
+ B - 664611 - Image Load, Start
+ D - 2116 - SEC Image Loaded, Delta - (2048 Bytes)
+ B - 674745 - Image Load, Start
+ D - 187171 - APPSBL Image Loaded, Delta - (444263 Bytes)
- B - 862309 - QSEE Execution, Start
+ D - 56 - QSEE Execution, Delta
- B - 868531 - SBL1, End
+ D - 674334 - SBL1, Delta
+ S - Flash Throughput, 2087 KB/s (715906 Bytes, 342873 us)
+ S - DDR Frequency, 672 MHz
If you are familiar with Qualcomm-based devices, you may recognize the typical boot flow where the PBL and SBL1 are printing timestamped log lines. If you’re interested, more details about the boot process of Qualcomm-based mobile phones is provided by this great blog post by Quarkslab. However, the boot process of our target devices has more commonalities with the one of older mobile phones, as shown in this advisory (2017) by Aleph Security.
Once the execution of the SBL1 completes, the control is passed to the U-Boot bootloader, which is a common bootloader responsible for loading Linux. Conveniently, we were able to break into the U-Boot console by pressing a key during boot, which is shown below.
+ U-Boot 2012.07 [Chaos Calmer 15.05.1,r35193] (Nov 02 2017 - 16:33:09)
+
- CBT U-Boot ver: 1.2.9
+
+ smem ram ptable found: ver: 1 len: 3
+ DRAM: 256 MiB
+ machid : 0x8010006
+ NAND: ID = 9590daef
+ Vendor = ef
+ Device = da
+ ONFI device found
+ SF NAND unsupported id:ff:ff:ff:ffSF: Unsupported manufacturer ff
+ ipq_spi: SPI Flash not found (bus/cs/speed/mode) = (0/0/48000000/0)
+ 256 MiB
+ MMC: qca_mmc: 0
+ PCI0 Link Intialized
+ In: serial
+ Out: serial
+ Err: serial
+ machid: 8010006
+ flash_type: 2
+ Net: MAC0 addr:0:3:7f:ba:db:ad
+ PHY ID1: 0x4d
+ PHY ID2: 0xd0b1
+ ipq40xx_ess_sw_init done
+ eth0
+
+ Updating boot_count ... done
+
- Hit any key to stop autoboot: 0
- (IPQ40xx) #
The U-Boot console typically includes very useful commands. However, it really depends on the device which commands are available, as the manufacturer is free to add or remove commands when deemed necessary. Luckily for us, the U-Boot console on this target is fairly rich and we are presented with lots of useful functionality.
ARM TrustZone refresher
In order to have a clear understanding of the different security boundaries let’s quickly revisit some basic concepts. The Rich Execution Environment (REE), or Non-secure World, is the environment where the typical user applications are executed. The Security Extensions of the ARMv7-A architecture, i.e. ARM TrustZone, introduce support for an additional Trusted Execution Environment (TEE), or Secure World, which is the environment where the security critical tasks are executed.
The underlying platform (i.e. hardware) is required to securely implement both these environments.
These two environments are distinguished by the Non-Secure (NS) bit (i.e. SCR.NS). This bit set to 1 for execution of REE code and set to 0 when executing TEE code. The transition between these two execution modes is governed by the Monitor mode, which traps the execution of Secure Monitor Call (SMC) instructions. More details about this technology is available in ARM’s Architecture Reference Manual for the ARMv7-A architecture.
When the IPQ40xx SoC is released from reset, execution starts at the highest level of privilege. This allows the code to have unrestricted access to the hardware. The Primary Boot Loader (PBL), implemented in the SoC’s read-only memory (ROM), loads the second stage bootloader (SBL1) into internal SRAM. The SBL1 is responsible for several things, including initializing the external DDR, loading QSEE from flash and loading U-Boot (i.e. APPSBL) from flash.
It’s important to raelize that the PBL and SBL1 must executed at the highest privilege level as they are responsible for loading the QSEE binary. Moreover, it’s likely that the U-Boot bootloader is running at a lower privilege as it’s only responsible for loading Linux.
Extracting the QSEE binary
The U-Boot console provides a convenient and powerful environment for accessing the flash. For example, we can use the smeminfo
command in order to get an overview of the flash partitions, which is shown below. The QSEE binary that we are after is actually stored in a dedicated partition.
+ (IPQ40xx) # smeminfo
+ flash_type: 0x2
+ flash_index: 0x0
+ flash_chip_select: 0x0
+ flash_block_size: 0x20000
+ flash_density: 0x100000
+ partition table offset 0x0
+ No.: Name Attributes Start Size
+ 0: 0:SBL1 0x0000ffff 0x0 0x100000
+ 1: 0:MIBIB 0x0000ffff 0x100000 0x100000
- 2: 0:QSEE 0x0000ffff 0x200000 0x100000
+ 3: 0:CDT 0x0000ffff 0x300000 0x80000
+ 4: 0:APPSBLENV 0x0000ffff 0x380000 0x80000
+ 5: 0:ART 0x0000ffff 0x400000 0x80000
+ 6: 0:APPSBL 0x0000ffff 0x480000 0x200000
+ 7: u_env 0x0000ffff 0x680000 0x80000
+ 8: s_env 0x0000ffff 0x700000 0x40000
+ 9: devinfo 0x0000ffff 0x740000 0x40000
+ 10: kernel 0x0000ffff 0x780000 0x5800000
+ 11: rootfs 0x0000ffff 0xa80000 0x5500000
+ 12: alt_kernel 0x0000ffff 0x5f80000 0x5800000
+ 13: alt_rootfs 0x0000ffff 0x6280000 0x5500000
+ 14: sysdiag 0x0000ffff 0xb780000 0x100000
+ 15: syscfg 0x0000ffff 0xb880000 0x4680000
+ (IPQ40xx) #
Extracting the flash contents is fairly easy using the commands provided by the U-Boot console. First, we use the nand
command to read the flash contents to volatile memory (e.g. SRAM or DDR). Then, we use the tftpput
command to dump the flash contents from volatile memory via the network to our TFTP server. This allows us to extract the entire flash without any soldering. Let’s extract the QSEE partition!
We use the python-based TFTP server named ptftpd
which is very convinient to use.
$ ptftpd -p 6969 eth0 /home/raelize/Desktop
Then, we set the serverip
environment variable within the U-Boot console and point it to our workstation.
(IPQ40xx) # setenv serverip 192.168.1.128
Now we need to load the QSEE paritition in flash into memory. But where do we load it? Well, typically we can simply use an unused area within the DDR memory. We determined using the print statements by the U-Boot bootloader that the DDR memory is likely mapped at address 0x8000000
as the entry point of the Linux Kernel is 0x80208000
. We simply guessed address 0x89000000
is unused and can be used to temporarily store the QSEE partition.
(IPQ40xx) # nand read 0x89000000 0x200000 0x100000
Then, we use the tftpput
command to send the QSEE partition to our TFTP server.
(IPQ40xx) # tftpput 0x89000000 0x100000 QSEE.bin
If all went well, we have succesfully extracted the QSEE partition! :)
Analyzing the QSEE binary
The QSEE partition is actually a flat binary that can be analyzed directly. Unfortunately, being a flat binary, there is no meta data present in the binary which could tell us about its structure. At least, we know that the IPQ4019 SoC implements the ARMv7 architecture and therefore we know to expect ARM AArch32 Little Endian (LE) code. We load the QSEE binary into IDA Pro and select the ARM32 Little Endian architecture. We quickly determined that the loading address of the QSEE binary is 0x87E80000
by analyzing the absolute addresses used by the code.
The ARMv7 exception vector is found at the start of the QSEE binary. It’s used to handle the processor’s exceptions, including the exception caused when a SMC instruction is executed. The exception vector is standardized and therefore we could simply define the correct names ourselves.
We easily identified the code responsible for handling the SMC instruction by following the Software Interrupt exception handler. This code extracts the SMC ID from register r0
in order to determine which SMC handler routine should be called. We determined that all SMC handler routines are defined in a table located at address 0x87EB465C
.
The SMC handler routines can be called using their unique SMC ID, which is also present in the table. For example, the SMC handler routine tzbsp_pil_init_image_ns
can be called by using the SMC ID 0x805
. The table also contains other useful information when reverse engineering the code, like the name of the SMC handler routine.
Secure range checks
The memory is partitioned in Secure and Non-secure memory, using hardware controllers that are configured when the TEE is initialized. This is likely done by the SBL1 bootloader during boot. All code and data related to QSEE, including any Trusted Application (TA), should be stored within secure memory. In other words, none of the code and data used by QSEE should be accessible by the REE.
The REE passes the SMC handler routine’s arguments by register. For example, ARG1 is stored in register R1
, ARG2 is stored in R2
and so on. Buffers are passed by reference using memory that’s accessible by both the REE and TEE. Typically this is simply just non-secure memory. As QSEE has no knowledge of the REE’s virtual mapping, all pointers passed by the REE should point to physical memory.
It’s a QSEE responsibility to carefully check the arguments received from the REE. QSEE should check whether the buffer passed by the REE, described by a pointer and a size argument, is not located within secure memory. While analyzing the SMC handler routines, we’ve identified the functions responsible for performing these range checks.
The function naming is done by us. Nonetheless, we aimed to remain somewhat consistent with the naming used by Gal Beniamini in his excellent exploitation of TEE vulnerability.
The function tzbsp_is_nsec_range
validates the buffer passed by the REE using the is_allowed_range
function. This function uses a table with secure ranges to determine what memory should be considered secure memory. This function checks, among a few other things, if the start of the buffer (i.e. pointer
) and end of the buffer (i.e. pointer
+ size
) are overlapping with secure memory.
The table, that defines three secure ranges, is shown below.
This means, that whenever the tzbsp_is_nsec_range
function is used to sanitize the SMC handler routine’s arguments, the buffer passed by the REE cannot overlap with: 0x0
to 0x7ffffff
, 0x90000000
to 0xffffffff
and 0x87E80000
to 0x87ffffff
. In other words, buffers are only considered to be allowed when they are between 0x80000000
to 0x87E80000
and 0x88000000
to 0x90000000
.
Until now, everything looks secure!
Identifying QSEE vulnerabilities
It’s expected that functionality exists to sanitize the arguments passed to the SMC handler routines. However, it would definitely not be the first time that such functionality is not used, or used incorrectly. Therefore, it’s always a good idea to first explore the code handling data under attacker control.
Long story short, we’ve identified 4 vulnerable code paths where the arguments passed to the SMC handler routines are not properly sanitized. The range checks were either not used at all or used incorrectly.
CVE-2020-11256 tzbsp_blow_fuses_and_reset
The SMC handler routine named tzbsp_blow_fuses_and_reset()
requires two arguments, namely arg1
and arg2
. The decompilation for this routine is shown below.
The argument arg1
is an input pointer, which is sanitized using the function is_allowed_range()
. The argument arg2
is an output pointer, which is only checked to be NULL
. The argument arg2
is dereferenced in order to write a value to an arbitrary memory address. The value that’s written, is either 1
or 2
, which depends on whether arg1
is set to 0
or not.
This vulnerability allows an attacker to write 1
or 2
to an arbitrary address accessible by QSEE, including QSEE memory. Even though it’s not possible to write arbitrary memory, these type of restricted writes may lead to arbitrary QSEE code execution (i.e. compromise the TEE).
CVE-2020-11257 usb_calib
The SMC handler routine named usb_calib()
requires one parameter, namely arg1
. The decompilation for this routine is shown below.
The argument arg1
is an output pointer which is not sanitized. This means that there is no check to assure that arg1
points to REE memory. It’s dereferenced in order to write the value stored at 0x580e0
to the passed memory address. For our target device this value is set to 0x787
.
This vulnerability allows an attacker to write 0x787
to an arbitrary address accessible by QSEE, including QSEE memory. Even though it’s not possible to write arbitrary values, these type of restricted writes may lead to arbitrary QSEE code execution (i.e. compromise the TEE).
CVE-2020-11258 tzbsp_version_set
The SMC handler routine named tzbsp_version_set()
requires four arguments, namely arg1
, arg2
, arg3
and arg4
. The decompilation for this routine is shown below.
All four arguments are passed into a function (i.e. sub_87E9056
), which returns different values depending on the value of arg1
, arg2
and arg4
. The argument arg3
is an output pointer and the code does not assure it points only to REE memory. It’s dereferenced in order to write the return value of sub_87E9056
(i.e. at least 0
, 5
and 0xfffffffc
) or 0x7fffffff
to the passed memory address.
This vulnerability allows an attacker to write various values to an arbitrary address accessible by QSEE, including QSEE memory. Even though it’s not possible to write arbitrary values, these type of restricted writes may lead to arbitrary QSEE code execution (i.e. compromise the TEE).
CVE-2020-11259 tzbsp_version_get
The SMC handler routine named tzbsp_version_set()
requires four arguments, namely arg1
, arg2
and arg3
. The decompilation for this routine is shown below.
The parameter arg1
is used as input and can be used to influence the value being written to the output pointer arg2
. The arguments arg2
and arg3
are both output pointers and the code does not assure that they point to REE memory.
The arg2
argument is dereferenced in order to write return value of sub_87E904CE()
or sub_87E90370() | 0xf0000
to an arbitrary memory address. This includes different values such as 0x0
, 0x000f0000
and 0xfffffffc
. Moreover, the arg3
argument is dereferenced in order to write 0x0
to the passed memory address.
This vulnerability allows an attacker to write various values to an arbitrary address accessible by QSEE, including QSEE memory. Even though it’s not possible to write arbitrary values, these type of restricted writes may lead to arbitrary QSEE code execution (i.e. compromise the TEE).
Exploitation
The above vulnerabilities require the ability to issue an SMC request to QSEE, either directly or indirectly. “Directly” can be achieved by executing any code in the REE with sufficient privileges to execute an SMC instruction (i.e. kernel or even higher privileges). “Indirectly” can be achieved by leveraging functionality that’s already present on the device (i.e. a driver).
In other words, an attacker in control of e.g. the Linux Kernel or U-Boot, is able to directly exploit the vulnerabilities we identified. It’s not sufficient for an attacker to have user privileges, even root privileges do not allow to execute SMC instructions. Nonetheless, an attacker with root privileges will likely have the ability to load kernel modules and/or inject code via /dev/(k)mem.
It’s important to raelize that a non-privileged attacker may still be able to leverage functionality that communicates with QSEE and be able to issue SMC calls. These type of drivers are often available for unprivileged applications. Whether the vulnerabilities are exploitable, really depends on what control the attacker has on the SMC arguments.
Impact
An attacker that’s able to successfully exploit the vulnerabilities, is able to:
- get unrestricted access to the underlying hardware
- gain full control of QSEE and the assets it protects
- escalate privileges in the REE (e.g. from user to kernel)
- bypass any security features implemented by QSEE (e.g. IPS, AV)
The Linksys EA8300 does not use QSEE for anything relevant during runtime, as far as we can tell. No Trusted Application (TA) is present. This means the attack surface from an unprivileged application is likely minimal.
The largest risk is that remote attackers (i.e. malware), already in full control of the REE, can hide execution of his code in the secure world. Effectively, this means the REE will be unable to detect if the device is compromised or not. Assuming Secure Boot is implemented securely, it will be required to exploit the REE first before the QSEE vulnerabilities can be exploited. In our research we did not explore ways to modify QSEE persistently (i.e. within the flash), although this may be definitely a possibility under certain conditions.
Conclusion
In this blog post we described our approach for identifying and analyzing the vulnerabilities we discovered in the Qualcomm TEE, named QSEE, as implemented on Qualcomm IPQ40xx-based devices.
Our next blog post will dive into the exploitation of one of the vulnerabilities. We demonstrate that arbitrary code execution within QSEE is possible by leveraging the restricted writes the vulnerabilities grant us.
Feel free to contact us if you have any questions and stay tuned for the next blog post!
- Raelize.