impalabs space base graphics
Huawei TrustZone CHINADRM_COMMON_TA Vulnerabilities
This advisory contains information about the following vulnerabilities:

We identified 3 vulnerabilities affecting Huawei's CHINADRM_COMMON_TA that can lead to compromise of the trusted application executing as S-EL0:

  • lack of locking when accessing global variables, leading to double-free and use-after-free issues
  • opening sessions is possible prior to initialization, leading to null pointer dereferences
  • session IDs are pointers, leading to information leak of heap pointers

We also identified an additional bug affecting Huawei's CHINADRM_COMMON_TA trusted application:

  • wrong memcpy_s destination sizes in CencDecrypt, leading to potential ION buffer overflow

Lack of Locking when Accessing Global Variables

As visible in the logs when the trustlet is loaded, it is multi sessions. As a result, proper care must be taken when accessing or modifying global variables. Unfortunately, this trustlet is not doing any kind of locking, so many race conditions issues arise.

[GTask] TA name: CHINADRM_COMMON_TA, UUID: 95b9ad1e, ELF: 361867, stack: 300000, heap: 1179648, multi session: True, keepalive: False, singleInstance: True

As evidenced in the snippets of code below, no lock is taken when accessing the sessions list.

int g_sessions_count;      /* in the BSS */
list_head g_sessions_list; /* in the BSS */

int CDRMC_FindSession(container_t *container, session_t **session_p) {
    // ...
    if (g_sessions_count && container) {
        if (list_contains(&container->list, &g_sessions_list)) {
            *session_p = container->session;
            return 0;
        } else { /* ... */ }
    } else {
        // ...
        return -1;
    }
    // ...
}

As a result, it possible to have a race condition where two cores are in the same instructions window in CDRMC_CloseSession, resulting in a session being freed twice.

Note: It is also possible to trigger use-after-frees as well, by having one core make use of the session (reading or writing to it), while another core is freeing it.

int CDRMC_CloseSession(container_t *container) {
    // ...
    if (CDRMC_FindSession(container, &session)) { /* ... */ }
    /* --- start of the race window --- */
    if (container->session) {
        LicenseExtractInfoCleanup(&container->session->inner);
        CDRMR_SecureMemory_Free(container->session);
        container->session = NULL;
    }
    /* ---  end of the race window  --- */
    list_del(&container->list);
    CDRMR_SecureMemory_Free(container);
    --g_sessions_count;
    // ...
}

A proof of concept triggering this double-free results in the following crash:

[HM] [ERROR][228]unmap errno = 22
[HM] ERROR: free: __munmap return code= -1
[HM] ERROR: free: double free
[HM] Dump SPI notification entries:
[HM] ----------
[HM] Stats: SHADOW.tx=0 SHADOW.rx=0 WAKEUP.tx=467 WAKEUP.rx=467
[HM] Stats: SET_AFFINITY.tx=0 SET_AFFINITY.rx=0
[HM] Dump current threads:
[HM] ---------
[HM] CPU0: name=/teesmcmgr.elf
[HM] ctx_map[ta=0/0 ca=0/0 target=0/0 exit=0/0]
...

This lack of locking is common to all global variables. For example, it could also be exploited via the g_cipherHandle, g_cencCipherHandle, the various certificate caches, etc. instead of the sessions list.

Opening Sessions Before Initialization

The global sessions list is initialized in the CDRMC_Initialize function (command ID #1). If this command is not called explicitly, the list will be left in an uninitialized state.

int g_sessions_count;      /* in the BSS */
list_head g_sessions_list; /* in the BSS */

int CDRMC_Initialize() {
    // ...
    INIT_LIST_HEAD(&g_sessions_list);
    g_sessions_count = 0;
    // ...
}

The function CDRMC_OpenSession, as its name suggests, can be used to open a session. It first calls CDRMC_FindSession to make sure the value stored in container_p is not already a valid session. If the session is not found or if the list is empty (i.e. g_sessions_count == 0), this function will return -1. Since the error path is taken only when CDRMC_FindSession returns 0, CDRMC_OpenSession will continue executing even if the list is uninitialized. As a result, the next pointer of the newly allocated session container will be NULL and g_sessions_count will be incremented. This can later cause a null pointer dereference.

int CDRMC_OpenSession(container_t **container_p) {
    // ...
    if (!CDRMC_FindSession(*container_p, &session)) {
        /* ...error... */
    }
    session = CDRMR_SecureMemory_Malloc(0x2CA8);
    container = CDRMR_SecureMemory_Malloc(0xC);
    memset_s(session, 0x2CA8, 0, 0x2CA8);
    memset_s(container, 0xC, 0, 0xC);
    // ...
    session->container = container;
    container->session = session;
    list_add(&container->list, &g_sessions_list);
    ++g_sessions_count;
    *container_p = container;
    // ...
}

For example, it is possible to trigger the null pointer dereference using the CDRMC_FindSession call in the CDRMS_SetPolicy function (command ID #0x29).

int CDRMS_SetPolicy(uint32_t paramTypes, TEE_Param params[4]) {
    // ...
    container_t *container = params[3].memref.buffer;
    CDRMC_FindSession(container, &session);
    // ...
}

A proof of concept triggering this null pointer dereference results in the following crash:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1e00000028 (tid: 40) data abort: 
[HM] [ERROR][2498]Bad memory access on address: 0x0, fault_code: 0x92000006
[HM] 
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[CHINADRM_COMMON] tid=40 is-idle=0 is-curr=0
[HM]     state=BLOCKED@MEMFAULT sched.pol=0 prio=46 queued=1
[HM]     aff[0]=ff
[HM]     flags=1000 smc-switch=0 ca=7849 prefer-ca=7849
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <CDRMC_FindSession+0x3c/0xdc>
[HM] <CDRMS_SetPolicy>+0xd0/0x1fc
[HM] <CDRMS_SetPolicy>+0xd0/0x1fc
[HM] <TA_InvokeCommandEntryPoint>+0x31c/0x1358
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END

Session IDs Are Pointers

The session IDs, used by the CA to refer to a session, are pointers to heap-allocated instances of a structure. This structure is allocated in CDRMC_OpenSession, and the pointer is returned to the CA in the first value of the first TEE_Param.

TEE_Result TA_InvokeCommandEntryPoint(
        void *sessionContext,
        uint32_t commandID,
        uint32_t paramTypes,
        TEE_Param params[4])
{
    switch (commandID) {
        case 0x20u:
            // ...checking of the paramTypes...
            container_t *container = NULL;
            retval = CDRMC_OpenSession(&container);
            params->value.a = container;
            params->value.b = retval;
        break;
        // ...
    }
}

When running the first proof of concept code, we can indeed observe the pointers returned to the CA:

Session = 1014db0 (retval = 0)

This information leak can come in handy, for example when exploiting the UAF on the sessions list.

Wrong memcpy_s Destination Sizes in CencDecrypt

The CencDecrypt function contains multiple memcpy_s calls. Some of them have an incorrect destination size argument. For example, in the incorrect case showed in the code snippet above, the destination size should be *decrypt->outbuf_len_p - offset to account for the offset at which the value will be copied into decrypt->outbuf.

int CencDecrypt(asym_decrypt_t *decrypt, int algorithm) {
    // CORRECT memcpy_s arguments
    if (memcpy_s(
            &decrypt->outbuf[offset],
            *decrypt->outbuf_len_p - offset,
            &decrypt->inbuf[offset],
            params_->subSamples[idx].sample_dword_0)) {
        tee_print(0, "%s %d:copy clear Header data failed\n ", "[error]", 0x50B);
        // ...
    }
    // ...
    // INCORRECT memcpy_s arguments
    if (memcpy_s(
            &decrypt->outbuf[offset],
            *decrypt->outbuf_len_p, // <-- the destination size is incorrect
            &decrypt->inbuf[offset],
            drmInfo_dword_8_times_0x10)) {
        tee_print(0, "%s %d:copy payload pattern clear data failed\n ", "[error]", 0x40A);
        // ...
    }
    // ...
}

We did not attempt to trigger this bug, but it could lead to a buffer overflow in a mapped ION buffer.

Affected Devices

We have verified that the vulnerabilities impacted the following device(s):

  • Kirin 990: P40 Pro (ELS)

Please note that other models might have been affected.

Patch

Name Severity CVE Patch
Lack of Locking when Accessing Global Variables Low N/A Fixed
Opening Sessions Before Initialization Low N/A Fixed
Session IDs Are Pointers Low N/A Fixed
Wrong memcpy_s Destination Sizes in CencDecrypt High CVE-2021-40052 August 2022

Timeline

  • Dec. 09, 2021 - A vulnerability report is sent to Huawei PSIRT.
  • Jan. 12, 2022 - Huawei PSIRT acknowledges the vulnerability report.
  • Aug. 01, 2022 - Huawei PSIRT states that this issue was fixed in the August 2022 update.
  • From Nov. 30, 2022 to Jul, 19 2023 - We exchange regularly about the release of our advisories.