签名内核模块

作者:Greg Kroah-Hartman

签名内核模块已成为其他操作系统多年的一个特性。有些人和公司喜欢只安装那些被某个权威机构认可的模块(或驱动程序,有时也这样称呼)。鉴于 Linux 加载内核模块的方式发生了变化,签名内核模块可以很容易地添加到 Linux 内核中。本文讨论了我如何实现这个特性,并详细介绍了如何使用它。

在一个签名内核模块中,有人在模块中插入了一个数字签名,声明他们信任这个特定的模块。我不会试图说服任何人 Linux 应该具备这种能力,或者应该强制要求它,甚至它能提高安全性。我只是描述如何做到这一点,并提供其实现方法,如果有人想使用它的话。

公钥密码学被用来使签名内核模块工作。有关 RSA 公钥密码算法的概述——它是什么以及它是如何工作的——请参阅 Linux Journal 网站上的文章 www.linuxjournal.com/article/6826。本文假设读者熟悉公钥密码学的基本知识,并且能够修补、构建和加载新的 Linux 内核到他们的机器上。有关如何构建和加载新内核的说明,请参阅位于 www.tldp.org 的非常有用的 Linux 内核 HOWTO。

在 2.5 内核开发系列中,Rusty Russell 重写了 Linux 内核模块的工作方式。在以前的内核中,大部分模块加载逻辑都存储在用户空间中。随着 Rusty 的修改,所有这些逻辑都转移到了内核中,减少了架构无关逻辑的数量,并大大简化了用户界面。一个很好的副作用是,内核现在可以以原始格式访问整个模块文件。内核模块只是 ELF 格式的文件。ELF 代表可执行和链接格式,是用于可执行程序的格式。ELF 规范可以在 www.muppetlabs.com/~breadbox/software/ELF.txt 中找到文本形式。

ELF 文件由不同的 section(节)组成。这些节可以通过运行 readelf 程序来查看。例如

$ readelf -S visor.ko
There are 23 section headers, starting at offset 0x3954:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000040 0017e0 00  AX  0   0 16
  [ 2] .rel.text         REL             00000000 003cec 000cd0 08     21   1  4
  [ 3] .init.text        PROGBITS        00000000 001820 000210 00  AX  0   0 16
  [ 4] .rel.init.text    REL             00000000 0049bc 0001c8 08     21   3  4
  [ 5] .exit.text        PROGBITS        00000000 001a30 000030 00  AX  0   0 16
  [ 6] .rel.exit.text    REL             00000000 004b84 000030 08     21   5  4
  [ 7] .rodata           PROGBITS        00000000 001a60 000020 00   A  0   0 16
  [ 8] .rel.rodata       REL             00000000 004bb4 000028 08     21   7  4
  [ 9] .rodata.str1.1    PROGBITS        00000000 001a80 000449 01 AMS  0   0  1
  [10] .rodata.str1.32   PROGBITS        00000000 001ee0 0009c0 01 AMS  0   0 32
  [11] .modinfo          PROGBITS        00000000 0028a0 0006c0 00   A  0   0 32
  [12] .data             PROGBITS        00000000 002f60 000600 00  WA  0   0 32
  [13] .rel.data         REL             00000000 004bdc 0001e0 08     21   c  4
  [14] .gnu.linkonce.thi PROGBITS        00000000 003560 000120 00  WA  0   0 32
  [15] .rel.gnu.linkonce REL             00000000 004dbc 000010 08     21   e  4
  [16] __obsparm         PROGBITS        00000000 003680 000180 00  WA  0   0 32
  [17] .bss              NOBITS          00000000 003800 00000c 00  WA  0   0  4
  [18] .comment          PROGBITS        00000000 003800 00006e 00      0   0  1
  [19] .note             NOTE            00000000 00386e 000028 00      0   0  1
  [20] .shstrtab         STRTAB          00000000 003896 0000bd 00      0   0  1
  [21] .symtab           SYMTAB          00000000 004dcc 000760 10     22  58  4
  [22] .strtab           STRTAB          00000000 00552c 000580 00      0   0  1

因为 ELF 文件由节组成,所以很容易向模块文件添加一个新的节,并让内核在尝试加载模块时将其读入内存。如果我们把一个 RSA 签名的节放入模块中,内核可以解密签名,并将其与刚刚加载的文件的签名进行比较。如果匹配,则签名有效,模块将成功插入内核内存。如果签名不匹配,则说明模块中的某些内容已被篡改,或者模块没有使用正确的密钥签名。然后可以拒绝该模块——这就是我的补丁所做的。

内核代码如何工作

当内核被告知加载一个模块时,文件 kernel/module.c 中的代码会被运行。在该文件中,函数 load_module 完成了将模块分解成适当的节、检查内存位置、检查符号以及链接器通常所做的所有其他工作。该补丁修改了这个函数,并添加了以下代码行

if (module_check_sig(hdr, sechdrs, secstrings)) {
   err = -EPERM;
    goto free_hdr;
}

这个新函数 module_check_sig 完成了所有的模块签名检查逻辑。如果它返回一个错误,则错误权限不正确会被返回给用户,模块加载会被中止。如果该函数返回 0,表示没有发生错误,则模块加载过程将继续成功进行。

module_check_sig 函数位于文件 kernel/module-sig.c 中。该函数首先检查模块内是否包含签名。这是通过以下代码行完成的

sig_index = 0;
for (i = 1; i < hdr->e_shnum; i++)
    if (strcmp(secstrings+sechdrs[i].sh_name,
               "module_sig") == 0) {
        sig_index = i;
        break;
}
if (sig_index <= 0)
    return -EPERM;

这段代码循环遍历内核模块中所有不同的 ELF 节,并查找名为 module_sig 的节。如果找不到签名,它会返回一个错误,并阻止加载该模块。如果找到了签名,该函数继续执行。

一旦内核找到模块签名,它就需要确定它被要求加载的模块的哈希值是多少。为了做到这一点,它生成包含内核使用的可执行代码或数据的 ELF 节的 SHA1 哈希值。内核已经包含了生成 SHA1 哈希值(以及其他类型的哈希值,包括 MD5 和 MD4)的代码,所以这一步的大部分逻辑已经存在。

该函数首先通过请求 SHA1 算法来分配一个加密转换结构。然后它用以下代码行初始化这个结构

sha1_tfm = crypto_alloc_tfm("sha1", 0);
if (sha1_tfm == NULL)
    return -ENOMEM;
crypto_digest_init(sha1_tfm);

sha1_tfm 变量用于创建我们想要的 ELF 文件的特定部分的 SHA1 哈希值,如下面的代码所示
for (i = 1; i < hdr->e_shnum; i++) {
    name = secstrings+sechdrs[i].sh_name;

    /* We only care about sections with "text" or
       "data" in their names */
    if ((strstr(name, "text") == NULL) &&
        (strstr(name, "data") == NULL))
        continue;
    /* avoid the ".rel.*" sections too. */
    if (strstr(name, ".rel.") != NULL)
        continue;

    temp = (void *)sechdrs[i].sh_addr;
    size = sechdrs[i].sh_size;
    do {
        memset(&sg, 0x00, sizeof(*sg));
        sg.page = virt_to_page(temp);
        sg.offset = offset_in_page(temp);
        sg.length = min(size,
                        (PAGE_SIZE - sg.offset));
        size -= sg.length;
        temp += sg.length;
        crypto_digest_update(sha1_tfm, &sg, 1);
    } while (size > 0);
}

在这段代码中,我们只关心名称中包含 text 或 data 单词的 ELF 节,但不关心包含 .rel. 字符的节。在找到所有节并将其馈送到 SHA1 算法后,SHA1 哈希值被放入变量 sha1_result 中,代码如下

crypto_digest_final(sha1_tfm, sha1_result);
crypto_free_tfm(sha1_tfm);

现在 SHA1 哈希值已经计算出来,并且已经找到了带有签名哈希值的位置,剩下的就是解密签名哈希值并将其与计算出的哈希值进行比较。这一步是在这个函数的最后一行完成的

return rsa_check_sig(sig, &sha1_result[0]);

rsa_check_sig 函数位于 security/rsa/rsa.c 文件中,并使用 GnuPG 代码本身,该代码被移植到内核中运行,以解密签名并比较值。关于这如何工作的描述超出了本文的范围。

用户空间代码如何工作

现在我们已经了解了内核如何确定模块是否已正确签名,那么我们首先如何将签名放入模块中呢?在内核补丁中可以找到两个用户空间程序 extract_pkey 和 mod,以及一个小脚本 sign(在 security/rsa/userspace/ 目录中)。这两个程序可以通过运行该目录中的 Makefile 来构建。extract_pkey 程序用于将公钥放入内核,mod 程序供 sign 脚本用于签名内核模块。

为了签名一个模块,必须生成一个 RSA 签名密钥,这可以使用 gnupg 程序来完成。要生成 RSA 签名密钥,请将 --gen-key 选项传递给 gpg

$ gpg --gen-key
gpg (GnuPG) 1.2.1; Copyright (C) 2002 Free Software Foundation, Inc.
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. See the file COPYING for details.

Please select what kind of key you want:
   (1) DSA and ElGamal (default)
   (2) DSA (sign only)
   (5) RSA (sign only)
Your selection?

我们要创建一个 RSA 密钥,所以我们选择选项 5,然后选择默认密钥大小 1024

Your selection? 5
What keysize do you want? (1024)
Requested keysize is 1024 bits

继续回答其余问题,最终您的 RSA 密钥将被生成。但是为了使用这个密钥,我们必须创建它的加密版本。要做到这一点,再次运行 gpg 并编辑您刚刚创建的密钥(在下面的文本中,我将我的密钥命名为 testkey)

$ gpg --edit-key testkey
gpg (GnuPG) 1.2.1; Copyright (C) 2002 Free Software Foundation, Inc.
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. See the file COPYING for details.

Secret key is available.

gpg: checking the trustdb
gpg: checking at depth 0 signed=0 ot(-/q/n/m/f/u)=0/0/0/0/0/1
pub  1024R/77540AE9  created: 2003-10-09 expires: never      trust: u/u
(1). testkey

Command>

我们要添加一个新密钥,所以输入addkey在提示符下

Command> addkey
Please select what kind of key you want:
   (2) DSA (sign only)
   (3) ElGamal (encrypt only)
   (5) RSA (sign only)
   (6) RSA (encrypt only)
Your selection?

同样,我们想要一个 RSA 密钥,所以选择选项 6 并回答其余问题。密钥生成后,键入quit在提示符下

Command> quit
Save changes? yes

现在我们有了密钥,我们可以用它来签名内核模块。

要签名一个模块,请使用 sign 脚本,这是一个简单的 shell 脚本

#!/bin/bash
module=$1
key=$2

# strip out only the sections that we care about
./mod $module $module.out

# sha1 the sections
sha1sum $module.out | awk "{print \$1}" > \
$module.sha1

# encrypt the sections
gpg --no-greeting -e -o - -r $key $module.sha1 > \
$module.crypt

# add the encrypted data to the module
objcopy --add-section module_sig=$module.crypt \
$module

# remove the temporary files
rm $module.out $module.sha1 $module.crypt

脚本做的第一件事是在内核模块上运行程序 mod。这个程序只剥离我们在 ELF 文件中关心的节,并将它们输出到一个临时文件。mod 程序将在稍后详细描述。

在我们得到一个只包含我们想要的节的 ELF 文件后,我们使用 sha1sum 程序生成该文件的 SHA1 哈希值。然后使用 GPG 对这个 SHA1 哈希值进行加密,密钥被传递给它,这个加密文件被写入到一个临时文件。加密文件作为名为 module-sig 的新 ELF 节添加到原始模块中。这是通过程序 objcopy 完成的。就是这样。使用 Linux 机器上已经存在的常用程序,很容易创建 SHA1 哈希值,对其进行加密并将其添加到 ELF 文件中。

mod 程序也很简单。它利用了 libbfd 库知道如何处理 ELF 文件并以不同方式操作它们的事实;它是基于 binutils 程序 objdump 的。因为 libbfd 库处理了所有繁重的 ELF 逻辑,所以 mod 程序可以简单地遍历它想要的所有 ELF 文件的节,代码如下

for (section = abfd->sections;
     section != NULL;
     section = section->next) {
    if (section->flags & SEC_HAS_CONTENTS) {
        if (bfd_section_size(abfd, section) == 0)
            continue;

        /* We only care about sections with "text"
           or "data" in their names */
        name = section->name;
        if ((strstr(name, "text") == NULL) &&
            (strstr(name, "data") == NULL))
            continue;

        size = bfd_section_size(abfd, section));
        data = (bfd_byte *)malloc(size);

        bfd_get_section_contents(abfd, section,
                                 (PTR)data,
                                 0, size);

        stop_offset = size / opb;

        for (addr_offset = 0;
             addr_offset < stop_offset;
             ++addr_offset) {
            fprintf(out, "%c", data[addr_offset]);
        }
        free(data);
    }
}

现在我们可以签名一个内核模块,并且内核知道如何检测这个签名,剩下的唯一一步是将我们的公钥放入内核中,以便它可以成功解密签名。linux-kernel 邮件列表最近有很多关于如何在内核中正确处理密钥的讨论。该讨论为如何在 2.7 内核系列中处理这方面提出了一些好的建议。但就目前而言,我们不担心以灵活的方式正确处理密钥,所以我们直接将其编译进去。

首先我们需要获得公钥的副本。为此,告诉 GPG 将密钥提取到名为 public_key 的文件中

$ gpg --export -o public_key

为了帮助操作 GPG 公钥,Ericsson 的一些开发人员创建了一个名为 extract_pkey 的简单程序,以帮助将密钥分解成不同的部分。我修改了这个程序,为公钥生成 C 代码。

运行 extract_pkey 程序并将其指向您之前生成的 public_key 文件。让它将输出发送到名为 rsa_key.c 的文件

$ extract_pkey public_key > rsa_key.c

完成此步骤后,将 rsa_key.c 移动到 security/rsa/ 目录中的文件之上,用您的公钥替换我的公钥

$ mv rsa_key.c ~/linux/linux-2.6/security/rsa/

现在您已经生成了一个公钥和私钥 RSA 密钥对,并将您的公钥放入了内核目录中。构建打过补丁的内核,确保选择“模块签名检查”选项,然后安装它。如果您启动到这个内核中,您将只被允许加载您用您的密钥签名的模块,所以要小心,并且只在开发机器上测试它。

还剩下什么要做?

正如本文所示,生成密钥、签名内核模块并将公钥放入内核镜像需要许多不同的步骤。这仍然是一个粗糙的开发项目。为了使其更易于内核开发人员和整个 Linux 社区接受,这些步骤需要自动化,使其更容易签名所有内核模块和处理公钥。

除了简化此功能的使用之外,该项目的一些其他未来目标包括

  • 将 RSA 代码移动到通用加密框架中,允许其他内核功能使用它。

  • 允许内核中存在多个公钥,允许在单个机器中运行来自多个签名内核模块来源的模块。

  • 简化签名逻辑,以允许使用 GPG 的原生签名功能或 bsign 程序中提供的功能,而不是自定义的 mod 程序。

致谢

我要感谢 Ericsson 的开发人员,他们创建了一个名为 digsig 的内核补丁和程序,允许我使用他们移植到内核的 GPG。我之前也做过这件事,但实现很糟糕; 幸运的是,他们发布了他们的移植版本,并且非常乐于助人。digsig 内核补丁允许用户签名程序,并阻止内核运行任何未签名的程序。有关此项目的更多信息,请访问 sourceforge.net/projects/disec

我还要感谢我的雇主 IBM 允许我从事这个项目,以及 Don Marti,感谢他敦促我完成它并撰写这篇文章。

Greg Kroah-Hartman 目前是各种不同驱动程序子系统的 Linux 内核维护者。他在 IBM 工作,从事与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。

加载 Disqus 评论