Qualcomm IPQ40xx: Analysis of Critical QSEE Vulnerabilities

Tuesday, Mar 2, 2021

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:

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 not a surprise our upcoming training week is focused on exactly such two embedded security technologies.

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 - 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

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).


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.


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.


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!