Summary

In this advisory we are disclosing a heap overflow vulnerability in the MediaTek baseband. The vulnerability can be exploited to gain arbitrary code execution in the context of the baseband runtime. The vulnerability was fixed in 2020 in some models, and received a CVE and more widely deployed fix in 2021.

Vulnerability Details

When processing the CSN.1 decoding of the “E-UTRAN Individual Priority Parameters” element, the function rr_decode_eutran_individual_priority_para_description implements a two-depth nested repetition (Repeated Individual E-UTRAN Priority Parameters Description struct and its child element EARFCN).

The outer loop is iterated by checking on the single bit representing the ongoing repetition, and while that equals “1”, a new Repeated Individual E-UTRAN Priority Parameters Description struct is processed. Within that struct, the repeated EARFCN is handled in a different way, which could be described as “count-and-allocate”: first iterate over all the repeating elements and count them, then wind back the CSN.1 stream to the beginning of repetition, allocate memory based on the number of items and this time iterate over again, but put the values into the allocated memory.

As the commented decompiled pseudocode below shows, the amount of the stream windback is calculated by the following formula: number_of_earfcn_items * 17 + 4, which is reasonable as one repeated item consists of 16 useful bits plus one extra repeate-signalling bit (that’s 17 bits per iteration), a repetition-closing “0” bit and also 3 bits for the priority number. But the calculation is performed on 8-bit wide numbers (like uint8_t), thus when number_of_earfcn_items reaches 15, the computation overflows and the required 259 bits of rewind becomes 3 bits, essentially corrupting the CSN.1 stream. And this rewinded and corrupted CSN.1 stream goes over the repeated-element parsing again.

void FDD_rr_decode_eutran_individual_priority_para_description(byte **bs,undefined *global_context_ptr)

{
  char eutran_priority;
  int global_context_;
  int has_default_priority;
  undefined uVar1;
  uint current_earfcn;
  int is_repeated_individual_priority_param_end;
  int iVar1;
  void *new_earfcn_memory;
  void *pvVar2;
  int iVar5;
  uint number_of_earfcn;
  undefined4 *write_ptr;
  void *previous_earfcn_memory;
  int iVar8;
  uint previous_earfcn_number;
  uint new_earfcn_number;
  
                    /* global context to store the decoded ind. prio. list */
  global_context_ = *(int *)global_context_ptr;
  has_default_priority = FDD_rr_bit_stream_read(bs,1);
  uVar1 = 0x2b;
  if (has_default_priority == 1) {
                    /* { 0 | 1 < DEFAULT_E-UTRAN_PRIORITY : bit(3) > } */
    current_earfcn = FDD_rr_bit_stream_read(bs,3);
    uVar1 = (undefined)current_earfcn;
  }
  *(undefined *)(global_context_ + 0x48) = uVar1;
  dhl_brief_trace(6,0,(byte *)0xf0000c6,PTR_s_ch_9068afd0);
                    /* repetition of ind. E-UTRAN prio. params */
  while (FDD_rr_bit_stream_read(bs,1) != 0) {
    number_of_earfcn = 0;
    while( true ) {
      iVar1 = bit_stream_read(bs,1);
      if (iVar1 == 0) break;
      bit_stream_read(bs,0x10);
      number_of_earfcn = number_of_earfcn + 1 & 0xff;      /* number_of_earfcn is byte! */
    }
    eutran_priority = bit_stream_read(bs,3);

    /*
     * VULNERABILITY:
     *   bit_stream_move_back 2nd parameter susceptible to integer overflow.
     *   the calculation here can be overflown, 
     *   so stream will be misaligned by 256 bits!
     */
    bit_stream_move_back(bs,((number_of_earfcn * 0x11 + 1) & 0xff) + 3 & 0xff);


    global_context_priority_ptr = global_context_ + (int)eutran_priority * 8;
    previous_earfcn_number = *(byte *)(global_context_priority_ptr + 0x4c);
    if (number_of_earfcn != 0) {
      previous_earfcn_memory = *(void **)(global_context_priority_ptr + 0x50);
                    /* here allocate the memory based on the previous iteration */
      if (previous_earfcn_memory == (void *)0x0) {
        pvVar2 = get_ctrl_buffer_ext(number_of_earfcn << 2,PTR_s_pcore/modem/gas/common/src/rr_lt_9068afd4,0x883);
        *(void **)(global_context_priority_ptr + 0x50) = pvVar2;
      }
      else {
        new_earfcn_memory = get_ctrl_buffer_ext((previous_earfcn_number + number_of_earfcn) * 4, PTR_s_pcore/modem/gas/common/src/rr_lt_9068afd4,0x878);
        *(void **)(global_context_priority_ptr + 0x50) = new_earfcn_memory;
        memcpy(new_earfcn_memory,previous_earfcn_memory,previous_earfcn_number << 2);
        free_ctrl_buffer_ext(previous_earfcn_memory,PTR_s_pcore/modem/gas/common/src/rr_lt_9068afd4,0x87e);
      }
      new_earfcn_number = previous_earfcn_number + number_of_earfcn;
      *(byte *)(global_context_priority_ptr + 0x4c) = new_earfcn_number;
    }
                    /* start the processing again, and write to the allocate buffer 
                      There is no counter-based stop condition, only stream-based! */
    while (FDD_rr_bit_stream_read(bs,1) != 0) {
      /*
       * OVERFLOW: 
       *   on second iteration, write_ptr may end up pointing
       *   outside the allocated heap buffer
       */
      write_ptr = global_context_priority_ptr + 0x50 + previous_earfcn_number * 4;
      current_earfcn = FDD_rr_bit_stream_read(bs,0x10);
      *write_ptr = current_earfcn;
      dhl_brief_trace(6,0,(byte *)0xf0000c7,PTR_DAT_9068afd8);
                    /* max 256*4 byte overwrite */
      previous_earfcn_number = previous_earfcn_number + 1 & 0xff;
    }
    FDD_rr_bit_stream_read(bs,3);
  }
  return;
}

To be precise, there are actually TDD and FDD versions of this function, but the implementation is identical for both of them. So in practice we can think about the integer overflow to heap overflow as one issue, but there are two functions that have the same vulnerable code in the binary (FDD_rr_decode_eutran_ individual_priority_para_description and TDD_rr_decode_eutran_individual_priority_para_description).

Brief Impact Analysis

To read more about the exploitation of this heap overflow, check out our companion blog post which also provides details on the reverse engineering of the MediaTek baseband firmware to find the vulnerability.

Mitigation

MediaTek security update from September 2021 provides the fixes for this issue.

Timeline

  • 2019.12.11. Vulnerability reported to Huawei PSIRT (Huawei uses Helio chipsets in some devices)
  • 2019.12.16. Huawei PSIRT sends the report to MediaTek
  • 2020.02.26. MediaTek completes analysis and provides fix for their affected devices
  • 2021.04.23. We request CVE for publication, Huawei PSIRT indicates it will be handled by MediaTek and should be requested directly
  • 2021.05.06. Vulnerability reported directly to the MediaTek product security team
  • 2021.05.19. MediaTek assigns CVE-2021-32484, embargo until August 2021 agreed
  • 2021.09.01. MediaTek releases advisory, vulnerability rated Medium/DoS
  • 2022.01.10. Blog draft shared with MediaTek and Huawei
  • 2022.01.12. MediaTek upgraded vulnerability rating to High/RCE, vendors ask for a small delay to notify customers and update advisory
  • 2022.01.21. Delay requested by MediaTek ends
  • 2022.02.28. Delay requested by Huawei ends
  • 2022.03.10. Report released publicly