Exploring Qualcomm's TrustZone implementation

来源:互联网 发布:mac人名中间的点怎么打 编辑:程序博客网 时间:2024/06/03 07:14

探索高通的TrustZone实施

我们从哪里开始?

首先,由于Qualcomm的TrustZone实现是封闭源码,据我所知,没有公开文档详细介绍其架构或设计,我们可能需要对包含TrustZone代码的二进制进行逆向工程,并对其进行分析。

获取TrustZone镜像

我们可以尝试从两个不同的位置提取图像; 无论是从设备本身还是从设备的出厂映像。

我的个人Nexus 5设备已经扎根,所以从设备中提取图像应该很简单。由于图像存储在eMMC芯片上,eMMC芯片的块和分区在“/dev/block/platform/msm_sdcc.1”下可用,我可以简单地将相关的分区复制到我的桌面上(使用“dd” )。

此外,分区在“/dev/block/platform/msm_sdcc.1/by-name”下有意义的链接:

这里写图片描述

正如你所看到的,这里有两个分区,一个名为“tz”(T rust Z一个的缩写),另一个名为“tzb”,作为“tz”图像的备用图像,与之相同。

然而,以这种方式提取图像,我仍然不满意,原因有二:

尽管TrustZone映像存储在eMMC芯片上,但是由于“Normal World”(通过要求系统总线上的AxPROT位可以设置),因此很容易造成这种影响,或者可能会丢失其中的几个部分。
拉动整个分区的数据不会显示关于图像的真实(逻辑)边界的信息,因此需要额外的工作来确定图像实际结束的位置。(实际上,由于“tz”图像是ELF二进制文件,所以它的大小包含在ELF头文件中,但这只是我们的一个侥幸)。

所以,从设备中提取出一个图像,让我们来看看一个工厂的图像。

Nexus 5的工厂图片均可从Google下载。工厂图像包含所有默认图像的ZIP,并且还包含引导加载程序映像。

在下载了与TrustZone相关的字符串的工厂映像和grep后,很快就显示引导程序映像包含所需的代码。

然而,在这里仍然有一个小问题需要解决 - 引导程序的图像是一个未知的格式(尽管也许一些Google-fu可以揭示所需的答案)。无论如何,使用十六进制编辑器打开文件并猜测其结构显示格式其实很简单:

这里写图片描述

引导加载程序文件具有以下结构:

魔法值(“BOOTLDR!”) - 8字节图像数量 - 4个字节从文件开始到图像数据开始的偏移量 - 4个字节图像中包含的数据的总大小 - 4个字节具有与“图像数量”字段匹配的数量的数组的数组。数组   中的每个条目都有两个字段:        图像名称 - 64字节(零填充)        图像长度 - 4字节

如上图所示,引导加载程序映像包含一个称为“tz”的映像,它是我们之后的映像。为了解压缩这个文件,我写了一个很小的python脚本(可以在这里),它接收一个引导加载程序的映像,并解压缩其中包含的所有文件。

在提取图像后,将其与先前从设备中提取的图像进行比较,我证实它们确实是相同的。所以我想这意味着我们现在可以继续检查TrustZone的图像。

修复TrustZone图像

首先,检查文件显示它实际上是一个ELF文件,这是一个很好的消息!这意味着内存段及其映射地址应该对我们可用。

使用IDA Pro打开文件后,让自动分析运行一段时间,我想开始翻转文件。然而,令人惊讶的是,未映射的地址(或者不包含在“tz”二进制文件中的地址)似乎有很多分支。

仔细观察之后,似乎所有指向无效地址的绝对分支都在文件的第一个代码段内,并且它们指向未映射的高地址。此外,第一个代码段的地址没有绝对的分支。

这似乎有点腥味…那么我们来看看ELF文件的结构呢?执行readelf显示以下内容:似乎有很多分支到未映射的地址(或者说,地址不包含在“tz”二进制文件中)。仔细观察之后,似乎所有指向无效地址的绝对分支都在文件的第一个代码段内,并且它们指向未映射的高地址。此外,第一个代码段的地址没有绝对的分支。这似乎有点腥味…那么我们来看看ELF文件的结构呢?执行readelf显示以下内容:似乎有很多分支到未映射的地址(或者说,地址不包含在“tz”二进制文件中)。仔细观察之后,似乎所有指向无效地址的绝对分支都在文件的第一个代码段内,并且它们指向未映射的高地址。此外,第一个代码段的地址没有绝对的分支。这似乎有点腥味…那么我们来看看ELF文件的结构呢?执行readelf显示以下内容:似乎所有指向无效地址的绝对分支都在文件的第一个代码段内,并且它们指向未映射的高地址。此外,第一个代码段的地址没有绝对的分支。这似乎有点腥味…那么我们来看看ELF文件的结构呢?执行readelf显示以下内容:似乎所有指向无效地址的绝对分支都在文件的第一个代码段内,并且它们指向未映射的高地址。此外,第一个代码段的地址没有绝对的分支。这似乎有点腥味…那么我们来看看ELF文件的结构呢?执行readelf显示以下内容:

这里写图片描述

有一个NULL段映射到更高的地址,这实际上与无效绝对分支指向的地址范围相对应!高通的人们都是偷偷摸摸的大熊猫:)

无论如何,我做了一个相当安全的猜测,这是第一个代码段实际上映射到错误的地址,应该被实际映射到更高的地址 - 0xFE840000。很自然地,我想使用IDA的rebase功能来重新分段,但是请看看!这导致IDA惊人地崩溃:

这里写图片描述

我实际上不确定这是否是高通的反反转功能,或者如果NULL段只是内部构建过程的结果,但是这可以通过手动修复ELF文件来轻松绕过。所有这些都是将NULL段移动到未使用的地址(由于IDA忽略),并将第一个代码段从其错误的地址(0xFC86000)移动到正确的地址(0xFE840000),如下所示:

这里写图片描述

现在,在IDA中加载图像后,所有的绝对分支都是有效的!这意味着我们可以继续分析图像。

分析TrustZone映像

首先,应该注意的是,TrustZone映像是一个相当大的(285.5 KB)二进制文件,具有相当少的字符串,没有公共文档。此外,TrustZone系统由完整的内核组成,其功能包括执行应用程序等等。所以…不清楚我们应该从哪里开始,因为逆转整个二进制文件可能需要太长时间。

由于我们要从应用处理器攻击TrustZone内核,因此最大的攻击面可能是安全的监视器调用,使“正常世界”能够与“安全世界”进行交互。

应当注意的是,还有其他向量可以与TrustZone进行交互,例如共享内存,甚至是中断处理,但由于它们构成的攻击面积要小得多,所以最好从分析SMC电话。

那么我们如何找到TrustZone内核处理SMC调用的位置?首先,我们回想起当执行SMC调用时,类似于处理SVC调用(即“普通世界”中的常规系统调用)),“安全世界”必须注册向量的向量的地址当遇到这样的指令时,处理器将跳转。

“安全世界” s等效是MVBAR(中号 onitor V厄克托乙 ASE 甲 ddress ř egister),其提供了含有用于由所述处理器在“安全的世界”处理的不同的事件处理函数的向量的地址。

使用MRC / MCR操作码进行MVBAR访问,具有以下操作数:

这里写图片描述

所以这意味着我们可以在TrustZone映像中简单地搜索具有以下操作数的MCR操作码,我们应该能够找到“监视器向量”。的确,在IDA中搜索操作码返回以下匹配:

这里写图片描述

可以看到,“开始”符号的地址(顺便说一下,唯一导出的符号)被加载到MVBAR中。

根据ARM文档,“Monitor Vector”具有以下结构:

这里写图片描述

这意味着如果我们查看前面提到的“开始”符号,我们可以为该表中的地址分配以下名称:

这里写图片描述

现在我们可以分析SMC_VECTOR_HANDLER函数。其实这个功能呢是负责相当多的工作呢?首先,它将所有状态寄存器和返回地址保存在预定义的地址(“安全世界”)中,然后将堆栈切换到预分配区域(也称为“安全环境”)。最后,在进行必要的准备工作后,继续分析用户要求的操作,并根据操作进行操作。

由于发布SMC的代码存在于高通公司的Linux内核的MSM分支中,我们可以看看“普通世界”可以发布到“安全世界”的命令格式。

SMC and SCM

令人困惑的是,高通公司选择通过SMC操作码(“安全通道管理器”)将“正常世界”与“安全世界”进行交互的渠道。

无论如何,正如我之前的博客文章中提到的,“qseecom”驱动程序用于使用SCM与“安全世界”进行通信。

Qualcomm在相关源文件中提供的文档是相当广泛的,足以很好地掌握SCM命令的格式。

不久之后,SCM命令可以分为两类:

常规SCM呼叫 - 当需要从“正常世界”传递到“安全世界”的信息时,会使用这些呼叫,这是为了服务SCM呼叫所必需的。 内核填充以下结构:

这里写图片描述

而TrustZone内核在服务SCM调用后,将响应写回“scm_response”结构:

这里写图片描述

为了分配和填充这些结构,内核可以调用包装函数“scm_call”,该函数接收到包含要发送的数据的内核空间缓冲区的指针,应该返回数据的位置,最重要的是服务标识符和命令标识符。

每个SCM调用都有一个“类别”,这意味着哪个TrustZone内核子系统负责处理该调用。这由服务标识符表示。命令标识符是在给定服务内指定哪个命令被请求的代码。

在“scm_call”函数分配并填充“scm_command”和“scm_response”缓冲区之后,它调用一个内部“__scm_call”函数,该函数刷新所有缓存(内部和外部高速缓存),并调用“smc”功能。

最后一个函数实际上执行SMC操作码,将控制传递给TrustZone内核,如下所示:

这里写图片描述

注意,R0设置为1,R1被设置为指向本地内核栈地址,该地址用作该调用的“上下文ID”,R2被设置为指向分配的“scm_command”结构的物理地址。

在R0中设置的这个“魔术”值表示这是使用“scm_command”结构的常规SCM调用。然而,对于需要较少数据的某些命令,无理由地分配所有这些数据结构将是相当浪费的。为了解决这个问题,引入了另一种形式的单片机通话。

原子SCM调用 - 对于参数数量相当低(最多四个参数)的调用,存在一种替代方式来请求SCM调用。

有四个包装函数,“scm_call_atomic_ [1-4]”,这与所要求的参数数量相对应。可以调用这些函数,以便使用给定的服务和命令ID以及给定的参数直接发出SCM调用的SMC。

以下是“scm_call_atomic1”函数的代码:

这里写图片描述

其中SCM_ATOMIC被定义为:

这里写图片描述

请注意,服务ID和命令ID都被编码为R0,以及调用中的参数数(在这种情况下为1)。这不是以前用于常规SCM调用的“魔术”值1。

R0中的这个不同的值表示TrustZone内核下面的SCM调用是一个原子调用,这意味着这些参数将使用R2-R5传递(而不是使用R2指向的结构)。

分析SCM调用

现在我们了解了SCM调用如何工作,并且我们已经在TrustZone内核中找到了用于处理这些SCM调用的处理函数,我们可以开始反汇编SCM调用来尝试在其中一个中找到一个漏洞。

我将略过SCM处理函数的大部分分析,因为大部分是用户输入的样板处理等。然而,在将堆栈切换到TrustZone区域并保存执行了呼叫的原始寄存器之后,处理功能继续处理服务ID和命令ID以查看应该调用哪个内部处理函数。

为了轻松映射服务和命令ID和相关的处理功能,将一个静态列表编译到TrustZone映像的数据段中,并由SCM处理函数引用。这是从列表中删除的一个短的:处理功能继续处理服务ID和命令ID,以查看应调用哪个内部处理函数。为了轻松映射服务和命令ID和相关的处理功能,将一个静态列表编译到TrustZone映像的数据段中,并由SCM处理函数引用。这是从列表中删除的一个短的:处理功能继续处理服务ID和命令ID,以查看应调用哪个内部处理函数。为了轻松映射服务和命令ID和相关的处理功能,将一个静态列表编译到TrustZone映像的数据段中,并由SCM处理函数引用。这是从列表中删除的一个短的:

这里写图片描述

您可以看到,列表具有以下结构:

指向包含SCM功能名称的字符串“键入”呼叫指向处理功能的指针参数数量每个参数的大小(每个参数一个DWORD)服务ID和命令ID,连接到单个DWORD中 - 例如,上面的“tz_blow_sw_fuse”功能的类型为0x2002,这意味着它属于服务ID 0x20,其命令ID为0x02。

现在剩下的是开始拆卸这些功能,希望找到一个可以利用的bug。

错误!

所以在上述所有上述SMC调用(其中69个)之后,我终于到达了以下功能:

这里写图片描述

通常,当使用常规SCM调用机制调用SCM命令时,R0将包含指向由内核分配的“scm_response”缓冲区的“结果地址”,但也由TrustZone内核验证它实际上是“允许”范围内的物理地址,也就是对应于Linux内核的内存的物理地址,而不是例如TrustZone二进制文件中的内存位置。

这个检查是使用内部函数执行的,我将在下一篇博文中更详细地介绍(所以保持发布!)。

但是如果我们使用原子SCM调用来执行函数会怎么样?在这种情况下,使用的“结果地址”是原子调用传递的第一个参数。

现在 - 你可以看到上面的功能中的错误吗?

与其他SCM处理函数相反,此函数无法验证R0中的值“结果地址”,因此如果我们传入:

R1为非零值(为了通过第一个分支)第四个参数(在上面的var_1C中传递)是非零R0作为任何物理地址,包括TrustZone地址空间范围内的地址

该函数将到达上述函数中最左侧的分支,并在R0中包含的地址上写入零DWORD。