We have identified a new stack buffer overflow vulnerability in Mediatek’s Linux Kernel driver implementation of cellular-to-application processor communication interface (CCCI). The vulnerability can be exploited by a malicious (compromised) baseband runtime to achieve arbitrary code execution in the Linux Kernel.

The vulnerability we are disclosing in this advisory affected a wide range of Mediatek devices, including phones on the newest chipsets (Dimensity 700, 1000, etc). The July 2022 issue of the Mediatek Security Bulletin contains this vulnerability as CVE-2022-21766.

Vulnerability Details

There is a kernel stack buffer overflow vulnerability in the implementation of the modem-kernel communication interface. The stack overflow can be used to overwrite the return address of a kernel function, with attacker controlled data.

The modem and the kernel exchange messages through a series of ring-buffers that reside in a shared memory region. The meta data of these ring-buffers are also stored in-line within the shared memory, thus both the kernel and modem can freely modify them. The vulnerability is the result of the kernel inherently trusting these values and failing to verify them.

When the modem sends a message to the kernel, the ccci_ringbuf_readable() function is called (defined in drivers/misc/mediatek/eccci/hif/ccci_ringbuf.c) to receive the message header and footer from the shared memory and evaluate the message size. The offsets and length fields are taken from the ring-buffer header (struct ccci_ringbuf), which is also stored in the shared memory, and they are used without further verification.

The header and a footer is read from the shared memory into 8 byte-long stack buffers. Due to an integer overflow, when the ring-buffer overwrap is calculated, the vulnerable function copies unlimited data, from the shared memory, into these stack buffers. The extract of the vulnerable function is presented below.

int ccci_ringbuf_readable(int md_id, struct ccci_ringbuf *ringbuf)
{
  unsigned char *rx_buffer, *outptr;
  unsigned int read, write, ccci_pkg_len, ccif_pkg_len;
  unsigned int footer_pos, length;
  unsigned int header[2] = { 0 };
  unsigned int footer[2] = { 0 };
  int size;

  [...]
  // [0] Offsets and lengths are retrieved from SHMEM
  read = (unsigned int)(ringbuf->rx_control.read);
  write = (unsigned int)(ringbuf->rx_control.write);
  length = (unsigned int)(ringbuf->rx_control.length);
  rx_buffer = ringbuf->buffer;
  size = write - read;
  if (size < 0)
    size += length;

  CCCI_DEBUG_LOG(md_id, TAG,
  "rbrdb:rbf=%p,rx_buf=0x%p,read=%d,write=%d,len=%d\n",
  ringbuf, rx_buffer, read, write, length);
  
  // [1] Size can be an arbitrary controlled value, this check can pass
  if (size < CCIF_HEADER_LEN + CCIF_FOOTER_LEN + CCCI_HEADER_LEN)
    return -CCCI_RINGBUF_EMPTY;
  outptr = (unsigned char *)header;
  // [2] The header is read into the stack buffer
  CCIF_RBF_READ(rx_buffer, outptr, CCIF_HEADER_LEN, read, length);
  if (header[0] != CCIF_PKG_HEADER) {
    CCCI_NORMAL_LOG(md_id, TAG,
    "rbrdb:rbf=%p,rx_buf=0x%p,read=%d,write=%d,len=%d\n",
    ringbuf, rx_buffer, read, write, length);
    CCCI_ERROR_LOG(md_id, TAG,
      "rbrdb:rx_buffer=0x%p header 0x%x!=0xAABBAABB\n",
      rx_buffer, header[0]);
    ccci_ringbuf_dump(md_id, "readable",
      rx_buffer, read, length, size);
    return -CCCI_RINGBUF_BAD_HEADER;
  }
  [...]
  return ccci_pkg_len;
}

#define CCIF_RBF_READ(bufaddr, output_addr, read_size, read_pos, buflen)\
do {\
  // [3] If read_pos (rx-read) is larger than buflen (rx-length) the else branch get executed
  if (read_pos + read_size < buflen) {\
    rbf_memcpy((unsigned char *)output_addr,\
      (unsigned char *)(bufaddr) + read_pos, read_size);\
  } else {\
    // [4] If read_pos (rx-read) larger than buflen (rx-length) the size underflows, resulting in a stack overflow
    rbf_memcpy((unsigned char *)output_addr,\
    (unsigned char *)(bufaddr) + read_pos, buflen - read_pos);\
    output_addr = (unsigned char *)output_addr + buflen - read_pos;\
    rbf_memcpy((unsigned char *)output_addr, \
      (unsigned char *)(bufaddr),\
      read_size - (buflen - read_pos));\
  } \
} while (0)

The ring-buffer implementation is not prepared for the scenario when the rx-read offset is higher than the rx-length. This is an invalid state that should never happen during normal operation, however a compromised modem can set such values in the shared memory. When this occurs, the length of the memcpy at (4) would underflow, since its value is calculated as rx-length - rx-read. The result is a large copy, from the shared memory, into the 8 byte-long stack buffer, in a function that does not have stack protector enabled on the observed devices. Due to the integer wrap, unless the copy is interrupted by preempting the executing kernel thread, the overflow invariably causes a data abort violation eventually.

Affected Devices

All Mediatek chipsets containing the CCCI kernel driver, namely: MT6580, MT6735, MT6737, MT6739, MT6753, MT6761, MT6765, MT6768, MT6771, MT6779, MT6781, MT6785, MT6833, MT6853, MT6873, MT6877, MT6879, MT6883, MT6885, MT6889, MT6893, MT6895, MT6983, MT8321, MT8666, MT8667, MT8675, MT8765, MT8766, MT8768, MT8786, MT8788, MT8789, MT8791, MT8797

Fix

Mediatek OTA images, released after July 2022, contain the fix for the vulnerability.

Timeline

  • 2022.01.31. Bug reported to Mediatek PSIRT
  • 2022.04.19. Mediatek confirms vulnerability
  • 2022.05.03. Mediatek confirms CVE
  • 2022.07.04. Mediatek releases security bulletin
  • 2023.08.26. TASZK informs Mediatek of advisory disclosure plan
  • 2023.08.26. Mediatek requests two more months of delay
  • 2023.11.03. Vulnerability released at Hardwear.io
  • 2023.11.26. Advisory release