- CVE-2021-39997 IsGmmModelLoaded OOB Access
- CVE-2021-39997 InitGetScoreParams OOB Access
- CVE-2021-39997 GmmGetScore OOB Access
- HWPSIRT-2022-85498 OOB Access in LTopProb
- HWPSIRT-2022-62034 Param Buffer Overflow in XvectorLoadModels
- HWPSIRT-2022-44993 Param Buffer Overread in InitGetScoreParams
We identified 3 vulnerabilities affecting the VprTa trusted application from /vendor/bin/
:
- in
GmmLoadModels
, the model type is unchecked, resulting in OOB accesses inIsGmmModelLoaded
andGetGmmInfo
; - in
InitGetScoreParams
, values taken from the input buffer are unchecked, resulting in an integer overflow duringTEE_Malloc
and consequently a heap buffer overflow; - in
GmmGetScore
, the return value ofGetGmmInfo
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.
Copyright © Impalabs 2021-2023