实施 dm-verity

Android 4.4 及更高版本通过可选的 device-mapper-verity (dm-verity) 内核功能支持 Verified Boot,该功能提供对块设备的透明完整性检查。dm-verity 有助于防止持久性 rootkit,这些 rootkit 可以保持 root 权限并危害设备。此功能可帮助 Android 用户在启动设备时确保设备处于上次使用的状态。

具有 root 权限的潜在有害应用 (PHA) 可以躲避检测程序并以其他方式掩盖自己。root 软件可以做到这一点,因为它通常比检测器具有更高的权限,从而使软件能够对检测程序“撒谎”。

dm-verity 功能使您可以查看块设备(文件系统的底层存储层),并确定它是否与其预期配置匹配。它使用加密哈希树来实现这一点。对于每个块(通常为 4k),都有一个 SHA256 哈希。

由于哈希值存储在页面树中,因此只需信任顶层“根”哈希即可验证树的其余部分。修改任何块的能力相当于破解加密哈希。请参阅下图,了解此结构的描述。

dm-verity-hash-table

图 1. dm-verity 哈希表

启动分区上包含一个公钥,该公钥必须由设备制造商在外部进行验证。该密钥用于验证该哈希的签名,并确认设备的系统分区受到保护且未更改。

操作

dm-verity 保护存在于内核中。因此,如果 root 软件在内核启动之前危害了系统,它将保留该访问权限。为了缓解这种风险,大多数制造商都会使用烧录到设备中的密钥来验证内核。一旦设备出厂,该密钥就无法更改。

制造商使用该密钥来验证第一级引导加载程序上的签名,第一级引导加载程序又验证后续级别(应用引导加载程序)和最终内核上的签名。希望利用verified boot的每个制造商都应具有一种验证内核完整性的方法。假设内核已通过验证,则内核可以查看块设备并在安装时对其进行验证。

验证块设备的一种方法是直接哈希其内容,并将其与存储的值进行比较。但是,尝试验证整个块设备可能需要很长时间,并消耗设备的大部分电量。设备将需要很长时间才能启动,然后在使用前会显著耗尽电量。

相反,dm-verity 会单独验证块,并且仅在访问每个块时进行验证。当读取到内存中时,会并行哈希该块。然后,在树上验证哈希。由于读取块是一项非常昂贵的操作,因此此块级验证引入的延迟相对较小。

如果验证失败,设备会生成 I/O 错误,指示无法读取该块。它看起来像是文件系统已损坏,正如预期的那样。

应用可以选择在没有结果数据的情况下继续,例如当应用的主要功能不需要这些结果时。但是,如果应用在没有数据的情况下无法继续,则会失败。

前向纠错

Android 7.0 及更高版本通过前向纠错 (FEC) 提高了 dm-verity 的稳健性。AOSP 实现从常见的 Reed-Solomon 纠错码开始,并应用一种称为交织的技术来减少空间开销并增加可以恢复的损坏块的数量。有关 FEC 的更多详情,请参阅通过纠错严格强制执行的 Verified Boot

实现

摘要

  1. 生成 ext4 系统映像。
  2. 为该映像生成哈希树
  3. 为该哈希树构建 dm-verity 表
  4. 签署该 dm-verity 表以生成表签名。
  5. 将表签名和 dm-verity 表捆绑到 verity 元数据中。
  6. 连接系统映像、verity 元数据和哈希树。

有关哈希树和 dm-verity 表的详细描述,请参阅Chromium 项目 - Verified Boot

生成哈希树

如引言中所述,哈希树是 dm-verity 的组成部分。cryptsetup 工具会为您生成哈希树。或者,此处定义了一个兼容的哈希树

<your block device name> <your block device name> <block size> <block size> <image size in blocks> <image size in blocks + 8> <root hash> <salt>

为了形成哈希,系统映像在第 0 层被拆分为 4k 块,每个块都分配有一个 SHA256 哈希。第 1 层是通过仅将这些 SHA256 哈希连接到 4k 块中而形成的,从而形成一个更小的映像。第 2 层的形成方式相同,使用第 1 层的 SHA256 哈希。

这样做一直持续到前一层的 SHA256 哈希可以容纳在单个块中为止。当获得该块的 SHA256 时,您就有了树的根哈希。

哈希树的大小(以及相应的磁盘空间使用量)随已验证分区的大小而变化。在实践中,哈希树的大小通常很小,通常小于 30 MB。

如果您在某一层中有一个块没有自然地被前一层的哈希完全填充,则应使用零填充它以达到预期的 4k。这使您可以知道哈希树未被删除,而是用空白数据完成的。

要生成哈希树,请将第 2 层哈希连接到第 1 层的哈希,将第 3 层哈希连接到第 2 层的哈希,依此类推。将所有这些写入磁盘。请注意,这不引用根哈希的第 0 层。

概括来说,构造哈希树的一般算法如下

  1. 选择一个随机 salt(十六进制编码)。
  2. 将您的系统映像解稀疏化为 4k 块。
  3. 对于每个块,获取其(加盐)SHA256 哈希。
  4. 连接这些哈希以形成一个层级
  5. 用 0 将层级填充到 4k 块边界。
  6. 将层级连接到您的哈希树。
  7. 使用前一个层级作为下一个层级的源重复步骤 2-6,直到您只有一个哈希为止。

这样做的结果是一个哈希,即您的根哈希。此哈希和您的 salt 在构建 dm-verity 映射表期间使用。

构建 dm-verity 映射表

构建 dm-verity 映射表,该表标识内核的块设备(或目标)和哈希树的位置(该位置是相同的值)。此映射用于fstab生成和启动。该表还标识块的大小和 hash_start,即哈希树的起始位置(具体来说,是从映像开头开始的块号)。

有关 verity 目标映射表字段的详细说明,请参阅cryptsetup

签署 dm-verity 表

签署 dm-verity 表以生成表签名。验证分区时,首先验证表签名。这是针对 boot 映像上固定位置的密钥完成的。密钥通常包含在制造商的构建系统中,以便自动包含在设备上的固定位置。

要使用此签名和密钥组合验证分区,请执行以下操作

  1. 将 libmincrypt 兼容格式的 RSA-2048 密钥添加到 /boot 分区中的 /verity_key。标识用于验证哈希树的密钥的位置。
  2. 在相关条目的 fstab 中,将 verify 添加到 fs_mgr 标志。

将表签名捆绑到元数据中

将表签名和 dm-verity 表捆绑到 verity 元数据中。整个元数据块都已版本化,因此可以对其进行扩展,例如添加第二种签名或更改某些排序。

作为健全性检查,每个表元数据集合都关联一个幻数,该幻数有助于识别表。由于长度包含在 ext4 系统映像标头中,因此这提供了一种在不知道数据本身内容的情况下搜索元数据的方法。

这确保您没有选择验证未经验证的分区。如果是这样,则缺少此幻数会停止验证过程。此数字类似于 0xb001b001

十六进制的字节值是

  • 第一个字节 = b0
  • 第二个字节 = 01
  • 第三个字节 = b0
  • 第四个字节 = 01

下图描述了 verity 元数据的分解

<magic number>|<version>|<signature>|<table length>|<table>|<padding>
\-------------------------------------------------------------------/
\----------------------------------------------------------/   |
                            |                                  |
                            |                                 32K
                       block content

下表描述了这些元数据字段。

表 1. Verity 元数据字段

字段 用途 大小
幻数 由 fs_mgr 用作健全性检查 4 字节 0xb001b001
版本 用于对元数据块进行版本控制 4 字节 当前为 0
签名 PKCS1.5 填充形式的表签名 256 字节
表长度 dm-verity 表的长度(以字节为单位) 4 字节
前面描述的 dm-verity 表 表长度字节
填充 此结构以 0 填充至 32k 长度 0

优化 dm-verity

为了获得 dm-verity 的最佳性能,您应该

  • 在内核中,为 ARMv7 启用 NEON SHA-2,并为 ARMv8 启用 SHA-2 扩展。
  • 试验不同的预读和 prefetch_cluster 设置,以找到最适合您设备的配置。