visit
In Figure 1, you can see interaction of the FastRPC components:
/dev/adsprpc-smd
or /dev/cdsprpc-smd
) on the applications processor (AP) through relevant ioctls.
It should be noted that Google enforces protection of Pixel devices through SELinux policy preventing access of third-party apps and adb shell
to the DSP RPC drivers.
QuRT is a Qualcomm-proprietary multithreaded Real Time OS (RTOS) managing the Hexagon DSP. The integrity of the QuRT is trusted by Qualcomm’s Secure Executable Environment (QSEE). The QuRT executable binary (separate for aDSP and cDSP) is signed and split to several files in the same way as any other trusted application on Qualcomm devices. Its default location is the /vendor/firmware
directory.
For each Android process initiating the remote invocation, QuRT creates a separate process on the DSP. The special shell process (/vendor/dsp/fastrpc_shell_0
for aDSP and /vendor/dsp/fastrpc_shell_3
for cDSP) is loaded on the DSP when a user process is spawned. The shell is responsible for invocation of the skeleton and object libraries. In addition, it implements the DSP RPC framework providing the API that may be required for the skel and object libraries.
Let’s take a look at the second and the third arguments of the remote_handle_invoke
function, that encode the target method and its arguments.
scalars
is a word that contains the following metadata information:
pra
is a pointer to an array of arguments (remote_arg
entries) of the target method. The order of the arguments is the following: input arguments, output arguments, input handles, and output handles.
As you can see, each input and output argument is converted to a universal remote_buf
entry.
It should be noted that if we prepare more remote_arg
array entries than required by the target method, then extra parameters are just ignored by the skeleton library.
scalars
and pra
parameters are transferred “as is” through the DSP RPC driver and DSP RPC framework, and are used as the first and the second arguments of the special invoke
function provided by each skeleton library. For example, libfastcvadsp_skel.so
library provides the fastcvadsp_skel_invoke
invoke function. The invoke function is only responsible for calling appropriate skel methods by their index. Each skel method by itself verifies received remote arguments, unmarshals the remote_bufs
to regular types, and calls the object method.
As you can see, to invoke a method from a skel library, you only need to know its index and wrap each argument by the remote_buf
structure. The fact that we do not have to provide the name of the invoking function, types and number of its arguments to perform the call, makes skeleton libraries a very convenient target for fuzzing.
There are a lot of skeleton libraries pre-installed by Qualcomm on Android phones. The vast majority of them are proprietary. However, there are open source examples like libdspCV_skel.so
and libhexagon_nn_skel.so.
Many skeleton libraries such as libfastcvadsp_skel.so
and libscveBlobDescriptor_skel.so
can be found on almost all Android devices. However, libraries like libVC1DecDsp_skel.so
and libsysmon_cdsp_skel.so
are presented only on modern Snapdragon SoCs.
There are libraries implemented by OEMs and only used on devices of specific vendors. For example, libedge_smooth_skel.so
can be found on Samsung S7 Edge, and libdepthmap_skel.so
is on OnePlus 6T devices.
Generally, all skel libraries are located either in /dsp
or /vendor/dsp
or /vendor/lib/rfsa/adsp
directories. By default, the remote_handle_open function scans exactly these paths. In addition, there is an environment variable ADSP_LIBRARY_PATH
into which a new search path can be appended.
As was mentioned previously, all DSP libraries are signed and cannot be patched. However, any Android application can bring a signed by Qualcomm skeleton library in its assets, extract it to the app’s data directory, add the path to the beginning of the ADSP_LIBRARY_PATH
, and then open a remote session. The library is successfully loaded on the DSP because its signature is correct.
The fact that there is no version check of loading skeleton libraries opens the possibility to run a very old skel library with a known 1-day vulnerability on the DSP. Even if the updated skeleton library already exists on the device, it is possible to load the old version of this library just by indicating its location in the ADSP_LIBRARY_PATH
before the path of the original file. In this way, any DSP patch can simply be bypassed by an attacker. In addition, through analyzing DSP software patches, an attacker can find out an internally fixed vulnerability in a library and then exploit it by loading the unpatched version.
scalars
word and remote_arg
array.libfastcvadsp_skel.so
depends on libapps_mem_heap.so
, libdspCV_skel.so
and libfastcvadsp.so
lib. All these libraries can be extracted from a firmware or pulled from a real device.scalars
and a pointer
to remote_arg
array as arguments. For example, fastcvadsp_skel_invoke
is the start point for fuzzing of libfastcvadsp_skel.so
library.
For each output argument, we allocate memory of the indicated size and fill it with the value 0x1F.
Most skeleton libraries widely use DSP framework and system calls. Our simple program cannot handle such requests. Therefore, we had to load the QuRT on the emulator before execution of the rest code. The easiest way to do so is not to use the real QuRT OS but its “lite” version runelf.pbn
, adopted by Qualcomm for execution on a Hexagon simulator and included in the Hexagon SDK.
The AFL fuzzer permutes the content of the data file and triggers execution of runelf.pbn
on the emulator. The QuRT loads the prepared ELF binary which then calls a target skeleton library. QEMU returns a code coverage matrix to the AFL after execution of the test case.
The AFL fuzzer permutes the content of the data file and triggers execution of runelf.pbn
on the emulator. The QuRT loads the prepared ELF binary which then calls a target skeleton library. QEMU returns a code coverage matrix to the AFL after execution of the test case.
We were surprised by the fuzzing result. Crashes were found in all DSP libraries that we chose to fuzz. Hundreds of unique crashes were detected in the libfastcvadsp_skel.so
library alone.
Let’s take a look at the open source hexagon_nn
library, which is part of the Hexagon SDK 3.5.1. This library exports a lot of functions intended for neural network-related calculations.
Hexagon SDK automatically generates hexagon_nn_stub.c
stub and hexagon_nn_skel.c
skel models at the compilation time of the library. Some security issues can be easily detected by manually reviewing the modules. We will show only two of them.
int hexagon_nn_op_name_to_id(const char* name, unsigned int* node_id)
function requires one input (name
) and one output (node_id
) argument. The following stub code is generated by the SDK for marshaling these two arguments:
We can see that in addition to the existing two arguments, the third remote_arg
entry was created at the beginning of the _pra
array. This special _pra[0]
argument holds the length of the name
string.
The name
itself is saved in the second remote_arg
entry (_praIn[0]
), where its length will be stored again, but this time in the _praIn[0].buf.nLen
field.
The skel code extracts both these lengths and compares them as signed int
values. This is the bug. An attacker can ignore the stub code and write a negative value (greater than or equal to 0x80000000) into the first remote_arg
entry, bypassing this validation. This fake length is then used as a memory offset and causes a crash (read out of the heap boundary).
The same code is generated for all object functions that require string arguments.
Let’s take a look at the int hexagon_nn_snpprint(hexagon_nn_nn_id id, unsigned char* buf, int bufLen)
function that requires a buffer and its length as arguments. The buffer is used for both input and output data. Therefore, it is split into two separate buffers (the input and the output buffers) in the stub code. Once again, lengths of both buffers (_in1Len
and _rout1Len
) are stored in the additional remote_arg
entry (_pra[0]
).
The skel function copies (using _MEMMOVEIF
macro) the input buffer to the output buffer before calling the object function. The size of data to be copied is the length of the input buffer that was held in the special remote_arg
entry (_pra[0]
).
Type casting to the signed int
type on checking buffer boundaries is a bug leading to heap overflow.
To summarize, the automatically generated code injects vulnerabilities into the libraries of Qualcomm, OEMs and all other third-party developers who use the Hexagon SDK. Dozens of DSP skeleton libraries pre-installed on Android smartphones are vulnerable due to serious bugs in the SDK.
libfastcvadsp_skel.so
library can be found on most Android devices. In the example below, we use the library with version 1.7.1, extracted from the Sony Xperia XZ Premium device. A malicious Android application can cause the libfastcvadsp_skel.so
library to crash by providing specially crafted arguments to the remote_handle_invoke
function. The data file in Figure 5 shows an example of such crafted arguments.
As you can see, the 0x3F method is called and provided with one input and three output arguments. The content of the input argument begins with byte 0x14 and contains the following major fields:
The abbreviation POC code for reading the primitive is presented below.
Input arguments are always located in the DSP heap right after output arguments. Therefore, in the writing primitive, we need to shift the source address according to the length of the first output argument (all other arguments are empty).
An attacker can manipulate source and destination offsets for reading and writing in the address space of a DSP process (User PD). The offset between the first output argument and libfastcvadsp_skel.so
library in memory is a constant value. It is easy to find a pointer in a data segment of a skel or object library to trigger a call. For security reasons, we will not publish the rest of the POC of code execution in the DSP process.
libfastcvadsp_skel.so
libdepthmap_skel.so
libscveT2T_skel.so
libscveBlobDescriptor_skel.so
libVC1DecDsp_skel.so
libcamera_nn_skel.so
libscveCleverCapture_skel.so
libscveTextReco_skel.so
libhexagon_nn_skel.so
libadsp_fd_skel.so
libqvr_adsp_driver_skel.so
libscveFaceRecognition_skel.so
libthread_blur_skel.so
The libqurt.a
library, which is part of Hexagon SDK, contains the QDI infrastructure. The FastRPC shell is linked statically with the library.
Dozens of QDI drivers can be found in the QuRT executable binary. They are usually named as /dev/..
, /qdi/..
, /power/.
., /drv/..
, /adsp/..
or /qos/...
The int qurt_qdi_open(const char* drv)
function can be used to gain access to a QDI driver. A small integer device handle is returned. This is a direct parallel to the POSIX file descriptors.
The QDI provides only one macro that is the necessary user-visible API. This qurt_qdi_handle_invoke
macro is responsible for all generic driver operations. In fact, the qurt_qdi_open
is just a special case of this macro. These are the macro arguments:
A QDI driver uses the int qurt_qdi_devname_register(const char *name, qurt_qdi_obj_t *opener) API function to register itself in the QuRT. The driver provides its name and a pointer to an opener object as arguments.
The first field of the opener
object is the driver invocation function. QuRT calls this function to handle driver requests from the user PD or another driver, and provides the following arguments:
opener
object on which this QDI request is made.
In general, a driver invocation function is a switch operator by the QDI method ID. Each method can use a different number of arguments than the ones provided. The argument type is qurt_qdi_arg_t
.
Note that the driver invocation function is a good target for fuzzing-based vulnerability research because the methods are identified by ID but not by name, and the caller does not need to know the exact number of arguments and their actual type to invoke the driver method.
To fuzz QDI drivers on an Ubuntu PC, we used the same combination of QEMU Hexagon and AFL as for fuzzing DSP libraries. However, instead of the skel_loader
program, we implemented another Hexagon ELF binary qdi_exec
which is responsible for these actions:
QDI drivers are implemented as part of the QuRT ELF. They were not included by Qualcomm in the runelf.pbn version of QuRT which we ran on the emulator along with our program. Therefore, we had to patch the runelf.pbn ELF file as follows:
The AFL fuzzer permutes the content of the data file and triggers the execution of the patched runelf.pbn
on the emulator. The runelf.pbn
loads our qdi_exec program which directly calls a QDI driver invocation function.
We found the starting addresses of QDI driver invocation functions manually by reverse-engineering the QuRT binary. The opener
object is located in the code next to the driver name.
For research purposes, we successfully exploited several arbitrary kernel read and write vulnerabilities in the /dev/i2c
QDI driver and two code execution vulnerabilities in the /dev/glink
QDI driver. For security reasons, we cannot publish the POC code, but we do note that the exploitation is quite simple. This is an example of the reading primitive:
A malicious Android application can use discovered vulnerabilities in QDI drivers along with the described vulnerabilities in DSP libraries of the user PD to execute a custom code in the context of the DSP guest OS.
On a Pixel 4 device, startup commands for these daemons can be found in the init.sm8150.rc
file.
These highly privileged vendor.adsprpcd
and vendor.cdsprpcd
daemons handle DSP guest OS requests. They operate as the system user but at the same time they are very limited by SELinux. u:r:adsprpcd:s0
and u:r:cdsprpcd:s0
contexts have access only to DSP-related directories and objects.