- CVE-2021-46881 Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_LoadKeys
- CVE-2021-40034 Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys
- CVE-2021-46882 Heap Buffer Overflow in MDrm_TA_CMD_OEMCrypto_RefreshKeys
- CVE-2021-46883 Heap Buffer Overflow in MDrm_TA_OEMCryptoUsageTable_LoadUsageTableHeader
- CVE-2021-46884 OOB Write access in MDrm_TA_CMD_OEMCrypto_CopyBuffer
- CVE-2021-46885 OOB Read Access in MDrm_TA_CMD_Provision_GetRequest
- CVE-2021-46886 OOB Read Access in MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30
- CVE-2021-46814 OOB Read Access in MDrm_TA_CMD_OEMCrypto_DecryptCENC
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, ¶ms[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, ¶ms[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
with0xF1 = 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 theC_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
with0xA1 = 0xA0 | 1
. 0xA0 is the size of the session object (the first of the 3 objects allocated when a session is opened). We specify theC_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, ¶ms[1], ¶ms[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 addressc->prev + 8
; - write the value
c->prev
to the addressc->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, ¶ms[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.
Copyright © Impalabs 2021-2023