UBIFS 认证支持

简介

UBIFS 利用 fscrypt 框架为文件内容和文件名提供保密性。 这可以防止攻击者在单个时间点读取文件系统内容。 一个典型的例子是丢失的智能手机,攻击者无法在没有文件系统解密密钥的情况下读取设备上存储的个人数据。

然而,在当前状态下,UBIFS 加密并不能阻止攻击者修改文件系统内容,然后用户使用该设备的情况。 在这种情况下,攻击者可以任意修改文件系统内容而无需用户注意。 一个例子是修改二进制文件以在执行时执行恶意操作 [DMC-CBC-ATTACK]。 由于 UBIFS 的大多数文件系统元数据都以明文形式存储,因此可以很容易地交换文件并替换其内容。

其他全盘加密系统(如 dm-crypt)涵盖所有文件系统元数据,这使得此类攻击更加复杂,但并非不可能。 特别是,如果攻击者可以在多个时间点访问设备。 对于 dm-crypt 和其他基于 Linux 块 IO 层的的文件系统,可以使用 dm-integrity 或 dm-verity 子系统 [DM-INTEGRITY, DM-VERITY] 在块层获得完整的数据认证。 这些也可以与 dm-crypt 结合使用 [CRYPTSETUP2]。

本文档描述了一种获取文件内容_和_完整元数据认证的 UBIFS 方法。 由于 UBIFS 使用 fscrypt 进行文件内容和文件名加密,因此认证系统可以与 fscrypt 相关联,以便可以利用密钥派生等现有功能。 然而,也可以在不使用加密的情况下使用 UBIFS 认证。

MTD、UBI & UBIFS

在 Linux 上,MTD(内存技术设备)子系统提供了一个统一的接口来访问原始闪存设备。 UBI(未排序块镜像)是在 MTD 之上工作的更重要的子系统之一。 它为闪存设备提供卷管理,因此在某种程度上类似于块设备的 LVM。 此外,它还处理闪存特定的损耗均衡和透明 I/O 错误处理。 UBI 为其上层提供逻辑擦除块 (LEB),并将它们透明地映射到闪存上的物理擦除块 (PEB)。

UBIFS 是一个用于原始闪存的文件系统,它在 UBI 之上运行。 因此,损耗均衡和一些闪存特性留给了 UBI,而 UBIFS 则专注于可扩展性、性能和可恢复性。

+------------+ +*******+ +-----------+ +-----+
|            | * UBIFS * | UBI-BLOCK | | ... |
| JFFS/JFFS2 | +*******+ +-----------+ +-----+
|            | +-----------------------------+ +-----------+ +-----+
|            | |              UBI            | | MTD-BLOCK | | ... |
+------------+ +-----------------------------+ +-----------+ +-----+
+------------------------------------------------------------------+
|                  MEMORY TECHNOLOGY DEVICES (MTD)                 |
+------------------------------------------------------------------+
+-----------------------------+ +--------------------------+ +-----+
|         NAND DRIVERS        | |        NOR DRIVERS       | | ... |
+-----------------------------+ +--------------------------+ +-----+

    Figure 1: Linux kernel subsystems for dealing with raw flash

在内部,UBIFS 维护多个持久保存在闪存上的数据结构

  • 索引:一个在闪存上的 B+ 树,其中叶节点包含文件系统数据

  • 日志:一个额外的数据结构,用于在更新闪存上的索引之前收集 FS 更改并减少闪存损耗。

  • 树节点缓存 (TNC):一个内存中的 B+ 树,反映当前的 FS 状态,以避免频繁的闪存读取。 它基本上是索引的内存表示,但包含其他属性。

  • LEB 属性树 (LPT):一个闪存上的 B+ 树,用于每个 UBI LEB 的可用空间计算。

在本节的其余部分,我们将更详细地介绍闪存上的 UBIFS 数据结构。 TNC 在这里不太重要,因为它永远不会直接持久化到闪存上。 有关 UBIFS 的更多详细信息,请参见 [UBIFS-WP]。

UBIFS 索引 & 树节点缓存

基本的闪存上的 UBIFS 实体称为节点。 UBIFS 知道不同类型的节点。 例如,数据节点(struct ubifs_data_node),它存储文件内容的块,或 inode 节点(struct ubifs_ino_node),它表示 VFS inode。 几乎所有类型的节点都共享一个公共标头(ubifs_ch),其中包含基本信息,如节点类型、节点长度、序列号等(请参见内核源代码中的 fs/ubifs/ubifs-media.h)。 例外情况是 LPT 的条目和一些不太重要的节点类型,如填充节点,用于填充 LEB 末尾的不可用内容。

为了避免在每次更改时重新写入整个 B+ 树,它被实现为游荡树,其中只有更改的节点被重新写入,并且它们的先前版本被废弃,而不会立即擦除它们。 因此,索引不是存储在闪存上的单个位置,而是游荡的,并且只要包含它们的 LEB 没有被 UBIFS 重用,闪存上就会有废弃的部分。 为了找到索引的最新版本,UBIFS 将一个名为主节点的特殊节点存储到 UBI LEB 1 中,该节点始终指向 UBIFS 索引的最新根节点。 为了可恢复性,主节点还会被复制到 LEB 2。 因此,挂载 UBIFS 只是简单地读取 LEB 1 和 2 以获取当前主节点,并从那里获取最新闪存索引的位置。

TNC 是闪存上索引的内存表示。 它包含每个节点的一些额外的运行时属性,这些属性不会持久化。 其中之一是一个脏标志,用于标记下次将索引写入闪存时必须持久化的节点。 TNC 充当写回缓存,并且对闪存上索引的所有修改都通过 TNC 完成。 像其他缓存一样,TNC 不必将完整索引镜像到内存中,而是在需要时从闪存读取它的部分内容。 提交是 UBIFS 更新闪存上文件系统结构(如索引)的操作。 在每次提交时,标记为脏的 TNC 节点都会被写入闪存以更新持久化的索引。

日志

为了避免磨损闪存,只有在满足某些条件时才会持久化(提交)索引(例如,fsync(2))。 日志用于记录索引提交之间的任何更改(以 inode 节点、数据节点等形式)。 在挂载期间,从闪存读取日志并将其重放到 TNC 上(这将根据需要在闪存上索引创建)。

UBIFS 为日志保留了一堆 LEB,称为日志区域。 日志区域 LEB 的数量是在文件系统创建时配置的(使用 mkfs.ubifs)并存储在超级块节点中。 日志区域仅包含两种类型的节点:引用节点提交开始节点。 每当执行索引提交时,都会写入提交开始节点。 引用节点在每次日志更新时写入。 每个引用节点都指向闪存上其他节点(inode 节点、数据节点等)的位置,这些节点是此日志条目的一部分。 这些节点称为,并描述了实际的文件系统更改,包括它们的数据。

日志区域被维护为一个环。 每当日志几乎已满时,就会启动提交。 这也会写入提交开始节点,以便在挂载期间,UBIFS 将寻找最新的提交开始节点,并仅重放该节点之后的每个引用节点。 提交开始节点之前的每个引用节点都将被忽略,因为它们已经是闪存上索引的一部分。

在写入日志条目时,UBIFS 首先确保有足够的空间来写入此条目一部分的引用节点和芽。 然后,写入引用节点,然后写入描述文件更改的芽。 在重放时,UBIFS 将记录每个引用节点并检查引用 LEB 的位置以发现芽。 如果这些损坏或丢失,UBIFS 将尝试通过重新读取 LEB 来恢复它们。 但是,这仅适用于日志的最后一个引用的 LEB。 只有这一个 LEB 会因电源中断而损坏。 如果恢复失败,UBIFS 将无法挂载。 每个其他 LEB 的错误将直接导致 UBIFS 无法挂载操作。

| ----    LOG AREA     ---- | ----------    MAIN AREA    ------------ |

 -----+------+-----+--------+----   ------+-----+-----+---------------
 \    |      |     |        |   /  /      |     |     |               \
 / CS |  REF | REF |        |   \  \ DENT | INO | INO |               /
 \    |      |     |        |   /  /      |     |     |               \
  ----+------+-----+--------+---   -------+-----+-----+----------------
          |     |                  ^            ^
          |     |                  |            |
          +------------------------+            |
                |                               |
                +-------------------------------+


         Figure 2: UBIFS flash layout of log area with commit start nodes
                   (CS) and reference nodes (REF) pointing to main area
                   containing their buds

LEB 属性树/表

LEB 属性树用于存储每个 LEB 的信息。 这包括 LEB 类型和可用以及(旧的、废弃的内容)空间量[1] 在 LEB 上。 类型很重要,因为 UBIFS 永远不会在单个 LEB 上混合索引节点和数据节点,因此每个 LEB 都有特定的用途。 这对于可用空间计算也很有用。 有关更多详细信息,请参见 [UBIFS-WP]。

LEB 属性树再次是一个 B+ 树,但它比索引小得多。 由于其尺寸较小,因此每次提交时总是作为一个块写入。 因此,保存 LPT 是一项原子操作。

UBIFS 认证

本章介绍 UBIFS 认证,它使 UBIFS 能够验证存储在闪存上的元数据和文件内容的真实性和完整性。

威胁模型

UBIFS 认证支持检测离线数据修改。 虽然它不能阻止它,但它使(受信任的)代码能够检查闪存上文件内容和文件系统元数据的完整性和真实性。 这涵盖了文件内容被交换的攻击。

UBIFS 认证不会防止完整闪存内容的回滚。 也就是说,攻击者仍然可以转储闪存并在以后恢复它而不会被检测到。 它也不会防止单个索引提交的部分回滚。 这意味着攻击者能够部分撤消更改。 这是可能的,因为 UBIFS 不会立即覆盖索引树或日志的废弃版本,而是将它们标记为废弃,并且垃圾回收会在稍后擦除它们。 攻击者可以通过擦除当前树的部分内容并恢复仍然在闪存上且尚未被擦除的旧版本来利用这一点。 这是可能的,因为每次提交都会始终写入索引根节点和主节点的新版本,而不会覆盖以前的版本。 UBI 的损耗均衡操作进一步帮助了这一点,它将内容从一个物理擦除块复制到另一个物理擦除块,并且不会原子地擦除第一个擦除块。

如果攻击者能够在提供身份验证密钥后在设备上执行代码,则 UBIFS 身份验证不会涵盖此类攻击。 必须采取其他措施,如安全启动和可信启动,以确保只有受信任的代码才能在设备上执行。

认证

为了能够完全信任从闪存读取的数据,所有存储在闪存上的 UBIFS 数据结构都经过身份验证。 那就是

  • 索引,其中包括文件内容、文件元数据(如扩展属性、文件长度等)。

  • 日志,它还包含文件内容和元数据,通过记录对文件系统的更改

  • LPT,它存储 UBI LEB 元数据,UBIFS 使用该元数据进行可用空间计算

索引认证

通过 UBIFS 的游荡树概念,它已经注意只更新和持久化从叶节点到完整 B+ 树的根节点的更改部分。 这使我们能够使用每个节点子节点的哈希来增强树的索引节点。 因此,索引基本上也是一棵 Merkle 树。 由于索引的叶节点包含实际的文件系统数据,因此它们的父索引节点的哈希值涵盖了所有文件内容和文件元数据。 当文件更改时,UBIFS 索引会相应地从叶节点到根节点(包括主节点)进行更新。 可以挂钩此过程以仅同时为每个更改的节点重新计算哈希。 每当读取文件时,UBIFS 可以验证从每个叶节点到根节点的哈希,以确保节点的完整性。

为了确保整个索引的真实性,UBIFS 主节点存储了一个密钥哈希 (HMAC),该哈希包含它自己的内容和索引树根节点的哈希。 如上所述,每当索引被持久化时(即,在索引提交时),主节点总是被写入闪存。

使用此方法,只会更改 UBIFS 索引节点和主节点以包含哈希。 所有其他类型的节点将保持不变。 这减少了存储开销,这对 UBIFS 的用户(即嵌入式设备)来说是宝贵的。

                  +---------------+
                  |  Master Node  |
                  |    (hash)     |
                  +---------------+
                          |
                          v
                 +-------------------+
                 |  Index Node #1    |
                 |                   |
                 | branch0   branchn |
                 | (hash)    (hash)  |
                 +-------------------+
                    |    ...   |  (fanout: 8)
                    |          |
            +-------+          +------+
            |                         |
            v                         v
 +-------------------+       +-------------------+
 |  Index Node #2    |       |  Index Node #3    |
 |                   |       |                   |
 | branch0   branchn |       | branch0   branchn |
 | (hash)    (hash)  |       | (hash)    (hash)  |
 +-------------------+       +-------------------+
      |   ...                     |   ...   |
      v                           v         v
    +-----------+         +----------+  +-----------+
    | Data Node |         | INO Node |  | DENT Node |
    +-----------+         +----------+  +-----------+


Figure 3: Coverage areas of index node hash and master node HMAC

对于稳健性和断电安全性来说,最重要的部分是原子地持久化哈希和文件内容。 在这里,现有的 UBIFS 逻辑如何持久化更改的节点已经为此目的而设计,以便 UBIFS 可以在发生断电时安全地恢复。 向索引节点添加哈希不会改变这一点,因为每个哈希将与其各自的节点原子地一起持久化。

日志认证

日志也被认证。 由于日志是持续写入的,因此也有必要经常向日志添加认证信息,以便在发生断电时,不会有太多数据无法认证。 这是通过从提交开始节点开始,在先前的引用节点、当前引用节点和芽节点上创建一个连续的哈希来完成的。 不时地,只要合适,就在芽节点之间添加认证节点。 这种新的节点类型包含一个 HMAC,它包含哈希链的当前状态。 这样,就可以将日志认证到最后一个认证节点。 日志的尾部可能没有认证节点,因此无法认证,并且在日志重放期间会被跳过。

我们得到这张用于日志认证的图片

,,,,,,,,
,......,...........................................
,. CS  ,               hash1.----.           hash2.----.
,.  |  ,                    .    |hmac            .    |hmac
,.  v  ,                    .    v                .    v
,.REF#0,-> bud -> bud -> bud.-> auth -> bud -> bud.-> auth ...
,..|...,...........................................
,  |   ,
,  |   ,,,,,,,,,,,,,,,
.  |            hash3,----.
,  |                 ,    |hmac
,  v                 ,    v
, REF#1 -> bud -> bud,-> auth ...
,,,|,,,,,,,,,,,,,,,,,,
   v
  REF#2 -> ...
   |
   V
  ...

由于哈希还包括引用节点,因此攻击者无法重新排序或跳过任何日志头以进行重放。 攻击者只能从日志的末尾删除芽节点或引用节点,从而有效地将文件系统倒回到最多最后一个提交。

日志区域的位置存储在主节点中。 由于主节点如上所述使用 HMAC 进行认证,因此无法篡改它而不被检测到。 日志区域的大小是在使用 mkfs.ubifs 创建文件系统时指定的,并存储在超级块节点中。 为了避免篡改此值和存储在那里的其他值,HMAC 被添加到超级块结构中。 超级块节点存储在 LEB 0 中,并且仅在功能标志或类似更改时才会被修改,而不是在文件更改时。

LPT 认证

闪存上 LPT 根节点的位置存储在 UBIFS 主节点中。 由于 LPT 在每次提交时都会原子地写入和读取,因此无需认证树的各个节点。 通过存储在主节点中的简单哈希来保护完整 LPT 的完整性就足够了。 由于主节点本身经过认证,因此可以通过验证主节点的真实性并将存储在那里的 LTP 哈希与从读取的闪存上 LPT 计算出的哈希进行比较来验证 LPT 的真实性。

密钥管理

为了简单起见,UBIFS 认证使用单个密钥来计算超级块、主节点、提交开始节点和引用节点的 HMAC。 此密钥必须在创建文件系统时可用(mkfs.ubifs)以认证超级块节点。 此外,它必须在挂载文件系统时可用,以验证认证的节点并为更改生成新的 HMAC。

UBIFS 认证旨在与 UBIFS 加密 (fscrypt) 并行运行,以提供保密性和真实性。 由于 UBIFS 加密对每个目录都有不同的加密策略,因此可能有多个 fscrypt 主密钥,并且可能存在没有加密的文件夹。 另一方面,UBIFS 认证采用全有或全无的方法,因为它要么认证文件系统的所有内容,要么不认证任何内容。 因此,并且因为 UBIFS 认证也应该在没有加密的情况下使用,它不与 fscrypt 共享相同的主密钥,而是管理专用的认证密钥。

提供身份验证密钥的 API 尚未定义,但密钥可以由用户空间通过类似于当前在 fscrypt 中完成的方式的密钥环提供。 然而,应该注意的是,当前的 fscrypt 方法已经显示出它的缺陷,用户空间 API 最终会改变 [FSCRYPT-POLICY2]。

尽管如此,用户仍然可以在用户空间中提供一个涵盖 UBIFS 认证和加密的单个密码或密钥。 这可以通过相应的用户空间工具来解决,这些工具除了用于加密的派生的 fscrypt 主密钥之外,还会为认证派生第二个密钥。

为了能够检查挂载时是否提供了正确的密钥,UBIFS 超级块节点还将存储身份验证密钥的哈希。 这种方法类似于为 fscrypt 加密策略 v2 提出的方法 [FSCRYPT-POLICY2]。

未来扩展

在某些情况下,供应商想要向客户提供经过认证的文件系统镜像,可以这样做而无需共享秘密的 UBIFS 认证密钥。 相反,除了每个 HMAC 之外,还可以存储一个数字签名,其中供应商与文件系统镜像一起共享公钥。 如果此文件系统必须在之后进行修改,UBIFS 可以在第一次挂载时将所有数字签名与 HMAC 交换,类似于 IMA/EVM 子系统处理此类情况的方式。 然后必须以正常方式预先提供 HMAC 密钥。

参考

[CRYPTSETUP2] https://www.saout.de/pipermail/dm-crypt/2017-November/005745.html

[DMC-CBC-ATTACK] https://www.jakoblell.com/blog/2013/12/22/practical-malleability-attack-against-cbc-encrypted-luks-partitions/

[DM-INTEGRITY] https://linuxkernel.org.cn/doc/Documentation/device-mapper/dm-integrity.rst

[DM-VERITY] https://linuxkernel.org.cn/doc/Documentation/device-mapper/verity.rst

[FSCRYPT-POLICY2] https://www.spinics.net/lists/linux-ext4/msg58710.html

[UBIFS-WP] http://www.linux-mtd.infradead.org/doc/ubifs_whitepaper.pdf