Eight months ago, the Zimperium research team (zLabs) disclosed to Qualcomm two kernel vulnerabilities affecting all Android devices with Qualcomm chipsets.
These vulnerabilities, combined with other vulnerabilities previously discovered by zLabs (CVE-2017-13253, CVE-2018-9411 and CVE-2018-9539) could allow a malicious app which was not given any permissions to achieve full root capabilities. The potential negative impact for enterprises are many. Meaning, the app could perform actions like:
- Reading all private data stored on the device, like photos, videos, text messages (including end-to-end encrypted messaging apps like WhatsApp or Telegram), email attachments or practically anything else you can think of that is on the device.
- Obtaining all credentials to any app the user is logged onto. With the username and password, the attacker has direct access to valuable company or personal data.
- Recording video/audio through camera and microphone without the user knowledge.
After we reported these vulnerabilities, Qualcomm designated CVE-2019-14040 and CVE-2019-14041 for them and published fixes as part of their February security bulletin.
In this blog post, I will go over the technical details of the two vulnerabilities. Links to the proof-of-concepts for both vulnerabilities are available at the end of the blog post.
The driver where the vulnerabilities lie is QSEECOM, which stands for QTI Secure Execution Environment Communicator. This driver allows Normal World user-space processes to communicate with the Secure World, AKA the TrustZone. The API exposed by the driver consists of ioctl calls to the /dev/qseecom device.
While only a set of privileged Android services has access to /dev/qseecom, I have described multiple vulnerabilities affecting this sort of services in previous blog posts. Therefore, this blog post is a continuation of these previous blog posts, describing the next step in achieving complete kernel privileges.
The vulnerabilities themselves are:
- CVE-2019-14041 – A race condition which can cause all sorts of memory corruptions, depending on where exactly the race hits.
- CVE-2019-14040 – A use after free of a kernel memory mapping.
As the race condition is less reliable and can cause different results, I believe the use after free is more interesting. Therefore, I am going to expand a bit more about the use after free vulnerability.
Race condition – CVE-2019-14041
The race condition issue stems from one function: __qseecom_update_cmd_buf (and also its 64 bit equivalent __qseecom_update_cmd_buf_64 which for the sake of the race condition operates the same way). In short, the purpose of this function is to update a buffer intended to be sent to the TrustZone with pointers the TrustZone can understand.
In order to avoid duplicate code the function is designed to handle two different cases in which such buffers are needed to be modified (unfortunately no such design exists for handling 64 bit cases; the 64 bit function has a lot of code duplication). Therefore, this function can be reached from two completely different ioctls. The thing is, in some parts the function still has to behave differently depending on which ioctl it was originally called from. In order to do this, the function checks data->type:
As can be seen, in one case data->type would be QSEECOM_CLIENT_APP. In the other case it would be QSEECOM_LISTENER_SERVICE. Checks like this exist all throughout the function.
The issue is that data is actually a pointer to a qseecom_dev_handle struct, which is attached to the open /dev/qseecom file the ioctl is being performed on. What if it is possible to modify data->type while __qseecom_update_cmd_buf is running?
Turns out that running the ioctl QSEECOM_IOCTL_APP_LOADED_QUERY_REQ instantly modifies data->type! Even though there are some mechanisms to limit ioctls from running concurrently, they only come into effect after data->type was changed. Running this ioctl at the same time __qseecom_update_cmd_buf is running would cause a race condition.
The function will start with a behavior that matches a data->type of QSEECOM_LISTENER_SERVICE but will continue with a behavior that matches a data->type of QSEECOM_CLIENT_APP.
This can cause different types of memory corruption, depending on where exactly the race hits. While most of these are relatively harmless like a NULL dereference, some conditions can cause a buffer overflow, which is actually dangerous and can be possibly exploited. That is the vulnerability.
Use after free – CVE-2019-14040
In order to let Normal World user-space processes communicate with the TrustZone the ION mechanism is used (the reason for this is unfortunately out of the scope of this blog post). In short, the ION mechanism allows user-space processes to allocate memory out of special heaps which behave differently than other regular memory. The user-space processes can then map it into their own memory space and read/write to it as they wish.
The way ION works with QSEECOM is that an open /dev/qseecom file can be made to reference an allocated ION buffer. The kernel then knows to use that buffer for further communications with the TrustZone. While it’s mostly intended for the user-space process to read/write from that ION buffer, the kernel itself can sometimes modify it as well (done by __qseecom_update_cmd_buf which was described in the previous section).
The code above handles a request from user-space to reference an allocated ION buffer. As can be seen, multiple parameters regarding the ION buffer are saved, like a handle and a mapping of it in kernel space (data is the same as in the previous section, a pointer to a struct which is attached to the open /dev/qseecom file).
One thing to note is that when releasing the ION buffer the kernel handle to it is set to NULL:
This possible NULL is then used when trying to use the ION buffer from the kernel. The handle is verified to not be NULL:
Let’s take a closer look at the code which references an ION buffer from an open /dev/qseecom file:
Obviously, this code doesn’t just allow any request from user-space without checking it first. There are multiple checks along this function to completely verify the request coming from user-space. The check displayed in the code above is one example; if the length in the request is more than the length of the ION buffer (the len variable) then the request is invalid.
The issue, though, is exactly in this check! What would happen to the parameters in data if the length in the request is too big? More importantly, what would happen if that open /dev/qseecom file already referenced an ION buffer before but that was freed?
In this scenario, the handle would no longer be NULL, but the rest of the parameters won’t be modified, so they will point to the previously referenced ION buffer! For instance, sb_virt would point to a memory mapping that was already freed. This is all while attempts to use the referenced ION buffer from the kernel should work, since, again, the handle is no longer NULL. This is the use after free! When attempting to use the ION buffer the kernel will modify a memory mapping that was already freed.
These vulnerabilities could allow an attacker to reach full root/kernel privileges. Especially the use after free, as that one is way more reliable than the race condition. As was mentioned in the intro, this blog post is essentially a continuation of a previous set of blog posts describing vulnerabilities in privileged Android services. In theory, it could be possible for a completely unprivileged attacker to create a chain out of these vulnerabilities in order to achieve complete root privileges.
Full source code for PoCs for both vulnerabilities is available on GitHub:
- July 31, 2019 – Vulnerabilities discovered
- August 4, 2019 – Vulnerabilities details + PoCs sent to Qualcomm
- November 4, 2019 – Qualcomm says they sent patches to vendors
- February 3, 2020 – Qualcomm published security bulletin
- March 3, 2020 – Samsung distributed fixes as part of their March security update
- April 6, 2020 – Google distributed fixes as part of their April security update
If you have any questions, you are welcome to DM me on Twitter (@tamir_zb). To learn more about how Zimperium delivers protection against device, network, phishing and malicious app attacks, please contact us.