tee漏洞学习-翻译-2:探索 Qualcomm TrustZone的实现

2024-02-06 08:12

本文主要是介绍tee漏洞学习-翻译-2:探索 Qualcomm TrustZone的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文:http://bits-please.blogspot.com/2015/08/exploring-qualcomms-trustzone.html

获取 TrustZone image

从两个不同的位置提取image

  • 从手机设备本身
  • 从google factory image

已经root的Nexus 5设备,image存储在eMMC芯片上,并且eMMC芯片的分区在/dev/block/platform/msm_sdcc.1下,可以通过dd命令进行复制 。

此外,在/dev/block/platform/msm_sdcc.1/by-name分区下,包含tztzb这些有意义的名称:
在这里插入图片描述

tz(TrustZone 的缩写),另一个名为tzb,作为tz映像的备份映像,并且与tz映像相同。

直接从手机内部提取,可能存在两个问题:

  • 尽管 TrustZone 映像存储在 eMMC 芯片上,但“正常世界”很容易无法访问它(通过要求设置系统总线上的 AxPROT 位),或者它的多个部分可能会丢失。
  • 拉取整个分区的数据不会显示有关image真实(逻辑)边界的信息,因此需要一些额外的工作来确定image实际结束的位置。 (实际上,由于“tz”image是 ELF 二进制文件,因此它的大小包含在 ELF 标头中)。

因此,从设备中提取了一个image后,让我们看一下google factory image。

Nexus 5 的出厂镜像均可从 Google 下载。出厂映像包含一个包含所有默认映像的 ZIP,另外还包含引导加载程序映像。KTU84P

下载工厂映像并查找与 TrustZone 相关的字符串后,很快就发现bootloader包含所需的代码。

然而,这里仍然有一个小问题需要解决 - 引导加载程序image的格式未知。无论如何,使用十六进制编辑器打开该文件并猜测其结构实际上非常简单:
在这里插入图片描述

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

  • magic值(“BOOTLDR!”)- 8 个字节
  • image数量 - 4 字节
  • 从文件开头到image数据开头的偏移量 - 4 个字节
  • image中包含的数据的总大小 - 4 字节
  • 一个数组,其中包含与上面的“image数量”字段匹配的多个条目。数组中的每个条目都有两个字段:
    • image名称 - 64 字节(零填充)
    • image长度 - 4 字节

正如您在上图中看到的,引导加载程序映像包含一个名为“tz”的映像,这就是我们要查找的映像。为了解压该文件,我编写了一个小型 python 脚本(可在此处获取),该脚本接收引导加载程序映像并解压其中包含的所有文件。

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

import sys, struct, osdef main():#Reading the commandline argumentsif len(sys.argv) != 3:print "USAGE: %s <BOOTLOADER_IMAGE> <OUTPUT_DIR>" % sys.argv[0]returnbootloader_path = sys.argv[1]output_path = sys.argv[2]#Verifying the magicbootloader_file = open(bootloader_path, 'rb')magic = bootloader_file.read(8)if magic != "BOOTLDR!":print "[-] Read incorrect magic: %s" % magic.encode("hex")returnprint "[+] Read correct magic"#Reading in the metadata blockimage_count,data_start_addr,total_size = struct.unpack("<III", bootloader_file.read(12))print "[+] Found %d images, starting at %08X, total size: %08X" % (image_count, data_start_addr, total_size)image_metadata = []for i in range(0, image_count):image_name = bootloader_file.read(64).strip('\x00')image_len = struct.unpack("<I", bootloader_file.read(4))[0]image_metadata.append((image_name, image_len))print "[+] Images: %s" % str(image_metadata)#Dumping each imagebootloader_file.seek(data_start_addr, 0)for image_name, image_len in image_metadata:print "[+] Dumping %s" % image_namedata = bootloader_file.read(image_len)open(os.path.join(output_path, image_name), 'wb').write(data)print "[+] Done"if __name__ == "__main__":main()

修复 TrustZone 映像

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

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

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

这看起来有点可疑…那么我们看一下 ELF 文件的结构怎么样?执行 readelf 会显示以下内容:
在这里插入图片描述

有一个 NULL 段映射到更高的地址,它实际上对应于无效绝对分支指向的地址范围!

不管怎样,我做了一个相当安全的猜测,那就是第一个代码段实际上映射到了错误的地址,实际上应该映射到更高的地址 - 0xFE840000。很自然地,我想使用 IDA 的 rebase 功能对段进行 rebase,但是你瞧!这会导致 IDA 严重崩溃:
在这里插入图片描述

在这里插入图片描述
0xFC58C48地址太低了,不在加载地址范围之内)

我实际上不确定这是否是高通的反逆向功能,或者 NULL 段是否只是其内部构建过程的结果,但这可以通过手动修复 ELF 文件轻松绕过。所需要做的就是将 NULL 段移动到未使用的地址(因为 IDA 无论如何都会忽略它)Type 类型为NULL,没啥用,所以会被忽略,除非专门为tz编写了加载器,并将第一个代码段从错误的地址 (0xFC86000) 移动到正确的地址 (0xFE840000)这个需要自己用IDA打开提取出的tz,稍微看看就能理解,如下所示:
在这里插入图片描述

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

分析 TrustZone image

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

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

当然,应该指出的是,我们还可以通过其他方式与 TrustZone 进行交互,例如共享内存甚至中断处理,但由于这些攻击面要小得多,因此最好从分析 SMC 调用。

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

“安全世界”的等效项是MVBAR(监视器向量基地址寄存器),它提供向量的地址,该向量包含“安全世界”中处理器处理的不同事件的处理函数。

正向的MRC/MSR

MRS x0, TTBR0_EL1 // Move TTBR0_EL1 into x0
MSR TTBR0_EL1, x0 // Move x0 into TTBR0_EL1

每个系统寄存器都可看做是一个标号 正向的源码中可以写寄存器名称,编译器认识,但逆向的IDA中只能看到寄存器标号

使用任意一个插件,IDA将会识别系统寄存器
https://github.com/gdelugre/ida-arm-system-highlight
https://github.com/NeatMonster/AMIE

访问 MVBAR 是使用 MRC/MCR 操作码和以下操作数完成的:
在这里插入图片描述

因此,这意味着我们可以简单地在 TrustZone 映像中搜索具有以下操作数的 MCR 操作码,并且我们应该能够找到“监视器向量”。事实上,在 IDA 中搜索操作码会返回以下匹配项:
在这里插入图片描述

正如您所看到的,“开始”符号的地址(顺便说一下,这是唯一导出的符号)被加载到 MVBAR 中
根据ARM文档,Monitor Vector具有以下结构:
在这里插入图片描述

这意味着,如果我们查看前面提到的“开始”符号,我们可以将以下名称分配给该表中的地址:
下图中解析的有问题,Monitor Vector的首地址是0xFE810000
在这里插入图片描述
现在,我们可以分析SMC_VECTOR_HANDLER函数。
实际上,这个函数负责很多任务;

  • 首先,它将所有状态寄存器和返回地址保存在预定义的地址中(在“安全世界”中),
  • 然后,它将堆栈切换到预分配区域(也在“安全世界”中)。
  • 最后,在进行必要的准备之后,它会继续分析用户请求的操作并据此进行操作。

由于发出 SMC 的代码存在于 Linux 内核的高通 MSM 分支中,因此我们可以看一下“正常世界”可以向“安全世界”发出的命令格式。

SMC and SCM(SCM没啥意义,就是高通自己给自己的SMC调用取了个名字)

令人困惑的是,高通选择将“正常世界”通过 SMC 操作码与“安全世界”交互的通道命名为 SCM(安全通道管理器)

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

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

简而言之,SCM 命令分为两类:

  • 常规 SCM call - 参数很的调用方式,通过共享内存进行传参
  • Atomic SCM call - 轻量的调用方式,通过寄存器传参

常规 SCM call - 当需要将信息从“正常世界”传递到“安全世界”时使用这些call,这是为 SCM call提供服务所必需的。
内核填充以下结构:
在这里插入图片描述

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 调用,使用“scm_command”结构。然而,对于某些需要较少数据的命令,无缘无故地分配所有这些数据结构将是相当浪费的。为了解决这个问题,引入了另一种形式的 SCM 调用。

Atomic SCM call - 对于参数数量相当低(最多四个参数)的调用,存在另一种请求 SCM 调用的方法。

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

这是“scm_call_atomic1”函数的代码:
在这里插入图片描述

其中 SCM_ATOMIC 定义为:
在这里插入图片描述
请注意,服务 ID 和命令 ID 以及调用中的参数数量(在本例中为 1)都被编码到 R0 中。这取代了之前用于常规 SCM 调用的“神奇”值 1。
R0 中的这个不同值向 TrustZone 内核表明以下 SCM 调用是原子调用,这意味着参数将使用 R2-R5 传递(而不使用 R2 指向的结构)。

分析 SCM 调用

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

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

为了轻松映射服务和命令 ID 以及相关处理函数,静态列表被编译到 TrustZone 映像的数据段中,并由 SCM 处理函数引用。以下是列表中的一小段内容:
在这里插入图片描述
如您所见,该列表具有以下结构:

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

现在剩下的就是开始反汇编这些函数,并希望找到可利用的错误。

The Bug!

因此,在研究了所有上述 SMC 调用(全部 69 个)之后,我终于得到了以下函数:
在这里插入图片描述
通常,当使用常规 SCM 调用机制调用 SCM 命令时,R0 将包含结果地址,该地址指向由内核分配的“scm_response”缓冲区,但也由 TrustZone 内核验证以确保它实际上是“允许”范围内的物理地址 - 即对应于 Linux 内核内存的物理地址,而不是 TrustZone 二进制文件中的内存位置。

此检查是使用内部函数执行的,我将在下一篇博客文章中更详细地介绍该函数。


但是如果我们使用原子 SCM 调用来执行函数会发生什么?在这种情况下,使用的结果地址是原子调用传递的第一个参数。
在这里插入图片描述

现在 - 你能看到上面函数中的错误吗?

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

  • R1为非零值(为了通过第一个分支)(原文有问题,是R0为非0
    在这里插入图片描述

  • 第四个参数(在上面的 var_1C 处传入)非零

    • LDR R0,[SP, #x28+var_1C]
    • CBZ R0, loc_FE84B372
    • 进入最左侧的分支
  • R0 为任何物理地址,包括 TrustZone 地址空间范围内的地址

    • MOVS R6, R0
    • MOVS R1, #0
    • STR R1, [R6]

该函数将到达上面函数中最左边的分支,并在 R0 中包含的地址写入一个零 DWORD

What’s next? 下一步是什么?

在下一篇博文中,我将分享针对上述漏洞的详细(而且相当复杂!)利用,该漏洞可以在 TrustZone 内核中实现完整的代码执行。我还将发布完整的漏洞利用代码,敬请期待!

这篇关于tee漏洞学习-翻译-2:探索 Qualcomm TrustZone的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/683669

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja