在这篇博客文章中,我们将涵盖利用先前文章中描述的 TrustZone 漏洞的完整过程。如果你还没有阅读它,请先阅读!
负责任的披露
首先,我要指出我已经向高通负责任地披露了这个漏洞,并且问题已经得到解决(见下文的时间轴)。
我还想利用这个机会指出,高通在回应披露方面做得非常出色,非常积极地解决问题。
他们还送了我一台全新的(当时)Moto X 2014,它将成为以后许多帖子的主题(深入探讨 TrustZone 的架构和设备上的其他安全组件)。
零号患者
在开发此漏洞利用程序时,我只有我的可靠(个人)Nexus 5 设备可供使用。这意味着下面所有的内存地址和其他具体信息都是从该设备中取得的。
如果有人想重新创建下面所描述的精确研究,或者由于任何其他原因,我的设备在当时的确切版本是:
google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys
有了这个,我们就开始吧!
漏洞原语
如果你读过上一篇文章,你已经知道这个漏洞允许攻击者在 TrustZone 内核的虚拟地址空间中的任何地址写入 DWORD 0。
基于个人经验,零写原语并不是很有趣。它们通常相当有限,不一定导致可利用的情况。为了使用这种弱原语创建一个健壮的漏洞利用程序,首要行动是尝试利用这个弱原语来建立一个更强的原语。
创建任意写原语
由于 TrustZone 内核加载在已知的物理地址上,这意味着所有地址已经事先知道,不需要在执行时发现。
然而,TrustZone 内核的内部数据结构和状态很大程度上是未知的,并且由于许多不同的进程与 TrustZone 内核交互(从外部中断到“安全世界”应用程序等),它们可能会发生变化。
此外,TrustZone 代码段被映射为只读访问权限,并且在安全启动过程中进行验证。这意味着一旦 TrustZone 的代码加载到内存中,理论上不应该再发生任何改变。
因此,我们该如何利用零写原语来实现完整代码执行呢?
我们可以尝试编辑 TrustZone 内核中的任何可修改数据(例如堆、栈或全局变量),这可能会为我们创建一个更好的原语的垫脚石。
正如我们在上一篇博客文章中提到的那样,通常在调用 SCM 命令时,任何指向内存的参数都会被 TrustZone 内核验证。验证是为了确保物理地址在“允许”的范围内,而不是例如在 TrustZone 内核使用的内存范围内。
这些验证听起来像是我们要研究的主要候选项,因为如果我们能够禁用它们的操作,我们就可以利用其他 SCM 调用来创建不同类型的原语。
TrustZone 内存验证
让我们开始给内存验证函数起一个名称 - 从现在开始,我们将称之为 tzbsp_validate_memory
。
以下是该函数的反编译代码:
这个函数实际上调用了两个内部函数来执行验证,我们分别称它们为 is_disallowed_range
和 is_allowed_range
。
is_disallowed_range
正如你所看到的,该函数实际上使用给定地址的前12位,具体如下:
- 高 7 位用作索引,索引一个包含 128 个 32 位宽值的表。
- 低 5 位用作在先前索引的位置上存在的 32 位条目中要检查的位索引。
换句话说,对于要进行验证的内存区域交叉的每个 1MB 块,存在上述表格中用于表示该数据区域是否“不允许”的位。如果给定区域中的任何块都被禁止,则该函数返回表示此类情况的值。否则,该函数将给定的内存区域视为有效。
is_allowed_range
虽然稍微有些长,但这个函数也很简单。它基本上只是遍历一个静态数组,其中包含以下结构的条目:
该函数遍历在给定内存地址处的表中的每个条目,当当前条目的 end_marker
字段为 0xFFFFFFFF
时停止遍历。
每个条目指定的范围都会进行验证,以确保内存范围是允许的。但是,正如上面的反编译所示,设置 flags
字段第二位的条目将被跳过!
攻击验证函数
现在我们理解了验证函数的操作方式,让我们看看如何使用零写入原语来禁用它们的操作。
首先,如上所述,is_disallowed_range
函数使用一个 32 位条目的表,其中每个位对应 1MB 内存块。设置为1的位表示禁止块,而零位表示允许块。
这意味着我们可以通过使用零写入原语将表中的所有条目设置为零来轻松地使该函数无效。这样做将使所有内存块被标记为允许。
转到下一个函数 is_allowed_range
。这有点棘手-如上所述,在标志字段中设置第二位的块会根据给定地址进行验证。但是,对于在其中未设置此位的块,不执行验证,并跳过该块。
由于设备中的块表只与驻留在 TrustZone 内核内存范围内的内存范围有关,因此我们只需要将此字段设置为零即可。这样做将导致验证函数跳过它,因此验证函数将接受 TrustZone 内核内存中的内存地址为有效。
回到编写原语
现在我们已经摆脱了边界检查函数,可以自由地提供任何内存地址作为 SCM 调用的参数,而不会有任何障碍。
但是,我们是否更接近创建编写原语?理想情况下,如果有一个 SCM 调用,我们可以控制写入到受控位置的一块数据块,那就足够了。
不幸的是,在查看所有 SCM 调用之后,似乎没有符合此描述的候选者。
尽管如此,不需要担心!无法通过单个 SCM 调用实现的操作,可能通过串联几个调用来实现。从逻辑上讲,我们可以将创建任意写入原语拆分为以下步骤:
- 在受控位置创建一个未受控的数据块
- 控制创建的数据块,使其实际包含所需内容
- 将创建的数据块复制到目标位置
创建
虽然没有SCM调用似乎是创建受控数据块的好选择,但有一个调用可以用于在受控位置创建未受控的数据块 tzbsp_prng_getdata_syscall
。
如其名称所示,可以使用此函数在给定位置生成随机字节缓冲区。通常,Android 使用它以利用 Snapdragon SoC 中存在的硬件 PRNG。
在任何情况下,SCM 调用接收两个参数;输出地址和输出长度(以字节为单位)。
一方面,这很好 - 如果我们(在某种程度上)信任硬件随机数生成器(RNG),我们可以相当确信使用此调用生成的每个字节都可能是输出范围内的任何字节值。另一方面,这意味着我们对所生成的数据完全没有控制权。
控制
即使在使用 PRNG 时可能会生成任何输出,也许有一种方法可以验证所生成的数据是否实际上是我们要写入的数据。
为了做到这一点,让我们想象以下游戏 - 假设您有一个有四个槽的老虎机,每个槽有 256 种可能的值。每次拉动手柄时,所有槽都同时旋转,并呈现出随机输出。您需要拉多少次手柄才能使结果完全匹配之前选择的值?嗯,有 4294967296(2^32)种可能的值,因此每次拉手柄时,结果与所需结果匹配的几率约为 10^(-10)。听起来你要在这里呆上一段时间了…
但是,如果你能够作弊呢?例如,如果每个插槽都有一个不同的手柄?这样你就只能每次更改一个插槽的值。这意味着现在每次拉动手柄,有 1/256 的机会,结果将与该插槽的期望值匹配。
听起来游戏现在变得更容易了,对吧?但是变得有多容易?在概率论中,这种单个“游戏”的分布被称为伯努利分布,实际上只是一种说法,即每次试验都有一组成功的概率,表示为 p
,所有其他结果都被标记为失败,并且有 1-p
的概率发生。
假设我们希望有 90% 的成功率,结果表明在原始版本的游戏中,我们需要进行大约 10^8 次尝试(!),但如果我们作弊,我们每个插槽只需要大约 590 次尝试,这是几个数量级的差异。
那么你是否已经弄清楚了这与我们的写入原语有什么关系呢?以下是解释:
首先,我们需要找到一个 SCM 调用,它可以将内存中可写的位置的值返回给调用者。
有很多这样的函数。其中一个候选项是 tzbsp_fver_get_version
调用。该函数可用于“普通世界”,以检索不同 TrustZone 组件的内部版本号。它通过接收表示要检索版本号的组件的整数和要写入版本代码的地址来实现。然后,该函数只是在一个静态的包含组件 ID 和版本代码的对的数组中遍历。当找到具有给定 ID 的组件时,版本代码将写入输出地址。
现在,我们可以使用 tzbsp_prng_getdata_syscall
函数,开始逐字节操作任何版本代码的值。为了知道在每次迭代中生成的字节的值,我们可以简单地调用上述 SCM,同时传递与要修改其版本代码的组件匹配的组件 ID,以及提供一个返回地址,该地址指向可读取的(即不在 TrustZone 中的)内存位置。
我们可以重复这前两个步骤,直到满意为止,然后再生成下一个字节。这意味着经过几次迭代后,我们可以确定特定版本代码的值与我们想要的 DWORD 相匹配。
复制
最后,我们想要将生成的值写入一个可控制的位置。幸运的是,这一步非常简单。我们只需要简单地调用 tzbsp_fver_get_version
SCM 调用,但现在我们可以将目标地址作为返回地址参数提供。这将导致函数将我们生成的 DWORD 写入可控制的位置,从而完成我们的写入工具。
喘口气…现在怎么办?
从这里开始,事情变得容易了。首先,尽管我们有一个写入原语,但仍然很麻烦使用。也许,如果我们能够使用之前的一个更简单的工具来创建一个更简单的工具,那就会更容易些。
我们可以通过创建我们自己的 SCM 调用来实现这一点,这只是一个写入-何处工具。这听起来很棘手,但实际上很简单。
在先前的博客文章中,我们提到所有 SCM 调用都通过一个包含指向每个 SCM 调用的指针(以及它们提供的参数数量,名称等)的大型数组间接调用。
这意味着我们可以使用先前创建的写入工具,将一些我们认为“不重要”的 SCM 调用的地址更改为已存在写入工具的地址。快速查看 TrustZone 内核的代码会发现有许多这样的工具。以下是其中一个例子:
最后,能够读取 TrustZone 内核虚拟地址空间中的任何内存位置也很有用。这可以通过使用上面描述的方法中的另一个“不重要”的 SCM 调用来创建读取 gadget。实际上,这个 gadget 要比写入 gadget 更加难以找到。但是,在 TrustZone 内核中发现了这样一个 gadget:
这个 gadget 返回 R0 中储存的地址、加上 R1 中的偏移值中读取的值,太棒了!
编写新代码
在这个阶段,我们已经可以完全读写 TrustZone 内核内存。但我们还没有在 TrustZone 内核中执行任意代码的能力。当然,我们可以在内核中找到不同的 gadget,并将它们串联起来创建所需的效果。但这样做手动操作非常繁琐(我们需要找到相当多的 gadget),而且自动化难度很大。
有几种可能的方法来解决这个问题。
一种可能的方法是在“正常世界”中编写一段代码,然后从“安全世界”跳转到它。这听起来是一个容易的方法,但实际上比说起来要困难得多。
正如在第一篇博客中提到的那样,当处理器在安全模式下运行时,也就是 SCR(安全配置寄存器)中的 NS(非安全)位关闭时,它只能执行标记为“安全”的页面。在 MMU 使用的翻译表中(也就是 NS 位关闭)。
这意味着,为了在"正常世界"中执行我们编写的代码块,我们首先必须修改 TrustZone 内核的转换表,以将我们编写代码的地址映射为 secure
。
虽然这是可能的,但有点繁琐。
另一种方法可能是在 TrustZone 内核的代码段中编写新代码或覆盖现有代码。这也有优点,可以让我们修改内核中的现有行为,这在后面也可能很有用。
但是,乍一看,这似乎不比之前的方法更容易实现。毕竟,TrustZone 内核的代码段被映射为只读,肯定不可写。
但是,这只是一个小问题!事实上,这实际上可以通过使用 ARM MMU 的一个方便的功能 domains
来解决,而无需修改转换表。
在 ARM 转换表中,每个条目都有一个字段,用于列出其权限,以及一个指示转换所属 domain
的字段。有16个域,每个转换属于其中的一个。
在 ARM MMU 中,有一个称为 DACR(Domain Access Control Register)的寄存器。该 32 位寄存器具有 16 对位,每对位用于一个域,用于指定给定域的转换是否应该为读取访问,写入访问,两者都是或者都不是生成故障。
每当处理器尝试访问给定的内存地址时,MMU 首先检查使用给定转换的访问权限是否可以访问该地址。如果访问是允许的,则不会生成故障。
否则,MMU 检查 DACR 中对应于给定域的位是否设置。如果设置了,则抑制故障并允许访问。
这意味着简单地将 DACR 的值设置为 0xFFFFFFFF
将使 MMU 启用对任何映射的内存地址的读写访问,而不会生成故障(更重要的是,无需修改转换表)。
但是如何设置 DACR 呢?显然,在 TrustZone 内核的初始化期间,它也将显式将 DACR 的值设置为预定值(0x55555555
),如下所示:
然而,我们可以简单地跳转到初始化函数中的下一个操作码,同时在 R0 中提供我们自己的值,从而导致 DACR 被设置为我们控制的值。
现在 DACR 已经设置好了,路径就都清晰了 - 我们可以简单地写入或覆盖 TrustZone 内核中的代码。
为了使事情变得更容易(并且更少受干扰),最好将代码写入 TrustZone 内核中未使用的位置。其中之一是“代码洞”。
代码洞只是未使用的区域(通常在分配的内存区域的末尾),但仍然是映射和有效的。它们通常是由于内存映射具有粒度而引起的,因此在映射段末尾通常存在内部碎片。
在 TrustZone 内核中有几个这样的代码洞,使我们能够在其中编写小段代码并执行它们,几乎没有任何麻烦。
将所有步骤整合起来
这个漏洞有点复杂。以下是我们必须完成的所有阶段的总结:
- 使用零写入原语禁用内存验证功能
- 使用 TrustZone PRNG 在受控位置制作想要的 DWORD
- 通过读取相应的版本代码验证制作的 DWORD
- 将制作的版本代码写入现有 SCM 调用函数指针的位置(这样可以创建快速写入工具)
- 使用快速写入工具创建读取工具
- 使用快速写入工具将函数指针写入一个工具,使我们能够修改 DACR
- 修改 DACR 以完全启用(
0xFFFFFFFF
) - 在 TrustZone 内核中的代码洞中编写代码
- 执行! 😃
代码
该漏洞的利用程序已经被开发出来了,包括了 Nexus 5(先前已经给出了指纹)所需的所有符号。
首先,为了使漏洞利用程序能够向 TrustZone 内核发送所需的制作 SCM 调用,我创建了一个修补版本的 msm-hammerhead
内核,其中添加了此类功能并将其公开给用户空间的 Android。
我选择通过向现有驱动程序 QSEECOM(在第一篇博客文章中提到)添加一些新的 IOCTL 来实现这一点,该驱动程序是用于与 TrustZone 内核进行交互的 Qualcomm 驱动程序。这些 IOCTL 使调用者能够向 TrustZone 内核发送“原始”的 SCM 调用(普通或原子),包含任意数据。
您可以在此处找到所需的内核修改。
对于使用 Nexus 5 设备的用户,我个人建议遵循 Marcin Jabrzyk 的优秀教程 - 这里(它是一个完整的教程,描述了如何编译和启动自定义内核而不将其刷到设备上)。
在使用修改后的内核启动设备后,您将需要一个用户空间应用程序,它可以使用新添加的 IOCTL 向内核发送 SCM。
我编写了这样的应用程序,您可以在此处获取。
最后,漏洞利用程序本身是用 Python 编写的。它使用用户空间应用程序通过自定义内核直接向 TrustZone 内核发送 SCM 调用,并允许在内核中执行任何任意代码。
您可以在此处找到完整的漏洞利用程序代码。
使用漏洞利用程序
使用漏洞利用程序非常简单。这是您需要执行的操作:
- 使用修改后的内核启动设备(请参见 Marcin 的教程)
- 编译 FuzzZone 二进制文件并将其放置在
/data/local/tmp/
下 - 在
shellcode.S
文件中编写任何 ARM 代码 - 执行
build_shellcode.sh
脚本以创建 shellcode 二进制文件 - 执行
exploit.py
以在 TrustZone 内核中运行您的代码
影响的设备
在披露漏洞时,这个漏洞影响所有使用MSM8974 SoC的设备。我创建了一个脚本来静态检查许多这样的设备的ROM,发现以下设备是有漏洞的:
注意:此漏洞已经被高通修复,因此不应该影响当前更新的设备。同时请注意,以下列表并不是详尽无遗的,仅仅是我在当时静态分析的结果。
- Samsung Galaxy S5
- Samsung Galaxy S5
- Samsung Galaxy Note III
- Samsung Galaxy S4
- Samsung Galaxy Tab Pro 10.1
- Samsung Galaxy Note Pro 12.2
- HTC One
- LG G3
- LG G2
- LG G Flex
- Sony Xperia Z3 Compact
- Sony Xperia Z2
- Sony Xperia Z Ultra
- Samsung Galaxy S5 Active
- Samsung Galaxy S5 TD-LTE
- Samsung Galaxy S5 Sport
- HTC One (E8)
- Oneplus One
- Acer Liquid S2
- Asus PadFone Infinity
- Gionee ELIFE E7
- Sony Xperia Z1 Compact
- Sony Xperia Z1s
- ZTE Nubia Z5s
- Sharp Aquos Xx 302SH
- Sharp Aquos Xx mini 303SH
- LG G Pro 2
- Samsung Galaxy J
- Samsung Galaxy Note 10.1 2014 Edition (LTE variant)
- Samsung Galaxy Note 3 (LTE variant)
- Pantech Vega Secret UP
- Pantech Vega Secret Note
- Pantech Vega LTE-A
- LG Optimus Vu 3
- Lenovo Vibe Z LTE
- Samsung Galaxy Tab Pro 8.4
- Samsung Galaxy Round
- ZTE Grand S II LTE
- Samsung Galaxy Tab S 8.4 LTE
- Samsung Galaxy Tab S 10.5 LTE
- Samsung Galaxy Tab Pro 10.1 LTE
- Oppo Find 7 Qing Zhuang Ban
- Vivo Xshoot Elite
- IUNI U3
- Hisense X1
- Hisense X9T Pantech Vega Iron 2 (A910)
- Vivo Xplay 3S
- ZTE Nubia Z5S LTE
- Sony Xperia Z2 Tablet (LTE variant)
- Oppo Find 7a International Edition
- Sharp Aquos Xx304SH
- Sony Xperia ZL2 SOL25
- Sony Xperia Z2a
- Coolpad 8971
- Sharp Aquos Zeta SH-04F
- Asus PadFone S
- Lenovo K920 TD-LTE (China Mobile version)
- Gionee ELIFE E7L
- Oppo Find 7
- ZTE Nubia X6 TD-LTE 128 GB
- Vivo Xshot Ultimate
- LG Isai FL
- ZTE Nubia Z7
- ZTE Nubia Z7 Max
- Xiaomi Mi 4
- InFocus M810
时间线
- 2014年9月19日 - 披露漏洞
- 2014年9月19日 - QC 初步回应
- 2014年9月22日 - QC 确认问题存在
- 2014年10月1日 - QC 向客户发布通知
- 2014年10月16日 - QC 向运营商发布通知,请求禁令 14 天
- 2014年10月30日 - 禁令到期
我想指出的是,在向高通报告此问题后,我被告知他们在我之前就已经内部识别了此问题。然而,这种问题需要相当长的时间来推动修复,因此在我的研究时,修复程序尚未部署(至少,据我所知)。
最后的话
我很想听听您的反馈,请在下面留言!如有任何问题,请随时询问。