原文链接:https://blog.quarkslab.com/attacking-titan-m-with-only-one-byte.html

仅用一个字节攻击 Titan M

继我们在 Black Hat USA 上的演讲之后,在这篇博文中,我们将介绍 CVE-2022-20233 的一些详细信息,这是我们在 Titan M 上发现的最新漏洞,并且探究如何利用它在芯片上执行代码。

介绍

在过去一年半的时间里,我们(Damiano Melotti、Maxime Rossi Bellom 和 Philippe Teuwen)研究了 Titan M,这是谷歌从 Pixel 3 开始在其 Pixel 智能手机中引入的一种安全芯片。我们在 Black Hat EU 2021 上展示了研究成果,以及包含本研究第一部分中获得的所有背景知识的白皮书。在这篇博文中,我们将深入探讨 CVE-2022-20233,这是在该芯片固件中发现的最新漏洞。首先,我们将展示如何使用 AFL++ 在 Unicorn 模式下基于仿真模拟进行模糊测试,来找到此漏洞。然后,我们将展示漏洞利用及其固有的挑战,最终我们在芯片上获得了代码执行。

背景

Titan M 于 2018 年由谷歌在其 Pixel 设备中推出。主要目标是减少攻击者可用的攻击面,减轻硬件篡改和侧信道攻击。事实上,该芯片位于设备中独立的片上系统 (SoC) 上,运行自己的固件,并通过 SPI 总线与应用处理器 (AP) 通信。它实现了多个 API,为智能手机最安全敏感的功能提供更高级别的保护,例如安全启动或带有 StrongBox 的硬件支持的 Keystore。

在这项研究的第一步中,我们专注于对 Titan M 固件进行逆向工程,该固件基于嵌入式控制器 (EC),这是一种用于微控制器的轻量级开源操作系统。这个操作系统相当简单,并且是围绕任务(tasks)的概念构建的,具有固定的堆栈大小并且没有堆(因此没有复杂的动态分配)。这是重要的一点,稍后会证明是有用的:Titan M 芯片本质上具有静态内存布局,因此我们可以假设某些对象始终位于同一地址。

保持操作系统的简单在安全方面也有帮助,特别是通过完全消除与动态分配相关的一些临时内存安全漏洞。此外,得益于其内存保护单元 (MPU),芯片的内存不会同时对同一区域赋予写入和执行权限。另外,还通过在加载固件之前执行一些签名检查来实现安全启动。尽管具有这些特性,但除了在任务堆栈末尾放置硬编码的 canary 用于检测错误之外,在芯片上找不到其他常见的漏洞利用缓解技术。这使得 Titan M 相当容易受到内存损坏漏洞的影响,因此我们决定探索如何对其进行模糊测试以发现漏洞。

对 Titan M 进行模糊测试

众所周知,模糊测试常用于在不安全语言(例如 C)编写的代码库中查找内存损坏漏洞,是一种极其有效的方法。但是,在无法访问源代码且具有大量硬件相关代码的情况下,对安全芯片进行模糊测试非常有挑战。我们决定探索两种不同的技术,即黑盒模糊测试和基于仿真的模糊测试。

黑盒模糊测试

我们已经在之前的演示中展示了如何执行黑盒模糊测试,这里简要回顾一下,有助于理解它与其他方法的区别。通常,对 Titan M 这样的目标进行黑盒模糊测试非常简单:我们只需要一个与待测目标进行交互的渠道,以及一种判断它是否崩溃或达到意外状态的方法。在本研究开始之前,我们开发了一个自定义客户端 nosclient,在 Android 上本地运行,并直接与负责与芯片通信的内核驱动程序进行通信。其发送任意消息的能力让我们可以与 Titan M 进行通信,并且通过库函数的返回码推断处理它们时发生了什么信号。

大多数与 Android 通信的任务使用 Protobuf 来序列化消息。因此,我们可以利用开源项目 AOSP 中的语法定义,通过 libprotobuf-mutator 来变异这些消息。对于 Android N,其中一项没有使用 Protobuf 的任务,我们使用 Radamsa 来生成测试用例。

这种方法确实带来了一些有趣的结果,就模糊测试而言,可以用漏洞数量来衡量。我们在对旧版本固件进行模糊测试时发现了几个已知的漏洞,包括去年研究期间利用过的缓冲区溢出漏洞。此外,在当时最新版本的固件中,我们还发现了两个崩溃问题,尽管两者都是由同一个空指针引用导致的,并未被认为严重到足以作为漏洞列入 Android 安全公告中。

尽管设置相对简单,并且具有在真实环境中进行测试的优势,但黑盒模糊测试也有许多缺点。首先,检测漏洞非常困难,因为我们只能发现导致崩溃或错误返回码的漏洞。但最重要的是,黑盒模糊测试往往只对目标的表面状态进行测试,因为无法看到它们的内部结构。因此,我们可能只触及了 Titan M 固件的表面,这也可以解释为什么所有检测到的崩溃都发生在模糊测试几分钟之后。

基于仿真的模糊测试

我们关注的另一种方法是基于仿真的模糊测试。简而言之,既然固件是公开可用的,为什么不尝试在笔记本电脑上模拟执行呢?根据我们从数小时的逆向工程中对其工作原理的了解,我们可以建立一个仿真框架来逐条运行固件指令,并分析其行为。对于覆盖率引导的 fuzzer 来说,这是一个很好的反馈,它可以对输入进行优先排序,从而到达新的指令,并相应地调整变异器。

有许多不同的方法可以实现此类解决方案。我们尝试了几种,但最终还是选择了 Unicorn 仿真器引擎和 AFL++ 作为模糊测试框架。Unicorn 是一个基于 QEMU 的项目,只支持 CPU 仿真,不支持全系统仿真。在我们的案例这是一个优势,因为我们可以非常容易地开发脚本,实现一些特定调整以提高漏洞检测或解决某些问题。此外,Unicorn 与 AFL++ 的集成非常好,这要归功于它的 unicorn 模式,基本上可以对所有可以用 Unicorn 模拟的东西进行模糊测试。唯一需要做的是定义 place_input_callback 函数,在每次交互时将输入写入目标内存中。

因此,我们还可以测试 Titan M 固件中的其他功能,同时也不忘合理的攻击场景。然而,对 SPI 功能进行了几次模糊测试都没有得到显著结果,我们决定再次关注 tasks,复制在黑盒环境中所做的类似实验。AFL++ 允许自定义变异器,因此我们再次插入 libprotobuf-mutator 并分别模拟固件的三个任务:KeymasterIdentityWeaver。我们决定忽略 AVB,因为它暴露的攻击面很有限,且大部分交互都发生在设备处于引导加载程序模式时。

首先,我们需要再次找到已知漏洞,以证明这个新解决方案的有效性。幸运的是,答案是肯定的,只漏掉了一个漏洞,这也让我们意识到基于仿真的模糊测试的局限性。众所周知,天下没有免费的午餐,这种做法也带来了一些弊端。

  • 多个硬件相关的函数:这​​些代码部分不容易被模拟,因此我们不得不 hook 它们,这不可避免地降低了测试覆盖率。
  • 检测能力仍然有限:在改进纯黑盒方案的同时,我们仍然只能检测到导致 Unicorn 错误的漏洞,因此遗漏了各种页内溢出、off-by-one 等。
  • 缺乏完整的系统仿真:这种选择本身也会导致忽略某些功能,这意味着必然会遗漏一些漏洞。这就是为什么我们没有成功复现刚才提到的漏洞的原因。

为了解决第二个问题,Unicorn 允许设置一些自定义钩子来监视某些内存访问或特定的代码片段。我们实现了一些启发式方法来捕获某些错误模式,例如对 memcpy 的中断调用最终从 Boot ROM(映射到地址 0x0)中读取数据。然而,这是有代价的:钩子会影响性能,并且在识别这些模式所花费的时间与让 fuzzer 自由运行之间总是存在权衡。

漏洞分析

现在,让我们进入关键部分。在对 Keymaster 任务进行模糊测试时,我们发现了一个有趣的崩溃,该崩溃是由 UC_ERR_WRITE_UNMAPPED 在处理 ImportKey 请求时引起的。这个崩溃发生在 strb 指令中,意味着固件试图在未映射的内存区域中写入 1 个字节。请注意,易受攻击的固件是由谷歌 2022 年 5 月的 Pixel 安全更新引入的。

触发该漏洞的消息很简单,如下所示:

ImportKeyRequest
params {
  params {
    tag: ALGORITHM
    integer: 4
  }
  params {
    tag: DIGEST
    integer: 40706
  }
}
symmetric_key {
  material: "<1h5\003H\232@\233"
}

从 Protobuf 定义 中可以看出,ImportKey 消息包含一个 KeyParameters 字段,该字段由一个或多个 KeyParameter 对象组成。该错误发生在处理密钥参数中的标签时:易受攻击的函数循环遍历参数列表,并对每个参数检查其标签是否为 0x20005(对应于 DIGEST)。当找到这样的参数时,函数会获取关联的 integer 字段,并在一些检查后将其用作堆栈缓冲区的偏移量,然后将一个字节设置为 1。通过传递足够大的整数作为参数,就可以超出边界,写入一个字节值 0x01

下面的代码片段,无论是汇编和反编译代码,都显示了这些检查以及导致越界写和随后崩溃的 strb 指令。此时,r10x1r7 为缓冲区地址,r3 为当前 KeyParameter 的整数字段。

ldr.w   r1,[r2,#-0x4]
ldr     r3,[PTR_DAT_0005d808] ; 0x20005
cmp     r1,r3
bne     increment_loop_vars
ldr     r3,[r2,#0x0]
uxtb    r0,r3
cmp     r0,#0x4
bhi     error_exit
movs    r1,#0x1
lsl.w   r0,r1,r0
tst     r0,#0x15
beq     error_exit
strb    r1,[r7,r3]
if (((nugget_app_keymaster_KeyParameter *)(offset + -1))->tag == 0x20005) {
  masked = *offset & 0xff;
  if ((4 < masked) || ((1 << masked & 0x15U) == 0)) {
    return 0x26;
  }
  *(undefined *)(buffer + *offset) = 1;
  *param_3 = *param_3 + 1;
  *param_4 = offset;
}

该漏洞实际上可以被多次触发,与带有 DIGEST 标签的参数数量相同,唯一的限制是 KeyParameters 列表的最大大小。这会导致多次的 1 字节越界写。但是由于对偏移量的检查,这种写远非任意写。这里不深入讲解按位运算的细节,结论是最低有效字节只能是 0x00x20x4

此时,这似乎是一个非常小且难以利用的问题。但是,不要忘记,正如我们前面提到的,Titan M 的内存是完全静态的。此外,考虑到包含 KeyParameters 字段的各种消息,可以通过不同的代码路径触发漏洞。只要我们在正确的地方写入数据,就可以通过将单个字节设置为 1 来完成许多不同的事情,从简单的 DoS 到更改内存中某个大小变量并导致其他地方损坏等等。

漏洞利用开发

此时,我们编写了一个小脚本在 Ghidra 中生成并突出显示所有可能的可写地址。然后我们开始寻找有趣的目标,事实证明,只要触发一次漏洞就足以破坏芯片。

覆盖什么

在测试过程中,我们了解到可以让 r1 包含 0x14019 来到达易受攻击的代码。对此应用 0xa204 偏移量后(请注意,最低有效字节允许通过检查),我们可以到达 0x1e21d。该地址是我们称为 KEYMASTER_SPI_DATA 的结构的一部分,其中包含的信息与 Android 交换 Keymaster 消息有关。特别是,我们可以覆盖指向存储 Keymaster 任务的传入请求的内存位置的一个字节:默认情况下为 0x192c8,但如果我们将 0x1 写入 0x1e21d,则该值变为 0x101c8。因此,以下 Keymaster 请求将存储在远离它们应有位置的地方,从而对 Titan M 造成灾难性后果。

UART 控制台

在深入探讨如何利用此漏洞之前,让我们花点时间回顾一下我们是如何与芯片进行交互的。我们唯一使用的工具是 nosclient,它允许我们伪造任意消息并将直接发送给驱动程序。发送请求后,Titan M 回复一个返回码和一个响应,如果出现错误则响应为空。只有这些信息可以用,所以开发漏洞利用特别具有挑战性:我们不仅无法访问任何调试器功能,也看不到任何堆栈跟踪或攻击的直接副作用。

这就是芯片暴露的 UART 控制台非常方便的地方。有两种访问它的方法,都有不同的挑战。第一种方法依赖于称为 SuzyQable 的特殊调试电缆。这被官方称为调试 Chrome OS microcontrollers 的电缆,由于 Titan M 基于相同的操作系统,所以它实际上也适用。要激活它,我们只需要在 fastboot 模式启动 Pixel 手机,并使用命令 fastboot oem citadel suzyq on。然后,在使用 SuzyQable 连接设备时,我们可以将 UART 控制台作为 tty 接口访问到我们的笔记本电脑上。同时,电缆还允许通过 adb 与设备通信。不幸的是,一旦芯片崩溃,或者通过控制台发送重启命令后,通道就会关闭,需要再次激活接口返回 fastboot 模式。这是非常不实用的,也是我们选择第二种方式的原因。

第二种方法可以看作是 MacGyver 方法。Titan M 暴露了主板上的 UART 引脚,因此我们只需要焊接两根电线,就能获得与使用电缆时完全相同的 shell。在这种情况下,无论设备或芯片发生什么情况,控制台都不会关闭并保持活动状态。

Titan M 控制台非常基础,可以与芯片进行简单的交互,比如调查统计数据、版本和类似的事情。但最重要的是,它是打印调试日志的地方。回到利用开发,它显然没有提供任何有关出现问题时出错原因的信息,但它仍然非常有用:例如,我们总是可以尝试跳转到打印某些东西的函数,来验证到目前为止漏洞利用是否工作正常。

劫持执行流

正如前面所说,由于越界写入,我们更改了存储传入 Keymaster 请求的地址。该地址仍在芯片的内存空间中,因此传入的消息最终会覆盖 Titan M 内存中的其他数据。出于这个原因,我们尝试发送更大的命令,并监控 UART 日志。经过一些测试,我们意识到如果在 556 个字节的载荷之后放置一个有效的代码地址,就可以跳转到该地址,将执行流重定向到我们选择的函数。通过 UART 控制台打印一些日志可以检测到这一点。

我们猜测这部分内存正被某个任务使用(可能是 idle),该任务可能已将返回地址放置到其堆栈中,并且现在被覆盖了。此时,我们知道可以跳转到任意函数,那么如何将其转化为实际的漏洞利用呢?

返回导向编程

由于存在内存保护机制,我们无法编写自己的 shellcode 并跳转到它,因为可写的内存是不可执行的。相反,我们可以依靠代码重用,基于返回导向编程进行攻击。目标是构建一个读取任意地址数据的原语:这样,我们就可以通过 nosclient 的恶意命令泄露 Titan M 正在保护的秘密。

不幸的是,我们无法将 ROP 链写到第一个 gadget 的位置,因为这会破坏一些内存,导致芯片在漏洞利用完成之前崩溃。因此,第一个挑战是转移堆栈。理想情况下,我们希望减小堆栈指针:假设堆栈向较大的地址移动,我们希望可以依靠放置在那 556 个字节中的载荷,在不引起任何恶劣副作用的情况下设置它们。我们只找到了一个 gadget 可以实现,并且它还可以做更多的事情:

sub sp, #0x20; mov r4, r0; ldr r3, [r0]; add.w r5, r4, #0x70; ldr r3, [r3, #8]; blx r3;

尽管如此,我们编写了一个 ROP 链来多次调用它,同时确保 r0 指向我们控制的内存区域,并且我们还在其中编写了一些 gadget,使 ldrblx 指令按我们的要求工作。在下图中,我们以图形方式展示了这种方法的第一次迭代(实际上又进行了几次)。红色箭头跟随执行流,蓝色箭头跟随堆栈指针。

在多次抬升了堆栈之后,我们才能进行实际的攻击。将泄露的字节返回给 Android 的唯一方法是将其复制到 SPI 命令的响应中。因此,我们需要处于命令处理程序的上下文中,在解析命令代码后从内存中获取地址,并调用该处理程序。因此,我们可以覆盖其地址,但应该如何设置呢?由于替换实际代码是不可能的,我们只能让它跳转到一些可控的内存区域中,并在那里编写 ROP 链需要的更多 gadget。这是在第一步创建的空间中完成的,并确保忽略我们在之前的 ROP 链中使用过的 slots(将它们分配给我们不使用的寄存器)。

这一步是最具挑战性的,因为它需要找到合适的 gadget,使堆栈在处理程序被触发时转移到一个仍然可控的内存区域。再次不幸的是,不存在这样的 gadget,因此我们必须发挥创造力。我们解决问题的方法非常巧妙:通过模拟 Keymaster DestroyAttestationIds 命令处理程序的执行,我们了解到其堆栈中有一个 32 位的 slot 没有被调用链中的函数覆盖。因此,我们在那里编写了一个 gadget,它可以充当跳板,将堆栈从 Keymaster 堆栈移到足够远的地方,并到达保存载荷的区域。

到目前为止,漏洞利用如下:

  1. 从我们执行代码的地方向上移动堆栈;
  2. 一旦有足够的空间,就执行一个 ROP 链:
    • 将 Keymaster DestroyAttestationIds 命令处理程序替换为一个 gadget,并从堆栈中弹出一些寄存器;
    • 在 Keymaster 堆栈上写入一个 gadget,在触发处理程序时被执行,并将堆栈指针移动到正常固件执行不会篡改我们所写内容的区域;
    • 将最终的 ROP 链复制到这样的区域。

一旦到达这里,完成漏洞利用就相对简单了,因为我们可以控制来自处理程序上下文的执行。我们使用用户提供的参数调用 memcpy,将想要读取到 Keymaster SPI 响应缓冲区中的内容进行复制。然后跳回到 Keymaster 堆栈,就像普通命令处理程序所做的那样。

漏洞影响

由于我们利用此漏洞构建了泄露功能,现在可以读取芯片上的任意内存,访问任意可读地址。因此,我们可以转储存储在芯片中的秘密(例如更新 Titan M 时 Pixel 引导加载程序发送的信任根)。此外,我们可以访问以前无法访问的 Boot ROM。尽管芯片中使用的是定制版的 memcpy 函数,它会检查 srcdst 缓冲区是否等于 0x0,但这依然是可能的。事实上,我们不是跳转到函数的入口点,而是跳过这些检查并直接跳转到发生复制操作的基本块。

这种攻击最有趣的结果是能够检索任何受 StrongBox 保护的密钥,从而破坏了 Android Keystore 的最高级别保护。与 TrustZone 中发生的情况类似,这些密钥只能在 Titan M 内部使用,同时它们存储在设备上加密的密钥块(key blob)中。

这个过程如下所示:

  • 从 Android 系统(任何应用程序)中读取密钥块;
  • 发送一个有效的、格式正确的 BeginOperation 请求,其中包含这样一个密钥块:
    • 此命令的处理程序解密密钥并将其存储到特定的内存地址中。这使芯片准备好执行请求的操作;
  • 运行漏洞利用程序并泄露现在存储明文密钥的内存。

为了展示这种攻击的有效性,我们还开发了一个虚拟应用程序,它创建一个受 StrongBox 保护的 AES 密钥并用它加密字符串。通过漏洞利用和上面解释的方法,我们可以从 Titan M 中泄露相应的密钥,并且通过重复使用相同的初始化向量,我们可以成功地离线解密字符串。

https://blog.quarkslab.com/resources/2022-08-11_titan-m/demo.mp4

提醒一下,执行此攻击有两个条件。首先,我们需要能够从已 root 的设备(需要使用 nosclient)或物理访问 SPI 总线向芯片发送命令。

然后,我们需要一种方法来访问 Android 文件系统上的密钥块,这可以通过 root 来完成,或者利用一些漏洞来绕过基于文件的加密或 uid 访问控制。

缓解措施

我们向谷歌报告了这个漏洞,当然最好的缓解措施是 6 月安全公告中提供的补丁。但是,我们想指出一个有趣的特性,它可以使 StrongBox 密钥块泄露变得不可能。事实上,应用程序可以创建一个身份验证绑定的密钥,在使用 KeyGenParameterSpec 构建时指定 setUserAuthenticationRequired(true)。这样,用户需要在使用密钥之前进行身份验证,并且密钥块会使用用户密码派生的特殊密钥进行第二次加密,这是我们无法获得的。

结论

Titan M 芯片是谷歌 Pixel 手机中最安全的组件,也是设备所有安全性最终关联的锚点。尽管做出了良好的架构决策并付出了许多努力来最小化其攻击面,使漏洞利用变得非常困难,同时缺乏调试芯片的适当工具,但在该项目中,我们成功地:

  • 对 Android 和芯片之间的通信进行逆向工程。
  • 开发了 nosclient,这是一种开源工具,可以让研究人员向芯片发送任意命令。
  • 使用黑盒模糊测试发现漏洞。
  • 模拟芯片并使用基于模拟的模糊测试发现漏洞。
  • 利用一些实现上的弱点并利用该漏洞在芯片上执行代码,泄露了不应离开芯片的敏感数据(加密密钥)。

该漏洞已于 2022 年 5 月报告给谷歌,并在 2022 年 6 月的 Pixel 安全更新中发布了修复程序。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖