- CVE-2021-40036 OOB Access in DecryptData
- CVE-2021-40010 Heap Buffer Overflow in SendTaGmmBuf
- CVE-2021-40027 OOB Access in restore
- CVE-2021-40032 Information Leak in compare
- CVE-2021-40014 Information Leak in restore
- HWPSIRT-2021-56065 Null Pointer Dereference in CheckModelHash
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
, aTEE_Param
input parametermiddle_buffer
, a heap-allocated buffersave_callback
, a pointer to the functionSaveTemplateWithNoDecrypt
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 buffertempl_data
, another heap-allocated bufferfloat0_p
, a pointer to a stack variablefloat1_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 theAuth
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 callResetVoiceDB(0)
, that will setg_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" tog_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.
Copyright © Impalabs 2021-2023