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

We identified 3 vulnerabilities affecting the VprTa trusted application from /vendor/bin/:

  • in GmmLoadModels, the model type is unchecked, resulting in OOB accesses in IsGmmModelLoaded and GetGmmInfo;
  • in InitGetScoreParams, values taken from the input buffer are unchecked, resulting in an integer overflow during TEE_Malloc and consequently a heap buffer overflow;
  • in GmmGetScore, the return value of GetGmmInfo is unchecked, resulting in an OOB access.

We identified 3 additional vulnerabilities affecting Huawei's VprTa trusted application from HwVAssistant_OVE.apk.

The VprTa 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.

IsGmmModelLoaded OOB Access

The arguments of GmmLoadModels aren't validated, in particular modelType that is given as argument to IsGmmModelLoaded and GetGmmInfo. By specifying a modelType greater than one, accesses to the g_gmmModel array will be out of bound. The pointer returned by GetGmmInfo can reference memory located after the g_gmmModel array, as well as before it by making use of the integer overflow on 0x18 * modelType.

bool IsGmmModelLoaded(int modelType) {
    return g_gmmModel[0x18 * modelType]
        && *(uint32_t *)&g_gmmModel[0x18 * modelType + 0x14] != 0;
}

void *GetGmmInfo(int modelType) {
    if (g_gmmModel[0x18 * modelType])
        return &g_gmmModel[0x18 * modelType + 4];
    return 0;
}

int GmmLoadModels(const char *path, int modelType, int model, int modelSize) {
    // [...]
    if (IsGmmModelLoaded(modelType)) {
        GmmInfo = GetGmmInfo(modelType);
        if (!memcpy_s(model, modelSize, GmmInfo + 0x10, 0x21008))
            return 0;
    }
    return Load(path, strlen(path), model, modelSize);
}

If the address of g_gmmModel is known, an attacker can compute the right offset, and thus modelType value, needed to have GetGmmInfo return a pointer to a user-controlled buffer. Since this pointer is then dereferenced and the value used as the source in the memcpy_s call, and since the destination is another user-provided output buffer, this can be used to read arbitrary memory locations.

We were unable to find such an information leak, so the a proof of concept we wrote only triggers the OOB memory access:

[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: 0xe51954cc, fault_code: 0x92000005
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[VprTa] 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=6856 prefer-ca=6856
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <IsGmmModelLoaded+0x10/0x44>
[HM] <GmmLoadModels>+0x30/0x100
[HM] invalid fp. backtrace abort
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=43 exit_status=130

InitGetScoreParams OOB Access

The world argument of InitGetScoreParams is a user-provided input buffer. In contains integer values, in particular:

  • the number of frames at offset 4;
  • the number of features at offset 0x70814.
unsigned int InitGetScoreParams(uint32_t *world, world_info_t *info) {
    // [...]
    info->frames = world[1];
    info->features = (uint32_t *)TEE_Malloc(4 * world[1], 0);
    // [...]
    for (int frame_idx = 0; frame_idx < world[1]; ++frame_idx) {
        info->features[frame_idx] = (uint32_t *)TEE_Malloc(4 * world[0x1C205], 0);
        // [...]
        for (int feat_idx = 0; feat_idx < world[0x1C205]; ++feat_idx) {
            info->features[frame_idx][feat_idx] = world[frame_idx * 0x20 + feat_idx + 0x19002];
        }
    }
    return 0;
}

The number of frames is checked in the calling function GmmGetScore and must be smaller than 400. The number of features is unchecked. By specifying a very large number of features, 4 * world[0x1C205] will overflow and the buffer allocated in the second TEE_Malloc call will be smaller than what is actually needed. As a result, the write accesses info->features[frame_idx][feat_idx] will be out of bounds of the heap-allocated buffer.

During our testing, it appeared that the info->features[frame_idx] buffer was allocated at a smaller address than the info->features buffer, allowing an attacker to control the destination address of the writes. But an issue remains: how to stop the overflow before reaching the end of the heap? Theoretically, it should be possible to make each write overwrite to the info->features[frame_idx] pointer by specifying decreasing offsets, resulting in writes at the same location. But we did not pursue that avenue, and our proof of concepts simply triggers a crash at a user-controlled address:

[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: 0xfa7000, fault_code: 0x92000047
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[VprTa] 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=6480 prefer-ca=6480
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <InitGetScoreParams+0x100/0x150>
[HM] <memset_s>+0x28/0x38
[HM] <GmmGetScore>+0xf0/0x240
[HM] <TA_ProcGmmGetScore>+0x5c/0xd4
[HM] <TA_InvokeCommandEntryPoint>+0xb0/0x180
[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=[VprTa] 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]

GmmGetScore OOB Access

In GmmGetScore, modelType is validated properly, but the return value of GetGmmInfo is used without checking if the model is loaded.

void *GetGmmInfo(int modelType) {
    if (g_gmmModel[0x18 * modelType])
        return &g_gmmModel[0x18 * modelType + 4];
    return 0;
}

unsigned int GmmGetScore(int path, int pathSize, unsigned int modelType, int model, double *ret_p) {
    // [...]
    // modelType validation
    // [...]
    GmmInfo = GetGmmInfo(modelType);
    if (GmmInfo->field_10) {
        // use GmmInfo
        // [...]
    }
    return 0;
}

If the model was not loaded, GetGmmInfo returns NULL, and a read access at address 0x10 happens on GmmInfo->field_10:

[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: 0x10, fault_code: 0x92000006
[HM]
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[VprTa] 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=7343 prefer-ca=7343
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <GmmGetScore+0x6c/0x240>
[HM] <GmmGetScore>+0x64/0x240
[HM] <TA_ProcGmmGetScore>+0x5c/0xd4
[HM] <TA_InvokeCommandEntryPoint>+0xb0/0x180
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]
[HM] [TRACE][1212]pid=46 exit_status=130

OOB Access in LTopProb

There is an OOB access in the function LMixProb called by LTopProb in the command TA_ProcGmmGetScore (ID #1).

uint32_t TA_ProcGmmGetScore(TEE_Param params[4], uint32_t *result_p) {
    /* [...] */
    ret = GmmGetScore(
        params[0].memref.buffer,
        params[0].memref.size,
        params[1].value.a,
        params[2].memref.buffer,
        &score)
    /* [...] */
}

In GmmGetScore, the function calls TopDistribs which is responsible for filling the structure info with information from the the third TEE_Param input buffer.

uint32_t GmmGetScore(void *ibuf0_addr, uint32_t ibuf0_size, uint32_t ival1_a,
        void *ibuf2_addr, uint32_t score_p)
{
    /* [...] */
    info = TEE_Malloc(0x14, 0);
    /* [...] */
    model = GetGmmInfo(ival1_a);
    /* [...] */
    // Copies the offset from the TEE_Param input buffer into the toplist of
    // `info`
    ret = TopDistribs(info, ibuf2_addr, 0x1E);
    /* [...] */
    // Copies the toplist of `info` into `model`
    ret = CopyTopDistribs(model, info);
    /* [...] */
    value = LTopProb(model, info->features);
    /* [...] */
}

TopDistribs loads values from ibuf2_addr into the topList array, as can be seen below:

uint32_t TopDistribs(model_t *info, uint32_t *ibuf2_addr, uint32_t topDistribNB) {
    /* [...] */
    info->topDistribNB = topDistribNB;
    /* [...] */
    info->topList = TEE_Malloc(4 * info->frames, 0);
    /* [...] */
    for (i = 0; i < info->frames; ++i) {
        info->topList[i] = TEE_Malloc(4 * info->topDistribNB, 0);
        /* [...] */
        for (j = 0; j < info->topDistribNB; ++j)
            info->topList[i][j] = ibuf2_addr[2 + j];
        /* [...] */
    }
}

topList is then copied from info into model in the function CopyTopDistribs:

uint32_t CopyTopDistribs(model_t *model, model_t *info) {
    /* [...] */
    model->topDistribNB = info->topDistribNB;
    /* [...] */
    model->topList = TEE_Malloc(4 * info->frames, 0);
    /* [...] */
    for (i = 0; i < model->frames; ++i) {
        model->topList[i] = TEE_Malloc(4 * model->topDistribNB, 0);
        /* [...] */
        for (j = 0; j < model->topDistribNB; ++j)
            model->topList[i][j] = info->topList[i][j];
        /* [...] */
    }
}

Afterwards, GmmGetScore calls the function LTopProb which then calls LMixProb with elements from topList, which are user-controlled:

uint32_t LTopProb(model_t *model, uint32_t **features) {
    /* [...] */
    value = LMixProb(model, *features, model->topList[frame][i]);
    /* [...] */
}

LMixProb finally uses top, which can be an arbitrary 32-bit integer, to compute the address of buf_addr before dereferencing it. This results in an OOB access (relatively to model->buffer).

uint32_t LMixProb(model_t *model, uint32_t *features, uint32_t top) {
    /* [...] */
    buffer = model->buffer;
    buf_addr = &model->buffer[0x40 * top + 0x400];
    /* [...] */
    value = *(uint64_t *)(buf_addr + 8);
    /* [...] */
}

We triggered this bug with a proof of concept and obtained the following crash on a read access at the unmapped address:

[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: 0xb1675f18, fault_code: 0x92000005
[HM] 
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[VprTa] 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=7323 prefer-ca=7323
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <LMixProb+0xa0/0x120>
[HM] <LTopProb>+0x80/0xfc
[HM] <LTopProb>+0x80/0xfc
[HM] <GmmGetScore>+0x1dc/0x284
[HM] <TA_ProcGmmGetScore>+0x38/0xac
[HM] <TA_InvokeCommandEntryPoint>+0x118/0x1c8
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM] 

Param Buffer Overflow in XvectorLoadModels

There is a param buffer overflow in the XvectorLoadModels function called from the TA_ProcLoadXvectorModel command (ID #7).

int TA_ProcLoadXvectorModel(TEE_Param params[4], uint32_t *result_p) {
    // ...
    vectorLoadModels(params[0].memref.buffer, params[1].memref.buffer);
    // ...
}

In XvectorLoadModels, after the models have been loaded, they are copied into the param output buffer. The size of the param output buffer is never checked, and there can be a maximum of 0x33 * 0x464 = 0xdfec bytes copied into it. This will result in a buffer overflow if the buffer is smaller than expected.

unsigned int XvectorLoadModels(void *inbuf, void *outbuf) {
    // ...
    for (int i = 0; i != 0x33; ++i) {
        outbuf[i * 0x464 + 0x460] = XvectorInfo[i].size;
        memcpy_s(&outbuf[i * 0x464], 0x460, XvectorInfo[i].data, 0x460);
    }
}

We triggered this bug with a proof of concept and obtained the following crash on a write access at an address located after the param output buffer:

[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: 0x7000631c, fault_code: 0x92000047
[HM] 
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[VprTa] 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=7829 prefer-ca=7829
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <XvectorLoadModels+0x80/0xd0>
[HM] <memcpy_s>+0x60/0x6c
[HM] <TA_ProcLoadXvectorModel>+0x14/0x2c
[HM] <TA_InvokeCommandEntryPoint>+0x16c/0x1c8
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM] 

Param Buffer Overread in InitGetScoreParams

There is a param buffer overread in the InitGetScoreParams called from the TA_ProcGmmGetScore command (ID #1).

int TA_ProcGmmGetScore(TEE_Param params[4], uint32_t *result_p) {
    // ...
    GmmGetScore(
        params[0].memref.buffer,
        params[0].memref.size,
        params[1].value.a,
        params[2].memref.buffer,
        &score);
    // ...
}
int GmmGetScore(void *ibuf0_addr, int ibuf0_size, int ival1_a, void *ibuf2_addr, int score_p) {
    // ...
    InitGetScoreParams(ibuf2_addr, info);
    // ...
}

In this function, many values are extracted from the param input buffer, including the number of features which is at offset 0x70814. But the actual size of the param input buffer is never checked prior to entering this function, and thus can be smaller than 0x70818 bytes, resulting in a buffer overread.

int InitGetScoreParams(uint32_t *ibuf2_addr, model_t *info) {
    // ...
    featureNB = ibuf2_addr[0x1C205];
    // ...
}

We triggered this bug with a proof of concept and obtained the following crash on a read access at an address located after the param input buffer:

[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: 0x70074814, fault_code: 0x92000007
[HM] 
[HM] Dump task states for tcb
[HM] ----------
[HM]     name=[VprTa] 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=7971 prefer-ca=7971
[HM] Registers dump:
[HM] ----------
[HM] 32 bits userspace stack dump:
[HM] ----------
[HM] <InitGetScoreParams+0x74/0x10c>
[HM] <memset_s>+0x28/0x38
[HM] <GmmGetScore>+0x128/0x284
[HM] <TA_ProcGmmGetScore>+0x38/0xac
[HM] <TA_InvokeCommandEntryPoint>+0x118/0x1c8
[HM] <tee_task_entry>+0x398/0xcd4
[HM] Dump task states END
[HM]

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
IsGmmModelLoaded OOB Access Critical CVE-2021-39997 February 2022
InitGetScoreParams OOB Access Critical CVE-2021-39997 February 2022
GmmGetScore OOB Access Critical CVE-2021-39997 February 2022
OOB Access in LTopProb Duplicate N/A (*) Fixed
Param Buffer Overflow in XvectorLoadModels Duplicate N/A (*) Fixed
Param Buffer Overread in InitGetScoreParams Duplicate N/A (*) Fixed

(*) Huawei's statement about the medium, high, and critical severity APP vulnerabilities:

They are resolved through the AppGallery upgrade. Generally, CVE numbers are not assigned for this type [of] vulnerabilities.

Timeline

  • Oct. 12, 2021 - A first vulnerability report is sent to Huawei PSIRT.
  • Oct. 28, 2021 - Huawei PSIRT acknowledges the first vulnerability report.
  • Feb. 01, 2022 - Huawei PSIRT states that these issues were fixed in the February 2022 update.
  • Feb. 01, 2022 - A second vulnerability report is sent to Huawei PSIRT.
  • Apr. 01, 2022 - Huawei PSIRT acknowledges the second vulnerability report and states that the second batch of vulnerabilities were also discovered internally when fixing the first batch.
  • From Nov. 30, 2022 to Jul, 19 2023 - We exchange regularly about the release of our advisories.