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

Huawei's TEE_SERVICE_VOICE_REC 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. This trustlet implements 19 commands.

Information Leak in restore

There is an information leak in the restore function:

int RestoreTemplate(int ival0_a, int ibuf1_addr, int ibuf1_size) {
    // [...]
    uid_file_content = TEE_Malloc(0x80000, 0);
    // [...]
    for (int i = 0; i < 0xA; i++) {
        // [...]
        restore(
            g_voiceUserDb->templates[i].voiceType,
            ibuf1_addr,
            ibuf1_size,
            uid_file_content,
            uid_path_filesize,
            SaveTemplateWithNoDecrypt);
        // [...]
    }
    // [...]
}

int restore(unsigned int voiceType,
        void *ibuf1_addr,
        uint32_t ibuf1_size,
        void *middle_buffer,
        uint32_t middle_buffer_len,
        void *save_callback)
{
    // [...]
    if ( ... ) {
        SLog(
            "%s %s: parms NULL !!!%d %p %d %p %d %p\n",
            "[Error]",
            "restore",
            voiceType,
            ibuf1_addr,
            ibuf1_size,
            middle_buffer,
            middle_buffer_len,
            save_callback);
        return -1;
    }
    // [...]
}

The log string in restore will leak 3 pointers:

  • ibuf1_addr, a TEE_Param input parameter
  • middle_buffer, a heap-allocated buffer
  • save_callback, a pointer to the function SaveTemplateWithNoDecrypt

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

[TEE_SERVICE_VOICE_REC-1] [Error] restore: parms NULL !!!9 0x70003000 524288 0x69fe010 524288 0x3c69a8c

OOB Access in restore

There is an OOB access in the restore function:

int restore(unsigned int voiceType,
        void *ibuf1_addr,
        uint32_t ibuf1_size,
        void *middle_buffer,
        uint32_t middle_buffer_len,
        void *save_callback)
{
    // [...]
    buf_off = *(uint32_t *)ibuf1_addr;
    buf_ptr = ibuf1_addr + buf_off + 8;
    buf_len = *(uint32_t *)(ibuf1_addr + buf_off + 4);
    // [...]
}

In restore, buf_off is read from ibuf1_addr (a buffer whose content is fully user-controlled). This offset value is then used to read a length from ibuf1_addr. Since we control buf_off, we can make ibuf1_addr + buf_off + 4 point to anywhere in memory, triggering an OOB read.

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 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_VOI] 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=8347 prefer-ca=8347
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <restore+0x94/0x248>
[HM] <RestoreTemplate>+0x2f0/0x494
[HM] <RestoreTemplate>+0x2f0/0x494
[HM] <Restore>+0x74/0x98
[HM] <TA_InvokeCommandEntryPoint>+0x98/0xe4
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=72 exit_status=130

Null Pointer Dereference in CheckModelHash

There is a null pointer dereference in the CheckModelHash function:

int CompareVoiceTa(uint32_t label, uint8_t *inbuf, uint32_t inbuf_size, uint8_t *outbuf) {
    // [...]
    CheckModelHash(g_sendGmmBuf, g_sendGmmBufTotalLen, HashCheck);
    // [...]
}

int CheckModelHash(uint8_t * model_buf, uint32_t model_len, void *callback) {
    // [...]
    version = *(uint32_t *)(model_buf + 8);
    // [...]
}

CompareVoiceTa will call CheckModelHash with the global variables g_sendGmmBuf and g_sendGmmBufTotalLen as arguments. These variables are supposed to be set by the SendTaGmmBuf function, but CheckModelHash never checks if these values are non-zero, resulting in a NULL pointer dereference if the SendGmmBuf command was not called.

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 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x8, fault_code: 0x92000006
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_VOI] 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=8398 prefer-ca=8398
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <CheckModelHash+0x24/0xe0>
[HM] <CompareVoiceTa>+0x184/0x3a0
[HM] <CompareVoiceTa>+0x184/0x3a0
[HM] <Auth>+0x1cc/0x24c
[HM] <TA_InvokeCommandEntryPoint>+0x98/0xe4
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=74 exit_status=130

Information Leak in compare

There is an information leak in the compare function:

int Auth(unsigned int paramTypes, TEE_Param params[4]) {
    // [...]
    plaintext = TEE_Malloc(0x100010, 0);
    // [...]
    CompareVoiceTa(
        *(uint32_t *)plaintext,
        plaintext + 4,
        *(uint32_t *)(plaintext + 0x80004),
        obuf1_addr);
    // [...]
}

int CompareVoiceTa(uint32_t label, uint8_t *inbuf, uint32_t inbuf_size, uint8_t *outbuf) {
    // [...]
    templ_data = g_voiceUserDb->templates[i].data;
    // [...]
    float0 = 0.0;
    float1 = 0.0;
    // [...]
    compare(label, inbuf, inbuf_size, templ_data, templ_length, &float0, &float1);
    // [...]
}

int compare(
        uint32_t label,
        uint8_t *inbuf,
        uint32_t inbuf_size,
        void *templ_data,
        int templ_length,
        float *float0_p,
        float *float1_p)
{
    // [...]
    if ( ... ) {
        SLog(
            "%s %s: parms NULL !!! %p %d %p %d %p %p  \n",
            "[Error]",
            "compare",
            inbuf,
            inbuf_size,
            templ_data,
            templ_length,
            float0_p,
            float1_p);
        return -1;
    }
    // [...]
}

The log string in restore will leak 4 pointers:

  • inbuf, a heap-allocated buffer
  • templ_data, another heap-allocated buffer
  • float0_p, a pointer to a stack variable
  • float1_p, another pointer to a stack variable

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

[TEE_SERVICE_VOICE_REC-1] [Error] compare: parms NULL !!! 0x6bed014 0 0x6950010 360000 0x694f9dc 0x694f9e0

OOB Access in DecryptData

There is an OOB access in the DecryptData function:

int DecryptDataTa(int paramTypes, TEE_Param *params) {
    // [...]
    if (!params[1].memref.buffer
            || params[1].memref.size - 1 >= 0x32000
            || !params[2].memref.buffer
            || params[2].memref.size != 0x16
            || !params[3].memref.buffer) {
        SLog("%s: VO_TEE_DECRYPT_DATA_CMD_ID: Bad expected parameter types.\n", "[Error]");
        // [...]
    }
    // [...]
    decrypt_data.value_a = params->value.a;
    decrypt_data.src = params[1];
    decrypt_data.dest = params[3];
    decrypt_data.IV.memref.buffer = params[2].memref.buffer;
    decrypt_data.IV.memref.size = 0x10;
    // [...]
    DecryptData(&decrypt_data);
    // [...]
}

int DecryptData(decrypt_data_t *decrypt_data) {
    // [...]
    size = decrypt_data->src.memref.size;
    if (size > 0x32000) {
        SLog("%s: bad parameter.\n", "[Error]");
        // [...]
    }
    // [...]
    dest = TEE_Malloc(size, 0);
    TA_AesDecryptPKCS5(
        g_aesKey,
        0x10,
        decrypt_data->IV.memref.buffer,
        decrypt_data->IV.memref.size,
        decrypt_data->src.memref.buffer,
        size,
        dest,
        &size);
    // [...]
    if (size <= 4) {
        SLog("%s: decrypt data len is too short.\n", "[Error]");
        // [...]
    }
    // [...]
    memmove_s(decrypt_data->dest.memref.buffer, size - 4, dest + 4, size - 4) )
    // [...]
}

In DecryptData, after the data has been decrypted, it is copied into the TEE_Param output parameter. But the size used as the source and destination size of the memmove_s call is the output data size (capped to 0x32000). The actual size of the TEE_Param output parameter is never checked, and can be smaller than the output data size, resulting in an OOB write access.

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 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x70038000, fault_code: 0x92000047
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_VOI] 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=10397 prefer-ca=10397
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] sp > fpDump task states END
[HM]
[HM] [TRACE][1212]pid=88 exit_status=130

Heap Buffer Overflow in SendTaGmmBuf

There is an heap buffer overflow in the SendTaGmmBuf function:

int SendGmmBuf(uint8_t paramTypes, TEE_Param *params) {
    // [...]
    ibuf1_addr = params[1].memref.buffer;
    ibuf1_size = params[1].memref.size;
    if (params->value.a <= 0x680000 && ibuf1_addr && ibuf1_size <= 0x80000) {
        params->memref.size = SendTaGmmBuf(params->value.a, ibuf1_addr, ibuf1_size);
        return 0;
    }
    // [...]
}

int SendTaGmmBuf(uint32_t totalLen, int buf, unsigned int bufLen) {
    // [...]
    if (!buf || bufLen > 0x680000) {
        SLog("%s: buf is NULL or bufLen is too large\n", "[Error]");
        return 0x7A000003;
    }

    if (!g_sendGmmBufCount) {
        g_sendGmmBuf = TEE_Malloc(totalLen, g_sendGmmBufCount);
        // [...]
        g_sendGmmBufTotalLen = totalLen;
    }

    memcpy_s(g_sendGmmBuf + (g_sendGmmBufCount << 0x13), bufLen, buf, bufLen);
    // [...]
    ++g_sendGmmBufCount;
    // [...]
}

The SendGmmBuf command is used by the CA to send a model to the TA. The model is sent in chunks. When the first chunk is received, the TA will allocate a buffer to store the full model. The total size of the model is given in the value of the first TEE_Param. The chunk of data is in the second TEE_Param.

No validation is made to ensure that the sum of the sizes of chunks is not bigger that the total size of the model. As a result, one can specify a small value for the model size (e.g. 8), and overflow the allocated buffer with just one chunk (e.g. of size > 8).

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 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x10e8000, fault_code: 0x92000047
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_VOI] 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=10293 prefer-ca=10293
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] sp > fpDump task states END
[HM]
[HM] [ERROR][2171]vmem_as_ondemand_prepare failed
[HM] [ERROR][2496]process 2200000022 (tid: 34) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x8, fault_code: 0x92000046
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_VOI] 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]

Exploitation

In this section, we are going to demonstrate that we can use the vulnerabilities affecting the TEE_SERVICE_VOICE_REC trustlet to take control of the execution flow. In order to do that, we will use the information leak in the compare function and the heap buffer overflow in the SendTaGmmBuf function.

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.223.

The trustlet binary MD5 checksum is as follows:

HWELS:/ # md5sum /vendor/bin/859703f3-3cc5-4e88-b263-08f9ce82e3d0.sec
2e0202c5c2a3f28c55491e1bbb44a875  /vendor/bin/859703f3-3cc5-4e88-b263-08f9ce82e3d0.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_VOICE_REC TA can only be called by a native binary:

  • /vendor/bin/hw/vendor.huawei.hardware.biometrics.hwfacerecognize@1.1-service (uid 1000)

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.

Information Leak

To trigger the information leak in the compare function, we need to overcome several obstacles:

  • g_voiceUserDb must contain at least one "template"
  • the third TEE_Param input parameter of the Auth command must contain AES-encrypted data
  • compare will only be called if the model hash matches one of the 2 expected SHA256 hashes

To overcome the first obstacle, we do the following:

  • we send a SetActiveGroup command. This function will call ResetVoiceDB(0), that will set g_voiceUserDb->user_id to 0 (instead of its default value: 0xffffffff)
  • we send an Enroll command, which also requires AES-encrypted data. This function will add a "template" to g_voiceUserDb.

To overcome the second obstacle, we abuse the fact that the AES key g_decryptKey is initialized by the GetEccPublicKey command. If we don't call this function, g_decryptKey (which is located in the BSS) will be all zeroes. This allows us to encrypt our data with a known key.

To overcome the third obstacle, we reversed the client-side application that talks to the trustlet (voiceid_alg_ree.so), and found that one of the model corresponding to the SHA256 hashes can be found on the disk at /odm/etc/audio/voiceid/imedia/msbc_vpu/china/plda_combine_model.dat.

After triggering the vulnerable code path, we simply parse the logcat output, looking for messages similar to [TEE_SERVICE_VOICE_REC-1] [Error] compare: parms NULL !!! 0x6bed014 0 0x6950010 360000 0x694f9dc 0x694f9e0.

Control Flow Hijacking

There are many ways to exploit a heap buffer overflow. For this demonstration, we chose to perform a classical unlinking heap exploit triggered when our g_sendGmmBuf object is freed (in the Release command). This technique allows writing a controlled dword value at an arbitrary location in memory.

By targeting a link register saved on the stack, we can hijack the execution flow of the trustlet. We calculated the stack offset between the float0 local variable of CompareVoiceTa and the saved link register of TEE_Free in ReleaseTa. We then write an arbitrary value at the resulting address, and observe that the control flow is redirected.

In the exploit, we use 0x70000000 as we can only write values representing writeable addresses with the unlinking technique, but it should be possible to construct a non restricted arbitrary write using techniques similar to the one we used in the TEE_SERVICE_MULTIDRM exploit.

Here is the output of the exploit code, showing the leaked stack address:

adb wait-for-device shell su root sh -c "/data/local/tmp/tee_service_voice_rec"
stack_addr = 6b969dc

And the crash that happens right after, when the control flow is redirected:

[HM] ESR_EL1: 8200000f, ELR_EL1: 70000000, FAR is not valid
[HM] TEE_SERVICE_VOI vm fault prefetch abort: 70000000
[HM] fault: 8200000f tcb cref 2200000028
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <?>+0x0/0x0
[HM] [ERROR][2496]process 2200000028 (tid: 40) data abort:
[HM] [ERROR][2498]Bad memory access on address: 0x70000000, fault_code: 0x8200000f
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[TEE_SERVICE_VOI] 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=10335 prefer-ca=10335
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <?>+0x0/0x0
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=67 exit_status=130

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
OOB Access in DecryptData Critical CVE-2021-40036 January 2022
Heap Buffer Overflow in SendTaGmmBuf Critical CVE-2021-40010 January 2022
OOB Access in restore High CVE-2021-40027 January 2022
Information Leak in compare Medium CVE-2021-40032 January 2022
Information Leak in restore Medium CVE-2021-40014 January 2022
Null Pointer Dereference in CheckModelHash Low N/A Fixed

Timeline

  • Nov. 09, 2021 - A vulnerability report is sent to Huawei PSIRT.
  • Nov. 22, 2021 - Huawei PSIRT acknowledges the vulnerability report.
  • Jan. 01, 2022 - Huawei PSIRT states that these issues were fixed in the January 2022 update.
  • From Nov. 30, 2022 to Jul, 19 2023 - We exchange regularly about the release of our advisories.
  • Jun. 13, 2023 - We inform Huawei PSIRT that some of the vulnerabilities are not patched.
  • Jun. 20, 2023 - Huawei PSIRT replies that they will be fixed in the July 2023 update.