在这篇博客中,我们将逆向高通TrustZone的实现(SnapDragon SoC上)。
首先,因为高通TrustZone的实现是闭源的,所以据我所知,市面上没有任何关于它的架构或是设计的文档。所以,我们大概得要逆向它的二进制码来得到TrustZone的代码,并且分析它。
我们有两种地方来取得镜像文件;要么从设备本身,要么从设备的原厂镜像。
我的Nexus 5已经有了root权限,所以从设备中得到镜像十分直接。因为镜像存储在eMMC芯片中,而eMMC芯片的分区和板块可以从“/dev/block/platform/msm_sdcc.1 ”下取得,我只需要简单的将相关分区复制到我的电脑里“用dd”。
并且,这些分区的名字都有自己的意义:

就像在图中,有两个分区,一个叫做“tz”(TrustZone的简写),一个叫做“tzb”(TrustZone backup的简写)。他们两个的内容是一样的。
然而,通过这个方式取得镜像文件,我仍然觉得不满意,两个原因:
——虽然TrustZone镜像文件存储在eMMC芯片中,但高通很简单就可以让正常世界没法得到这个镜像(要求系统主线上的AxPROT比特位必须被置位),或者这个镜像可能缺少某些部分。
——从整个分区里提取数据并不能泄露实际(逻辑上)镜像的界限,所以我们还要做些额外的工作才能确定tz镜像究竟在何处完结(在这里,因为tz镜像是一个elf文件,所以我们可以通过elf头文件简单的解决这个问题,但这只是我们运气好罢了)
所以,用完第一种方法,我们再来看看第二种方法,原厂镜像。
Nexus 5的原厂镜像可以从Google中下载。这个原厂镜像包涵一个zip压缩文件,这个zip里有全部默认的镜像,并且还有BootLoader镜像。
在下载了原厂镜像后,我们用grep来找和TrustZone有关的字符串,很快我们就能发现在BootLoader镜像中有我们想要的代码。
然而,还有一个问题。BootLoader镜像是一个未知格式(也许google工作人员知道这是啥)。即使这样,用十六进制编辑器打开这个镜像文件就能猜到大部分结构,其实这个格式很简单:

这个BootLoader镜像文件有以下结构:
——特征值(“BOOTLDR!”) - 8字节
——镜像个数 - 4字节
——从文件开始到镜像开始的偏移 - 4字节
——镜像中数据的总大小 - 4字节
——一个数列,这个数列的大小和“镜像个数”中的值相同,这个数列中每个向量有两个参数:
——镜像名字 - 64字节(用0来填充)
——镜像长度 - 4字节
你可以看到上图中叫tz的镜像就是我们要找的镜像。为了解开这个文件,我写了一个小的python脚本(在这儿),这个脚本会打开BootLoader镜像然后弄出里面的镜像。
将镜像弄出来以后,我们将它与我们用第一个方法得到的镜像相比较,结果是一样的。所以我觉得这意味我们可以接着分析TrustZone镜像咯。
首先,我们发现这些镜像文件其实都是ELF文件,好消息呀!这意味着它们的内存分段和映射地址我们都应该能知道。
我们用IDA Pro打开这个文件,并且让IDA的自动分析跑了一会儿,我想开始逆向这个文件。然而,令人意外的是,好像有一大堆分支都指向没有映射的地址(甚至于,“tz”二进制文件里不包含的地址)。
我又看了一会儿,似乎所有指向没有映射的地址的分支都在第一个代码分段里,而且他们都指向没有映射的高地址。并且,没有任何绝对分支指向第一段代码分段。
这看上去有些可疑。。。所以我们来看看这个ELF文件的文件结构吧?使用readelf可以看到以下信息:

可以发现,有一个NULL类分段映射到高地址,这个分段相对应的映射地址就是那些无效绝对分支指向的地方!
所以,我做了一个安全的猜测,第一个代码分段其实映射到了错误的地址,它应该被映射到更高的地址 -- 0xfe840000. 所以自然的,我想要重定位这个分段的基地址,然而当我使用IDA的重定位基地址时,IDA崩溃了:

我实际上不确定这是高通故意来防止逆向工程的方法,还是说Null分段只是他们内部build过程的结果,但是这可以通过手动修复ELF文件来解决。我们需要做的只是将Null分段移到一个未使用过的地址(反正他也被IDA忽略了),然后再移第一个代码分段,从0x0fc86000到0xfe840000,像以下这样:

现在,再次把镜像文件用IDA读取,所有的绝对分支都有效了!这意味着我们可以接着分析镜像咯。
首先,值得注意的是TrustZone镜像文件是一个相对比较大的二进制文件(285.5KB),其中的字符串相对较少,并且没有公开的文档。其次,TrustZone系统由一个完整的内核组成,这个内核具有例如执行应用程序的能力等等。所以。。。我们应该从哪里开始逆向并不明确,因为如果逆向整个二进制文件会花太多时间。
因为我们想要做的的是从应用处理器攻击TrustZone内核,所以最大的攻击面应该是那些是的正常世界可以与安全世界交互的SMC。
值得注意的是,当然,除了SMC以外,我们还有其他与TrustZone交互的方式,比如说共享内存或是中断处理,但因为这些交互方式提供的攻击面更窄,所以从分析SMC调用开始应该是个好的选择。
所以说,我们如何在TrustZone内核中找到处理SMC调用的部分?首先,让我们先回想一下当执行SMC调用时,类似于处理正常世界的系统调用(SVC调用),安全世界必须先注册当遇到这个指令时处理器将要跳转到的向量地址(向量就是指一小段代码,类似于x86的中断向量)。
安全世界的对应是MVBAR(监督向量基地址寄存器),它将提供向量地址,这个向量地址包涵安全世界处理器对不同世界的处理函数。
使用MRC/MCR操作码就可以控制MVBAR,比如以下操作码:

这意味着我们可以简单的通过在TrustZone镜像里搜索MCR操作码,来找到监督向量。事实上,在IDA里搜索操作码会得到以下结果:

如图,start符号的地址(顺便说一句,这是唯一的exported符号)被载入到了MVBAR里。
根据ARM文档,这个监督向量有以下结构:

这意味着如果我们看向之前提到的start符号,我们可以将下面的名字分配给那个表:

现在,我们就能够分析上图中的SMC_VECTOR_HANDLER函数(SMC向量处理函数)。事实上,这个函数负责了相当多的任务;首先,它保存所有的状态寄存器和返回地址到预先定义好的地址(在安全世界里),然后,它将栈切换到预先定义好的区域(也是安全世界里)。最后,在进行了必要的准备后,它接着分型用户要求的操作并且按照要求进行操作。
因为执行SMC的代码被包含在Linux内核的高通MSM分支,我们可以看看从正常世界向安全世界发送的操作要求的格式。
[培训]科锐逆向工程师培训第53期2025年7月8日开班!