实施 dm-verity

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

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

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

因为哈希值存储在页面树中,所以只有顶级“根”哈希必须被信任才能验证树的其余部分。修改任何块的能力等同于破坏加密哈希。有关此结构的描述,请参见下图。

dm-verity-哈希表

图 1. dm-verity 哈希表

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

手术

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

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

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

相反,dm-verity 单独验证块,并且仅在每个块被访问时验证。当读入内存时,该块被并行散列。然后在树上验证哈希。而且由于读取块是一项非常昂贵的操作,因此这种块级验证引入的延迟相对来说是微不足道的。

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

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

前向纠错

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

执行

概括

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

有关哈希树和 dm-verity 表的详细说明,请参阅The Chromium Projects - 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. 选择随机盐(十六进制编码)。
  2. 将您的系统映像解稀疏为 4k 块。
  3. 对于每个块,获取其(加盐的)SHA256 哈希。
  4. 连接这些哈希以形成一个级别
  5. 用 0 填充关卡到 4k 块边界。
  6. 将级别连接到您的哈希树。
  7. 使用上一个级别作为下一个级别的源重复步骤 2-6,直到您只有一个哈希值。

其结果是单个哈希,即您的根哈希。在构建 dm-verity 映射表时会用到这个和你的 salt。

构建 dm-verity 映射表

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

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

签署 dm-verity 表

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

要使用此签名和密钥组合验证分区:

  1. 将 libmincrypt 兼容格式的 RSA-2048 密钥添加到/verity_key/boot分区。识别用于验证哈希树的密钥的位置。
  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 设置,为您的设备找到最佳配置。