Bootstrap

GRUB 源代码分析:打开 LUKS 加密的boot磁盘分区的过程

GRUB 源代码分析:打开 LUKS 加密的 boot 磁盘分区的过程

1. 安装 GRUB 时生成加密配置文件

在安装 GRUB 的过程中,grub-install 会检测 --boot-directory 参数对应的磁盘分区是否为加密盘。如果检测到是加密盘,生成的配置文件中会包含 cryptomount 命令。这个命令非常重要,因为它在 GRUB 启动时用于挂载加密盘。若缺少这一步,GRUB 将无法识别并解锁加密盘。

  • 注意,这里的 boot 分区并不是 ESP 分区,ESP 分区不能被加密。
  • 这里的 boot 分区是存放 Linux 内核镜像initramfs 的分区。

1.1 grub.cfg 配置示例

生成的 grub.cfg 文件通常包含以下内容:

  • 注意,这个 grub.cfg 配置文件是在 ESP 分区中的,用于引导加载过程的第一阶段, 打开加密盘之前的配置。
  • configfile $prefix/grub.cfg 指定的这个配置文件,才是用于加载 Linux 内核的配置文件,通常位于加密的 boot 分区中。
cryptomount -u 868ca75c-932a-4d20-aa53-05101e356bc2
search.fs_uuid 6a31fab0-7b6d-49d4-95b2-53ed48ebeabf root cryptouuid/868ca75c932a4d20aa5305101e356bc2
set prefix=($root)'/grub'
configfile $prefix/grub.cfg

在这里,cryptomount 使用 UUID 来标识并解锁特定加密磁盘,从而为后续的文件系统挂载和配置文件加载提供必要的支持。

2. GRUB 挂载加密盘的过程

2.1 加密盘模块的初始化

GRUB 在启动时会首先初始化加密盘相关模块,并注册 cryptomount 命令。相关代码如下:

static struct grub_disk_dev grub_cryptodisk_dev = {
    .name = "cryptodisk",
    .id = GRUB_DISK_DEVICE_CRYPTODISK_ID,
    .disk_iterate = grub_cryptodisk_iterate,
    .disk_open = grub_cryptodisk_open,
    .disk_close = grub_cryptodisk_close,
    .disk_read = grub_cryptodisk_read,
    .disk_write = grub_cryptodisk_write,
#ifdef GRUB_UTIL
    .disk_memberlist = grub_cryptodisk_memberlist,
#endif
    .next = 0
};

GRUB_MOD_INIT(cryptodisk)
{
    grub_disk_dev_register(&grub_cryptodisk_dev);
    cmd = grub_register_extcmd("cryptomount", grub_cmd_cryptomount, 0,
                               N_("[ [-p password] | [-k keyfile"
                                  " [-O keyoffset] [-S keysize] ] ] [-H file]"
                                  " <SOURCE|-u UUID|-a|-b>"),
                               N_("Mount a crypto device."), options);
    grub_procfs_register("luks_script", &luks_script);
}
  • grub_disk_dev_register 把加密盘模块注册成一个磁盘设备,在 grub_disk_open 时会调用到这里的回调函数 grub_cryptodisk_open

当然,还会初始化具体的加密模块,例如 LUKS

GRUB_MOD_INIT(luks)
{
    COMPILE_TIME_ASSERT(sizeof(((struct grub_luks_phdr *)0)->uuid)
                        < GRUB_CRYPTODISK_MAX_UUID_LENGTH);
    grub_cryptodisk_dev_register(&luks_crypto);
}

LUKS 通过 grub_cryptodisk_dev_register 注册到加密盘模块中,也就是把 LUKS 添加到全局加密模块列表 grub_cryptodisk_list 中。后面 cryptomount 会利用这个加密模块列表来匹配和解锁参数中指定的加密磁盘。

通过以上代码,GRUB 的加密磁盘支持被系统注册和准备好,用于解锁指定加密设备。

2.2 执行挂载加密盘命令

做好准备工作后,GRUB 会执行 grub.cfg 配置文件中的指令,第一条就是 cryptomount 命令,cryptomount 会触发调用 GRUB 源代码中的 grub_cmd_cryptomount 函数。这一函数的作用是扫描并尝试挂载所有支持的加密盘。

2.2.1 扫描加密盘的过程

grub_cmd_cryptomount 会调用 grub_cryptodisk_scan_device_real 来遍历 grub_cryptodisk_list,并调用对应的 scan 函数来检测是否支持该加密盘格式。如果找到匹配的加密盘,则继续解锁并插入到全局的加密盘列表 cryptodisk_list 中。

示例代码如下:

for (cr = grub_cryptodisk_list; cr; cr = cr->next)
{
    dev = cr->scan(source, cargs);
    if (grub_errno)
        goto error_no_close;
    if (!dev)
        continue;

    ret = cr->recover_key(source, dev, cargs);
    if (ret != GRUB_ERR_NONE)
        goto error;

    ret = grub_cryptodisk_insert(dev, name, source);
    if (ret != GRUB_ERR_NONE)
        goto error;
}
  • recover_key 函数会设置好用于磁盘加解密的密钥,为后续的磁盘读写做好准备。

3. GRUB 打开加密盘的过程

3.1 search.fs_uuid 的处理流程

  • 一旦加密盘成功挂载,search.fs_uuid 命令会调用 grub_device_open -> grub_disk_open 来完成磁盘打开操作。
3.1.1 grub_disk_open 函数的处理逻辑
  • grub_disk_open 会依次调用已注册的磁盘模块中的 dev->disk_open 函数来尝试打开磁盘。如果某个模块能够成功打开指定磁盘,就会停止后续调用。否则,继续尝试下一个模块的 open 函数。
    • 还记得吗,在 GRUB 初始化时,通过 grub_disk_dev_register 函数注册了一个磁盘模块。
    • 所以这里最终会调用到 GRUB 初始化时注册的 grub_cryptodisk_dev->grub_cryptodisk_open() 函数来打开加密盘。
  • 由于加密盘已经在 cryptomount 时被插入到了 cryptodisk_list 列表中,grub_cryptodisk_open 可以顺利找到磁盘。
static grub_err_t
grub_cryptodisk_open(const char *name, grub_disk_t disk)
{
    if (grub_memcmp(name, "cryptouuid/", sizeof("cryptouuid/") - 1) == 0)
    {
        for (dev = cryptodisk_list; dev != NULL; dev = dev->next)
            if (grub_uuidcasecmp(name + sizeof("cryptouuid/") - 1, dev->uuid, sizeof(dev->uuid)) == 0)
                break;
    }

    if (dev == NULL) {
        return GRUB_ERR_UNKNOWN_DEVICE;
    }

    disk->data = dev;
    disk->log_sector_size = dev->log_sector_size;
    disk->total_sectors = dev->total_sectors;
    disk->max_agglomerate = GRUB_DISK_MAX_MAX_AGGLOMERATE;
    disk->id = dev->id;
    dev->ref++;
    return GRUB_ERR_NONE;
}

3.2 加载 boot 分区中的 grub.cfg

  • 一旦 grub_disk_open 成功打开加密盘,search.fs_uuid 会将 $root 变量的值设置为 cryptouuid/868ca75c932a4d20aa5305101e356bc2,用于后续的文件系统操作和引导加载。
  • configfile $prefix/grub.cfg 命令就可以成功打开加密盘中的配置文件了。
  • 然后加载 GRUB 菜单,并继续引导流程。

4. 磁盘透明加解密的过程简述

由于本文重点讨论的是加密盘打开的过程,这里只是简要说明一下加密磁盘读写的过程,以保持逻辑的完整性。

  • 还记得在 GRUB 初始化时注册的 grub_cryptodisk_dev 结构吗?读写磁盘时最终会调用到这里的函数。
    • 例如在从磁盘读取数据时会调用到 .disk_read = grub_cryptodisk_read
    • grub_cryptodisk_read 中会先使用 grub_disk_read 从磁盘读取加密的数据,然后调用 grub_cryptodisk_endecrypt 解密数据后返回。
    • 写入数据的过程也类似,先调用 grub_cryptodisk_endecrypt 加密数据,然后再写入磁盘。
  • 由于在 cryptomount 时已经设置好了密钥,所以 grub_cryptodisk_endecrypt 可以正确地进行加解密。

;