TEE漏洞学习——CVE-2015-6639

Posted by GToad on November 26, 2019

最近在看TEE相关内容,顺便把CVE-2015-6639这个QSEE上的著名漏洞看一下。

概述

TEE即可信执行环境,全称Trusted Execution Environment。该环境可以保证不被常规的Rich OS(如Windows,Android)干扰,因此是可信的。TEE目前被用于向Rich OS提供各种各样的安全服务。TEE独立于Rich OS上的各种应用,并可以访问硬件和软件安全资源。TEE在x86/x64/ARM/ARM64上都有相关支持与实现,另一个我们比较熟悉的称呼是ARM/ARM64架构手机上的TrustZone。

讲得简单点,现在我们用的Android手机上都有指纹识别,这就需要用到TEE,因为指纹这些生物信息保存在Android系统里的话并不安全,因为手机可能被恶意应用root掉,然后里面的文件都是可以被获取查看的。因此,这就需要把一些非常隐私的数据保存在更加深的地方。

那么有多深呢?以ARM架构的Android系统为例,普通的应用进程运行在EL0层;Android系统包括系统框架、内核等代码运行在EL1层;部分设备还有虚拟化需求,那就需要在EL2层运行hypervisor;而如果要使用TEE相关技术的话,那就需要EL3层运行Secure Monitor,由该层进行安全环境(TEE kernel)与普通环境(Rich OS)的切换。

当前主流的TEE厂商与产品有:Intel的SGXQualcomm的QSEEHuawei HiSilicon的Trusted Core以及Samsung的Kinibi。那么既然TEE能做到如此强的隔离,是不是其中的信息安全就坚不可破了呢?并不是,我们知道,大部分Android手机会使用高通SoC,因此上面运行的TEE Kernel就是QSEE,本文将实践分析一下该TEE产品上出现过的漏洞CVE-2015-6639。

基本信息

本漏洞广泛存在于各类使用了高通芯片的Android手机,具体存在于QSEE的widevine中,该模块主要用于实现Widevine DRM以提供安全的媒体播放。我们使用Nexus6手机作为测试手机,因为其属于谷歌Nexus/Pixel系列,可以较容易地进行AOSP编译与root。

模块提取

分析漏洞的第一步自然是先获得我们要分析的漏洞模块。该模块在/system/vendor/firmware目录下,用grep命令搜索widevine关键字后获得如下图5个文件。

很显然,该trustlet已经被分成了多个文件,因此我们需要知道其拼装的过程。QSEE为了从“普通环境”中加载trustlet向系统应用程序提供了libQSEECom.so共享对象,输出函数“QSEECom_start_app”:

不过由于大多数TEE相关产品都不开源,因此我们需要逆向分析该.so文件来理解这个加载的过程。本人通过使用IDA Pro对该库文件进行逆向,发现其主要做了如下几件事:

  1. 在函数_QSEECom_get_handle中,打开/dev/qseecom设备,并调用一些ioctl函数进行配置
  2. 在函数sub_DD0中,打开与trustlet相关的.mdt文件,并读取前0x34字节
  3. 在函数sub_DD0中,使用.mdt文件中的0x34字节计算.bXX文件的数量
  4. 在函数sub_DD0中,使用ion机制分配一个连续的缓冲区,并将.mdt和.bXX文件复制到其中
  5. 在函数sub_DD0中,调用ioctl函数加载trustlet,使用已分配的缓冲区

这看起来就是把这几个文件直接首尾连接起来组装成一个大文件送入TEE,因此依然不清楚该镜像使如何加载的。但是从0x34这个数字依然可以意识到这是个ELF头的大小,于是使用二进制编辑器打开mdt文件,发现前0x34的数据的确是一个ELF头。

此外接下来有连续4个程序头,正好对应了另外四个.bXX文件。其中前两个使NULL类型表示它们是“保留的”,不会被加载入镜像,实际打开.b00和.b01后也发现它们的内容都是mdt文件里已经有的,的确没必要再加载一次。于是就可以写脚本用mdt文件与.b02和.b03恢复出镜像widevine。

漏洞成因

既然已经得到了widevine镜像,那就继续使用IDA Pro对其中的代码进行分析。镜像最开始是entry_func函数,其中有调用get_handler_function,逐步调用后会调用关键函数WIDEVINE_HANDLE_CMD函数,该函数是trustlet注册的用于传入命令的函数。

从代码可知,命令的前32位位指令代码,且高16位为分类,可将命令分为大致4类:0X命令、2X命令、5X命令和6X命令。根据漏洞现有公开资料,本漏洞发生于5X命令的PRDiagMaintenanceHandler中:

该函数代码中似乎并没有问题,于是进一步查看其中调用的PRDiagClearProvisioningPRDiagVerifyProvisioning函数,果然在后者中发现了溢出漏洞:

在两次使用memcpy函数时,其长度是“普通环境”发来的指令内容的第三个dword,是普通环境下用户完全可控的。因此攻击者可以利用这个函数对trustlet中的global_data进行溢出攻击。

漏洞利用

本部分参考公开利用代码,首先,针对PRDiagVerifyProvisioning中的两个溢出点进行筛选,我们选择将指令的第四个dowrd设为0后触发第二个溢出点。因为这个执行路径更加直接快捷。

由于我们的溢出数据在global_data中,QSEE中使用R9寄存器一直保存global_data的指针,因此我们目前可以通过搜索使用R9寄存器的函数来寻找可能对我们exploit有帮助的函数。最终,找到了wv_get_session函数:

从该函数中我们可以知道该trustlet允许多进程与其交互,因此存在session机制,并且session的指针也保存在global_data中的一个数组里。那么,如果我们就可以利用溢出数据去覆盖这些指针,从而修改这些session指针的值。(该部分改写session指针原语的具体实现请见exploit中的overwrite_session_pointer函数)当然,伪造的session指针的值必须也指向一个符合session数据结构的合法地址,因此,我们需要确定该trustlet的内存位置。

通过分析函数OEMCrypto_DeriveKeysFromSessionKey可知其存在如下逻辑:

  1. 尝试访问session_pointer + 0xDA的数据,如果该值等于1,则返回24
  2. 如果该值不等于1,则返回35
  3. 如果session_pointer + 0xDA不是secapp region已经使用的地址,则trustlet崩溃

使用特性3,我们可以找出所有已分配给各个trustlet的内存地址,然后使用特性1和2可以得知目标位置内存的内容是否为1。于是通过对当前Widevine镜像进行特征提取,我们可以得到其利用特性1和2时返回值的模式,从而匹配到该trustlet的地址。

那么,当前我们还没有写入任意数据的能力,但是如下函数貌似可以帮上忙:

该函数生成一个随机数并通过调用addNonceToCache将该随机数添加到session数据结构中,session数据结构中有个数组可以保存16个随机数,每个随机数占一个DWORD,每次新来一个,别的15个就向后滚动一下。因此,我们可以利用随机数的最低8bit对任意我们设定的session+offset位置进行修改,修改的值是随机的但是范围在0-255内,因此多随机几次就行。改好一个Byte再修改下一个,方法详情如下:

这种方法会对后面15个DOWRD造成污染数据,因此需要小心使用,我们通过排查内存中的函数执政排布,发现可以利用wrapper_get_hdcp_capability函数进行exploit,因为其函数执政后面的函数在之后的exploit过程中不会再被使用了,因此数据污染了也无所谓。

至此通过修改该函数指针后即可在调用wrapper_get_hdcp_capability函数时劫持控制流。

接下来exploit作者就用了图灵机的思路把exploit执行流程的主体放在了“普通环境”,通过exploit_utilities.c中的execution_function函数来实现上文中的exploit原语。在”普通环境”下使用一条条指令来不断推进exploit过程。

参考

Bits, Please! – Exploring Qualcomm’s Secure Execution Environment

Freebuf译文1

Bits, Please! – QSEE privilege escalation vulnerability and exploit (CVE-2015-6639)

Freebuf译文2