转载:http://www.freebuf.com/articles/system/103152.html
在本篇文章中,我们将讨论高通安全执行环境(QSEE)。
之前讨论过,设备使用TrustZone的最主要的原因之一是它可以提供“可信执行环境(TEE)”,该环境可以保证不被常规操作系统干扰的计算,因此称为“可信”。
这是通过创建一个可以在TrustZone的“安全世界”中独立运行的小型操作系统实现的,该操作系统以系统调用(由TrustZone内核直接处理)的方式直接提供少数的服务。另外,TrustZone内核可以安全加载并执行小程序“Trustlets”,以便在扩展模型中添加“可信”功能。Trustlets程序可以为不安全(普通世界)的操作系统(本文指的是Android)提供安全的服务。
以下为设备上常用的Trustlets:
keymaster:实现由Android“keystore”守护进程提供的**管理API,它可以安全的生成和存储**,并运行用户使用这些**操作数据
widevine:实现Widevine DRM,提供安全的媒体播放
实际上,根据OEM和设备的不同有很多DRM-relatedtrustlets,但是以上两种是通用的。
开始
下面通过对widevine模块的分析,来了解工作原理。
在设备固件中搜索widevine trustlet:
很显然,trustlet被分成多个文件..打开这些文件发现显示比较混乱..有些文件中包含代码、ELF头和元数据。在分解trustlet前,需要了解一下它的格式。我们可以挨个儿打开每个文件,尝试猜测内容的含义,或者查看用于加载该trustlet的代码路径。下面来都尝试一下。
加载TRUSTLET
为了从“普通世界”加载trustlet,应用程序可以使用libQSEECom.so共享对象,输出函数“QSEECom_start_app”:
遗憾的是,该函数的源代码是不可用的,因此我们需要使用****获取其实现代码。我们发现它可以执行以下操作:
打开/dev/qseecom设备,并调用一些ioctl函数来配置它
打开与trustlet相关的.mdt文件,并读取前0×34字节
使用.mdt文件中的0×34字节计算.bXX文件的数量
分配一个连续的缓冲区(使用ion),并将.mdt和.bXX文件复制到其中
最后,调用ioctl函数加载trustlet,使用已分配的缓冲区
但是目前仍然不清楚镜像如何加载,但是我们也获得了一些信息:
首先,数字0×34可能看起来很熟悉——这是32比特ELF头的大小,打开.mdt文件文件发现,第一个0×34字节确实是一个有效的ELF头:
另外,QSEECOM_start_app函数使用比特于0x2C偏移的字,以便于计算.bXX文件的数量,对应上图中的ELF头中的e_phnum字段。
e_phnum字段通常用来指定程序头文件的数量,这就表示可能每个.bXX文件都包含独立的trustlet段。打开每个文件发现它们确实看起来像一段加载的程序。但是保险起见,我们还是得找到程序的头部(并检查是否与.bXX文件匹配)。
实际上,.mdt文件中接下来的几个数据库确实是程序头,对应每一个.bXX文件。
正如之前猜想,它们的大小与.bXX文件文件完全匹配。
注意,上图中的前两个程序头看起来有些奇怪——它们都是NULL类型,表示它们是“保留的”,不会被加载到最终的ELF镜像中。更加奇怪的是,打开相应的.bXX文件发现第一个数据块包含与.mdt中相同的ELF头和程序头,第二个数据块中包含剩余的.mdt文件。
下图为根据目前所知的简单示意图:
需要注意的是,由于ELF头和程序头都包含在.mdt文件中,我们可以使用readelf命令快速转储trustlet中与程序头相关的信息:
此时,我们从.mdt和.bXX文件中获得了所有创建完整和有效的ELF文件所需的信息;我们有ELF头和程序头,已经每个片段。我们只需要使用这些数据写一个小脚本就可以创建ELF文件。
可信TRUSTLETS的反射
现在,我们对trustlets如何组装成一个可执行文件已经有了基本的了解,但是还不清楚它怎么样验证。我们知道,.bXX文件中只是包含加载的片段,这就意味着,该数据也必须存在于.mdt文件中。
假设,要创建一个可信加载器,我们要怎么做?
一个通用的方法是使用hash-and-sign(基于CRHF和数字签名)。本质上,我们计算用于身份验证的数据的哈希值,并使用私钥(其对应的公钥加载器已知)对其进行签名。
因此,我们需要在.mdt文件中找到两个信息:
证书链
签名的二进制对象
接下来,让我们通过查找证书链开始。证书的格式有很多种,但是由于.mdt文件只包含二进制数据,我们可以猜想它可能是二进制格式,最常见的是DER。
这里有一个快速找到DER编码的证书的**方法——它们通常以“ASN.1 SEQUENCE”(编码后为0×30 0×82)开始。那么我们需要做的就是在.mdt文件中查找这两个字节,并将每个结果都保存在文件中。然后,只需要检查这些数据是否为使用“openssl”的结构良好的证书:
经比对,这些就是我们要找的证书。
事实上,trustlet中一次包含了三个证书。保险起见,我们需要检查这三个证书是否真实形成了一个有效的可信链,具体方法是将证书转储成独立的证书链文件,使用openssl验证该证书:
对于该链的可信根,通过查看该链的根证书发现,与验证高通的“安全引导”进程的引导链的其他部分的证书相同。目前已经有一些关于这种机制的研究,该验证通过匹配根证书的SHA256和一个特殊的值“OEM_PK_HASH”,该值在生产过程中被混淆到设备的QFuses中,并且在理论上是不可修改的,这就意味着建立一个这样的根证书需要对SHA256实施二次原像攻击。
现在,让我们回到.mdt文件——我们已经找到了证书链,现在需要的是签名。通常,私钥用于生成签名,公钥用于恢复签名数据。由于我们拥有该链最顶层证书的公钥,我们可以使用该值重新检查文件,尝试恢复每一个二进制对象。
但是,我们要怎样知道是否成功呢?
RSA是一个陷门置换家族——每一个相同比特的二进制对象都通过一个公共模N映射到相同大小的二进制对象中。然而,当RSA的公共模长度为2048比特时,多数哈希值会比这更短(SHA1为160比特,SHA256为265比特)。也就是当我们尝试使用该公钥解密二进制对象时,会出现很多“空”空间(例如,零字节)。但是我们有很大可能得到想要的签名(对于一个完全随机的排列,连续n个零比特的可能性为2^-n——几率非常小)。
我写了一个小程序(使用带有PKCS #1 v1.5填充的rsa_public_decrypt函数),用于加载证书链最顶层证书的公钥,并尝试恢复文件中的二进制对象。如果恢复的二进制对象以一串零字节结尾,那么输出结果。以下为运行结果:
我们成功获取了一个签名。
更重要的是,该签名长度为265比特——这说明它可能是一个SHA256哈希。如果.mdt中存在一个SHA256,就说明可能有更多的SHA256:
如上图,.bXX文件中的SHA256哈希值同样存储在.mdt文件中,并且是连续的。我们可能做一个可靠的猜测,这可能是用于生成之前的签名的数据。
注意,.b01文件的哈希值丢失了——为什么?记住.b01文件中包含处理ELF头和程序头之外的.mdt文件中的所有数据。由于该数据中包含以上签名,并且该数据又用于生成数据块文件的哈希值,这就会形成一个依赖循环。因此,说该数据块的哈希值不存在是有道理的。
到现在为止我们已经解码了.mdt文件中的所有数据,处理位于程序头后的一个小结构体。经观察,该结构体中只是简单地包含了“.mdt”的各个部分的指针和长度:
因此,我们已经解码了.mdt文件中的所有数据:
摩托罗拉HAB(High Assurance Boot)
尽管.mdt文件的格式对所有原始设备供应商通用,但是摩托罗拉则有些不同。
与我们之前看到的提供RSA签名不同,摩托罗拉设备中签名字段为空(以上展示的签名来自Nexus 5设备),签名如下:
那么这样的镜像怎样验证呢?
这是通过摩托罗拉调用HAB(High Assurance Boot)的机制完成的。该机制允许将整个.mdt文件的证书链和签名附加到文件的末尾,并使用HAB的专有格式编码:
有关该机制的详细信息,可参看Tal Aloni调研。简而言之,就是.mdt文件使用证书链顶端的**哈希编码并签名,该链的根证书使用在引导程序阶段被硬编码的Super Root Key进行验证。
TRUSTLET的生命周期
在以上的验证程序之后,TrustZone内核将trustlet程序段加载到“普通世界”无法访问的安全存储区域(secapp-region),并为其分配ID。
随后,内核切换到“安全世界”用户模式,并运行trustlet的入口函数:
如上图,trustlet会使用“handler”函数在TrustZone内核中自动完成注册,注册完成后,trustlet会将控制权重新交给TrustZone内核,完成加载进程。
一旦trustlet加载完成,“普通世界”就可以通过调用SCM(QSEOS_CLIENT_SEND_DATA_COMMAND,其中包含已加载trustlet ID和请求响应缓冲区)向trustlet发送命令,如下:
TrustZone内核(TZBSP)接收到SCM调用后,将其映射到QSEOS,查找给定ID的应用程序,调用“handler”函数,处理请求。
后续
现在我们对trustlets及其加载有了一定的了解,接下来我们就可以发起攻击。在下一篇文章中,我们将在一个热门的trustlet中挖漏洞,并利用该漏洞在QSEE中执行代码。