目录

破解高通骁龙660上的Android Bootloader

翻译自Breaking the Android Bootloader on the Qualcomm Snapdragon 660 | Pen Test Partners

作者提取手机中的bootloader,逆向后对bootloader发送序列化数据导致crash,并通过patch生成了能够解锁的bootloader;在Userdata分区,作者观察到验证和启动映像分别在两个函数中进行,从而能够通过加载patch过的fastboot命令绕过映像签名的检查,从而形成Time of Check to Time of Use 攻击,在不解锁bootloader的情况下运行未签名映像

几个月前,我购买了一部 Android 手机,围绕特定系列的 NFC 芯片进行了一些研究,这要求我获得该设备的 root 访问权限才能完全访问其硬件功能。

在 Android 手机上获得 root 访问权限通常需要解锁引导加载程序,该操作会禁用手机的签名验证要求,以便可以部署修改后的 Android 引导映像。在 Qualcomm 芯片组上,这是一个标准化过程,它使用 Android Bootloader 中的命令来执行解锁。智能手机制造商通常会修改引导加载程序以添加自己的限制,并需要自己的工具。

这些自定义限制包括强制创建用户帐户以请求解锁,并再允许解锁之前只能够进行等待。

企业希望对其硬件设置这些限制有几个实际原因:

  • 没有经验的用户不会被诱骗,从而故意减弱手机安全
  • 第三方不能在销售前加载带有恶意软件的设备(例如,供应链攻击)
  • 制造商可以跟踪谁正在解锁他们的引导加载程序

我购买的手机有这些限制,要求我等待 7 天才能获得设备的 root 权限。因此,我决定调查是否可以绕过同一制造商生产的旧智能手机的等待期。我给自己设定了一个挑战,在 7 天的等待期结束之前打破引导加载程序保护。

标准绕过方法

有一些标准方法可以绕过这些限制,但它们都有自己的风险。

最常用的方法是进入高通紧急下载模式。这是一种低级紧急状态启动模式,可使用诊断工具将签名的“loader”负载上传到芯片,该负载可用于直接修改设备的分区。虽然这种方法是有效的,但它需要能够启动到一个模式,即用户不能够直接访问,并且需要一个签名加载ELF程序,而这个针对设备的ELF一般是无法获取的。

第二种常见方法是在硬件级别攻击设备。通过拆机,连接板上的EMMC芯片,可以在配置分区中设置“unlock”位,获得解锁访问权限。公共资源中有应连接哪些引脚的大纲说明,但这通常需要一些硬件知识和一双稳定的手。而且这样也存在不可修复地损坏设备的高风险。

我不想使用这两种方法中的任何一种,而是想攻击第二阶段的bootloader。紧急下载模式可以通过第一阶段的bootloader进入,但bootloader下一阶段启动链会加载额外的诊断和管理工具。

SDM660安卓手机

目标设备为2017年发布的中端手机,采用高通骁龙660作为核心芯片组。

对制造商实施的自定义解锁功能的分析表明,从手机向解锁工具发送了一个小的唯一值,并在他们的服务器上生成了一个签名。 7 天后,这将发送到手机并进行验证,执行解锁。我使用Windows主机和USBPCAP USB分析软件分析了这个过程。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot1-163086099813633.png

请求唯一值、发送签名和请求解锁bootloader都是通过 Android Bootloader 的 Fastboot 执行的。通过手机上的 ADB 重新启动进入bootloader,或在启动时按住音量下按钮,可以进入fastboot。然后可以使用fastboot命令行工具从 USB 访问它。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot2-163086098776131.png

Fastboot

此工具可实现标准和自定义功能,包括刷新分区、获取 OEM 特定数据或启动到不同模式。对该工具的分析表明,fastboot 协议非常简单,可以使用基本的 C++ 代码和 LibUSB 来实现。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot3-163086089258921.png

所有命令都是通过单个 USB 端点作为 ASCII 文本发送的,并且对命令的响应是从另一个端点异步发送的。我发现库的存在是为了方便访问 fastboot 接口,但是我决定使用 LibUSB,因为这可以让我更好地控制通信。

ABL Bootloader

fastboot 接口由 Android bootloader提供,这是一个存储在手机“abl”分区中的第二阶段bootloader。此bootloader的目的是验证和加载 Android 和手机的Recovery镜像,以允许其标准功能或接收fastboot命令。此bootloader的高通版本是开源的,允许手机制造商更轻松地进行修改。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot4-163086088611419.png

当我攻击bootloader时,我想确保我能够在较低的水平分析它的功能。手机制造商添加的自定义命令不会在源代码中,因此我决定分析编译后的引导程序。我通过下载最新的 OTA 更新文件、解压缩并访问存储在其中的“abl.img”文件来访问手机“abl”分区的内容。分析镜像发现它是一个 ELF 文件,但它不包含任何可执行代码。

相反,运行“binwalk”文件分析工具显示 这个ELF文件 包含一个 EFI 系统分区。这是用于在嵌入式和非嵌入式设备上启动操作系统的标准格式,使用“uefi-firmware-parser”工具可以提取它。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot5-163086087909517.png

其中存储的“LinuxLoader”文件是一个便携式可执行文件,一个包含bootloader代码的可执行文件。这种标准格式可以直接加载到 IDA 中并进行反汇编。

对bootloader的简要分析表明,所有 fastboot 命令都存储在一个表中,该表由 ASCII 命令和该命令的函数回调组成。这允许快速分析任何潜在的隐藏命令,并有助于分析特定功能。同时发现bootloader包含大量调试字符串,这使得对代码的理解变得更加容易。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot6-163086087100215.png

我的主要目标是找到一个内存损坏漏洞,它可以让我绕过解锁限制。为此,我决定将重点放在“flash:”命令上。我选择它是因为它需要从主机 PC 接收大量有效负载去刷写设备分区。

flash:”命令传统上不允许在bootloader被锁定时在设备上写入分区,但是我注意到制造商已经修改了他们的bootloader以允许在锁定情况下刷写特定的,自定义的分区。这些分区对上传的数据执行了额外的解析,增加了存在漏洞的可能性。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot7-163086086232313.png

当我从逆向工程的角度分析 fastboot 协议而不是阅读文档,我对如何执行数据上传做了一些假设。正确的上传顺序如下:

  • download:< payload size >
  • < send full payload >
  • flash:< partition >

但是,我尝试了以下顺序:

  • flash:< partition >
  • < send payload >

我错误地在代码中的这个序列之后留下了一个额外的“flash:”命令。运行此序列导致引导bootloader在发送第二个“flash:”命令后crash,USB 接口不再接收命令。

Crash 分析

从 USB 端口拔下设备并重新插入发现USB不再能够发现该设备,这意味着它完全不起作用。按住电源和音量下按钮十秒钟会导致硬重启,并且对设备没有持续性的伤害。

在阅读了有关在 fastboot 中如何上传数据后,使用“download:”命令,我推测发生了缓冲区溢出,我发送的大负载被视为 fastboot 命令而不是要处理的分区上传。这意味着崩溃是由于 Qualcomm bootloader的命令处理功能的缺陷造成的,而不是手机制造商实施的任何自定义功能。

为了确认我偶然发现的是有效的缓冲区溢出,我决定尝试更小的payload大小。我从 10 兆字节的payload开始,并尝试了 4 KB payload的相同序列。这个较小的有效载荷没有使手机崩溃,它继续正常运行。

由于较小的payload不会导致崩溃,因此我选择执行二分搜索以识别不会使手机崩溃的最大payload。我发送了介于 small 和 large 值之间的payload大小,然后根据手机是否崩溃,减少了最大的payload大小,或者增加最小的payload大小1。这使我获得了不会使设备崩溃的最大有效负载,其大小为 0x11bae0 (1161952) 字节。

由于这是一个不寻常的内存大小,我有理由确定这是某种缓冲区溢出,但是由于我无法访问内部硬件、任何调试功能或bootloader的内存映射概述,即关于究竟是什么溢出存在很多不确定性。我注意到bootloader使用随机值实现stack canaries,这意味着如果我执行栈溢出但覆盖了canary,这可能导致无法利用。

我决定发送一个 0x11bae1 (1161953) 字节的有效载荷,并将最后一个字节从 0x00 增加到 0xff。如果手机没有因为某个特定值而崩溃,那么我就没有覆盖canary,而是找到了序列中的下一个字节。在此位置发现有效字节为 0xff

通过持续电源循环,我找到有效的字节值,然后移动到序列中的下一个字节,可以生成该点内存中的合法数据。这个数据不一定是最准确的,但足够接近并不会使bootloader崩溃。一旦生成了这个序列,就有可能使用它来在bootloader中执行代码,但是如果不能自动化,这将是一个漫长的过程。

自动循环启动

自动化的电源启动循环可以通过从手机中取出电池,并在连接到主机 PC 时使用 USB 继电器切断电源来实现,但这需要拆卸手机并去除外壳周围的胶水。此外,如果我正在拆卸手机,我可以直接访问 EMMC 并使用基于硬件的方法解锁bootloader。

在尝试为此提出解决方案时,我试图不断按住手机的电源和音量下按钮。这导致手机进入启动循环:它会重启并加载bootloader几秒钟,然后再次重启。我注意到在此过程中 USB 接口将开始运行足够的时间让我发送 fastboot 命令。

我在导致启动循环的两个按钮上缠了一根发带,发现这种方法一直能够起效,使我能够完全自动化该过程。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot8-163086084303011.png

我修改了我编写的 fastboot 工具以实现这种内存dump。它将等待 USB 接口连接,尝试序列,然后验证bootloader是否已崩溃,以及是否收到“Flashing is not allowed”响应。我验证了两件事,因为这更有可能获得准确的内存dump。每次尝试需要 10-30 秒,这意味着内存dump需要花费一段时间。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot9-16308608371709.png

我把手机放了一夜,尝试进行内存dump,然后醒来发现 0x34 字节的数据不会使bootloader崩溃:

FF 43 02 51 60 02 00 0C 60 02 00 0C 60 02 00 0C 60 02 00 0C E8 00 00 B0 34 00 00 10 01 00 00 0A 08 0D 40 F9 00 00 00 08 C0 00 04 0B 60 02 00 0A D3 9F FF 97

我注意到有很多重复的值,但这些与默认的stack canary值 0xc0c0c0c0 不对应,这意味着它们可能是不相关的。此外,数据看起来不像堆栈中常见的任何内容,但是,我发现每个 32 位的word都是有效的 ARM64 操作码。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot10-16308608276777.png

大多数这些操作码尽管有效,但不一定一定与bootloader中的相同,然而所有堆栈管理和分支操作都必须相当准确,以保证bootloader不会崩溃。我试图搜索我在 IDA 中反汇编的bootloader中识别出的“SUB WSP”和“BL”指令,但是没有结果。

之所以会出现这种情况,有几个原因。就算在操作码中翻转比特位, ARM64 操作通常可以具有相同或相似的功能,寄存器可以在 32 位 (Wx) 和 64 位 (Xx) 模式同时访问,并且分支指令可能由于暴力执行而达到不期望的情况。

由于这些表面上的差异,我决定尝试寻找类似的操作码,选择“BL”指令,因为我已经确定这依赖于相对寻址,并且地址必须非常相似才能正确执行分支。我对操作码的 32 位值执行了文本搜索,但删除了第一个 nybble。这将找到相似的分支并且确定一个单一有效的指令,这一步出现在分析初始,我试图刷写分区解析器的时候。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot11-16308608199775.png

对其余操作的进一步检查表明,这些操作也非常相似。这意味着我的bootloader缓冲区溢出覆盖了bootloader本身。这也意味着bootloader是从 EFI 文件系统中提取的,并在 RAM 中执行。

对使用中的地址的进一步分析表明,bootloader代码在 0x101000 字节后被覆盖,并允许我用我从 PE 文件中提取的代码覆盖整个bootloader,覆盖bootloader本身。这将防止后续崩溃,并允许我通过修补bootloader本身来修改我需要的任何功能,包括引导加载程序解锁代码。

解锁 Bootloader

bootloader代码可以验证解锁工具提供的RSA签名,然后进行解锁。我想跳过此验证并直接解锁。我注意到bootloader解锁功能是在分支链接指令中执行的,这些指令是由编译器为函数调用生成的,我决定修改我用于初始缓冲区溢出的代码以跳转到该指令。我为此生成了正确的相对地址,并使用在线 ARM64 编译器生成了适当的 BL 指令。然后我将其patch到bootloader中以进行此跳转。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot12-16308608090853.png

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot13.png

虽然调试这个过程会很困难,但成功解锁是显而易见的:手机将重新启动,擦除用户数据分区,并启动到解锁的状态。这种方法是有效的,并且设备被解锁。

影响

我现在可以在没有制造商的工具或必须等待 7 天的情况下 root 我的旧手机。然而,对较新设备执行类似攻击的尝试无效。因此,我认为它仅适用于基于 SDM660 的设备。

bootloader的额外修补可以允许更多功能,例如出于调试目的从设备 dump 一些有限的 RAM,但是由于bootloader可以访问的 RAM 区域的限制,这将不允许任何 Cold-boot 攻击出现。

此外,高通芯片能够对手机的“userdata”分区进行加密,而无需用户提供密码或 PIN 。这可以防止从未签名的 Android 映像和未锁定的 bootloader 直接访问它,这意味着即使执行此漏洞利用,用户的数据仍将受到保护。此外, bootloader 默认会解锁擦除此分区。

复制漏洞

为了证实我的理论这在所有 SDM660 设备上都是可行的,我购买了不同制造商生产的具有相同芯片组的手机。这是在第一个设备(上文中的设备)发布几年后发布的,并发现可以完全禁用 bootloader 解锁。

制造商通过实施类似于第一台设备上的,识别 bootloader 解锁的签名保护机制来实现这一点,但没有发布任何工具来执行此操作。

我使用新设备的 OTA 映像来提取 bootloader ,其方式与我对之前设备所做的相同。

我试图执行与以前相同的缓冲区溢出,不幸的是它没有成功。然而,通过发送更大的有效载荷,新设备确实崩溃了。通过使用与之前相同的二进制搜索,可以确定该 bootloader 在 0x403000 字节之后被覆盖,而不是第一个的 0x101000 字节。有了这些信息,可以快速开发 bootloader 解锁。

我确定了一个单独的分支指令,如果未验证签名,它将跳过解锁过程。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot14-16308607931821.png

使用单个 NOP 操作码,可以删除此分支,并可以在设备上执行bootloader解锁。

由于该漏洞可能存在于所有基于 SDM660 的手机上,因此该漏洞已直接披露给 Qualcomm。

Bootloader不应该被不允许解锁的用户访问,完全禁用 fastboot 访问以防止对其进行攻击是一种可能的方法。主 Android 操作系统通过工程模式应用程序可以重新激活 Fastboot。那些防止 bootloader 被用户解锁的制造商通常使用这种方法。

绕过高通的用户数据保护

如前所述,高通的芯片可以加密“userdata”分区,使用内部密钥来保护用户数据,而无需在启动时输入密码。这可以防止chip-off分析和通过未签名的 Android 映像访问数据。我想看看我是否可以通过修改bootloader的功能来绕过这种保护并访问用户数据分区。

我使用 Qualcomm 的源代码来确定如何访问加密密钥和解密分区。我发现bootloader被设定为无法直接访问这些密钥,并且只能通过内部 API 访问密钥,而我的攻击无法修改该 API。此 API 执行时启动 Android 映像的验证,以及验证bootloader是否已解锁。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot15.png

我注意到这个函数调用并没有启动映像,这意味着启动 Android 映像的验证和实际启动它是两个独立的函数。

我决定查看“boot” fastboot 命令,该命令用于启动并执行从主机 PC 上传的 Android 映像。我注意到映像的验证和开始执行,确实是两个独立的功能。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot16.png

我决定看看是否可以修改bootloader以在这两个函数调用之间在调换签名和未签名的 Android 映像。如果成功,我将能够执行未签名的 Android 映像,而无需解锁bootloader并获得对加密用户数据分区的完全访问权限。

修改 Boot 命令

boot”命令通过 fastboot “download:”命令接收完整的 Android “boot”镜像,然后从 RAM 验证并执行。我决定修改这个函数以获取多个 Android 映像。

我修改了我的 fastboot 工具,所以它不会只发送一个映像,而是发送:

  • 一个未签名的映像的4字节偏移
  • 一个签名的映像
  • 一个修改过的,未签名的映像

在此之后,我需要修补bootloader以正确使用paylaod。我需要做的第一件事就是绕过解锁检查。传统上,“boot”命令仅在bootloader未锁定时引导上传的映像,即使映像已签名。我决定用一个操作覆盖代码中的这个检查,该操作将指向新payload的指针向上移动了四个字节——将其指向签名的 Android 映像。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot17.png

这将导致代码执行“LoaderImageAndAuth”函数时验证已签名的映像

在此之后,我需要在这个函数和“BootLinux”函数之间留出空间,以便交换映像。我注意到在这两者之间调用的函数仅用于内务处理,提供“OK”响应并关闭 bootloader 的某些功能,即一旦 Android 映像启动,这些功能无论如何都会被禁用。因此,我决定用交换映像所需的少量操作来覆盖它们。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot18.png

我只需要在这些上面添加四个额外的指令,这意味着有足够的空间来做我需要的。四个附加说明如下:

  • 将指针移回payload的开头 —— sub x19, x19, 4
  • 读取偏移值 —— ldr w22, [x19]
  • 将偏移值添加到图像指针 —— add x19,x19,x22
  • 将新的指针值推入“Info”结构的“ImageBuffer”指针 —— str x19, [x21,#0xa0]

这足以将指针从签名映像交换到未签名的映像,并且将促进这种 Time of Check to Time of Use 攻击,并允许在不解锁bootloader的情况下运行未签名映像。

在锁定的Bootloader上运行未签名的代码

有一些很好的理由说明为什么有人希望使用锁定的bootloader在他们的设备上运行未签名的 Android 映像,其中第一个是 Tethered root 访问。通过使用 Magisk 启动一个未签名的 root 映像,人们可以完全访问他们设备的数据,包括他们的照片、消息和联系人,而无需备份、擦除和恢复它。此外,这将允许某人在具有 root 访问权限的个人设备上执行安全研究,然后通过重新启动删除访问权限,并启动到已签名的映像。很少有证据表明他们获得了对设备的特权访问。

虽然这对研究目的很有用,但攻击者还可以利用这些功能做其他邪恶的事情。使用此漏洞和自定义映像,可以访问用户的所有文件或仅禁用锁屏来访问他们的应用程序。具有物理访问权限的攻击者也可以使用用户通过绑定的 root 访问获得相同的访问权限(user 获取root攻击者也可以获取root)。

最后,Android 允许用户通过开发者选项菜单进一步加密他们的手机。这增加了一个额外的保护层,它增加了启动时对 PIN 或密码的要求,以便解密用户数据分区。这与设备的解锁屏幕不同,后者仅在软件级别保护设备。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot19.png

虽然这确实保护了用户数据,但它并不能保护核心 Android 启动映像,并且获得了对某人手机的临时物理访问权限的攻击者可以轻松上传后门,一旦用户输入其 PIN,该后门就会激活。这不是一种可行的攻击,但很有趣。

https://gitee.com/jygzyc/blog_img/raw/master/pic/breakboot20.png

结论

引导加载程序中发现的所有漏洞都向高通公司披露,并与他们进行了协调披露。虽然这些缺陷确实使 SDM660 面临风险,但在修补之前,我无法确认任何其他的 Qualcomm Snapdragon 芯片是否存在风险。所有使用该芯片的设备都已部署补丁,该漏洞不再存在。

这些缺陷被分配的 CVE:CVE-2021-1931。


  1. 译者注:二分法的常规步骤 ↩︎